SOY NV AI commited on
Commit ยท
db693d8
1
Parent(s): 16db560
Add Character Creation and Webtoon Line Art Challenge features, and enhance API integration
Browse files- app/clients/comfyui_client.py +1 -0
- app/database.py +0 -1
- app/gemini_client.py +73 -93
- app/models/webtoon_conti.py +4 -0
- app/prompts/webtoon_conti.py +1 -0
- app/routers/creation.py +238 -15
- app/routers/webtoon_conti.py +114 -8
- app/routes.py +4 -0
- app/services/webtoon_conti_service.py +38 -17
- force_update_menu.py +1 -0
- templates/admin_webtoon_milestone_producer_manager.html +1 -0
- templates/character_creation.html +505 -0
- templates/webtoon_conti_dashboard.html +222 -31
- templates/webtoon_line_art_challenge.html +648 -0
app/clients/comfyui_client.py
CHANGED
|
@@ -90,3 +90,4 @@ class ComfyUIClient:
|
|
| 90 |
|
| 91 |
|
| 92 |
|
|
|
|
|
|
| 90 |
|
| 91 |
|
| 92 |
|
| 93 |
+
|
app/database.py
CHANGED
|
@@ -1155,4 +1155,3 @@ class WebtoonLineArtJob(db.Model):
|
|
| 1155 |
'created_at': self.created_at.isoformat() if self.created_at else None,
|
| 1156 |
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
| 1157 |
}
|
| 1158 |
-
|
|
|
|
| 1155 |
'created_at': self.created_at.isoformat() if self.created_at else None,
|
| 1156 |
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
| 1157 |
}
|
|
|
app/gemini_client.py
CHANGED
|
@@ -343,99 +343,76 @@ class GeminiClient:
|
|
| 343 |
pass
|
| 344 |
|
| 345 |
# REST API ๋ฒ ์ด์ค URL (v1beta ์ฐ์ ์ฌ์ฉ)
|
| 346 |
-
|
| 347 |
-
|
|
|
|
|
|
|
| 348 |
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
logger.info(f"[Gemini] - ์ ๊ทํ๋ ๋ชจ๋ธ ์ด๋ฆ: {model_name_clean}")
|
| 352 |
-
logger.info(f"[Gemini] - ์ ์ฒด URL: {url}")
|
| 353 |
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
"
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
# ... (API ํค ๋ง์คํน ๋ก๊น
๋ฑ ๊ธฐ์กด ๋ก์ง ์ ์ง) ...
|
| 385 |
-
|
| 386 |
-
logger.info(f"[Gemini] REST API ์๋ต ์ํ ์ฝ๋: {rest_response.status_code}")
|
| 387 |
-
|
| 388 |
-
# ์๋ต ๋ณธ๋ฌธ ํ์ธ
|
| 389 |
-
response_has_error = False
|
| 390 |
-
try:
|
| 391 |
-
response_data_check = rest_response.json()
|
| 392 |
-
if 'error' in response_data_check:
|
| 393 |
-
response_has_error = True
|
| 394 |
-
error_info = response_data_check['error']
|
| 395 |
-
error_code = error_info.get('code', rest_response.status_code)
|
| 396 |
-
error_message = error_info.get('message', '์ ์ ์๋ ์ค๋ฅ')
|
| 397 |
-
logger.error(f"[Gemini] ์๋ต ๋ณธ๋ฌธ์ ์๋ฌ ๊ฐ์ง: code={error_code}, message={error_message}")
|
| 398 |
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
timeout=timeout_seconds
|
| 412 |
-
)
|
| 413 |
-
# ... (์ดํ v1 ์๋ต ์ฒ๋ฆฌ ๋ฐ Fallback ๋ก์ง์ ๋๋ฌด ๊ธธ์ด ์๋ตํ๋ ์๋ณธ ์ ์ง ํ์) ...
|
| 414 |
-
# ํธ์์ ์๋ณธ์ 404/429 ์ฒ๋ฆฌ ๋ก์ง์ ๋์ผํ๊ฒ ์๋ํ๋ค๊ณ ๊ฐ์ ํ๊ณ ์ถ์ฝํ์ง ์์
|
| 415 |
-
# ์ค์ ๊ตฌํ ์์๋ ์๋ณธ ํ์ผ ๋ด์ฉ์ ๊ทธ๋๋ก ๋ณต์ฌํด์ผ ํจ.
|
| 416 |
-
# ์ฌ๊ธฐ์๋ ์ง๋ฉด ๊ด๊ณ์ ํต์ฌ ๋ก์ง๋ง ๋ณ๊ฒฝํ๊ณ ๋๋จธ์ง๋ ... ์ฒ๋ฆฌ ํ์์ผ๋
|
| 417 |
-
# ์ค์ write ์์๋ ์ ์ฒด ๋ด์ฉ์ ๋ฃ์ด์ผ ํจ.
|
| 418 |
-
|
| 419 |
-
# (์ค๋ต๋ 404/429 ์ฒ๋ฆฌ ๋ก์ง)
|
| 420 |
-
pass # ์ค์ ์ฝ๋์์๋ ์๋ณธ ๋ด์ฉ ํฌํจ
|
| 421 |
-
elif error_code == 429:
|
| 422 |
-
# ... (429 ์ฒ๋ฆฌ ๋ก์ง) ...
|
| 423 |
-
raise Exception(f"Gemini API ํ ๋น๋ ์ด๊ณผ (429): {error_message}")
|
| 424 |
else:
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
if
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
|
| 437 |
response_data = rest_response.json()
|
| 438 |
-
|
| 439 |
# ํ ํฐ ์ฌ์ฉ๋ ๋ฑ ์ถ์ถ
|
| 440 |
input_tokens = None
|
| 441 |
output_tokens = None
|
|
@@ -618,9 +595,12 @@ class GeminiClient:
|
|
| 618 |
}
|
| 619 |
|
| 620 |
if is_image_gen_model:
|
| 621 |
-
# ์ด๋ฏธ์ง ์์ฑ ๋ชจ๋ธ์ฉ ์ต์ ํ ์ค์
|
| 622 |
-
|
| 623 |
-
|
|
|
|
|
|
|
|
|
|
| 624 |
else:
|
| 625 |
# ์ผ๋ฐ ํ
์คํธ ๋ชจ๋ธ์ฉ ์ค์
|
| 626 |
config_params.update({
|
|
@@ -784,12 +764,12 @@ class GeminiClient:
|
|
| 784 |
generated_images.append(part['inline_data'].get('data'))
|
| 785 |
|
| 786 |
# ์๋ต์ ์ด๋ฏธ์ง๋ ์์ง๋ง ํ
์คํธ๋ง ์๋ ๊ฒฝ์ฐ, ํน์ ์๋ฌ ๋ฉ์์ง์ธ์ง ํ์ธ
|
| 787 |
-
if not generated_images and response_text:
|
| 788 |
if any(kw in response_text.lower() for kw in ["error", "cannot", "sorry", "unable", "failed"]):
|
| 789 |
logger.warning(f"[Gemini Multimodal] AI๊ฐ ์ด๋ฏธ์ง ์์ฑ ๊ฑฐ๋ถ ๋ฉ์์ง๋ฅผ ๋ณด๋ธ ๊ฒ์ผ๋ก ๋ณด์: {response_text[:200]}")
|
| 790 |
|
| 791 |
-
# ์๋ต์ด ๋น์ด์๊ฑฐ๋ ์ด๋ฏธ์ง๊ฐ ์๋ ๊ฒฝ์ฐ ์์ธ ๋ก๊น
|
| 792 |
-
if not generated_images:
|
| 793 |
try:
|
| 794 |
# ์ฐจ๋จ ์ฌ์ (Safety) ํ์ธ
|
| 795 |
block_reason = "No blocks"
|
|
|
|
| 343 |
pass
|
| 344 |
|
| 345 |
# REST API ๋ฒ ์ด์ค URL (v1beta ์ฐ์ ์ฌ์ฉ)
|
| 346 |
+
rest_base_urls = [
|
| 347 |
+
'https://generativelanguage.googleapis.com/v1beta',
|
| 348 |
+
'https://generativelanguage.googleapis.com/v1'
|
| 349 |
+
]
|
| 350 |
|
| 351 |
+
rest_response = None
|
| 352 |
+
success = False
|
|
|
|
|
|
|
| 353 |
|
| 354 |
+
for base_url in rest_base_urls:
|
| 355 |
+
url = f"{base_url}/models/{model_name_clean}:generateContent"
|
| 356 |
+
logger.info(f"[Gemini] - API ํธ์ถ ์๋: {url}")
|
| 357 |
+
|
| 358 |
+
# REST API ์์ฒญ ๋ณธ๋ฌธ ๊ตฌ์ฑ
|
| 359 |
+
request_body = {
|
| 360 |
+
"contents": [{
|
| 361 |
+
"parts": [{
|
| 362 |
+
"text": prompt
|
| 363 |
+
}]
|
| 364 |
+
}],
|
| 365 |
+
"generationConfig": generation_config
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
# REST API ํค๋
|
| 369 |
+
headers = {
|
| 370 |
+
"Content-Type": "application/json",
|
| 371 |
+
"x-goog-api-key": api_key_clean
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
api_params = {"key": api_key_clean}
|
| 375 |
+
|
| 376 |
+
try:
|
| 377 |
+
rest_response = requests.post(
|
| 378 |
+
url,
|
| 379 |
+
headers=headers,
|
| 380 |
+
json=request_body,
|
| 381 |
+
params=api_params,
|
| 382 |
+
timeout=timeout_seconds
|
| 383 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
|
| 385 |
+
logger.info(f"[Gemini] REST API ์๋ต ์ํ ์ฝ๋ ({base_url}): {rest_response.status_code}")
|
| 386 |
+
|
| 387 |
+
if rest_response.status_code == 200:
|
| 388 |
+
response_data = rest_response.json()
|
| 389 |
+
if 'error' not in response_data:
|
| 390 |
+
success = True
|
| 391 |
+
break
|
| 392 |
+
else:
|
| 393 |
+
logger.warning(f"[Gemini] API ์๋ต์ ์๋ฌ ํฌํจ: {response_data['error']}")
|
| 394 |
+
elif rest_response.status_code == 404:
|
| 395 |
+
logger.warning(f"[Gemini] ๋ชจ๋ธ์ ์ฐพ์ ์ ์์ (404): {url}")
|
| 396 |
+
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
else:
|
| 398 |
+
logger.warning(f"[Gemini] API ํธ์ถ ์คํจ ({rest_response.status_code}): {rest_response.text[:200]}")
|
| 399 |
+
except Exception as e:
|
| 400 |
+
logger.error(f"[Gemini] REST API ํธ์ถ ์ค ์์ธ ๋ฐ์: {e}")
|
| 401 |
+
continue
|
| 402 |
+
|
| 403 |
+
if not success:
|
| 404 |
+
if rest_response is not None:
|
| 405 |
+
try:
|
| 406 |
+
error_data = rest_response.json()
|
| 407 |
+
error_msg = error_data.get('error', {}).get('message', '์ ์ ์๋ ์ค๋ฅ')
|
| 408 |
+
raise Exception(f"Gemini API ์ค๋ฅ ({rest_response.status_code}): {error_msg}")
|
| 409 |
+
except:
|
| 410 |
+
raise Exception(f"Gemini API ์ค๋ฅ ({rest_response.status_code})")
|
| 411 |
+
else:
|
| 412 |
+
raise Exception("Gemini API ํธ์ถ์ ์คํจํ์ต๋๋ค.")
|
| 413 |
|
| 414 |
response_data = rest_response.json()
|
| 415 |
+
|
| 416 |
# ํ ํฐ ์ฌ์ฉ๋ ๋ฑ ์ถ์ถ
|
| 417 |
input_tokens = None
|
| 418 |
output_tokens = None
|
|
|
|
| 595 |
}
|
| 596 |
|
| 597 |
if is_image_gen_model:
|
| 598 |
+
# ์ด๋ฏธ์ง ์์ฑ ๋ชจ๋ธ์ฉ ์ต์ ํ ์ค์
|
| 599 |
+
# imagen ๊ณ์ด์ ํ
์คํธ ์ถ๋ ฅ์ด ์์ผ๋ฏ๋ก ์๊ฒ ์ ์ง, gemini ๊ณ์ด(๋ฉํฐ๋ชจ๋ฌ)์ ํ
์คํธ ๋ถ์๋ ํ๋ฏ๋ก ํฌ๊ฒ ํ์ฉ
|
| 600 |
+
if "imagen" in model_name_clean.lower():
|
| 601 |
+
config_params['max_output_tokens'] = 1024
|
| 602 |
+
else:
|
| 603 |
+
config_params['max_output_tokens'] = 4096 # ๋ฉํฐ๋ชจ๋ฌ ๋ชจ๋ธ์ ํ
์คํธ ์๋ต ํ์ฉ๋ ํ๋
|
| 604 |
else:
|
| 605 |
# ์ผ๋ฐ ํ
์คํธ ๋ชจ๋ธ์ฉ ์ค์
|
| 606 |
config_params.update({
|
|
|
|
| 764 |
generated_images.append(part['inline_data'].get('data'))
|
| 765 |
|
| 766 |
# ์๋ต์ ์ด๋ฏธ์ง๋ ์์ง๋ง ํ
์คํธ๋ง ์๋ ๊ฒฝ์ฐ, ํน์ ์๋ฌ ๋ฉ์์ง์ธ์ง ํ์ธ
|
| 767 |
+
if is_image_gen_model and not generated_images and response_text:
|
| 768 |
if any(kw in response_text.lower() for kw in ["error", "cannot", "sorry", "unable", "failed"]):
|
| 769 |
logger.warning(f"[Gemini Multimodal] AI๊ฐ ์ด๋ฏธ์ง ์์ฑ ๊ฑฐ๋ถ ๋ฉ์์ง๋ฅผ ๋ณด๋ธ ๊ฒ์ผ๋ก ๋ณด์: {response_text[:200]}")
|
| 770 |
|
| 771 |
+
# ์๋ต์ด ๋น์ด์๊ฑฐ๋ ์ด๋ฏธ์ง๊ฐ ์๋ ๊ฒฝ์ฐ ์์ธ ๋ก๊น
(์ด๋ฏธ์ง ๋ชจ๋ธ์ธ ๊ฒฝ์ฐ๋ง)
|
| 772 |
+
if is_image_gen_model and not generated_images:
|
| 773 |
try:
|
| 774 |
# ์ฐจ๋จ ์ฌ์ (Safety) ํ์ธ
|
| 775 |
block_reason = "No blocks"
|
app/models/webtoon_conti.py
CHANGED
|
@@ -11,6 +11,8 @@ class WebtoonConti(db.Model):
|
|
| 11 |
episode_id = db.Column(db.Integer, db.ForeignKey('episode_analysis.id'), nullable=True)
|
| 12 |
title = db.Column(db.String(255), nullable=False)
|
| 13 |
description = db.Column(db.Text, nullable=True)
|
|
|
|
|
|
|
| 14 |
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
| 15 |
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
| 16 |
|
|
@@ -28,6 +30,8 @@ class WebtoonConti(db.Model):
|
|
| 28 |
'episode_id': self.episode_id,
|
| 29 |
'title': self.title,
|
| 30 |
'description': self.description,
|
|
|
|
|
|
|
| 31 |
'created_at': self.created_at.isoformat() if self.created_at else None,
|
| 32 |
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
| 33 |
'panel_count': len(self.panels) if self.panels else 0
|
|
|
|
| 11 |
episode_id = db.Column(db.Integer, db.ForeignKey('episode_analysis.id'), nullable=True)
|
| 12 |
title = db.Column(db.String(255), nullable=False)
|
| 13 |
description = db.Column(db.Text, nullable=True)
|
| 14 |
+
analysis_prompt = db.Column(db.Text, nullable=True) # ๋ถ์ ์ฐธ์กฐ ํ๋กฌํํธ ์ถ๊ฐ
|
| 15 |
+
gen_prompt = db.Column(db.Text, nullable=True) # ๊ทธ๋ฆผ ์ฝํฐ์ฉ ํ๋กฌํํธ ์ถ๊ฐ
|
| 16 |
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
| 17 |
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
| 18 |
|
|
|
|
| 30 |
'episode_id': self.episode_id,
|
| 31 |
'title': self.title,
|
| 32 |
'description': self.description,
|
| 33 |
+
'analysis_prompt': self.analysis_prompt,
|
| 34 |
+
'gen_prompt': self.gen_prompt,
|
| 35 |
'created_at': self.created_at.isoformat() if self.created_at else None,
|
| 36 |
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
| 37 |
'panel_count': len(self.panels) if self.panels else 0
|
app/prompts/webtoon_conti.py
CHANGED
|
@@ -75,3 +75,4 @@ def get_webtoon_conti_prompt(
|
|
| 75 |
|
| 76 |
|
| 77 |
|
|
|
|
|
|
| 75 |
|
| 76 |
|
| 77 |
|
| 78 |
+
|
app/routers/creation.py
CHANGED
|
@@ -6,6 +6,7 @@ import os
|
|
| 6 |
import asyncio
|
| 7 |
import re
|
| 8 |
import json
|
|
|
|
| 9 |
from sqlalchemy import case
|
| 10 |
from app.database import db, NovelProject, UploadedFile, ParentChunk, DocumentChunk, GraphEntity, GraphRelationship, GraphEvent, NovelProjectFact, StyleAnalysis, SystemConfig
|
| 11 |
from app.routes import inject_admin_menu as shared_inject_admin_menu
|
|
@@ -70,6 +71,215 @@ def get_deps(project_id: str, user_id: int, style_content: str = None) -> NovelW
|
|
| 70 |
style_content=style_content
|
| 71 |
)
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
@creation_bp.route('/webnovel', methods=['GET'])
|
| 74 |
@login_required
|
| 75 |
def webnovel():
|
|
@@ -129,11 +339,16 @@ def upload_image():
|
|
| 129 |
@creation_bp.route('/api/generate-image', methods=['POST'])
|
| 130 |
@login_required
|
| 131 |
def api_generate_image():
|
| 132 |
-
"""ํ
์คํธ
|
| 133 |
data = request.json
|
| 134 |
prompt = data.get('prompt')
|
| 135 |
model_name = data.get('model_name')
|
| 136 |
-
image_url = data.get('image_url') #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
if not prompt:
|
| 139 |
return jsonify({'error': 'ํ๋กฌํํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.'}), 400
|
|
@@ -145,24 +360,32 @@ def api_generate_image():
|
|
| 145 |
|
| 146 |
async def generate():
|
| 147 |
# gemini_client๋ฅผ ํตํด ์ง์ ์ด๋ฏธ์ง ์์ฑ ์์ฒญ
|
| 148 |
-
# model_name์์ 'gemini:' ์ ๋์ฌ ์ฒ๋ฆฌ
|
| 149 |
target_model = model_name.replace('gemini:', '')
|
| 150 |
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
contents = [system_instruction]
|
| 154 |
|
| 155 |
-
#
|
| 156 |
-
if
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
|
|
|
| 166 |
|
| 167 |
contents.append(f"Prompt: {prompt}")
|
| 168 |
|
|
|
|
| 6 |
import asyncio
|
| 7 |
import re
|
| 8 |
import json
|
| 9 |
+
import requests
|
| 10 |
from sqlalchemy import case
|
| 11 |
from app.database import db, NovelProject, UploadedFile, ParentChunk, DocumentChunk, GraphEntity, GraphRelationship, GraphEvent, NovelProjectFact, StyleAnalysis, SystemConfig
|
| 12 |
from app.routes import inject_admin_menu as shared_inject_admin_menu
|
|
|
|
| 71 |
style_content=style_content
|
| 72 |
)
|
| 73 |
|
| 74 |
+
@creation_bp.route('/api/character/global-prompt', methods=['GET'])
|
| 75 |
+
@login_required
|
| 76 |
+
def get_character_global_prompt():
|
| 77 |
+
"""์บ๋ฆญํฐ ์ ์์ฉ ์ ์ญ ๊ธฐ๋ณธ ํ๋กฌํํธ ์กฐํ"""
|
| 78 |
+
return jsonify({
|
| 79 |
+
"prompt": SystemConfig.get_config('character_creation_default_prompt', '')
|
| 80 |
+
})
|
| 81 |
+
|
| 82 |
+
@creation_bp.route('/api/character/global-prompt', methods=['POST'])
|
| 83 |
+
@login_required
|
| 84 |
+
def save_character_global_prompt():
|
| 85 |
+
"""์บ๋ฆญํฐ ์ ์์ฉ ์ ์ญ ๊ธฐ๋ณธ ํ๋กฌํํธ ์ ์ฅ"""
|
| 86 |
+
data = request.get_json()
|
| 87 |
+
prompt = data.get('prompt')
|
| 88 |
+
if prompt is not None:
|
| 89 |
+
SystemConfig.set_config('character_creation_default_prompt', prompt)
|
| 90 |
+
return jsonify({"success": True, "message": "์บ๋ฆญํฐ ์ ์ ๊ธฐ๋ณธ ํ๋กฌํํธ๋ก ์ ์ฅ๋์์ต๋๋ค."})
|
| 91 |
+
|
| 92 |
+
@creation_bp.route('/api/line-art-challenge/global-prompt', methods=['GET'])
|
| 93 |
+
@login_required
|
| 94 |
+
def get_line_art_challenge_global_prompt():
|
| 95 |
+
"""์นํฐ ์ ํ ๋์ ์ฉ ์ ์ญ ๊ธฐ๋ณธ ํ๋กฌํํธ ์กฐํ"""
|
| 96 |
+
return jsonify({
|
| 97 |
+
"prompt": SystemConfig.get_config('line_art_challenge_default_prompt', '')
|
| 98 |
+
})
|
| 99 |
+
|
| 100 |
+
@creation_bp.route('/api/line-art-challenge/global-prompt', methods=['POST'])
|
| 101 |
+
@login_required
|
| 102 |
+
def save_line_art_challenge_global_prompt():
|
| 103 |
+
"""์นํฐ ์ ํ ๋์ ์ฉ ์ ์ญ ๊ธฐ๋ณธ ํ๋กฌํํธ ์ ์ฅ"""
|
| 104 |
+
data = request.get_json()
|
| 105 |
+
prompt = data.get('prompt')
|
| 106 |
+
if prompt is not None:
|
| 107 |
+
SystemConfig.set_config('line_art_challenge_default_prompt', prompt)
|
| 108 |
+
return jsonify({"success": True, "message": "์นํฐ ์ ํ ๋์ ๊ธฐ๋ณธ ํ๋กฌํํธ๋ก ์ ์ฅ๋์์ต๋๋ค."})
|
| 109 |
+
|
| 110 |
+
@creation_bp.route('/character', methods=['GET'])
|
| 111 |
+
@login_required
|
| 112 |
+
def character_creation():
|
| 113 |
+
"""์บ๋ฆญํฐ ์ ์ ๋์๋ณด๋ ํ์ด์ง"""
|
| 114 |
+
default_analysis_model = SystemConfig.get_config('default_analysis_model', '')
|
| 115 |
+
return render_template('character_creation.html', title="์บ๋ฆญํฐ ์ ์", default_analysis_model=default_analysis_model)
|
| 116 |
+
|
| 117 |
+
@creation_bp.route('/webtoon-line-art-challenge', methods=['GET'])
|
| 118 |
+
@login_required
|
| 119 |
+
def webtoon_line_art_challenge():
|
| 120 |
+
"""์นํฐ ์ ํ ๋์ ํ์ด์ง"""
|
| 121 |
+
default_analysis_model = SystemConfig.get_config('default_analysis_model', '')
|
| 122 |
+
return render_template('webtoon_line_art_challenge.html', title="์นํฐ ์ ํ ๋์ ", default_analysis_model=default_analysis_model)
|
| 123 |
+
|
| 124 |
+
@creation_bp.route('/api/character/extract-main', methods=['POST'])
|
| 125 |
+
@login_required
|
| 126 |
+
def extract_main_characters():
|
| 127 |
+
"""์น์์ค์ ๋ฑ๋ก๋ ์บ๋ฆญํฐ ๋ชฉ๋ก ๋ฐ ์ ๋ณด ๋ฐํ (Parent Chunk ์ปจํ
์คํธ ํฌํจ)"""
|
| 128 |
+
data = request.get_json()
|
| 129 |
+
file_id = data.get('file_id')
|
| 130 |
+
|
| 131 |
+
if not file_id:
|
| 132 |
+
return jsonify({"error": "ํ์ผ ID๊ฐ ํ์ํฉ๋๋ค."}), 400
|
| 133 |
+
|
| 134 |
+
try:
|
| 135 |
+
file_id = int(file_id)
|
| 136 |
+
except (ValueError, TypeError):
|
| 137 |
+
return jsonify({"error": "์ ํจํ์ง ์์ ํ์ผ ID์
๋๋ค."}), 400
|
| 138 |
+
|
| 139 |
+
current_app.logger.info(f"Fetching character data for file_id: {file_id}")
|
| 140 |
+
|
| 141 |
+
# 1. DB์ ๋ฑ๋ก๋ ์บ๋ฆญํฐ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ (์ด๋ฏธ์ง ์ ๋ณด ํฌํจ)
|
| 142 |
+
from app.database import WebnovelCharacter
|
| 143 |
+
db_characters = WebnovelCharacter.query.filter_by(file_id=file_id).order_by(WebnovelCharacter.created_at.asc()).all()
|
| 144 |
+
|
| 145 |
+
# 2. Parent Chunk ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ (์ธ๊ณ๊ด, ์ค๊ฑฐ๋ฆฌ ๋ฑ ๋ฐฐ๊ฒฝ ์ ๋ณด์ฉ)
|
| 146 |
+
parent_chunk = ParentChunk.query.filter_by(file_id=file_id).first()
|
| 147 |
+
world_view = parent_chunk.world_view if parent_chunk else ""
|
| 148 |
+
story = parent_chunk.story if parent_chunk else ""
|
| 149 |
+
others = parent_chunk.others if parent_chunk else ""
|
| 150 |
+
|
| 151 |
+
chars = []
|
| 152 |
+
|
| 153 |
+
if db_characters:
|
| 154 |
+
# ๋ฑ๋ก๋ ์บ๋ฆญํฐ๊ฐ ์์ผ๋ฉด ํด๋น ๋ฐ์ดํฐ ์ฌ์ฉ
|
| 155 |
+
for c in db_characters:
|
| 156 |
+
chars.append({
|
| 157 |
+
"id": c.id,
|
| 158 |
+
"name": c.name,
|
| 159 |
+
"description": c.description or "",
|
| 160 |
+
"image_path": c.image_path,
|
| 161 |
+
"images": [img.to_dict() for img in c.images] if hasattr(c, 'images') else []
|
| 162 |
+
})
|
| 163 |
+
else:
|
| 164 |
+
# ๋ฑ๋ก๋ ์บ๋ฆญํฐ๊ฐ ์์ผ๋ฉด Parent Chunk์์ ์ถ์ถ (๊ธฐ์กด ๋ก์ง ํ์ ํธํ์ฑ ์ ์ง)
|
| 165 |
+
if parent_chunk and parent_chunk.characters:
|
| 166 |
+
char_text = parent_chunk.characters
|
| 167 |
+
lines = char_text.split('\n')
|
| 168 |
+
current_char = None
|
| 169 |
+
attr_keywords = ['์ญํ ', '์ฑ๊ฒฉ', 'ํน์ง', '์ฑ๋ณ', '๋์ด', '์ธ๋ชจ', '๋ฅ๋ ฅ', '์์', '๋ฐฐ๊ฒฝ', '๋ชฉํ', '๋๊ธฐ']
|
| 170 |
+
|
| 171 |
+
for line in lines:
|
| 172 |
+
line = line.strip()
|
| 173 |
+
if not line: continue
|
| 174 |
+
clean_line = re.sub(r'^[-*\d\.\s]+', '', line).strip()
|
| 175 |
+
if not clean_line: continue
|
| 176 |
+
|
| 177 |
+
if ':' in clean_line and len(clean_line.split(':', 1)[0]) < 15:
|
| 178 |
+
name_part, desc_part = clean_line.split(':', 1)
|
| 179 |
+
name_part = re.sub(r'[\[\](){}]', '', name_part).strip().replace('*', '')
|
| 180 |
+
is_attr_kw = any(name_part == kw for kw in attr_keywords)
|
| 181 |
+
if is_attr_kw:
|
| 182 |
+
if current_char: current_char["description"] += "\n" + clean_line
|
| 183 |
+
else:
|
| 184 |
+
current_char = {"name": name_part, "description": clean_line, "image_path": None, "images": []}
|
| 185 |
+
chars.append(current_char)
|
| 186 |
+
continue
|
| 187 |
+
|
| 188 |
+
is_attribute = any(clean_line.startswith(kw) for kw in attr_keywords)
|
| 189 |
+
if is_attribute:
|
| 190 |
+
if current_char: current_char["description"] += "\n" + clean_line
|
| 191 |
+
else:
|
| 192 |
+
if len(clean_line) > 15:
|
| 193 |
+
if current_char: current_char["description"] += "\n" + clean_line
|
| 194 |
+
continue
|
| 195 |
+
name = re.sub(r'[\[\](){}]', '', clean_line).strip().replace('*', '')
|
| 196 |
+
if name:
|
| 197 |
+
current_char = {"name": name, "description": clean_line, "image_path": None, "images": []}
|
| 198 |
+
chars.append(current_char)
|
| 199 |
+
|
| 200 |
+
return jsonify({
|
| 201 |
+
"characters": chars,
|
| 202 |
+
"world_view": world_view,
|
| 203 |
+
"story": story,
|
| 204 |
+
"others": others
|
| 205 |
+
})
|
| 206 |
+
|
| 207 |
+
@creation_bp.route('/api/character/analyze', methods=['POST'])
|
| 208 |
+
@login_required
|
| 209 |
+
def analyze_character():
|
| 210 |
+
"""์บ๋ฆญํฐ ์ ๋ณด๋ฅผ ๋ถ์ํ์ฌ ์ด๋ฏธ์ง ์์ฑ ํ๋กฌํํธ ์์ฑ"""
|
| 211 |
+
data = request.get_json()
|
| 212 |
+
char_name = data.get('name')
|
| 213 |
+
char_desc = data.get('description')
|
| 214 |
+
project_context = data.get('context', '') # ์ํ ๋ฐฐ๊ฒฝ ์ ๋ณด
|
| 215 |
+
model_name = data.get('model_name')
|
| 216 |
+
|
| 217 |
+
if not char_desc or not model_name:
|
| 218 |
+
return jsonify({"error": "๋ถ์ํ ์ ๋ณด์ ๋ชจ๋ธ๋ช
์ด ํ์ํฉ๋๋ค."}), 400
|
| 219 |
+
|
| 220 |
+
prompt = f"""
|
| 221 |
+
๋น์ ์ ์นํฐ ์บ๋ฆญํฐ ์ํธ ์ ์์ ์ํ ์ ๋ฌธ๊ฐ์
๋๋ค. ์๋ ์ ๊ณต๋ [์ํ ๋ฐฐ๊ฒฝ ์ ๋ณด]์ [์บ๋ฆญํฐ ์ ๋ณด]๋ฅผ ๋ฐํ์ผ๋ก, ์ด๋ฏธ์ง ์์ฑ ์ธ๊ณต์ง๋ฅ์ด ์ดํดํ๊ธฐ ์ฌ์ด ์์ธํ ์์ด ํ๋กฌํํธ๋ฅผ ์์ฑํด์ฃผ์ธ์.
|
| 222 |
+
|
| 223 |
+
[์ํ ๋ฐฐ๊ฒฝ ์ ๋ณด]
|
| 224 |
+
{project_context}
|
| 225 |
+
|
| 226 |
+
[์บ๋ฆญํฐ ์ ๋ณด]
|
| 227 |
+
์ด๋ฆ: {char_name}
|
| 228 |
+
์ค๋ช
: {char_desc}
|
| 229 |
+
|
| 230 |
+
[์์ฑ ๊ท์น]
|
| 231 |
+
1. ๋ฐ๋์ ์๋ 5๊ฐ์ง ์น์
๊ตฌ์กฐ๋ฅผ ์ ์งํ์ธ์.
|
| 232 |
+
2. ๊ฐ ์น์
์ [ ] ๋ถ๋ถ์ ์บ๋ฆญํฐ ์ ๋ณด์ ์ํ์ ๋ถ์๊ธฐ์ ๋ง์ถฐ ๊ตฌ์ฒด์ ์ธ ์์ด ๋จ์ด๋ก ์ฑ์ฐ์ธ์.
|
| 233 |
+
3. ์ ๋ณด๊ฐ ๋ถ์กฑํ ๋ถ๋ถ์ ์ํ ๋ฐฐ๊ฒฝ์ ๊ณ ๋ คํ์ฌ ์บ๋ฆญํฐ์ ์ฑ๊ฒฉ๊ณผ ์ธ๋ชจ์ ์ด์ธ๋ฆฌ๋ ์ ์ ํ ์ค์ ์ ์ถ์ธกํ์ฌ ๋ณด์ํ์ธ์.
|
| 234 |
+
4. ๊ฒฐ๊ณผ๋ฌผ์ ๋ฐ๋์ ์๋ ํ์์ ํ
์คํธ๋ง ์ถ๋ ฅํ์ธ์. ๋ค๋ฅธ ์ค๋ช
์ ์๋ตํฉ๋๋ค.
|
| 235 |
+
|
| 236 |
+
### [1. Style & Quality]
|
| 237 |
+
(Masterpiece), (High-quality Korean webtoon art style), (Clean line art), (Vibrant digital coloring), (8k resolution), (Detailed character design).
|
| 238 |
+
|
| 239 |
+
### [2. Character Core]
|
| 240 |
+
A [Age: ๋์ด] [Gender: ์ฑ๋ณ] with [Personality/Vibe: ๋ถ์๊ธฐ/์ฑ๊ฒฉ].
|
| 241 |
+
[Face: ์ด๋ชฉ๊ตฌ๋น ํน์ง], [Eyes: ๋๋งค์ ๋๋์ ์], [Hair: ํค์ด์คํ์ผ๊ณผ ๋จธ๋ฆฌ์].
|
| 242 |
+
|
| 243 |
+
### [3. Outfit & Accessories]
|
| 244 |
+
Wearing [Outfit: ์์ ์์ธ], [Accessories: ์ก์ธ์๋ฆฌ ๋ฐ ์ํ].
|
| 245 |
+
[Theme: ์ฅ๋ฅด์ ํน์ง - ์: imperial military uniform, modern streetwear].
|
| 246 |
+
|
| 247 |
+
### [4. Pose & Composition]
|
| 248 |
+
[Shot Type: ๊ตฌ๋ - ์: Full body portrait], [Pose: ์์ธ - ์: Standing facing front], (Neutral pose), (Simple background), (Character sheet format).
|
| 249 |
+
|
| 250 |
+
### [5. Atmosphere & Lighting]
|
| 251 |
+
[Mood: ๋ถ์๊ธฐ - ์: Calm and mysterious], [Lighting: ์กฐ๋ช
- ์: Soft rim lighting], (Atmospheric depth).
|
| 252 |
+
"""
|
| 253 |
+
|
| 254 |
+
try:
|
| 255 |
+
from app.gemini_client import GeminiClient
|
| 256 |
+
|
| 257 |
+
if model_name.startswith('gemini:'):
|
| 258 |
+
actual_model = model_name.split(':', 1)[1]
|
| 259 |
+
client = GeminiClient()
|
| 260 |
+
response = client.generate_response(prompt, model_name=actual_model)
|
| 261 |
+
analysis_result = response.get('response') if isinstance(response, dict) else (response.text if hasattr(response, 'text') else str(response))
|
| 262 |
+
else:
|
| 263 |
+
# Ollama API ํธ์ถ
|
| 264 |
+
ollama_response = requests.post(
|
| 265 |
+
f'{Config.OLLAMA_BASE_URL}/api/generate',
|
| 266 |
+
json={
|
| 267 |
+
'model': model_name,
|
| 268 |
+
'prompt': prompt,
|
| 269 |
+
'stream': False
|
| 270 |
+
},
|
| 271 |
+
timeout=60
|
| 272 |
+
)
|
| 273 |
+
if ollama_response.status_code == 200:
|
| 274 |
+
analysis_result = ollama_response.json().get('response', '')
|
| 275 |
+
else:
|
| 276 |
+
return jsonify({"success": False, "error": f"Ollama API ์ค๋ฅ: {ollama_response.status_code}"})
|
| 277 |
+
|
| 278 |
+
return jsonify({"success": True, "analysis": analysis_result})
|
| 279 |
+
except Exception as e:
|
| 280 |
+
current_app.logger.error(f"Character analysis error: {str(e)}")
|
| 281 |
+
return jsonify({"success": False, "error": str(e)})
|
| 282 |
+
|
| 283 |
@creation_bp.route('/webnovel', methods=['GET'])
|
| 284 |
@login_required
|
| 285 |
def webnovel():
|
|
|
|
| 339 |
@creation_bp.route('/api/generate-image', methods=['POST'])
|
| 340 |
@login_required
|
| 341 |
def api_generate_image():
|
| 342 |
+
"""ํ
์คํธ ํ๋กฌํํธ์ ์ฐธ์กฐ ์ด๋ฏธ์ง(์ฝํฐ, ์บ๋ฆญํฐ ๋ฑ)๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ด๋ฏธ์ง ์์ฑ"""
|
| 343 |
data = request.json
|
| 344 |
prompt = data.get('prompt')
|
| 345 |
model_name = data.get('model_name')
|
| 346 |
+
image_url = data.get('image_url') # ๋จ์ผ ์ด๋ฏธ์ง URL (ํ์ ํธํ์ฑ)
|
| 347 |
+
image_urls = data.get('image_urls', []) # ๋ค์ค ์ด๋ฏธ์ง URL ๋ฆฌ์คํธ
|
| 348 |
+
|
| 349 |
+
# image_url์ด ์๊ณ image_urls๊ฐ ๋น์ด์์ผ๋ฉด ํฉ์นจ
|
| 350 |
+
if image_url and not image_urls:
|
| 351 |
+
image_urls = [image_url]
|
| 352 |
|
| 353 |
if not prompt:
|
| 354 |
return jsonify({'error': 'ํ๋กฌํํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.'}), 400
|
|
|
|
| 360 |
|
| 361 |
async def generate():
|
| 362 |
# gemini_client๋ฅผ ํตํด ์ง์ ์ด๋ฏธ์ง ์์ฑ ์์ฒญ
|
|
|
|
| 363 |
target_model = model_name.replace('gemini:', '')
|
| 364 |
|
| 365 |
+
# ์์คํ
์ง์นจ ๊ตฌ์ฑ
|
| 366 |
+
system_instruction = (
|
| 367 |
+
"You are a professional webtoon artist specialized in clean line art. "
|
| 368 |
+
"Please generate a high-quality Korean webtoon style line art based on the provided references. "
|
| 369 |
+
"If multiple images are provided, the first image is usually a sketch/storyboard (conty), "
|
| 370 |
+
"and the subsequent images are character or style references. "
|
| 371 |
+
"Maintain the composition of the sketch while applying the detailed designs and features from the reference images. "
|
| 372 |
+
"Output ONLY the generated image."
|
| 373 |
+
)
|
| 374 |
|
| 375 |
contents = [system_instruction]
|
| 376 |
|
| 377 |
+
# ๋ชจ๋ ์ฐธ์กฐ ์ด๋ฏธ์ง ๋ก๋ ๋ฐ ์ถ๊ฐ
|
| 378 |
+
if image_urls:
|
| 379 |
+
for url in image_urls:
|
| 380 |
+
try:
|
| 381 |
+
# /uploads/ ์ ๋์ด ์ ๊ฑฐํ์ฌ ์๋ ๊ฒฝ๋ก ํ๋
|
| 382 |
+
rel_path = url.replace('/uploads/', '')
|
| 383 |
+
img_obj = await service.load_image(rel_path, max_size=1024)
|
| 384 |
+
if img_obj:
|
| 385 |
+
contents.append(img_obj)
|
| 386 |
+
current_app.logger.info(f"Reference image attached: {rel_path}")
|
| 387 |
+
except Exception as e:
|
| 388 |
+
current_app.logger.error(f"Failed to load image {url}: {e}")
|
| 389 |
|
| 390 |
contents.append(f"Prompt: {prompt}")
|
| 391 |
|
app/routers/webtoon_conti.py
CHANGED
|
@@ -10,6 +10,30 @@ import logging
|
|
| 10 |
logger = logging.getLogger(__name__)
|
| 11 |
webtoon_conti_bp = Blueprint('webtoon_conti', __name__, url_prefix='/webtoon-conti')
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
# ์ง์ฐ ๋ก๋ฉ์ ์ํด ์ ์ญ ๋ณ์๋ ์ ์งํ๋ ์ด๊ธฐํ๋ ํจ์ ๋ด๋ถ์์ ์งํ
|
| 14 |
_conti_service = None
|
| 15 |
_visual_service = None
|
|
@@ -106,7 +130,9 @@ def create_project():
|
|
| 106 |
file_id=file_id,
|
| 107 |
episode_id=episode_id,
|
| 108 |
title=title,
|
| 109 |
-
description=description
|
|
|
|
|
|
|
| 110 |
)
|
| 111 |
db.session.add(project)
|
| 112 |
db.session.commit()
|
|
@@ -129,9 +155,13 @@ def extract_visuals():
|
|
| 129 |
data = request.get_json()
|
| 130 |
text = data.get('text')
|
| 131 |
file_id = data.get('file_id')
|
|
|
|
|
|
|
| 132 |
project_id = data.get('project_id')
|
| 133 |
analysis_model = data.get('analysis_model', 'gemini-1.5-flash')
|
| 134 |
gen_model = data.get('gen_model')
|
|
|
|
|
|
|
| 135 |
|
| 136 |
if not text:
|
| 137 |
return jsonify({"error": "๋ถ์ํ ํ
์คํธ๊ฐ ์์ต๋๋ค."}), 400
|
|
@@ -140,12 +170,62 @@ def extract_visuals():
|
|
| 140 |
return jsonify({"error": "ํ๋ก์ ํธ ID๊ฐ ํ์ํฉ๋๋ค."}), 400
|
| 141 |
|
| 142 |
async def _process():
|
| 143 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
service = get_conti_service()
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
created_panels = []
|
| 148 |
-
#
|
| 149 |
image_tasks = []
|
| 150 |
|
| 151 |
for data in panels_data:
|
|
@@ -161,7 +241,11 @@ def extract_visuals():
|
|
| 161 |
|
| 162 |
# ์์ฑ ๋ชจ๋ธ์ด ์ง์ ๋์ด ์๊ณ Gemini/Imagen ๊ณ์ด์ธ ๊ฒฝ์ฐ ์ง์ ์ ์ ์์ฒญ
|
| 163 |
if gen_model and ('imagen' in gen_model.lower() or 'gemini' in gen_model.lower()):
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
image_tasks.append((panel, service.generate_image_directly(direct_prompt, gen_model)))
|
| 166 |
|
| 167 |
created_panels.append(panel)
|
|
@@ -202,6 +286,7 @@ def generate_samples():
|
|
| 202 |
panel_id = data.get('panel_id')
|
| 203 |
sample_count = data.get('sample_count', 3)
|
| 204 |
model = data.get('model') # ํ๋ก ํธ์๋์์ ๋ชจ๋ธ์ ๋ณด๋ผ ๊ฒฝ์ฐ ๋๋น
|
|
|
|
| 205 |
|
| 206 |
panel = ContiPanel.query.get_or_404(panel_id)
|
| 207 |
project = panel.conti
|
|
@@ -216,14 +301,19 @@ def generate_samples():
|
|
| 216 |
elif project.file: # ํ๋ก์ ํธ์ ์ฐ๊ฒฐ๋ ํ์ผ์ ๋ชจ๋ธ์ด ์์ผ๋ฉด ์ฌ์ฉ
|
| 217 |
visual_service.update_model(project.file.model_name or 'gemini-1.5-flash')
|
| 218 |
|
| 219 |
-
# 2. ํจ๋
|
| 220 |
-
|
|
|
|
| 221 |
|
| 222 |
# ์ ํ๋ ๋ชจ๋ธ์ด Gemini/Imagen์ด๋ฉด ์ง์ ์์ฑ
|
| 223 |
if model and ('imagen' in model.lower() or 'gemini' in model.lower()):
|
| 224 |
logger.info(f"Using direct AI generation for samples using {model}")
|
| 225 |
prompt_text = visual_service.generate_combined_prompt(visual_info)
|
| 226 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
# ๋ณ๋ ฌ ์ด๋ฏธ์ง ์์ฑ
|
| 228 |
coros = [service.generate_image_directly(prompt_text, model) for _ in range(sample_count)]
|
| 229 |
image_results = await asyncio.gather(*coros)
|
|
@@ -249,7 +339,8 @@ def generate_samples():
|
|
| 249 |
result = await service.generate_multi_samples(
|
| 250 |
visual_info,
|
| 251 |
project.file_id,
|
| 252 |
-
sample_count=sample_count
|
|
|
|
| 253 |
)
|
| 254 |
return result
|
| 255 |
|
|
@@ -333,6 +424,21 @@ def select_final():
|
|
| 333 |
"image_path": panel.image_path
|
| 334 |
})
|
| 335 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
@webtoon_conti_bp.route('/api/panels', methods=['POST'])
|
| 337 |
@login_required
|
| 338 |
def create_panel():
|
|
|
|
| 10 |
logger = logging.getLogger(__name__)
|
| 11 |
webtoon_conti_bp = Blueprint('webtoon_conti', __name__, url_prefix='/webtoon-conti')
|
| 12 |
|
| 13 |
+
@webtoon_conti_bp.route('/api/global-prompts', methods=['GET'])
|
| 14 |
+
@login_required
|
| 15 |
+
def get_global_prompts():
|
| 16 |
+
"""์ ์ญ(๊ธฐ๋ณธ) ํ๋กฌํํธ ์ค์ ์กฐํ"""
|
| 17 |
+
from app.database import SystemConfig
|
| 18 |
+
return jsonify({
|
| 19 |
+
"analysis_prompt": SystemConfig.get_config('webtoon_conti_default_analysis_prompt', ''),
|
| 20 |
+
"gen_prompt": SystemConfig.get_config('webtoon_conti_default_gen_prompt', '')
|
| 21 |
+
})
|
| 22 |
+
|
| 23 |
+
@webtoon_conti_bp.route('/api/global-prompts', methods=['POST'])
|
| 24 |
+
@login_required
|
| 25 |
+
def save_global_prompts():
|
| 26 |
+
"""์ ์ญ(๊ธฐ๋ณธ) ํ๋กฌํํธ ์ค์ ์ ์ฅ"""
|
| 27 |
+
data = request.get_json()
|
| 28 |
+
from app.database import SystemConfig
|
| 29 |
+
|
| 30 |
+
if 'analysis_prompt' in data:
|
| 31 |
+
SystemConfig.set_config('webtoon_conti_default_analysis_prompt', data.get('analysis_prompt'))
|
| 32 |
+
if 'gen_prompt' in data:
|
| 33 |
+
SystemConfig.set_config('webtoon_conti_default_gen_prompt', data.get('gen_prompt'))
|
| 34 |
+
|
| 35 |
+
return jsonify({"success": True, "message": "๊ธฐ๋ณธ ํ๋กฌํํธ๋ก ์ ์ฅ๋์์ต๋๋ค."})
|
| 36 |
+
|
| 37 |
# ์ง์ฐ ๋ก๋ฉ์ ์ํด ์ ์ญ ๋ณ์๋ ์ ์งํ๋ ์ด๊ธฐํ๋ ํจ์ ๋ด๋ถ์์ ์งํ
|
| 38 |
_conti_service = None
|
| 39 |
_visual_service = None
|
|
|
|
| 130 |
file_id=file_id,
|
| 131 |
episode_id=episode_id,
|
| 132 |
title=title,
|
| 133 |
+
description=description,
|
| 134 |
+
analysis_prompt=data.get('analysis_prompt'),
|
| 135 |
+
gen_prompt=data.get('gen_prompt')
|
| 136 |
)
|
| 137 |
db.session.add(project)
|
| 138 |
db.session.commit()
|
|
|
|
| 155 |
data = request.get_json()
|
| 156 |
text = data.get('text')
|
| 157 |
file_id = data.get('file_id')
|
| 158 |
+
episode_ids = data.get('episode_ids', []) # ๋ค์ค ํ์ฐจ ID
|
| 159 |
+
graph_episode_titles = data.get('graph_episode_titles', []) # GraphRAG ํ์ฐจ ์ ๋ชฉ๋ค
|
| 160 |
project_id = data.get('project_id')
|
| 161 |
analysis_model = data.get('analysis_model', 'gemini-1.5-flash')
|
| 162 |
gen_model = data.get('gen_model')
|
| 163 |
+
analysis_prompt = data.get('analysis_prompt') # ์ถ๊ฐ ๋ถ์ ์ง์นจ
|
| 164 |
+
gen_prompt = data.get('gen_prompt') # ์ถ๊ฐ ์์ฑ ์ง์นจ
|
| 165 |
|
| 166 |
if not text:
|
| 167 |
return jsonify({"error": "๋ถ์ํ ํ
์คํธ๊ฐ ์์ต๋๋ค."}), 400
|
|
|
|
| 170 |
return jsonify({"error": "ํ๋ก์ ํธ ID๊ฐ ํ์ํฉ๋๋ค."}), 400
|
| 171 |
|
| 172 |
async def _process():
|
| 173 |
+
# 0. ํ๋ก์ ํธ ํ๋กฌํํธ ์ ๋ณด ์
๋ฐ์ดํธ (์ ์ฅ ๊ด๋ฆฌ์ฉ)
|
| 174 |
+
project = WebtoonConti.query.get(project_id)
|
| 175 |
+
if project:
|
| 176 |
+
if analysis_prompt is not None:
|
| 177 |
+
project.analysis_prompt = analysis_prompt
|
| 178 |
+
if gen_prompt is not None:
|
| 179 |
+
project.gen_prompt = gen_prompt
|
| 180 |
+
db.session.commit()
|
| 181 |
+
logger.info(f"Updated prompts for project {project_id}")
|
| 182 |
+
|
| 183 |
+
# 1. ์ถ๊ฐ ์ปจํ
์คํธ ์์ง (ParentChunk + ์ ํ๋ EpisodeAnalysis + GraphRAG)
|
| 184 |
service = get_conti_service()
|
| 185 |
+
context_parts = []
|
| 186 |
+
|
| 187 |
+
if file_id and file_id != 0:
|
| 188 |
+
# ParentChunk (์ธ๊ณ๊ด, ์บ๋ฆญํฐ ์ค์ ๋ฑ) - ๋ฌด์กฐ๊ฑด ์ฐธ์กฐ
|
| 189 |
+
from app.database import ParentChunk
|
| 190 |
+
parent_chunk = ParentChunk.query.filter_by(file_id=file_id).first()
|
| 191 |
+
if parent_chunk:
|
| 192 |
+
context_parts.append(f"### ์ธ๊ณ๊ด ๋ฐ ์บ๋ฆญํฐ ๊ธฐ๋ณธ ์ค์ (Parent Chunk)\n{parent_chunk.world_view or ''}\n{parent_chunk.characters or ''}")
|
| 193 |
+
|
| 194 |
+
# ์ ํ๋ ํ์ฐจ๋ค์ ๋ถ์ ๋ด์ฉ (EpisodeAnalysis)
|
| 195 |
+
if episode_ids:
|
| 196 |
+
from app.database import EpisodeAnalysis
|
| 197 |
+
episodes = EpisodeAnalysis.query.filter(EpisodeAnalysis.id.in_(episode_ids)).all()
|
| 198 |
+
for ep in episodes:
|
| 199 |
+
context_parts.append(f"### ํ์ฐจ ์์ฝ: {ep.episode_title}\n{ep.analysis_content}")
|
| 200 |
+
|
| 201 |
+
# ์ ํ๋ ํ์ฐจ๋ค์ GraphRAG ์ ๋ณด
|
| 202 |
+
if graph_episode_titles:
|
| 203 |
+
from app.database import GraphEntity, GraphEvent, GraphRelationship
|
| 204 |
+
for title in graph_episode_titles:
|
| 205 |
+
entities = GraphEntity.query.filter_by(file_id=file_id, episode_title=title).all()
|
| 206 |
+
events = GraphEvent.query.filter_by(file_id=file_id, episode_title=title).all()
|
| 207 |
+
|
| 208 |
+
if entities or events:
|
| 209 |
+
graph_info = f"### GraphRAG ์ฐธ์กฐ ({title})\n"
|
| 210 |
+
if entities:
|
| 211 |
+
graph_info += "์ธ๋ฌผ/์ฅ์: " + ", ".join([f"{e.entity_name}({e.entity_type})" for e in entities]) + "\n"
|
| 212 |
+
if events:
|
| 213 |
+
graph_info += "์ฃผ์ ์ฌ๊ฑด: " + ", ".join([e.event_name for e in events]) + "\n"
|
| 214 |
+
context_parts.append(graph_info)
|
| 215 |
+
|
| 216 |
+
context_text = "\n\n".join(context_parts)
|
| 217 |
+
|
| 218 |
+
# 2. ํ
์คํธ๋ฅผ ์ปท๋ณ๋ก ๋ถํ (๋ถ์ ์ ์ฉ ๋ชจ๋ธ ์ฌ์ฉ)
|
| 219 |
+
panels_data = await service.divide_into_panels(
|
| 220 |
+
text,
|
| 221 |
+
int(file_id) if file_id else 0,
|
| 222 |
+
model=analysis_model,
|
| 223 |
+
context_text=context_text,
|
| 224 |
+
custom_prompt=analysis_prompt
|
| 225 |
+
)
|
| 226 |
|
| 227 |
created_panels = []
|
| 228 |
+
# 3. ๊ฐ ๋ถํ ๋ ๋ฐ์ดํฐ๋ฅผ ContiPanel๋ก ์ ์ฅ ๋ฐ ์ด๋ฏธ์ง ์ง์ ์์ฑ ์๋ (์์ฑ ์ ์ฉ ๋ชจ๋ธ ์ฌ์ฉ)
|
| 229 |
image_tasks = []
|
| 230 |
|
| 231 |
for data in panels_data:
|
|
|
|
| 241 |
|
| 242 |
# ์์ฑ ๋ชจ๋ธ์ด ์ง์ ๋์ด ์๊ณ Gemini/Imagen ๊ณ์ด์ธ ๊ฒฝ์ฐ ์ง์ ์ ์ ์์ฒญ
|
| 243 |
if gen_model and ('imagen' in gen_model.lower() or 'gemini' in gen_model.lower()):
|
| 244 |
+
# ์ปค์คํ
์์ฑ ํ๋กฌํํธ๊ฐ ์์ผ๋ฉด ๋ฐ์
|
| 245 |
+
direct_prompt = f"Scene: {panel.description}. Dialogue/Action: {panel.dialogue or ''}. Composition: {panel.composition}. Webtoon style."
|
| 246 |
+
if gen_prompt:
|
| 247 |
+
direct_prompt = f"{direct_prompt} Style Guidelines: {gen_prompt}"
|
| 248 |
+
|
| 249 |
image_tasks.append((panel, service.generate_image_directly(direct_prompt, gen_model)))
|
| 250 |
|
| 251 |
created_panels.append(panel)
|
|
|
|
| 286 |
panel_id = data.get('panel_id')
|
| 287 |
sample_count = data.get('sample_count', 3)
|
| 288 |
model = data.get('model') # ํ๋ก ํธ์๋์์ ๋ชจ๋ธ์ ๋ณด๋ผ ๊ฒฝ์ฐ ๋๋น
|
| 289 |
+
gen_prompt = data.get('gen_prompt') # ์ถ๊ฐ ์คํ์ผ ์ง์นจ
|
| 290 |
|
| 291 |
panel = ContiPanel.query.get_or_404(panel_id)
|
| 292 |
project = panel.conti
|
|
|
|
| 301 |
elif project.file: # ํ๋ก์ ํธ์ ์ฐ๊ฒฐ๋ ํ์ผ์ ๋ชจ๋ธ์ด ์์ผ๋ฉด ์ฌ์ฉ
|
| 302 |
visual_service.update_model(project.file.model_name or 'gemini-1.5-flash')
|
| 303 |
|
| 304 |
+
# 2. ํจ๋ ๋ฌ์ฌ์ ๋์ฌ/์ง๋ฌธ์ ํจ๊ป ๋ฐํ์ผ๋ก ์์ธ ์๊ฐ ์์ ์ถ์ถ
|
| 305 |
+
combined_text = f"Visual Description: {panel.description}\nDialogue/Narration: {panel.dialogue or ''}"
|
| 306 |
+
visual_info = await visual_service.extract_visuals(combined_text, project.file_id)
|
| 307 |
|
| 308 |
# ์ ํ๋ ๋ชจ๋ธ์ด Gemini/Imagen์ด๋ฉด ์ง์ ์์ฑ
|
| 309 |
if model and ('imagen' in model.lower() or 'gemini' in model.lower()):
|
| 310 |
logger.info(f"Using direct AI generation for samples using {model}")
|
| 311 |
prompt_text = visual_service.generate_combined_prompt(visual_info)
|
| 312 |
|
| 313 |
+
# ์ปค์คํ
์์ฑ ํ๋กฌํํธ๊ฐ ์์ผ๋ฉด ๋ฐ์
|
| 314 |
+
if gen_prompt:
|
| 315 |
+
prompt_text = f"{prompt_text} Style Guidelines: {gen_prompt}"
|
| 316 |
+
|
| 317 |
# ๋ณ๋ ฌ ์ด๋ฏธ์ง ์์ฑ
|
| 318 |
coros = [service.generate_image_directly(prompt_text, model) for _ in range(sample_count)]
|
| 319 |
image_results = await asyncio.gather(*coros)
|
|
|
|
| 339 |
result = await service.generate_multi_samples(
|
| 340 |
visual_info,
|
| 341 |
project.file_id,
|
| 342 |
+
sample_count=sample_count,
|
| 343 |
+
custom_prompt=gen_prompt # ์ปค์คํ
ํ๋กฌํํธ ์ถ๊ฐ
|
| 344 |
)
|
| 345 |
return result
|
| 346 |
|
|
|
|
| 424 |
"image_path": panel.image_path
|
| 425 |
})
|
| 426 |
|
| 427 |
+
@webtoon_conti_bp.route('/api/projects/<int:project_id>/prompts', methods=['PUT'])
|
| 428 |
+
@login_required
|
| 429 |
+
def update_project_prompts(project_id):
|
| 430 |
+
"""ํ๋ก์ ํธ์ ํ๋กฌํํธ ์ค์ ์
๋ฐ์ดํธ"""
|
| 431 |
+
data = request.get_json()
|
| 432 |
+
project = WebtoonConti.query.get_or_404(project_id)
|
| 433 |
+
|
| 434 |
+
if 'analysis_prompt' in data:
|
| 435 |
+
project.analysis_prompt = data.get('analysis_prompt')
|
| 436 |
+
if 'gen_prompt' in data:
|
| 437 |
+
project.gen_prompt = data.get('gen_prompt')
|
| 438 |
+
|
| 439 |
+
db.session.commit()
|
| 440 |
+
return jsonify({"success": True, "message": "ํ๋กฌํํธ๊ฐ ์ ์ฅ๋์์ต๋๋ค."})
|
| 441 |
+
|
| 442 |
@webtoon_conti_bp.route('/api/panels', methods=['POST'])
|
| 443 |
@login_required
|
| 444 |
def create_panel():
|
app/routes.py
CHANGED
|
@@ -54,6 +54,8 @@ def get_default_admin_menu():
|
|
| 54 |
"roles": ["admin", "webtoon_pm", "webtoon_admin", "producer", "user"],
|
| 55 |
"items": [
|
| 56 |
{"label": "์น์์ค", "endpoint": "creation.webnovel"},
|
|
|
|
|
|
|
| 57 |
{"label": "์นํฐ ์ฝํฐ", "endpoint": "webtoon_conti.dashboard"},
|
| 58 |
{"label": "์นํฐ ์ ํ", "endpoint": "main.webtoon_line_art_dashboard"},
|
| 59 |
{"label": "์ด๋ฏธ์ง ์์ฑ ํ
์คํธ", "endpoint": "creation.image_test"},
|
|
@@ -302,6 +304,8 @@ def get_user_menu():
|
|
| 302 |
"roles": ["user"],
|
| 303 |
"items": [
|
| 304 |
{"label": "์น์์ค", "endpoint": "creation.webnovel"},
|
|
|
|
|
|
|
| 305 |
],
|
| 306 |
},
|
| 307 |
{
|
|
|
|
| 54 |
"roles": ["admin", "webtoon_pm", "webtoon_admin", "producer", "user"],
|
| 55 |
"items": [
|
| 56 |
{"label": "์น์์ค", "endpoint": "creation.webnovel"},
|
| 57 |
+
{"label": "์บ๋ฆญํฐ ์ ์", "endpoint": "creation.character_creation"},
|
| 58 |
+
{"label": "์นํฐ ์ ํ ๋์ ", "endpoint": "creation.webtoon_line_art_challenge"},
|
| 59 |
{"label": "์นํฐ ์ฝํฐ", "endpoint": "webtoon_conti.dashboard"},
|
| 60 |
{"label": "์นํฐ ์ ํ", "endpoint": "main.webtoon_line_art_dashboard"},
|
| 61 |
{"label": "์ด๋ฏธ์ง ์์ฑ ํ
์คํธ", "endpoint": "creation.image_test"},
|
|
|
|
| 304 |
"roles": ["user"],
|
| 305 |
"items": [
|
| 306 |
{"label": "์น์์ค", "endpoint": "creation.webnovel"},
|
| 307 |
+
{"label": "์บ๋ฆญํฐ ์ ์", "endpoint": "creation.character_creation"},
|
| 308 |
+
{"label": "์นํฐ ์ ํ ๋์ ", "endpoint": "creation.webtoon_line_art_challenge"},
|
| 309 |
],
|
| 310 |
},
|
| 311 |
{
|
app/services/webtoon_conti_service.py
CHANGED
|
@@ -74,7 +74,7 @@ class WebtoonContiService:
|
|
| 74 |
logger.error(f"Direct image generation failed: {e}")
|
| 75 |
return None
|
| 76 |
|
| 77 |
-
async def divide_into_panels(self, text: str, file_id: int, model: str = 'gemini-1.5-flash'):
|
| 78 |
"""
|
| 79 |
์น์์ค ํ
์คํธ๋ฅผ ๋ถ์ํ์ฌ ์นํฐ ์ฝํฐ ํจ๋(์ปท)๋ค๋ก ๋ถํ ํฉ๋๋ค.
|
| 80 |
"""
|
|
@@ -89,8 +89,15 @@ class WebtoonContiService:
|
|
| 89 |
|
| 90 |
# 2. ํ๋กฌํํธ ์์ฑ
|
| 91 |
prompt = get_webtoon_conti_prompt(text, char_info, bg_info)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
-
#
|
| 94 |
response_text = ""
|
| 95 |
if 'gemini' in model.lower():
|
| 96 |
client = get_gemini_client()
|
|
@@ -146,43 +153,51 @@ class WebtoonContiService:
|
|
| 146 |
resp = resp.text
|
| 147 |
else:
|
| 148 |
resp = str(resp)
|
| 149 |
-
|
| 150 |
-
# 1. ๋งํฌ๋ค์ด ์ฝ๋ ๋ธ๋ก ์ ๊ฑฐ
|
| 151 |
resp = re.sub(r'```json\s*', '', resp)
|
|
|
|
| 152 |
resp = re.sub(r'```\s*', '', resp)
|
| 153 |
|
| 154 |
-
# 2.
|
| 155 |
-
|
| 156 |
|
| 157 |
# 3. ๋ฆฌ์คํธ([]) ๋๋ ๊ฐ์ฒด({})์ ์์๊ณผ ๋์ ์ฐพ์ ์ถ์ถ
|
|
|
|
| 158 |
start_list = resp.find('[')
|
| 159 |
end_list = resp.rfind(']')
|
| 160 |
|
| 161 |
start_obj = resp.find('{')
|
| 162 |
end_obj = resp.rfind('}')
|
| 163 |
|
| 164 |
-
|
| 165 |
-
if start_list != -1 and end_list != -1 and end_list > start_list:
|
| 166 |
return resp[start_list:end_list+1]
|
| 167 |
-
elif start_obj != -1 and end_obj != -1
|
| 168 |
return resp[start_obj:end_obj+1]
|
| 169 |
|
| 170 |
-
return resp
|
| 171 |
|
| 172 |
try:
|
| 173 |
cleaned_response = clean_json_response(response_text)
|
| 174 |
try:
|
|
|
|
| 175 |
panels_data = json.loads(cleaned_response)
|
| 176 |
except json.JSONDecodeError as e:
|
| 177 |
-
#
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
try:
|
|
|
|
| 183 |
panels_data = json.loads(cleaned_response.strip() + ']')
|
| 184 |
except:
|
| 185 |
-
# ๋ ๋ณต์กํ ๋ณต๊ตฌ๋ ์๋ตํ๊ณ ์๋ ์๋ฌ ๋ฐ์ ์ํด
|
| 186 |
raise e
|
| 187 |
else:
|
| 188 |
raise e
|
|
@@ -274,7 +289,8 @@ class WebtoonContiService:
|
|
| 274 |
visual_info: VisualExtractionOutput,
|
| 275 |
file_id: int,
|
| 276 |
workflow_name: str = "default_webtoon_conti",
|
| 277 |
-
sample_count: int = 3
|
|
|
|
| 278 |
):
|
| 279 |
"""
|
| 280 |
๋์ผํ ์๊ฐ ๋ฌ์ฌ๋ฅผ ๋ฐํ์ผ๋ก ์๋ก ๋ค๋ฅธ ์๋๊ฐ์ ์ฌ์ฉํ์ฌ ์ฌ๋ฌ ์ํ ์ด๋ฏธ์ง๋ฅผ ์์ฑํฉ๋๋ค.
|
|
@@ -284,6 +300,7 @@ class WebtoonContiService:
|
|
| 284 |
file_id: ์บ๋ฆญํฐ ๊ฒ์์ฉ ํ์ผ ID
|
| 285 |
workflow_name: ์ฌ์ฉํ ComfyUI ์ํฌํ๋ก์ฐ ์ด๋ฆ
|
| 286 |
sample_count: ์์ฑํ ์ํ ์ (๊ธฐ๋ณธ 3๊ฐ)
|
|
|
|
| 287 |
"""
|
| 288 |
# 1. ์บ๋ฆญํฐ ์ฐธ์กฐ ์ด๋ฏธ์ง ์กฐํ
|
| 289 |
character_image_path = None
|
|
@@ -305,6 +322,10 @@ class WebtoonContiService:
|
|
| 305 |
|
| 306 |
# 3. ์์ด ํ๋กฌํํธ ์์ฑ
|
| 307 |
prompt_text = self.visual_service.generate_combined_prompt(visual_info)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
|
| 309 |
samples = []
|
| 310 |
# 4. ์ฌ๋ฌ ์๋๋ก ์ด๋ฏธ์ง ์์ฑ ์์ฒญ
|
|
|
|
| 74 |
logger.error(f"Direct image generation failed: {e}")
|
| 75 |
return None
|
| 76 |
|
| 77 |
+
async def divide_into_panels(self, text: str, file_id: int, model: str = 'gemini-1.5-flash', context_text: str = "", custom_prompt: str = ""):
|
| 78 |
"""
|
| 79 |
์น์์ค ํ
์คํธ๋ฅผ ๋ถ์ํ์ฌ ์นํฐ ์ฝํฐ ํจ๋(์ปท)๋ค๋ก ๋ถํ ํฉ๋๋ค.
|
| 80 |
"""
|
|
|
|
| 89 |
|
| 90 |
# 2. ํ๋กฌํํธ ์์ฑ
|
| 91 |
prompt = get_webtoon_conti_prompt(text, char_info, bg_info)
|
| 92 |
+
|
| 93 |
+
# 3. ์ถ๊ฐ ์ปจํ
์คํธ ๋ฐ ์ปค์คํ
ํ๋กฌํํธ ๋ฐ์
|
| 94 |
+
if context_text:
|
| 95 |
+
prompt = f"### ์ถ๊ฐ ์ฐธ์กฐ ์ ๋ณด\n{context_text}\n\n{prompt}"
|
| 96 |
+
|
| 97 |
+
if custom_prompt:
|
| 98 |
+
prompt = f"{prompt}\n\n### ์ฌ์ฉ์ ์ถ๊ฐ ์ง์นจ\n{custom_prompt}"
|
| 99 |
|
| 100 |
+
# 4. LLM ํธ์ถ (์ ํ๋ ๋ชจ๋ธ ์ฌ์ฉ)
|
| 101 |
response_text = ""
|
| 102 |
if 'gemini' in model.lower():
|
| 103 |
client = get_gemini_client()
|
|
|
|
| 153 |
resp = resp.text
|
| 154 |
else:
|
| 155 |
resp = str(resp)
|
| 156 |
+
|
| 157 |
+
# 1. ๋งํฌ๋ค์ด ์ฝ๋ ๋ธ๋ก ์ ๊ฑฐ (๋ค์ํ ํ์ ๋์)
|
| 158 |
resp = re.sub(r'```json\s*', '', resp)
|
| 159 |
+
resp = re.sub(r'```JSON\s*', '', resp)
|
| 160 |
resp = re.sub(r'```\s*', '', resp)
|
| 161 |
|
| 162 |
+
# 2. ์๋ค ๊ณต๋ฐฑ ์ ๊ฑฐ
|
| 163 |
+
resp = resp.strip()
|
| 164 |
|
| 165 |
# 3. ๋ฆฌ์คํธ([]) ๋๋ ๊ฐ์ฒด({})์ ์์๊ณผ ๋์ ์ฐพ์ ์ถ์ถ
|
| 166 |
+
# ์ฌ๋ฌ ๊ฐ์ JSON ๋ธ๋ก์ด ์์ ๊ฒฝ์ฐ ์ฒซ ๋ฒ์งธ ๋ธ๋ก์ ์ฐ์ ์ ์ผ๋ก ์๋
|
| 167 |
start_list = resp.find('[')
|
| 168 |
end_list = resp.rfind(']')
|
| 169 |
|
| 170 |
start_obj = resp.find('{')
|
| 171 |
end_obj = resp.rfind('}')
|
| 172 |
|
| 173 |
+
if start_list != -1 and end_list != -1 and (start_obj == -1 or start_list < start_obj):
|
|
|
|
| 174 |
return resp[start_list:end_list+1]
|
| 175 |
+
elif start_obj != -1 and end_obj != -1:
|
| 176 |
return resp[start_obj:end_obj+1]
|
| 177 |
|
| 178 |
+
return resp
|
| 179 |
|
| 180 |
try:
|
| 181 |
cleaned_response = clean_json_response(response_text)
|
| 182 |
try:
|
| 183 |
+
# 1์ฐจ ์๋: ์ ์ฒด ํ์ฑ
|
| 184 |
panels_data = json.loads(cleaned_response)
|
| 185 |
except json.JSONDecodeError as e:
|
| 186 |
+
# 2์ฐจ ์๋: Extra data ์๋ฌ์ธ ๊ฒฝ์ฐ (์ ํจํ JSON ๋ค์ ๊ฐ๋น์ง๊ฐ ์๋ ๊ฒฝ์ฐ)
|
| 187 |
+
if "Extra data" in str(e):
|
| 188 |
+
try:
|
| 189 |
+
logger.warning(f"JSON has extra data, attempting raw_decode. Error: {e}")
|
| 190 |
+
decoder = json.JSONDecoder()
|
| 191 |
+
panels_data, index = decoder.raw_decode(cleaned_response)
|
| 192 |
+
except Exception as e2:
|
| 193 |
+
logger.error(f"raw_decode failed: {e2}")
|
| 194 |
+
raise e
|
| 195 |
+
# 3์ฐจ ์๋: ์๋ฆฐ JSON ๋ณต๊ตฌ ์๋
|
| 196 |
+
elif '[' in cleaned_response and not cleaned_response.strip().endswith(']'):
|
| 197 |
try:
|
| 198 |
+
logger.warning(f"Incomplete JSON list detected, attempting recovery. Error: {e}")
|
| 199 |
panels_data = json.loads(cleaned_response.strip() + ']')
|
| 200 |
except:
|
|
|
|
| 201 |
raise e
|
| 202 |
else:
|
| 203 |
raise e
|
|
|
|
| 289 |
visual_info: VisualExtractionOutput,
|
| 290 |
file_id: int,
|
| 291 |
workflow_name: str = "default_webtoon_conti",
|
| 292 |
+
sample_count: int = 3,
|
| 293 |
+
custom_prompt: str = ""
|
| 294 |
):
|
| 295 |
"""
|
| 296 |
๋์ผํ ์๊ฐ ๋ฌ์ฌ๋ฅผ ๋ฐํ์ผ๋ก ์๋ก ๋ค๋ฅธ ์๋๊ฐ์ ์ฌ์ฉํ์ฌ ์ฌ๋ฌ ์ํ ์ด๋ฏธ์ง๋ฅผ ์์ฑํฉ๋๋ค.
|
|
|
|
| 300 |
file_id: ์บ๋ฆญํฐ ๊ฒ์์ฉ ํ์ผ ID
|
| 301 |
workflow_name: ์ฌ์ฉํ ComfyUI ์ํฌํ๋ก์ฐ ์ด๋ฆ
|
| 302 |
sample_count: ์์ฑํ ์ํ ์ (๊ธฐ๋ณธ 3๊ฐ)
|
| 303 |
+
custom_prompt: ์ฌ์ฉ์ ์ถ๊ฐ ์คํ์ผ ์ง์นจ
|
| 304 |
"""
|
| 305 |
# 1. ์บ๋ฆญํฐ ์ฐธ์กฐ ์ด๋ฏธ์ง ์กฐํ
|
| 306 |
character_image_path = None
|
|
|
|
| 322 |
|
| 323 |
# 3. ์์ด ํ๋กฌํํธ ์์ฑ
|
| 324 |
prompt_text = self.visual_service.generate_combined_prompt(visual_info)
|
| 325 |
+
|
| 326 |
+
# ์ปค์คํ
ํ๋กฌํํธ(๊ทธ๋ฆผ ์ฝํฐ์ฉ ํ๋กฌํํธ) ๋ฐ์
|
| 327 |
+
if custom_prompt:
|
| 328 |
+
prompt_text = f"{prompt_text}, {custom_prompt}"
|
| 329 |
|
| 330 |
samples = []
|
| 331 |
# 4. ์ฌ๋ฌ ์๋๋ก ์ด๋ฏธ์ง ์์ฑ ์์ฒญ
|
force_update_menu.py
CHANGED
|
@@ -71,5 +71,6 @@ if __name__ == "__main__":
|
|
| 71 |
|
| 72 |
|
| 73 |
|
|
|
|
| 74 |
|
| 75 |
|
|
|
|
| 71 |
|
| 72 |
|
| 73 |
|
| 74 |
+
|
| 75 |
|
| 76 |
|
templates/admin_webtoon_milestone_producer_manager.html
CHANGED
|
@@ -365,5 +365,6 @@
|
|
| 365 |
|
| 366 |
|
| 367 |
|
|
|
|
| 368 |
|
| 369 |
|
|
|
|
| 365 |
|
| 366 |
|
| 367 |
|
| 368 |
+
|
| 369 |
|
| 370 |
|
templates/character_creation.html
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "creation_base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}์บ๋ฆญํฐ ์ ์ - SOYMEDIA{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block nav %}
|
| 6 |
+
{% set admin_nav_title = 'AI ์บ๋ฆญํฐ ์ ์' %}
|
| 7 |
+
{% set admin_nav_icon = '๐ค' %}
|
| 8 |
+
{% include '_admin_nav.html' %}
|
| 9 |
+
{% endblock %}
|
| 10 |
+
|
| 11 |
+
{% block extra_css %}
|
| 12 |
+
<style>
|
| 13 |
+
.character-card {
|
| 14 |
+
border: 1px solid var(--border);
|
| 15 |
+
border-radius: 12px;
|
| 16 |
+
background: white;
|
| 17 |
+
margin-bottom: 1.5rem;
|
| 18 |
+
overflow: hidden;
|
| 19 |
+
transition: all 0.3s;
|
| 20 |
+
}
|
| 21 |
+
.character-card:hover {
|
| 22 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
| 23 |
+
transform: translateY(-2px);
|
| 24 |
+
}
|
| 25 |
+
.character-header {
|
| 26 |
+
padding: 1rem 1.5rem;
|
| 27 |
+
background: #f8f9fa;
|
| 28 |
+
border-bottom: 1px solid var(--border);
|
| 29 |
+
display: flex;
|
| 30 |
+
justify-content: space-between;
|
| 31 |
+
align-items: center;
|
| 32 |
+
}
|
| 33 |
+
.character-body {
|
| 34 |
+
padding: 1.5rem;
|
| 35 |
+
}
|
| 36 |
+
.gen-input-column {
|
| 37 |
+
display: flex;
|
| 38 |
+
flex-direction: column;
|
| 39 |
+
}
|
| 40 |
+
#genPrompt {
|
| 41 |
+
flex-grow: 1;
|
| 42 |
+
min-height: 400px;
|
| 43 |
+
}
|
| 44 |
+
.result-image-container {
|
| 45 |
+
width: 100%;
|
| 46 |
+
max-width: 800px;
|
| 47 |
+
margin: 0 auto;
|
| 48 |
+
border-radius: 12px;
|
| 49 |
+
overflow: hidden;
|
| 50 |
+
background: #f0f0f0;
|
| 51 |
+
min-height: 400px;
|
| 52 |
+
display: flex;
|
| 53 |
+
align-items: center;
|
| 54 |
+
justify-content: center;
|
| 55 |
+
border: 1px solid var(--border);
|
| 56 |
+
box-shadow: var(--shadow);
|
| 57 |
+
}
|
| 58 |
+
.result-image {
|
| 59 |
+
max-width: 100%;
|
| 60 |
+
height: auto;
|
| 61 |
+
display: block;
|
| 62 |
+
}
|
| 63 |
+
.character-item {
|
| 64 |
+
display: flex;
|
| 65 |
+
align-items: center;
|
| 66 |
+
gap: 12px;
|
| 67 |
+
padding: 12px !important;
|
| 68 |
+
transition: background 0.2s;
|
| 69 |
+
}
|
| 70 |
+
.character-item:hover {
|
| 71 |
+
background-color: #f0f7ff;
|
| 72 |
+
}
|
| 73 |
+
.character-item-img {
|
| 74 |
+
width: 50px;
|
| 75 |
+
height: 50px;
|
| 76 |
+
border-radius: 8px;
|
| 77 |
+
object-fit: cover;
|
| 78 |
+
background: #eee;
|
| 79 |
+
flex-shrink: 0;
|
| 80 |
+
}
|
| 81 |
+
.character-item-info {
|
| 82 |
+
flex-grow: 1;
|
| 83 |
+
overflow: hidden;
|
| 84 |
+
}
|
| 85 |
+
.character-item-name {
|
| 86 |
+
font-weight: 600;
|
| 87 |
+
font-size: 14px;
|
| 88 |
+
color: var(--text-primary);
|
| 89 |
+
margin-bottom: 2px;
|
| 90 |
+
}
|
| 91 |
+
.character-item-desc {
|
| 92 |
+
font-size: 12px;
|
| 93 |
+
color: var(--text-secondary);
|
| 94 |
+
white-space: nowrap;
|
| 95 |
+
text-overflow: ellipsis;
|
| 96 |
+
overflow: hidden;
|
| 97 |
+
}
|
| 98 |
+
.character-img-gallery {
|
| 99 |
+
display: flex;
|
| 100 |
+
gap: 4px;
|
| 101 |
+
margin-top: 4px;
|
| 102 |
+
overflow-x: auto;
|
| 103 |
+
padding-bottom: 4px;
|
| 104 |
+
}
|
| 105 |
+
.character-img-gallery img {
|
| 106 |
+
width: 30px;
|
| 107 |
+
height: 30px;
|
| 108 |
+
border-radius: 4px;
|
| 109 |
+
object-fit: cover;
|
| 110 |
+
cursor: pointer;
|
| 111 |
+
border: 1px solid transparent;
|
| 112 |
+
}
|
| 113 |
+
.character-img-gallery img:hover {
|
| 114 |
+
border-color: var(--accent);
|
| 115 |
+
}
|
| 116 |
+
</style>
|
| 117 |
+
{% endblock %}
|
| 118 |
+
|
| 119 |
+
{% block content %}
|
| 120 |
+
<!-- ์๋จ ํค๋ ์์ญ -->
|
| 121 |
+
<div class="dashboard-header d-flex justify-content-between align-items-center mb-4">
|
| 122 |
+
<div>
|
| 123 |
+
<h1 class="title-main mb-1">AI ์บ๋ฆญํฐ ์ ์</h1>
|
| 124 |
+
<p class="text-meta mb-0">์์ ์ค์ ์ ๋ฐํ์ผ๋ก ์บ๋ฆญํฐ์ ๋น์ฃผ์ผ์ ์ค๊ณํ๊ณ ์ด๋ฏธ์ง๋ฅผ ์์ฑํฉ๋๋ค. <br />
|
| 125 |
+
* '์บ๋ฆญํฐ ์์ฑ ํ๋กฌํํธ'์ ์์ฃผ ์ฌ์ฉํ์๋ ๋ช
๋ น์ด๋ ์ค์ ์ ์ ์ฅํ์ค ์ ์์ต๋๋ค. ์ฐธ์กฐํ๋ ์ํ๋ค์ ๊ฒฝ์ฐ ์ ์ฅ๋ ์ ๋ณด์ ์ถ๊ฐ๋์ด ํ์๋ฉ๋๋ค. <br />
|
| 126 |
+
* ํ๋กฌํํธ๋ง ๊ฐ์ง๊ณ ์บ๋ฆญํฐ๋ฅผ ์์ฑํ ์ ์์ต๋๋ค. '์บ๋ฆญํฐ ์์ฑ ํ๋กฌํํธ'์ ์ํ๋ ํ๋กฌํํธ๋ฅผ ์
๋ ฅ ํ '์บ๋ฆญํฐ ์ด๋ฏธ์ง ์์ฑ ์์' ๋ฒํผ์ ๋๋ฌ์ฃผ์ธ์. '์์ฑ ๋ชจ๋ธ'๋ก ์ ํํ AI๊ฐ ์บ๋ฆญํฐ๋ฅผ ์์ฑํฉ๋๋ค. <br />
|
| 127 |
+
* ์์ ์น์์ค ์บ๋ฆญํฐ๋ฅผ ๋ถ์ ํ ์ด์์ ์ ์ํ ์ ์์ต๋๋ค. <br />
|
| 128 |
+
1. '์ฐธ์กฐ ์น์์ค ํ๋ก์ ํธ'์์ ์ํ๋ ์ํ์ ์ ํํ์ธ์. ํด๋น ์ํ ์บ๋ฆญํฐ ๋ชฉ๋ก์ '์บ๋ฆญํฐ ๋ชฉ๋ก'์ ํ์๋ฉ๋๋ค. <br />
|
| 129 |
+
2. '์บ๋ฆญํฐ ๋ชฉ๋ก'์์ ์ํ๋ ์บ๋ฆญํฐ๋ฅผ ํด๋ฆญํด ์ฃผ์ธ์. '๋ถ์ ๋ชจ๋ธ'์์ ์ ํํ AI๊ฐ ์ํ ์ ์บ๋ฆญํฐ๋ฅผ ๋ถ์ ํ '์บ๋ฆญํฐ ์์ฑ ํ๋กฌํํธ'์ ์บ๋ฆญํฐ ์ ๋ณด๋ฅผ ์ถ๊ฐํฉ๋๋ค.
|
| 130 |
+
</p>
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<!-- AI ์ค์ ๋ฐ -->
|
| 135 |
+
<div class="clean-card mb-4 py-2 px-4 d-flex align-items-center gap-4" style="cursor: default;">
|
| 136 |
+
<div class="d-flex align-items-center gap-2">
|
| 137 |
+
<label for="analysisModelSelect" class="small fw-bold text-secondary mb-0">
|
| 138 |
+
<i class="fas fa-search me-1 text-primary"></i> ๋ถ์ ๋ชจ๋ธ:
|
| 139 |
+
</label>
|
| 140 |
+
<select id="analysisModelSelect" class="form-select form-select-sm border-0 bg-transparent shadow-none" style="width: auto; min-width: 180px; font-weight: 600;">
|
| 141 |
+
<option value="">๋ชจ๋ธ ๋ก๋ฉ ์ค...</option>
|
| 142 |
+
</select>
|
| 143 |
+
</div>
|
| 144 |
+
<div class="vr"></div>
|
| 145 |
+
<div class="d-flex align-items-center gap-2">
|
| 146 |
+
<label for="modelSelect" class="small fw-bold text-secondary mb-0">
|
| 147 |
+
<i class="fas fa-paint-brush me-1 text-success"></i> ์์ฑ ๋ชจ๋ธ:
|
| 148 |
+
</label>
|
| 149 |
+
<select id="modelSelect" class="form-select form-select-sm border-0 bg-transparent shadow-none" style="width: auto; min-width: 180px; font-weight: 600;">
|
| 150 |
+
<option value="">๋ชจ๋ธ ๋ก๋ฉ ์ค...</option>
|
| 151 |
+
</select>
|
| 152 |
+
</div>
|
| 153 |
+
<button class="btn btn-sm text-muted p-0 hover-rotate" onclick="loadModels()">
|
| 154 |
+
<i class="fas fa-sync-alt"></i>
|
| 155 |
+
</button>
|
| 156 |
+
</div>
|
| 157 |
+
|
| 158 |
+
<div class="row g-4 align-items-stretch mb-5">
|
| 159 |
+
<!-- ์ผ์ชฝ: ํ๋กฌํํธ ์
๋ ฅ -->
|
| 160 |
+
<div class="col-lg-8 gen-input-column">
|
| 161 |
+
<div class="clean-card h-100 p-4">
|
| 162 |
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
| 163 |
+
<h2 class="h5 fw-bold mb-0">์บ๋ฆญํฐ ์์ฑ ํ๋กฌํํธ</h2>
|
| 164 |
+
<div class="d-flex gap-2">
|
| 165 |
+
<button class="btn btn-sm btn-outline-primary" onclick="saveGlobalPrompt()">
|
| 166 |
+
<i class="fas fa-save me-1"></i> ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ ์ฅ
|
| 167 |
+
</button>
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
<textarea id="genPrompt" class="form-control border-0 bg-light p-4"
|
| 171 |
+
style="border-radius: 12px; font-size: 14px; line-height: 1.6;"
|
| 172 |
+
placeholder="์์ฑํ ์บ๋ฆญํฐ์ ์ธ๋ชจ, ๋ณต์ฅ, ๋ถ์๊ธฐ ๋ฑ์ ์์ธํ ์
๋ ฅํ์ธ์."></textarea>
|
| 173 |
+
|
| 174 |
+
<div class="mt-4">
|
| 175 |
+
<button id="generateBtn" class="db-btn db-btn-primary w-100 py-3 fw-bold" onclick="generateCharacter()">
|
| 176 |
+
<i class="fas fa-wand-sparkles me-2"></i> ์บ๋ฆญํฐ ์ด๋ฏธ์ง ์์ฑ ์์
|
| 177 |
+
</button>
|
| 178 |
+
</div>
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
|
| 182 |
+
<!-- ์ค๋ฅธ์ชฝ: ์ค์ ๋ฐ ์ฐธ์กฐ -->
|
| 183 |
+
<div class="col-lg-4">
|
| 184 |
+
<div class="clean-card p-4 h-100">
|
| 185 |
+
<h2 class="h5 fw-bold mb-4">์ฐธ์กฐ ์ค์ </h2>
|
| 186 |
+
|
| 187 |
+
<div class="mb-4">
|
| 188 |
+
<label class="form-label small fw-bold text-secondary">์ฐธ์กฐ ์น์์ค ํ๋ก์ ํธ</label>
|
| 189 |
+
<select id="projectSelect" class="form-select border-0 shadow-sm mb-2" onchange="loadCharacters(this.value)">
|
| 190 |
+
<option value="">-- ํ๋ก์ ํธ ์ ํ --</option>
|
| 191 |
+
</select>
|
| 192 |
+
<p class="text-muted small mb-0"><i class="fas fa-info-circle me-1"></i> ํ๋ก์ ํธ๋ฅผ ์ ํํ๋ฉด ๋ฑ๋ก๋ ๋ฉ์ธ ์บ๋ฆญํฐ ๋ชฉ๋ก์ด ๋ํ๋ฉ๋๋ค.</p>
|
| 193 |
+
</div>
|
| 194 |
+
|
| 195 |
+
<div class="mb-4">
|
| 196 |
+
<label class="form-label small fw-bold text-secondary">์บ๋ฆญํฐ ๋ชฉ๋ก</label>
|
| 197 |
+
<div id="characterList" class="bg-light border rounded p-2" style="max-height: 300px; overflow-y: auto; min-height: 100px;">
|
| 198 |
+
<p class="text-muted small mb-0 p-2 text-center">ํ๋ก์ ํธ๋ฅผ ๋จผ์ ์ ํํด์ฃผ์ธ์.</p>
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
<!-- ํ๋จ ๊ฒฐ๊ณผ ์์ญ -->
|
| 206 |
+
<div id="resultArea" class="d-none mb-5">
|
| 207 |
+
<div class="clean-card p-4">
|
| 208 |
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
| 209 |
+
<h2 class="h5 fw-bold mb-0">์บ๋ฆญํฐ ์์ฑ ๊ฒฐ๊ณผ</h2>
|
| 210 |
+
<a id="downloadLink" href="#" class="db-btn db-btn-outline py-2" download>
|
| 211 |
+
<i class="fas fa-download"></i> ์ด๋ฏธ์ง ๋ค์ด๋ก๋
|
| 212 |
+
</a>
|
| 213 |
+
</div>
|
| 214 |
+
<div class="result-image-container" id="imageContainer">
|
| 215 |
+
<div class="text-center py-5">
|
| 216 |
+
<div class="spinner-border text-primary mb-3" role="status"></div>
|
| 217 |
+
<p class="text-muted mb-0" id="loadingText">์ด๋ฏธ์ง๋ฅผ ์์ฑํ๊ณ ์์ต๋๋ค. ์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์...</p>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
</div>
|
| 221 |
+
</div>
|
| 222 |
+
|
| 223 |
+
<script>
|
| 224 |
+
let currentFileId = null;
|
| 225 |
+
const defaultAnalysisModel = "{{ default_analysis_model }}";
|
| 226 |
+
|
| 227 |
+
async function loadModels() {
|
| 228 |
+
const genSelect = document.getElementById('modelSelect');
|
| 229 |
+
const analysisSelect = document.getElementById('analysisModelSelect');
|
| 230 |
+
try {
|
| 231 |
+
const response = await fetch('/api/ollama/models?all=true');
|
| 232 |
+
const data = await response.json();
|
| 233 |
+
|
| 234 |
+
if (data.models && data.models.length > 0) {
|
| 235 |
+
const optionsHtml = ['<option value="">๋ชจ๋ธ ์ ํ...</option>'];
|
| 236 |
+
const analysisOptionsHtml = ['<option value="">๋ชจ๋ธ ์ ํ...</option>'];
|
| 237 |
+
|
| 238 |
+
// Gemini ๋ชจ๋ธ ๊ทธ๋ฃน
|
| 239 |
+
const geminiModels = data.models.filter(m => m.type === 'gemini');
|
| 240 |
+
if (geminiModels.length > 0) {
|
| 241 |
+
optionsHtml.push('<optgroup label="โจ Google Gemini">');
|
| 242 |
+
analysisOptionsHtml.push('<optgroup label="โจ Google Gemini">');
|
| 243 |
+
geminiModels.forEach(m => {
|
| 244 |
+
const label = m.name.startsWith('gemini:') ? m.name.substring(7) : m.name;
|
| 245 |
+
const genSelected = m.name.includes('gemini-3-pro-image-preview') ? 'selected' : '';
|
| 246 |
+
|
| 247 |
+
// ๏ฟฝ๏ฟฝ๏ฟฝ์ ๋ชจ๋ธ ๊ธฐ๋ณธ๊ฐ ์ค์
|
| 248 |
+
let analysisSelected = '';
|
| 249 |
+
if (defaultAnalysisModel) {
|
| 250 |
+
if (m.name === defaultAnalysisModel || m.name === `gemini:${defaultAnalysisModel}`) {
|
| 251 |
+
analysisSelected = 'selected';
|
| 252 |
+
}
|
| 253 |
+
} else if (m.name.includes('gemini-1.5-flash')) {
|
| 254 |
+
analysisSelected = 'selected';
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
optionsHtml.push(`<option value="${m.name}" ${genSelected}>${label}</option>`);
|
| 258 |
+
analysisOptionsHtml.push(`<option value="${m.name}" ${analysisSelected}>${label}</option>`);
|
| 259 |
+
});
|
| 260 |
+
optionsHtml.push('</optgroup>');
|
| 261 |
+
analysisOptionsHtml.push('</optgroup>');
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
// Ollama ๋ชจ๋ธ ๊ทธ๋ฃน
|
| 265 |
+
const ollamaModels = data.models.filter(m => m.type !== 'gemini');
|
| 266 |
+
if (ollamaModels.length > 0) {
|
| 267 |
+
optionsHtml.push('<optgroup label="๐ค Ollama (Local)">');
|
| 268 |
+
analysisOptionsHtml.push('<optgroup label="๐ค Ollama (Local)">');
|
| 269 |
+
ollamaModels.forEach(m => {
|
| 270 |
+
let analysisSelected = (m.name === defaultAnalysisModel) ? 'selected' : '';
|
| 271 |
+
optionsHtml.push(`<option value="${m.name}">${m.name}</option>`);
|
| 272 |
+
analysisOptionsHtml.push(`<option value="${m.name}" ${analysisSelected}>${m.name}</option>`);
|
| 273 |
+
});
|
| 274 |
+
optionsHtml.push('</optgroup>');
|
| 275 |
+
analysisOptionsHtml.push('</optgroup>');
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
genSelect.innerHTML = optionsHtml.join('');
|
| 279 |
+
analysisSelect.innerHTML = analysisOptionsHtml.join('');
|
| 280 |
+
}
|
| 281 |
+
} catch (error) {
|
| 282 |
+
console.error('๋ชจ๋ธ ๋ก๋ ์ค๋ฅ:', error);
|
| 283 |
+
genSelect.innerHTML = '<option value="">๋ก๋ ์คํจ</option>';
|
| 284 |
+
analysisSelect.innerHTML = '<option value="">๋ก๋ ์คํจ</option>';
|
| 285 |
+
}
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
async function loadNovelProjects() {
|
| 289 |
+
const select = document.getElementById('projectSelect');
|
| 290 |
+
try {
|
| 291 |
+
const res = await fetch('/api/files?sort=uploaded_at_desc&public_only=true');
|
| 292 |
+
const data = await res.json();
|
| 293 |
+
|
| 294 |
+
if (data.files && data.files.length > 0) {
|
| 295 |
+
data.files.forEach(f => {
|
| 296 |
+
const opt = document.createElement('option');
|
| 297 |
+
opt.value = f.id;
|
| 298 |
+
opt.textContent = f.original_filename;
|
| 299 |
+
select.appendChild(opt);
|
| 300 |
+
});
|
| 301 |
+
}
|
| 302 |
+
} catch (e) {
|
| 303 |
+
console.error("Project load error:", e);
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
async function loadCharacters(fileId) {
|
| 308 |
+
if (!fileId) return;
|
| 309 |
+
currentFileId = fileId;
|
| 310 |
+
const list = document.getElementById('characterList');
|
| 311 |
+
const promptArea = document.getElementById('genPrompt');
|
| 312 |
+
list.innerHTML = '<div class="text-center p-3"><div class="spinner-border spinner-border-sm text-primary"></div></div>';
|
| 313 |
+
|
| 314 |
+
try {
|
| 315 |
+
const res = await fetch('/creation/api/character/extract-main', {
|
| 316 |
+
method: 'POST',
|
| 317 |
+
headers: {'Content-Type': 'application/json'},
|
| 318 |
+
body: JSON.stringify({ file_id: fileId })
|
| 319 |
+
});
|
| 320 |
+
const data = await res.json();
|
| 321 |
+
|
| 322 |
+
// ์ ์ญ ๋ณ์์ ํ๋ก์ ํธ ๋ฐฐ๊ฒฝ ์ ๋ณด ์ ์ฅ (๋ถ์ ์ ์ฌ์ฉ)
|
| 323 |
+
window.projectContext = `์ธ๊ณ๊ด: ${data.world_view || '์ ๋ณด ์์'}\n์ค๊ฑฐ๋ฆฌ: ${data.story || '์ ๋ณด ์์'}\n๊ธฐํ: ${data.others || '์ ๋ณด ์์'}`;
|
| 324 |
+
|
| 325 |
+
// 1. Parent Chunk ์ ๋ณด(์ธ๊ณ๊ด, ๊ธฐํ)๋ฅผ ํ๋กฌํํธ์ ์ถ๊ฐ
|
| 326 |
+
if (data.world_view || data.others) {
|
| 327 |
+
let contextPrompt = '';
|
| 328 |
+
if (data.world_view) contextPrompt += `### ์ธ๊ณ๊ด ์ค๋ช
\n${data.world_view}\n\n`;
|
| 329 |
+
if (data.others) contextPrompt += `### ๊ธฐํ ์ค์ \n${data.others}\n\n`;
|
| 330 |
+
|
| 331 |
+
if (confirm("์ ํํ ํ๋ก์ ํธ์ '์ธ๊ณ๊ด ์ค๋ช
' ๋ฐ '๊ธฐํ ์ค์ '์ ํ๋กฌํํธ์ ์ถ๊ฐํ ๊น์?")) {
|
| 332 |
+
promptArea.value = contextPrompt + promptArea.value;
|
| 333 |
+
}
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
if (data.characters && data.characters.length > 0) {
|
| 337 |
+
list.innerHTML = data.characters.map((c, idx) => {
|
| 338 |
+
const mainImg = c.image_path ? `/uploads/${c.image_path}` : '';
|
| 339 |
+
const hasImages = c.images && c.images.length > 0;
|
| 340 |
+
|
| 341 |
+
return `
|
| 342 |
+
<div class="border-bottom character-item cursor-pointer"
|
| 343 |
+
data-char-index="${idx}"
|
| 344 |
+
onclick='applyCharacterPrompt(${idx})'>
|
| 345 |
+
${mainImg ? `<img src="${mainImg}" class="character-item-img">` : `<div class="character-item-img d-flex align-items-center justify-content-center text-muted"><i class="fas fa-user"></i></div>`}
|
| 346 |
+
<div class="character-item-info">
|
| 347 |
+
<div class="character-item-name">${c.name}</div>
|
| 348 |
+
<div class="character-item-desc text-muted small">${c.description || '์ค๋ช
์์'}</div>
|
| 349 |
+
${hasImages ? `
|
| 350 |
+
<div class="character-img-gallery mt-1">
|
| 351 |
+
${c.images.map(img => `<img src="/uploads/${img.image_path}" onclick="event.stopPropagation(); window.open('/uploads/${img.image_path}', '_blank')" title="์ด๋ฏธ์ง ํฌ๊ฒ ๋ณด๊ธฐ">`).join('')}
|
| 352 |
+
</div>
|
| 353 |
+
` : ''}
|
| 354 |
+
</div>
|
| 355 |
+
</div>
|
| 356 |
+
`;
|
| 357 |
+
}).join('');
|
| 358 |
+
|
| 359 |
+
// ์ ์ญ ๋ณ์์ ์บ๋ฆญํฐ ๋ฐ์ดํฐ ์ ์ฅ
|
| 360 |
+
window.extractedCharacters = data.characters;
|
| 361 |
+
} else {
|
| 362 |
+
list.innerHTML = '<p class="text-muted small mb-0 p-2 text-center">๋ถ์๋ ์บ๋ฆญํฐ๊ฐ ์์ต๋๋ค.</p>';
|
| 363 |
+
}
|
| 364 |
+
} catch (e) {
|
| 365 |
+
list.innerHTML = '<p class="text-danger small mb-0 p-2 text-center">๋ก๋ ์คํจ</p>';
|
| 366 |
+
}
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
async function applyCharacterPrompt(index) {
|
| 370 |
+
const c = window.extractedCharacters[index];
|
| 371 |
+
if (!c) return;
|
| 372 |
+
|
| 373 |
+
const name = c.name;
|
| 374 |
+
const desc = c.description;
|
| 375 |
+
const promptArea = document.getElementById('genPrompt');
|
| 376 |
+
const analysisModel = document.getElementById('analysisModelSelect').value;
|
| 377 |
+
|
| 378 |
+
if (!analysisModel) {
|
| 379 |
+
alert("๋ถ์ ๋ชจ๋ธ์ ๋จผ์ ์ ํํด์ฃผ์ธ์.");
|
| 380 |
+
return;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
if (confirm(`'${name}' ์บ๋ฆญํฐ ์ค์ ์ ๋ถ์ํ์ฌ ํ๋กฌํํธ์ ์ ์ฉํ ๊น์?\n(์ ํํ ๋ถ์ ๋ชจ๋ธ: ${analysisModel})`)) {
|
| 384 |
+
const originalBtnText = document.querySelector(`.character-item[data-char-index="${index}"]`).innerHTML;
|
| 385 |
+
const charItem = document.querySelector(`.character-item[data-char-index="${index}"]`);
|
| 386 |
+
|
| 387 |
+
charItem.innerHTML = `<div class="fw-bold small text-primary"><span class="spinner-border spinner-border-sm me-1"></span>๋ถ์ ์ค...</div>`;
|
| 388 |
+
charItem.style.pointerEvents = 'none';
|
| 389 |
+
|
| 390 |
+
try {
|
| 391 |
+
const res = await fetch('/creation/api/character/analyze', {
|
| 392 |
+
method: 'POST',
|
| 393 |
+
headers: {'Content-Type': 'application/json'},
|
| 394 |
+
body: JSON.stringify({
|
| 395 |
+
name: name,
|
| 396 |
+
description: desc,
|
| 397 |
+
context: window.projectContext || '',
|
| 398 |
+
model_name: analysisModel
|
| 399 |
+
})
|
| 400 |
+
});
|
| 401 |
+
const data = await res.json();
|
| 402 |
+
|
| 403 |
+
if (data.success) {
|
| 404 |
+
const analyzedPrompt = data.analysis;
|
| 405 |
+
if (promptArea.value.trim()) {
|
| 406 |
+
promptArea.value = promptArea.value + "\n\n" + analyzedPrompt;
|
| 407 |
+
} else {
|
| 408 |
+
promptArea.value = analyzedPrompt;
|
| 409 |
+
}
|
| 410 |
+
// ํ๋กฌํํธ ์์ญ์ผ๋ก ์คํฌ๋กค
|
| 411 |
+
promptArea.scrollIntoView({ behavior: 'smooth' });
|
| 412 |
+
} else {
|
| 413 |
+
alert("๋ถ์ ์คํจ: " + data.error);
|
| 414 |
+
}
|
| 415 |
+
} catch (e) {
|
| 416 |
+
alert("ํต์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.");
|
| 417 |
+
} finally {
|
| 418 |
+
charItem.innerHTML = originalBtnText;
|
| 419 |
+
charItem.style.pointerEvents = 'auto';
|
| 420 |
+
}
|
| 421 |
+
}
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
async function generateCharacter() {
|
| 425 |
+
const prompt = document.getElementById('genPrompt').value.trim();
|
| 426 |
+
const model = document.getElementById('modelSelect').value;
|
| 427 |
+
const btn = document.getElementById('generateBtn');
|
| 428 |
+
const resultArea = document.getElementById('resultArea');
|
| 429 |
+
const imageContainer = document.getElementById('imageContainer');
|
| 430 |
+
|
| 431 |
+
if (!prompt) return alert("ํ๋กฌํํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.");
|
| 432 |
+
if (!model) return alert("๋ชจ๋ธ์ ์ ํํด์ฃผ์ธ์.");
|
| 433 |
+
|
| 434 |
+
btn.disabled = true;
|
| 435 |
+
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>์ด๋ฏธ์ง ์์ฑ ์ค...';
|
| 436 |
+
|
| 437 |
+
resultArea.classList.remove('d-none');
|
| 438 |
+
imageContainer.innerHTML = `
|
| 439 |
+
<div class="text-center py-5">
|
| 440 |
+
<div class="spinner-border text-primary mb-3" role="status"></div>
|
| 441 |
+
<p class="text-muted mb-0">์ด๋ฏธ์ง๋ฅผ ์์ฑํ๊ณ ์์ต๋๋ค. ์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์...</p>
|
| 442 |
+
</div>
|
| 443 |
+
`;
|
| 444 |
+
|
| 445 |
+
// ๊ฒฐ๊ณผ ์์ญ์ผ๋ก ์คํฌ๋กค
|
| 446 |
+
resultArea.scrollIntoView({ behavior: 'smooth' });
|
| 447 |
+
|
| 448 |
+
try {
|
| 449 |
+
const res = await fetch('/creation/api/generate-image', {
|
| 450 |
+
method: 'POST',
|
| 451 |
+
headers: {'Content-Type': 'application/json'},
|
| 452 |
+
body: JSON.stringify({
|
| 453 |
+
prompt: prompt,
|
| 454 |
+
model_name: model
|
| 455 |
+
})
|
| 456 |
+
});
|
| 457 |
+
|
| 458 |
+
const data = await res.json();
|
| 459 |
+
if (data.success) {
|
| 460 |
+
imageContainer.innerHTML = `<img src="${data.image_url}" class="result-image" alt="Generated Character">`;
|
| 461 |
+
document.getElementById('downloadLink').href = data.image_url;
|
| 462 |
+
} else {
|
| 463 |
+
alert("์์ฑ ์คํจ: " + (data.error || "์ ์ ์๋ ์ค๋ฅ"));
|
| 464 |
+
imageContainer.innerHTML = '<p class="text-danger small p-3">์ด๋ฏธ์ง ์์ฑ์ ์คํจํ์ต๋๋ค.</p>';
|
| 465 |
+
}
|
| 466 |
+
} catch (e) {
|
| 467 |
+
alert("ํต์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.");
|
| 468 |
+
imageContainer.innerHTML = '';
|
| 469 |
+
} finally {
|
| 470 |
+
btn.disabled = false;
|
| 471 |
+
btn.innerHTML = '<i class="fas fa-wand-sparkles me-2"></i> ์บ๋ฆญํฐ ์ด๋ฏธ์ง ์์ฑ ์์';
|
| 472 |
+
}
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
async function loadGlobalPrompt() {
|
| 476 |
+
try {
|
| 477 |
+
const res = await fetch('/creation/api/character/global-prompt');
|
| 478 |
+
const data = await res.json();
|
| 479 |
+
if (data.prompt) {
|
| 480 |
+
document.getElementById('genPrompt').value = data.prompt;
|
| 481 |
+
}
|
| 482 |
+
} catch (e) {}
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
async function saveGlobalPrompt() {
|
| 486 |
+
const prompt = document.getElementById('genPrompt').value;
|
| 487 |
+
try {
|
| 488 |
+
const res = await fetch('/creation/api/character/global-prompt', {
|
| 489 |
+
method: 'POST',
|
| 490 |
+
headers: {'Content-Type': 'application/json'},
|
| 491 |
+
body: JSON.stringify({ prompt: prompt })
|
| 492 |
+
});
|
| 493 |
+
if (res.ok) alert("์บ๋ฆญํฐ ์ ์ ๊ธฐ๋ณธ ํ๋กฌํํธ๋ก ์ ์ฅ๋์์ต๋๋ค.");
|
| 494 |
+
} catch (e) {
|
| 495 |
+
alert("์ ์ฅ ์คํจ");
|
| 496 |
+
}
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 500 |
+
loadModels();
|
| 501 |
+
loadNovelProjects();
|
| 502 |
+
loadGlobalPrompt();
|
| 503 |
+
});
|
| 504 |
+
</script>
|
| 505 |
+
{% endblock %}
|
templates/webtoon_conti_dashboard.html
CHANGED
|
@@ -110,9 +110,13 @@
|
|
| 110 |
border: none;
|
| 111 |
box-shadow: 0 4px 10px rgba(26, 115, 232, 0.3);
|
| 112 |
}
|
| 113 |
-
.
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
}
|
| 117 |
</style>
|
| 118 |
{% endblock %}
|
|
@@ -174,9 +178,9 @@
|
|
| 174 |
</div>
|
| 175 |
</div>
|
| 176 |
|
| 177 |
-
<div class="row g-4">
|
| 178 |
-
<div class="col-lg-8">
|
| 179 |
-
<textarea id="novelText" class="form-control border-0 bg-light p-4
|
| 180 |
style="border-radius: 12px; font-size: 14px; line-height: 1.6;"
|
| 181 |
placeholder="์น์์ค์ ํน์ ์ฅ๋ฉด์ด๋ ์ํผ์๋ ํ
์คํธ๋ฅผ ์
๋ ฅํ์ธ์."></textarea>
|
| 182 |
</div>
|
|
@@ -188,12 +192,35 @@
|
|
| 188 |
<option value="">-- ํ๋ก์ ํธ ์ ํ --</option>
|
| 189 |
</select>
|
| 190 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
<div class="mb-4">
|
| 192 |
-
<
|
| 193 |
-
|
| 194 |
-
<
|
| 195 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
</div>
|
|
|
|
| 197 |
<button class="db-btn db-btn-primary w-100 py-3 fw-bold" onclick="startExtraction()">
|
| 198 |
<i class="fas fa-wand-sparkles me-2"></i> ๋ถ์ ๋ฐ ์ฝํฐ ์์ฑ ์์
|
| 199 |
</button>
|
|
@@ -352,7 +379,7 @@ document.getElementById('genModelSelect').addEventListener('change', function()
|
|
| 352 |
|
| 353 |
async function loadNovelProjects() {
|
| 354 |
try {
|
| 355 |
-
const res = await fetch('/api/files?sort=uploaded_at_desc');
|
| 356 |
const data = await res.json();
|
| 357 |
const files = data.files || [];
|
| 358 |
const select = document.getElementById('projectSelect');
|
|
@@ -370,43 +397,136 @@ async function loadNovelProjects() {
|
|
| 370 |
}
|
| 371 |
|
| 372 |
async function loadEpisodes(fileId) {
|
| 373 |
-
const
|
| 374 |
-
|
| 375 |
|
| 376 |
currentFileId = fileId;
|
| 377 |
|
| 378 |
if (!fileId) {
|
| 379 |
-
|
| 380 |
return;
|
| 381 |
}
|
| 382 |
|
| 383 |
try {
|
| 384 |
-
|
|
|
|
|
|
|
| 385 |
const data = await res.json();
|
| 386 |
-
const episodes = data.episodes || [];
|
| 387 |
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
});
|
| 394 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
} catch (e) {
|
| 396 |
console.error("Episode load error:", e);
|
|
|
|
| 397 |
}
|
| 398 |
}
|
| 399 |
|
| 400 |
-
async function
|
| 401 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
try {
|
| 403 |
-
const res = await fetch(`/api/
|
| 404 |
const data = await res.json();
|
| 405 |
-
|
| 406 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
}
|
| 408 |
} catch (e) {
|
| 409 |
-
console.error("
|
| 410 |
}
|
| 411 |
}
|
| 412 |
|
|
@@ -416,10 +536,17 @@ async function startExtraction() {
|
|
| 416 |
|
| 417 |
const analysisModel = document.getElementById('analysisModelSelect').value;
|
| 418 |
const genModel = document.getElementById('genModelSelect').value;
|
|
|
|
|
|
|
| 419 |
|
| 420 |
if (!analysisModel) return alert("๋ถ์ ๋ชจ๋ธ์ ์ ํํด์ฃผ์ธ์.");
|
| 421 |
if (!genModel) return alert("์ฝํฐ ์์ฑ ๋ชจ๋ธ์ ์ ํํด์ฃผ์ธ์.");
|
| 422 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
// ์ฐธ์กฐ ํ๋ก์ ํธ ํ์ธ
|
| 424 |
if (!currentFileId && !document.getElementById('projectSelect').value) {
|
| 425 |
if (!confirm("์ฐธ์กฐํ ์น์์ค ํ๋ก์ ํธ๊ฐ ์ ํ๋์ง ์์์ต๋๋ค. ์บ๋ฆญํฐ/๋ฐฐ๊ฒฝ ์ค์ ์์ด ๋ถ์์ ์งํํ ๊น์?\n(ํ๋ก์ ํธ๋ฅผ ์ ํํ๋ฉด ๋ ์ ํํ ์บ๋ฆญํฐ ๋ฌ์ฌ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.)")) {
|
|
@@ -439,7 +566,10 @@ async function startExtraction() {
|
|
| 439 |
body: JSON.stringify({
|
| 440 |
title: "์ฝํฐ ๋ถ์: " + (text.substring(0, 15) + "..."),
|
| 441 |
file_id: currentFileId || 0,
|
| 442 |
-
|
|
|
|
|
|
|
|
|
|
| 443 |
})
|
| 444 |
});
|
| 445 |
|
|
@@ -460,9 +590,13 @@ async function startExtraction() {
|
|
| 460 |
body: JSON.stringify({
|
| 461 |
text: text,
|
| 462 |
file_id: currentFileId || 0,
|
|
|
|
|
|
|
| 463 |
project_id: currentProjectId,
|
| 464 |
analysis_model: analysisModel,
|
| 465 |
-
gen_model: genModel
|
|
|
|
|
|
|
| 466 |
})
|
| 467 |
});
|
| 468 |
|
|
@@ -485,6 +619,16 @@ async function loadProjectDetails(projectId) {
|
|
| 485 |
const res = await fetch(`/webtoon-conti/api/projects/${projectId}`);
|
| 486 |
const data = await res.json();
|
| 487 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
document.getElementById('panelListHeader').classList.remove('d-none');
|
| 489 |
document.getElementById('panelListHeader').classList.add('d-flex');
|
| 490 |
document.getElementById('panelCount').innerText = data.panels.length;
|
|
@@ -562,6 +706,7 @@ function renderSample(s, finalPath) {
|
|
| 562 |
|
| 563 |
async function generateSamples(panelId) {
|
| 564 |
const model = document.getElementById('genModelSelect').value;
|
|
|
|
| 565 |
const container = document.getElementById(`samples-${panelId}`);
|
| 566 |
|
| 567 |
const isDirectAI = model && (model.toLowerCase().includes('imagen') || model.toLowerCase().includes('gemini'));
|
|
@@ -580,7 +725,8 @@ async function generateSamples(panelId) {
|
|
| 580 |
body: JSON.stringify({
|
| 581 |
panel_id: panelId,
|
| 582 |
sample_count: 3,
|
| 583 |
-
model: model
|
|
|
|
| 584 |
})
|
| 585 |
});
|
| 586 |
|
|
@@ -709,6 +855,50 @@ async function openLoadModal() {
|
|
| 709 |
modal.show();
|
| 710 |
}
|
| 711 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
function loadProjectAndCloseModal(id) {
|
| 713 |
const modalEl = document.getElementById('loadProjectModal');
|
| 714 |
const modal = bootstrap.Modal.getInstance(modalEl);
|
|
@@ -725,6 +915,7 @@ function resetPage() {
|
|
| 725 |
document.addEventListener('DOMContentLoaded', () => {
|
| 726 |
loadModels();
|
| 727 |
loadNovelProjects();
|
|
|
|
| 728 |
});
|
| 729 |
</script>
|
| 730 |
{% endblock %}
|
|
|
|
| 110 |
border: none;
|
| 111 |
box-shadow: 0 4px 10px rgba(26, 115, 232, 0.3);
|
| 112 |
}
|
| 113 |
+
.novel-input-column {
|
| 114 |
+
display: flex;
|
| 115 |
+
flex-direction: column;
|
| 116 |
+
}
|
| 117 |
+
#novelText {
|
| 118 |
+
flex-grow: 1;
|
| 119 |
+
min-height: 500px;
|
| 120 |
}
|
| 121 |
</style>
|
| 122 |
{% endblock %}
|
|
|
|
| 178 |
</div>
|
| 179 |
</div>
|
| 180 |
|
| 181 |
+
<div class="row g-4 align-items-stretch">
|
| 182 |
+
<div class="col-lg-8 novel-input-column">
|
| 183 |
+
<textarea id="novelText" class="form-control border-0 bg-light p-4 h-100"
|
| 184 |
style="border-radius: 12px; font-size: 14px; line-height: 1.6;"
|
| 185 |
placeholder="์น์์ค์ ํน์ ์ฅ๋ฉด์ด๋ ์ํผ์๋ ํ
์คํธ๋ฅผ ์
๋ ฅํ์ธ์."></textarea>
|
| 186 |
</div>
|
|
|
|
| 192 |
<option value="">-- ํ๋ก์ ํธ ์ ํ --</option>
|
| 193 |
</select>
|
| 194 |
</div>
|
| 195 |
+
<div class="mb-3">
|
| 196 |
+
<label class="form-label small fw-bold text-secondary">ํ์ฐจ ๋ถ๋ฌ์ค๊ธฐ (๋ค์ค ์ ํ ๊ฐ๋ฅ)</label>
|
| 197 |
+
<div id="episodeList" class="bg-white border rounded p-2" style="max-height: 200px; overflow-y: auto;">
|
| 198 |
+
<p class="text-muted small mb-0 p-2">ํ๋ก์ ํธ๋ฅผ ๋จผ์ ์ ํํด์ฃผ์ธ์.</p>
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
|
| 202 |
+
<div class="mb-3">
|
| 203 |
+
<div class="d-flex justify-content-between align-items-center mb-1">
|
| 204 |
+
<label class="form-label small fw-bold text-secondary mb-0">๋ถ์ ์ฐธ์กฐ ํ๋กฌํํธ</label>
|
| 205 |
+
<button class="btn btn-sm p-0 text-primary" onclick="savePrompts()" title="ํ๋กฌํํธ ์ ์ฅ">
|
| 206 |
+
<i class="fas fa-save"></i>
|
| 207 |
+
</button>
|
| 208 |
+
</div>
|
| 209 |
+
<textarea id="analysisPrompt" class="form-control border-0 shadow-sm small" rows="3"
|
| 210 |
+
placeholder="๋ถ์ ์ ์ฐธ๊ณ ํ ์ถ๊ฐ ์ง์นจ์ ์
๋ ฅํ์ธ์."></textarea>
|
| 211 |
+
</div>
|
| 212 |
+
|
| 213 |
<div class="mb-4">
|
| 214 |
+
<div class="d-flex justify-content-between align-items-center mb-1">
|
| 215 |
+
<label class="form-label small fw-bold text-secondary mb-0">๊ทธ๋ฆผ ์ฝํฐ์ฉ ํ๋กฌํํธ</label>
|
| 216 |
+
<button class="btn btn-sm p-0 text-primary" onclick="savePrompts()" title="ํ๋กฌํํธ ์ ์ฅ">
|
| 217 |
+
<i class="fas fa-save"></i>
|
| 218 |
+
</button>
|
| 219 |
+
</div>
|
| 220 |
+
<textarea id="genPrompt" class="form-control border-0 shadow-sm small" rows="3"
|
| 221 |
+
placeholder="์ด๋ฏธ์ง ์์ฑ ์ ์ฐธ๊ณ ํ ์ถ๊ฐ ์คํ์ผ ์ง์นจ์ ์
๋ ฅํ์ธ์."></textarea>
|
| 222 |
</div>
|
| 223 |
+
|
| 224 |
<button class="db-btn db-btn-primary w-100 py-3 fw-bold" onclick="startExtraction()">
|
| 225 |
<i class="fas fa-wand-sparkles me-2"></i> ๋ถ์ ๋ฐ ์ฝํฐ ์์ฑ ์์
|
| 226 |
</button>
|
|
|
|
| 379 |
|
| 380 |
async function loadNovelProjects() {
|
| 381 |
try {
|
| 382 |
+
const res = await fetch('/api/files?sort=uploaded_at_desc&public_only=true');
|
| 383 |
const data = await res.json();
|
| 384 |
const files = data.files || [];
|
| 385 |
const select = document.getElementById('projectSelect');
|
|
|
|
| 397 |
}
|
| 398 |
|
| 399 |
async function loadEpisodes(fileId) {
|
| 400 |
+
const listContainer = document.getElementById('episodeList');
|
| 401 |
+
listContainer.innerHTML = '<div class="text-center p-2"><div class="spinner-border spinner-border-sm text-primary"></div></div>';
|
| 402 |
|
| 403 |
currentFileId = fileId;
|
| 404 |
|
| 405 |
if (!fileId) {
|
| 406 |
+
listContainer.innerHTML = '<p class="text-muted small mb-0 p-2">ํ๋ก์ ํธ๋ฅผ ๋จผ์ ์ ํํด์ฃผ์ธ์.</p>';
|
| 407 |
return;
|
| 408 |
}
|
| 409 |
|
| 410 |
try {
|
| 411 |
+
// 1. ํ์ฐจ๋ณ ๋ถ์ ์ ๋ณด(EpisodeAnalysis)์ GraphRAG(GraphEntity) ์ ๋ณด๋ฅผ ํจ๊ป ๊ณ ๋ คํ๊ธฐ ์ํด
|
| 412 |
+
// ๋จผ์ GraphRAG API๋ฅผ ๏ฟฝ๏ฟฝ๏ฟฝํด ์กด์ฌํ๋ ํ์ฐจ ๋ชฉ๋ก์ ๊ฐ์ ธ์ต๋๋ค.
|
| 413 |
+
const res = await fetch(`/api/files/${fileId}/graph`);
|
| 414 |
const data = await res.json();
|
|
|
|
| 415 |
|
| 416 |
+
// GraphRAG์ ๋ฑ๋ก๋ ํ์ฐจ ๋ชฉ๋ก (episodes)
|
| 417 |
+
const graphEpisodes = data.episodes || [];
|
| 418 |
+
|
| 419 |
+
// 2. ๊ธฐ์กด EpisodeAnalysis API์์๋ ํ์ฐจ ๋ชฉ๋ก์ ๊ฐ์ ธ์ต๋๋ค.
|
| 420 |
+
const res2 = await fetch(`/api/analysis/episodes/${fileId}`);
|
| 421 |
+
const data2 = await res2.json();
|
| 422 |
+
const analysisEpisodes = data2.episodes || [];
|
| 423 |
+
|
| 424 |
+
// ๋ ์์ค๋ฅผ ํตํฉํ์ฌ ์ ๋ํฌํ ํ์ฐจ ๋ชฉ๋ก ์์ฑ
|
| 425 |
+
// analysisEpisodes๋ [{id, episode_title, ...}] ํํ๊ณ , graphEpisodes๋ ['1ํ', '2ํ', ...] ํํ์
|
| 426 |
+
const allEpisodes = [];
|
| 427 |
+
|
| 428 |
+
// ๋จผ์ EpisodeAnalysis ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ชฉ๋ก ๊ตฌ์ฑ
|
| 429 |
+
analysisEpisodes.forEach(ep => {
|
| 430 |
+
allEpisodes.push({
|
| 431 |
+
id: ep.id,
|
| 432 |
+
title: ep.episode_title,
|
| 433 |
+
source: 'analysis'
|
| 434 |
+
});
|
| 435 |
+
});
|
| 436 |
+
|
| 437 |
+
// GraphRAG์๋ง ์๊ณ EpisodeAnalysis์๋ ์๋ ํ์ฐจ ์ถ๊ฐ (ํ์์)
|
| 438 |
+
graphEpisodes.forEach(title => {
|
| 439 |
+
if (!allEpisodes.find(e => e.title === title)) {
|
| 440 |
+
allEpisodes.push({
|
| 441 |
+
id: `graph-${title}`,
|
| 442 |
+
title: title,
|
| 443 |
+
source: 'graph'
|
| 444 |
+
});
|
| 445 |
+
}
|
| 446 |
+
});
|
| 447 |
+
|
| 448 |
+
if (allEpisodes.length === 0) {
|
| 449 |
+
listContainer.innerHTML = '<p class="text-muted small mb-0 p-2">ํ์ฐจ ์ ๋ณด๊ฐ ์์ต๋๋ค.</p>';
|
| 450 |
+
return;
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
// ์์ฐ์ด ์ ๋ ฌ (1ํ, 2ํ, 10ํ ์์ ๋ฑ)
|
| 454 |
+
allEpisodes.sort((a, b) => {
|
| 455 |
+
return a.title.localeCompare(b.title, undefined, {numeric: true, sensitivity: 'base'});
|
| 456 |
});
|
| 457 |
+
|
| 458 |
+
listContainer.innerHTML = allEpisodes.map(ep => `
|
| 459 |
+
<div class="form-check p-1 ms-4">
|
| 460 |
+
<input class="form-check-input episode-checkbox" type="checkbox"
|
| 461 |
+
value="${ep.id}"
|
| 462 |
+
data-title="${ep.title}"
|
| 463 |
+
data-source="${ep.source}"
|
| 464 |
+
id="ep-${ep.id}" onchange="updateContentFromSelection()">
|
| 465 |
+
<label class="form-check-label small" for="ep-${ep.id}">
|
| 466 |
+
${ep.title} ${ep.source === 'graph' ? '<span class="badge bg-info ms-1" style="font-size: 8px;">Graph</span>' : ''}
|
| 467 |
+
</label>
|
| 468 |
+
</div>
|
| 469 |
+
`).join('');
|
| 470 |
} catch (e) {
|
| 471 |
console.error("Episode load error:", e);
|
| 472 |
+
listContainer.innerHTML = '<p class="text-danger small mb-0 p-2">ํ์ฐจ ๋ก๋ ์คํจ</p>';
|
| 473 |
}
|
| 474 |
}
|
| 475 |
|
| 476 |
+
async function updateContentFromSelection() {
|
| 477 |
+
const checkedNodes = Array.from(document.querySelectorAll('.episode-checkbox:checked'));
|
| 478 |
+
if (checkedNodes.length === 0) {
|
| 479 |
+
return;
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
// ๊ฐ์ฅ ์ต๊ทผ์ ์ฒดํฌ๋ ๋
ธ๋
|
| 483 |
+
const lastNode = checkedNodes[checkedNodes.length - 1];
|
| 484 |
+
const source = lastNode.getAttribute('data-source');
|
| 485 |
+
const id = lastNode.value;
|
| 486 |
+
const title = lastNode.getAttribute('data-title');
|
| 487 |
+
|
| 488 |
+
if (source === 'analysis') {
|
| 489 |
+
await loadEpisodeContent(id);
|
| 490 |
+
} else if (source === 'graph') {
|
| 491 |
+
await loadGraphEpisodeContent(currentFileId, title);
|
| 492 |
+
}
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
async function loadGraphEpisodeContent(fileId, episodeTitle) {
|
| 496 |
try {
|
| 497 |
+
const res = await fetch(`/api/files/${fileId}/graph`);
|
| 498 |
const data = await res.json();
|
| 499 |
+
|
| 500 |
+
const entities = data.entities_by_episode[episodeTitle] || {characters: [], locations: []};
|
| 501 |
+
const relationships = data.relationships_by_episode[episodeTitle] || [];
|
| 502 |
+
const events = data.events_by_episode[episodeTitle] || [];
|
| 503 |
+
|
| 504 |
+
let content = `[${episodeTitle} GraphRAG ๋ถ์ ๋ด์ฉ]\n\n`;
|
| 505 |
+
|
| 506 |
+
if (entities.characters.length > 0) {
|
| 507 |
+
content += "โ ์ฃผ์ ์ธ๋ฌผ:\n" + entities.characters.map(c => `- ${c.entity_name}: ${c.description || ''} (์ญํ : ${c.role || '๋ฏธ์ '})`).join('\n') + "\n\n";
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
if (events.length > 0) {
|
| 511 |
+
content += "โ ์ฃผ์ ์ฌ๊ฑด:\n" + events.map(e => `- ${e.event_name}: ${e.description}`).join('\n') + "\n\n";
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
if (relationships.length > 0) {
|
| 515 |
+
content += "โ ๊ด๊ณ๋:\n" + relationships.map(r => `- ${r.source} -> ${r.target}: ${r.description || r.relationship_type}`).join('\n') + "\n";
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
const textarea = document.getElementById('novelText');
|
| 519 |
+
if (textarea.value && !textarea.value.includes(episodeTitle)) {
|
| 520 |
+
if (confirm(`'${episodeTitle}'์ GraphRAG ๋ถ์ ๋ด์ฉ์ ๊ธฐ์กด ํ
์คํธ์ ์ถ๊ฐํ ๊น์?`)) {
|
| 521 |
+
textarea.value += "\n\n" + content;
|
| 522 |
+
} else {
|
| 523 |
+
textarea.value = content;
|
| 524 |
+
}
|
| 525 |
+
} else {
|
| 526 |
+
textarea.value = content;
|
| 527 |
}
|
| 528 |
} catch (e) {
|
| 529 |
+
console.error("Graph content load error:", e);
|
| 530 |
}
|
| 531 |
}
|
| 532 |
|
|
|
|
| 536 |
|
| 537 |
const analysisModel = document.getElementById('analysisModelSelect').value;
|
| 538 |
const genModel = document.getElementById('genModelSelect').value;
|
| 539 |
+
const analysisPrompt = document.getElementById('analysisPrompt').value;
|
| 540 |
+
const genPrompt = document.getElementById('genPrompt').value;
|
| 541 |
|
| 542 |
if (!analysisModel) return alert("๋ถ์ ๋ชจ๋ธ์ ์ ํํด์ฃผ์ธ์.");
|
| 543 |
if (!genModel) return alert("์ฝํฐ ์์ฑ ๋ชจ๋ธ์ ์ ํํด์ฃผ์ธ์.");
|
| 544 |
|
| 545 |
+
// ์ ํ๋ ํ์ฐจ ์ ๋ณด ๋ชฉ๋ก
|
| 546 |
+
const checkedNodes = Array.from(document.querySelectorAll('.episode-checkbox:checked'));
|
| 547 |
+
const episodeIds = checkedNodes.filter(n => n.getAttribute('data-source') === 'analysis').map(n => parseInt(n.value));
|
| 548 |
+
const graphEpisodeTitles = checkedNodes.filter(n => n.getAttribute('data-source') === 'graph').map(n => n.getAttribute('data-title'));
|
| 549 |
+
|
| 550 |
// ์ฐธ์กฐ ํ๋ก์ ํธ ํ์ธ
|
| 551 |
if (!currentFileId && !document.getElementById('projectSelect').value) {
|
| 552 |
if (!confirm("์ฐธ์กฐํ ์น์์ค ํ๋ก์ ํธ๊ฐ ์ ํ๋์ง ์์์ต๋๋ค. ์บ๋ฆญํฐ/๋ฐฐ๊ฒฝ ์ค์ ์์ด ๋ถ์์ ์งํํ ๊น์?\n(ํ๋ก์ ํธ๋ฅผ ์ ํํ๋ฉด ๋ ์ ํํ ์บ๋ฆญํฐ ๋ฌ์ฌ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.)")) {
|
|
|
|
| 566 |
body: JSON.stringify({
|
| 567 |
title: "์ฝํฐ ๋ถ์: " + (text.substring(0, 15) + "..."),
|
| 568 |
file_id: currentFileId || 0,
|
| 569 |
+
episode_id: episodeIds.length > 0 ? episodeIds[0] : null,
|
| 570 |
+
description: text.substring(0, 100),
|
| 571 |
+
analysis_prompt: analysisPrompt,
|
| 572 |
+
gen_prompt: genPrompt
|
| 573 |
})
|
| 574 |
});
|
| 575 |
|
|
|
|
| 590 |
body: JSON.stringify({
|
| 591 |
text: text,
|
| 592 |
file_id: currentFileId || 0,
|
| 593 |
+
episode_ids: episodeIds,
|
| 594 |
+
graph_episode_titles: graphEpisodeTitles, // GraphRAG ํ์ฐจ ์ ๋ชฉ๋ค ์ถ๊ฐ
|
| 595 |
project_id: currentProjectId,
|
| 596 |
analysis_model: analysisModel,
|
| 597 |
+
gen_model: genModel,
|
| 598 |
+
analysis_prompt: analysisPrompt,
|
| 599 |
+
gen_prompt: genPrompt
|
| 600 |
})
|
| 601 |
});
|
| 602 |
|
|
|
|
| 619 |
const res = await fetch(`/webtoon-conti/api/projects/${projectId}`);
|
| 620 |
const data = await res.json();
|
| 621 |
|
| 622 |
+
// ํ๋กฌํํธ ์ ๋ณด ๋ก๋
|
| 623 |
+
if (data.project) {
|
| 624 |
+
if (data.project.analysis_prompt) {
|
| 625 |
+
document.getElementById('analysisPrompt').value = data.project.analysis_prompt;
|
| 626 |
+
}
|
| 627 |
+
if (data.project.gen_prompt) {
|
| 628 |
+
document.getElementById('genPrompt').value = data.project.gen_prompt;
|
| 629 |
+
}
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
document.getElementById('panelListHeader').classList.remove('d-none');
|
| 633 |
document.getElementById('panelListHeader').classList.add('d-flex');
|
| 634 |
document.getElementById('panelCount').innerText = data.panels.length;
|
|
|
|
| 706 |
|
| 707 |
async function generateSamples(panelId) {
|
| 708 |
const model = document.getElementById('genModelSelect').value;
|
| 709 |
+
const genPrompt = document.getElementById('genPrompt').value;
|
| 710 |
const container = document.getElementById(`samples-${panelId}`);
|
| 711 |
|
| 712 |
const isDirectAI = model && (model.toLowerCase().includes('imagen') || model.toLowerCase().includes('gemini'));
|
|
|
|
| 725 |
body: JSON.stringify({
|
| 726 |
panel_id: panelId,
|
| 727 |
sample_count: 3,
|
| 728 |
+
model: model,
|
| 729 |
+
gen_prompt: genPrompt
|
| 730 |
})
|
| 731 |
});
|
| 732 |
|
|
|
|
| 855 |
modal.show();
|
| 856 |
}
|
| 857 |
|
| 858 |
+
async function loadGlobalPrompts() {
|
| 859 |
+
try {
|
| 860 |
+
const res = await fetch('/webtoon-conti/api/global-prompts');
|
| 861 |
+
const data = await res.json();
|
| 862 |
+
if (data.analysis_prompt) document.getElementById('analysisPrompt').value = data.analysis_prompt;
|
| 863 |
+
if (data.gen_prompt) document.getElementById('genPrompt').value = data.gen_prompt;
|
| 864 |
+
} catch (e) {
|
| 865 |
+
console.error("Global prompts load error:", e);
|
| 866 |
+
}
|
| 867 |
+
}
|
| 868 |
+
|
| 869 |
+
async function savePrompts() {
|
| 870 |
+
const analysisPrompt = document.getElementById('analysisPrompt').value;
|
| 871 |
+
const genPrompt = document.getElementById('genPrompt').value;
|
| 872 |
+
|
| 873 |
+
try {
|
| 874 |
+
// 1. ๋จผ์ ์ ์ญ ์ค์ ์ผ๋ก ์ ์ฅ (ํ๋ก์ ํธ ์ ๋ฌด์ ๊ด๊ณ์์ด)
|
| 875 |
+
await fetch('/webtoon-conti/api/global-prompts', {
|
| 876 |
+
method: 'POST',
|
| 877 |
+
headers: {'Content-Type': 'application/json'},
|
| 878 |
+
body: JSON.stringify({
|
| 879 |
+
analysis_prompt: analysisPrompt,
|
| 880 |
+
gen_prompt: genPrompt
|
| 881 |
+
})
|
| 882 |
+
});
|
| 883 |
+
|
| 884 |
+
// 2. ํ์ฌ ์์
์ค์ธ ํ๋ก์ ํธ๊ฐ ์๋ค๋ฉด ํ๋ก์ ํธ์๋ ์ ์ฅ
|
| 885 |
+
if (currentProjectId) {
|
| 886 |
+
await fetch(`/webtoon-conti/api/projects/${currentProjectId}/prompts`, {
|
| 887 |
+
method: 'PUT',
|
| 888 |
+
headers: {'Content-Type': 'application/json'},
|
| 889 |
+
body: JSON.stringify({
|
| 890 |
+
analysis_prompt: analysisPrompt,
|
| 891 |
+
gen_prompt: genPrompt
|
| 892 |
+
})
|
| 893 |
+
});
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
alert("ํ๋กฌํํธ ์ค์ ์ด ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ ์ฅ๋์์ต๋๋ค.");
|
| 897 |
+
} catch (e) {
|
| 898 |
+
alert("ํ๋กฌํํธ ์ ์ฅ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: " + e.message);
|
| 899 |
+
}
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
function loadProjectAndCloseModal(id) {
|
| 903 |
const modalEl = document.getElementById('loadProjectModal');
|
| 904 |
const modal = bootstrap.Modal.getInstance(modalEl);
|
|
|
|
| 915 |
document.addEventListener('DOMContentLoaded', () => {
|
| 916 |
loadModels();
|
| 917 |
loadNovelProjects();
|
| 918 |
+
loadGlobalPrompts(); // ์ ์ญ ํ๋กฌํํธ ๋ถ๋ฌ์ค๊ธฐ ์ถ๊ฐ
|
| 919 |
});
|
| 920 |
</script>
|
| 921 |
{% endblock %}
|
templates/webtoon_line_art_challenge.html
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "creation_base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}์นํฐ ์ ํ ๋์ - SOYMEDIA{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block nav %}
|
| 6 |
+
{% set admin_nav_title = '์นํฐ ์ ํ ๋์ ' %}
|
| 7 |
+
{% set admin_nav_icon = '๐จ' %}
|
| 8 |
+
{% include '_admin_nav.html' %}
|
| 9 |
+
{% endblock %}
|
| 10 |
+
|
| 11 |
+
{% block extra_css %}
|
| 12 |
+
<style>
|
| 13 |
+
.character-card {
|
| 14 |
+
border: 1px solid var(--border);
|
| 15 |
+
border-radius: 12px;
|
| 16 |
+
background: white;
|
| 17 |
+
margin-bottom: 1.5rem;
|
| 18 |
+
overflow: hidden;
|
| 19 |
+
transition: all 0.3s;
|
| 20 |
+
}
|
| 21 |
+
.character-card:hover {
|
| 22 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
| 23 |
+
transform: translateY(-2px);
|
| 24 |
+
}
|
| 25 |
+
.character-header {
|
| 26 |
+
padding: 1rem 1.5rem;
|
| 27 |
+
background: #f8f9fa;
|
| 28 |
+
border-bottom: 1px solid var(--border);
|
| 29 |
+
display: flex;
|
| 30 |
+
justify-content: space-between;
|
| 31 |
+
align-items: center;
|
| 32 |
+
}
|
| 33 |
+
.character-body {
|
| 34 |
+
padding: 1.5rem;
|
| 35 |
+
}
|
| 36 |
+
.gen-input-column {
|
| 37 |
+
display: flex;
|
| 38 |
+
flex-direction: column;
|
| 39 |
+
}
|
| 40 |
+
#genPrompt {
|
| 41 |
+
flex-grow: 1;
|
| 42 |
+
min-height: 400px;
|
| 43 |
+
}
|
| 44 |
+
.result-image-container {
|
| 45 |
+
width: 100%;
|
| 46 |
+
max-width: 800px;
|
| 47 |
+
margin: 0 auto;
|
| 48 |
+
border-radius: 12px;
|
| 49 |
+
overflow: hidden;
|
| 50 |
+
background: #f0f0f0;
|
| 51 |
+
min-height: 400px;
|
| 52 |
+
display: flex;
|
| 53 |
+
align-items: center;
|
| 54 |
+
justify-content: center;
|
| 55 |
+
border: 1px solid var(--border);
|
| 56 |
+
box-shadow: var(--shadow);
|
| 57 |
+
}
|
| 58 |
+
.result-image {
|
| 59 |
+
max-width: 100%;
|
| 60 |
+
height: auto;
|
| 61 |
+
display: block;
|
| 62 |
+
}
|
| 63 |
+
.character-item {
|
| 64 |
+
display: flex;
|
| 65 |
+
align-items: center;
|
| 66 |
+
gap: 12px;
|
| 67 |
+
padding: 12px !important;
|
| 68 |
+
transition: background 0.2s;
|
| 69 |
+
}
|
| 70 |
+
.character-item:hover {
|
| 71 |
+
background-color: #f0f7ff;
|
| 72 |
+
}
|
| 73 |
+
.character-item-img {
|
| 74 |
+
width: 50px;
|
| 75 |
+
height: 50px;
|
| 76 |
+
border-radius: 8px;
|
| 77 |
+
object-fit: cover;
|
| 78 |
+
background: #eee;
|
| 79 |
+
flex-shrink: 0;
|
| 80 |
+
}
|
| 81 |
+
.character-item-info {
|
| 82 |
+
flex-grow: 1;
|
| 83 |
+
overflow: hidden;
|
| 84 |
+
}
|
| 85 |
+
.character-item-name {
|
| 86 |
+
font-weight: 600;
|
| 87 |
+
font-size: 14px;
|
| 88 |
+
color: var(--text-primary);
|
| 89 |
+
margin-bottom: 2px;
|
| 90 |
+
}
|
| 91 |
+
.character-item-desc {
|
| 92 |
+
font-size: 12px;
|
| 93 |
+
color: var(--text-secondary);
|
| 94 |
+
white-space: nowrap;
|
| 95 |
+
text-overflow: ellipsis;
|
| 96 |
+
overflow: hidden;
|
| 97 |
+
}
|
| 98 |
+
.character-img-gallery {
|
| 99 |
+
display: flex;
|
| 100 |
+
gap: 4px;
|
| 101 |
+
margin-top: 4px;
|
| 102 |
+
overflow-x: auto;
|
| 103 |
+
padding-bottom: 4px;
|
| 104 |
+
}
|
| 105 |
+
.character-img-gallery img {
|
| 106 |
+
width: 30px;
|
| 107 |
+
height: 30px;
|
| 108 |
+
border-radius: 4px;
|
| 109 |
+
object-fit: cover;
|
| 110 |
+
cursor: pointer;
|
| 111 |
+
border: 1px solid transparent;
|
| 112 |
+
}
|
| 113 |
+
.character-img-gallery img:hover {
|
| 114 |
+
border-color: var(--accent);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/* ๊ทธ๋ฆผ ์ฝํฐ ๊ฐค๋ฌ๋ฆฌ ์คํ์ผ */
|
| 118 |
+
.conty-gallery {
|
| 119 |
+
display: grid;
|
| 120 |
+
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
| 121 |
+
gap: 12px;
|
| 122 |
+
margin-top: 12px;
|
| 123 |
+
}
|
| 124 |
+
.conty-item {
|
| 125 |
+
position: relative;
|
| 126 |
+
aspect-ratio: 1;
|
| 127 |
+
border-radius: 8px;
|
| 128 |
+
overflow: hidden;
|
| 129 |
+
border: 2px solid transparent;
|
| 130 |
+
cursor: pointer;
|
| 131 |
+
transition: all 0.2s;
|
| 132 |
+
}
|
| 133 |
+
.conty-item:hover {
|
| 134 |
+
transform: scale(1.02);
|
| 135 |
+
}
|
| 136 |
+
.conty-item.selected {
|
| 137 |
+
border-color: var(--accent);
|
| 138 |
+
box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
|
| 139 |
+
}
|
| 140 |
+
.conty-item img {
|
| 141 |
+
width: 100%;
|
| 142 |
+
height: 100%;
|
| 143 |
+
object-fit: cover;
|
| 144 |
+
}
|
| 145 |
+
.conty-item .conty-remove {
|
| 146 |
+
position: absolute;
|
| 147 |
+
top: 4px;
|
| 148 |
+
right: 4px;
|
| 149 |
+
background: rgba(220, 35, 67, 0.8);
|
| 150 |
+
color: white;
|
| 151 |
+
border: none;
|
| 152 |
+
border-radius: 4px;
|
| 153 |
+
width: 20px;
|
| 154 |
+
height: 20px;
|
| 155 |
+
font-size: 12px;
|
| 156 |
+
display: flex;
|
| 157 |
+
align-items: center;
|
| 158 |
+
justify-content: center;
|
| 159 |
+
opacity: 0;
|
| 160 |
+
transition: opacity 0.2s;
|
| 161 |
+
}
|
| 162 |
+
.conty-item:hover .conty-remove {
|
| 163 |
+
opacity: 1;
|
| 164 |
+
}
|
| 165 |
+
.conty-selection-badge {
|
| 166 |
+
position: absolute;
|
| 167 |
+
bottom: 4px;
|
| 168 |
+
left: 4px;
|
| 169 |
+
background: var(--accent);
|
| 170 |
+
color: white;
|
| 171 |
+
padding: 2px 6px;
|
| 172 |
+
border-radius: 4px;
|
| 173 |
+
font-size: 10px;
|
| 174 |
+
font-weight: bold;
|
| 175 |
+
display: none;
|
| 176 |
+
}
|
| 177 |
+
.conty-item.selected .conty-selection-badge {
|
| 178 |
+
display: block;
|
| 179 |
+
}
|
| 180 |
+
</style>
|
| 181 |
+
{% endblock %}
|
| 182 |
+
|
| 183 |
+
{% block content %}
|
| 184 |
+
<!-- ์๋จ ํค๋ ์์ญ -->
|
| 185 |
+
<div class="dashboard-header d-flex justify-content-between align-items-center mb-4">
|
| 186 |
+
<div>
|
| 187 |
+
<h1 class="title-main mb-1">์นํฐ ์ ํ ๋์ </h1>
|
| 188 |
+
<p class="text-meta mb-0">์นํฐ์ ์ ํ ์คํ์ผ์ ์คํํ๊ณ ๊ณ ํ๋ฆฌํฐ ๋ผ์ธ ์ํธ๋ฅผ ์์ฑํฉ๋๋ค. <br />
|
| 189 |
+
* '์ ํ ์์ฑ ํ๋กฌํํธ'์ ์์ฃผ ์ฌ์ฉํ์๋ ๋ช
๋ น์ด๋ ์ค์ ์ ์ ์ฅํ์ค ์ ์์ต๋๋ค. <br />
|
| 190 |
+
* ์บ๋ฆญํฐ ๏ฟฝ๏ฟฝ์ ๋ชจ๋ธ์ ์ฌ์ฉํ์ฌ ์์ ์ค์ ์ ๋ฐํ์ผ๋ก ์ต์ ํ๋ ์ ํ ํ๋กฌํํธ๋ฅผ ๊ตฌ์ฑํ ์ ์์ต๋๋ค.
|
| 191 |
+
</p>
|
| 192 |
+
</div>
|
| 193 |
+
</div>
|
| 194 |
+
|
| 195 |
+
<!-- AI ์ค์ ๋ฐ -->
|
| 196 |
+
<div class="clean-card mb-4 py-2 px-4 d-flex align-items-center gap-4" style="cursor: default;">
|
| 197 |
+
<div class="d-flex align-items-center gap-2">
|
| 198 |
+
<label for="analysisModelSelect" class="small fw-bold text-secondary mb-0">
|
| 199 |
+
<i class="fas fa-search me-1 text-primary"></i> ๋ถ์ ๋ชจ๋ธ:
|
| 200 |
+
</label>
|
| 201 |
+
<select id="analysisModelSelect" class="form-select form-select-sm border-0 bg-transparent shadow-none" style="width: auto; min-width: 180px; font-weight: 600;">
|
| 202 |
+
<option value="">๋ชจ๋ธ ๋ก๋ฉ ์ค...</option>
|
| 203 |
+
</select>
|
| 204 |
+
</div>
|
| 205 |
+
<div class="vr"></div>
|
| 206 |
+
<div class="d-flex align-items-center gap-2">
|
| 207 |
+
<label for="modelSelect" class="small fw-bold text-secondary mb-0">
|
| 208 |
+
<i class="fas fa-paint-brush me-1 text-success"></i> ์์ฑ ๋ชจ๋ธ:
|
| 209 |
+
</label>
|
| 210 |
+
<select id="modelSelect" class="form-select form-select-sm border-0 bg-transparent shadow-none" style="width: auto; min-width: 180px; font-weight: 600;">
|
| 211 |
+
<option value="">๋ชจ๋ธ ๋ก๋ฉ ์ค...</option>
|
| 212 |
+
</select>
|
| 213 |
+
</div>
|
| 214 |
+
<button class="btn btn-sm text-muted p-0 hover-rotate" onclick="loadModels()">
|
| 215 |
+
<i class="fas fa-sync-alt"></i>
|
| 216 |
+
</button>
|
| 217 |
+
</div>
|
| 218 |
+
|
| 219 |
+
<div class="row g-4 align-items-stretch mb-5">
|
| 220 |
+
<!-- ์ผ์ชฝ: ํ๋กฌํํธ ์
๋ ฅ -->
|
| 221 |
+
<div class="col-lg-8 gen-input-column">
|
| 222 |
+
<div class="clean-card h-100 p-4">
|
| 223 |
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
| 224 |
+
<h2 class="h5 fw-bold mb-0">์ ํ ์์ฑ ํ๋กฌํํธ</h2>
|
| 225 |
+
<div class="d-flex gap-2">
|
| 226 |
+
<button class="btn btn-sm btn-outline-primary" onclick="saveGlobalPrompt()">
|
| 227 |
+
<i class="fas fa-save me-1"></i> ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ ์ฅ
|
| 228 |
+
</button>
|
| 229 |
+
</div>
|
| 230 |
+
</div>
|
| 231 |
+
<textarea id="genPrompt" class="form-control border-0 bg-light p-4"
|
| 232 |
+
style="border-radius: 12px; font-size: 14px; line-height: 1.6; min-height: 300px;"
|
| 233 |
+
placeholder="์์ฑํ ์ ํ์ ์คํ์ผ, ๊ตฌ๋, ์บ๋ฆญํฐ ํน์ง ๋ฑ์ ์
๋ ฅํ์ธ์."></textarea>
|
| 234 |
+
|
| 235 |
+
<!-- ๊ทธ๋ฆผ ์ฝํฐ ์
๋ก๋ ๋ฐ ๊ด๋ฆฌ ์น์
-->
|
| 236 |
+
<div class="mt-4 pt-4 border-top">
|
| 237 |
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
| 238 |
+
<h3 class="h6 fw-bold mb-0"><i class="fas fa-images me-2 text-primary"></i>์ฌ์ฉํ ๊ทธ๋ฆผ ์ฝํฐ (์ฐธ์กฐ ์ด๋ฏธ์ง)</h3>
|
| 239 |
+
<div class="d-flex gap-2">
|
| 240 |
+
<input type="file" id="contyUpload" class="d-none" accept="image/*" multiple onchange="uploadContyImages(this.files)">
|
| 241 |
+
<button class="btn btn-sm btn-outline-secondary" onclick="document.getElementById('contyUpload').click()">
|
| 242 |
+
<i class="fas fa-upload me-1"></i> ์ฝํฐ ์
๋ก๋
|
| 243 |
+
</button>
|
| 244 |
+
</div>
|
| 245 |
+
</div>
|
| 246 |
+
<div id="contyGallery" class="conty-gallery">
|
| 247 |
+
<div class="text-center w-100 py-4 text-muted border rounded-3 bg-light" style="grid-column: 1/-1;">
|
| 248 |
+
<i class="fas fa-cloud-upload-alt fa-2x mb-2 d-block"></i>
|
| 249 |
+
์
๋ก๋๋ ์ฝํฐ๊ฐ ์์ต๋๋ค. ์ ํ ์ ์์ ๊ธฐ๋ฐ์ด ๋ ๊ทธ๋ฆผ ์ฝํฐ๋ฅผ ์
๋ก๋ํด์ฃผ์ธ์.
|
| 250 |
+
</div>
|
| 251 |
+
</div>
|
| 252 |
+
<p class="text-muted small mt-2"><i class="fas fa-info-circle me-1"></i> ์
๋ก๋๋ ์ฝํฐ ์ค ํ๋๋ฅผ ์ ํํ๋ฉด ์ ํ ์์ฑ ์ ์ฐธ์กฐ ์ด๋ฏธ์ง๋ก ์ฌ์ฉ๋ฉ๋๋ค.</p>
|
| 253 |
+
</div>
|
| 254 |
+
|
| 255 |
+
<div class="mt-4">
|
| 256 |
+
<button id="generateBtn" class="db-btn db-btn-primary w-100 py-3 fw-bold" onclick="generateLineArt()">
|
| 257 |
+
<i class="fas fa-wand-sparkles me-2"></i> ์ ํ ์ด๋ฏธ์ง ์์ฑ ์์
|
| 258 |
+
</button>
|
| 259 |
+
</div>
|
| 260 |
+
</div>
|
| 261 |
+
</div>
|
| 262 |
+
|
| 263 |
+
<!-- ์ค๋ฅธ์ชฝ: ์ค์ ๋ฐ ์ฐธ์กฐ -->
|
| 264 |
+
<div class="col-lg-4">
|
| 265 |
+
<div class="clean-card p-4 h-100">
|
| 266 |
+
<h2 class="h5 fw-bold mb-4">์ฐธ์กฐ ์ค์ </h2>
|
| 267 |
+
|
| 268 |
+
<div class="mb-4">
|
| 269 |
+
<label class="form-label small fw-bold text-secondary">์ฐธ์กฐ ์น์์ค ํ๋ก์ ํธ</label>
|
| 270 |
+
<select id="projectSelect" class="form-select border-0 shadow-sm mb-2" onchange="loadCharacters(this.value)">
|
| 271 |
+
<option value="">-- ํ๋ก์ ํธ ์ ํ --</option>
|
| 272 |
+
</select>
|
| 273 |
+
<p class="text-muted small mb-0"><i class="fas fa-info-circle me-1"></i> ํ๋ก์ ํธ๋ฅผ ์ ํํ๋ฉด ๋ฑ๋ก๋ ๋ฉ์ธ ์บ๋ฆญํฐ ๋ชฉ๋ก์ด ๋ํ๋ฉ๋๋ค.</p>
|
| 274 |
+
</div>
|
| 275 |
+
|
| 276 |
+
<div class="mb-4">
|
| 277 |
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
| 278 |
+
<label class="form-label small fw-bold text-secondary mb-0">์บ๋ฆญํฐ ๋ชฉ๋ก</label>
|
| 279 |
+
<div class="form-check form-switch">
|
| 280 |
+
<input class="form-check-input" type="checkbox" id="includeCharImages" checked>
|
| 281 |
+
<label class="form-check-label small text-muted" for="includeCharImages">์ด๋ฏธ์ง ์ ๋ณด ํฌํจ</label>
|
| 282 |
+
</div>
|
| 283 |
+
</div>
|
| 284 |
+
<div id="characterList" class="bg-light border rounded p-2" style="max-height: 300px; overflow-y: auto; min-height: 100px;">
|
| 285 |
+
<p class="text-muted small mb-0 p-2 text-center">ํ๋ก์ ํธ๋ฅผ ๋จผ์ ์ ํํด์ฃผ์ธ์.</p>
|
| 286 |
+
</div>
|
| 287 |
+
</div>
|
| 288 |
+
</div>
|
| 289 |
+
</div>
|
| 290 |
+
</div>
|
| 291 |
+
|
| 292 |
+
<!-- ํ๋จ ๊ฒฐ๊ณผ ์์ญ -->
|
| 293 |
+
<div id="resultArea" class="d-none mb-5">
|
| 294 |
+
<div class="clean-card p-4">
|
| 295 |
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
| 296 |
+
<h2 class="h5 fw-bold mb-0">์ ํ ์์ฑ ๊ฒฐ๊ณผ</h2>
|
| 297 |
+
<a id="downloadLink" href="#" class="db-btn db-btn-outline py-2" download>
|
| 298 |
+
<i class="fas fa-download"></i> ์ด๋ฏธ์ง ๋ค์ด๋ก๋
|
| 299 |
+
</a>
|
| 300 |
+
</div>
|
| 301 |
+
<div class="result-image-container" id="imageContainer">
|
| 302 |
+
<div class="text-center py-5">
|
| 303 |
+
<div class="spinner-border text-primary mb-3" role="status"></div>
|
| 304 |
+
<p class="text-muted mb-0" id="loadingText">์ด๋ฏธ์ง๋ฅผ ์์ฑํ๊ณ ์์ต๋๋ค. ์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์...</p>
|
| 305 |
+
</div>
|
| 306 |
+
</div>
|
| 307 |
+
</div>
|
| 308 |
+
</div>
|
| 309 |
+
|
| 310 |
+
<script>
|
| 311 |
+
let currentFileId = null;
|
| 312 |
+
const defaultAnalysisModel = "{{ default_analysis_model }}";
|
| 313 |
+
let uploadedContyImages = [];
|
| 314 |
+
let selectedContyImageUrl = null;
|
| 315 |
+
|
| 316 |
+
async function loadModels() {
|
| 317 |
+
const genSelect = document.getElementById('modelSelect');
|
| 318 |
+
const analysisSelect = document.getElementById('analysisModelSelect');
|
| 319 |
+
try {
|
| 320 |
+
const response = await fetch('/api/ollama/models?all=true');
|
| 321 |
+
const data = await response.json();
|
| 322 |
+
|
| 323 |
+
if (data.models && data.models.length > 0) {
|
| 324 |
+
const optionsHtml = ['<option value="">๋ชจ๋ธ ์ ํ...</option>'];
|
| 325 |
+
const analysisOptionsHtml = ['<option value="">๋ชจ๋ธ ์ ํ...</option>'];
|
| 326 |
+
|
| 327 |
+
// Gemini ๋ชจ๋ธ ๊ทธ๋ฃน
|
| 328 |
+
const geminiModels = data.models.filter(m => m.type === 'gemini');
|
| 329 |
+
if (geminiModels.length > 0) {
|
| 330 |
+
optionsHtml.push('<optgroup label="โจ Google Gemini">');
|
| 331 |
+
analysisOptionsHtml.push('<optgroup label="โจ Google Gemini">');
|
| 332 |
+
geminiModels.forEach(m => {
|
| 333 |
+
const label = m.name.startsWith('gemini:') ? m.name.substring(7) : m.name;
|
| 334 |
+
const genSelected = m.name.includes('gemini-3-pro-image-preview') ? 'selected' : '';
|
| 335 |
+
|
| 336 |
+
// ๋ถ์ ๋ชจ๋ธ ๊ธฐ๋ณธ๊ฐ ์ค์
|
| 337 |
+
let analysisSelected = '';
|
| 338 |
+
if (defaultAnalysisModel) {
|
| 339 |
+
if (m.name === defaultAnalysisModel || m.name === `gemini:${defaultAnalysisModel}`) {
|
| 340 |
+
analysisSelected = 'selected';
|
| 341 |
+
}
|
| 342 |
+
} else if (m.name.includes('gemini-1.5-flash')) {
|
| 343 |
+
analysisSelected = 'selected';
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
optionsHtml.push(`<option value="${m.name}" ${genSelected}>${label}</option>`);
|
| 347 |
+
analysisOptionsHtml.push(`<option value="${m.name}" ${analysisSelected}>${label}</option>`);
|
| 348 |
+
});
|
| 349 |
+
optionsHtml.push('</optgroup>');
|
| 350 |
+
analysisOptionsHtml.push('</optgroup>');
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
// Ollama ๋ชจ๋ธ ๊ทธ๋ฃน
|
| 354 |
+
const ollamaModels = data.models.filter(m => m.type !== 'gemini');
|
| 355 |
+
if (ollamaModels.length > 0) {
|
| 356 |
+
optionsHtml.push('<optgroup label="๐ค Ollama (Local)">');
|
| 357 |
+
analysisOptionsHtml.push('<optgroup label="๐ค Ollama (Local)">');
|
| 358 |
+
ollamaModels.forEach(m => {
|
| 359 |
+
let analysisSelected = (m.name === defaultAnalysisModel) ? 'selected' : '';
|
| 360 |
+
optionsHtml.push(`<option value="${m.name}">${m.name}</option>`);
|
| 361 |
+
analysisOptionsHtml.push(`<option value="${m.name}" ${analysisSelected}>${m.name}</option>`);
|
| 362 |
+
});
|
| 363 |
+
optionsHtml.push('</optgroup>');
|
| 364 |
+
analysisOptionsHtml.push('</optgroup>');
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
genSelect.innerHTML = optionsHtml.join('');
|
| 368 |
+
analysisSelect.innerHTML = analysisOptionsHtml.join('');
|
| 369 |
+
}
|
| 370 |
+
} catch (error) {
|
| 371 |
+
console.error('๋ชจ๋ธ ๋ก๋ ์ค๋ฅ:', error);
|
| 372 |
+
genSelect.innerHTML = '<option value="">๋ก๋ ์คํจ</option>';
|
| 373 |
+
analysisSelect.innerHTML = '<option value="">๋ก๋ ์คํจ</option>';
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
async function loadNovelProjects() {
|
| 378 |
+
const select = document.getElementById('projectSelect');
|
| 379 |
+
try {
|
| 380 |
+
const res = await fetch('/api/files?sort=uploaded_at_desc&public_only=true');
|
| 381 |
+
const data = await res.json();
|
| 382 |
+
|
| 383 |
+
if (data.files && data.files.length > 0) {
|
| 384 |
+
data.files.forEach(f => {
|
| 385 |
+
const opt = document.createElement('option');
|
| 386 |
+
opt.value = f.id;
|
| 387 |
+
opt.textContent = f.original_filename;
|
| 388 |
+
select.appendChild(opt);
|
| 389 |
+
});
|
| 390 |
+
}
|
| 391 |
+
} catch (e) {
|
| 392 |
+
console.error("Project load error:", e);
|
| 393 |
+
}
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
async function loadCharacters(fileId) {
|
| 397 |
+
if (!fileId) return;
|
| 398 |
+
currentFileId = fileId;
|
| 399 |
+
const list = document.getElementById('characterList');
|
| 400 |
+
const promptArea = document.getElementById('genPrompt');
|
| 401 |
+
list.innerHTML = '<div class="text-center p-3"><div class="spinner-border spinner-border-sm text-primary"></div></div>';
|
| 402 |
+
|
| 403 |
+
try {
|
| 404 |
+
const res = await fetch('/creation/api/character/extract-main', {
|
| 405 |
+
method: 'POST',
|
| 406 |
+
headers: {'Content-Type': 'application/json'},
|
| 407 |
+
body: JSON.stringify({ file_id: fileId })
|
| 408 |
+
});
|
| 409 |
+
const data = await res.json();
|
| 410 |
+
|
| 411 |
+
// ์ ์ญ ๋ณ์์ ํ๋ก์ ํธ ๋ฐฐ๊ฒฝ ์ ๋ณด ์ ์ฅ (๋ถ์ ์ ์ฌ์ฉ)
|
| 412 |
+
window.projectContext = `์ธ๊ณ๊ด: ${data.world_view || '์ ๋ณด ์์'}\n์ค๊ฑฐ๋ฆฌ: ${data.story || '์ ๋ณด ์์'}\n๊ธฐํ: ${data.others || '์ ๋ณด ์์'}`;
|
| 413 |
+
|
| 414 |
+
// 1. Parent Chunk ์ ๋ณด(์ธ๊ณ๊ด, ๊ธฐํ) ๋ฐ ์บ๋ฆญํฐ ์ ๋ณด๋ฅผ ํ๋กฌํํธ์ ์ถ๊ฐ
|
| 415 |
+
let contextPrompt = '';
|
| 416 |
+
if (data.world_view || data.others) {
|
| 417 |
+
if (data.world_view) contextPrompt += `### ์ธ๊ณ๊ด ์ค๋ช
\n${data.world_view}\n\n`;
|
| 418 |
+
if (data.others) contextPrompt += `### ๊ธฐํ ์ค์ \n${data.others}\n\n`;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
// 2. ์บ๋ฆญํฐ ๋ชฉ๋ก ์ ๋ณด ์ถ๊ฐ
|
| 422 |
+
if (data.characters && data.characters.length > 0) {
|
| 423 |
+
contextPrompt += `### ์บ๋ฆญํฐ ์ ๋ณด ๋ชฉ๋ก\n`;
|
| 424 |
+
data.characters.forEach(c => {
|
| 425 |
+
contextPrompt += `- [${c.name}]: ${c.description || '์ ๋ณด ์์'}\n`;
|
| 426 |
+
});
|
| 427 |
+
contextPrompt += `\n`;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
if (contextPrompt && confirm("์ ํํ ํ๋ก์ ํธ์ '์ธ๊ณ๊ด/์ค์ ' ๋ฐ '์บ๋ฆญํฐ ์ ๋ณด'๋ฅผ ์ ํ ํ๋กฌํํธ์ ์ถ๊ฐํ ๊น์?")) {
|
| 431 |
+
promptArea.value = contextPrompt + promptArea.value;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
if (data.characters && data.characters.length > 0) {
|
| 435 |
+
list.innerHTML = data.characters.map((c, idx) => {
|
| 436 |
+
const mainImg = c.image_path ? `/uploads/${c.image_path}` : '';
|
| 437 |
+
const hasImages = c.images && c.images.length > 0;
|
| 438 |
+
|
| 439 |
+
return `
|
| 440 |
+
<div class="border-bottom character-item cursor-default"
|
| 441 |
+
data-char-index="${idx}">
|
| 442 |
+
${mainImg ? `<img src="${mainImg}" class="character-item-img">` : `<div class="character-item-img d-flex align-items-center justify-content-center text-muted"><i class="fas fa-user"></i></div>`}
|
| 443 |
+
<div class="character-item-info">
|
| 444 |
+
<div class="character-item-name">${c.name}</div>
|
| 445 |
+
<div class="character-item-desc text-muted small">${c.description || '์ค๋ช
์์'}</div>
|
| 446 |
+
${hasImages ? `
|
| 447 |
+
<div class="character-img-gallery mt-1">
|
| 448 |
+
${c.images.map(img => `<img src="/uploads/${img.image_path}" onclick="event.stopPropagation(); window.open('/uploads/${img.image_path}', '_blank')" title="์ด๋ฏธ์ง ํฌ๊ฒ ๋ณด๊ธฐ">`).join('')}
|
| 449 |
+
</div>
|
| 450 |
+
` : ''}
|
| 451 |
+
</div>
|
| 452 |
+
</div>
|
| 453 |
+
`;
|
| 454 |
+
}).join('');
|
| 455 |
+
|
| 456 |
+
window.extractedCharacters = data.characters;
|
| 457 |
+
} else {
|
| 458 |
+
list.innerHTML = '<p class="text-muted small mb-0 p-2 text-center">๋ถ์๋ ์บ๋ฆญํฐ๊ฐ ์์ต๋๋ค.</p>';
|
| 459 |
+
}
|
| 460 |
+
} catch (e) {
|
| 461 |
+
list.innerHTML = '<p class="text-danger small mb-0 p-2 text-center">๋ก๋ ์คํจ</p>';
|
| 462 |
+
}
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
async function loadGlobalLineArtPrompt() {
|
| 466 |
+
try {
|
| 467 |
+
const res = await fetch('/creation/api/line-art-challenge/global-prompt');
|
| 468 |
+
const data = await res.json();
|
| 469 |
+
if (data.prompt) {
|
| 470 |
+
document.getElementById('genPrompt').value = data.prompt;
|
| 471 |
+
}
|
| 472 |
+
} catch (e) {
|
| 473 |
+
console.error("Prompt load error:", e);
|
| 474 |
+
}
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
async function uploadContyImages(files) {
|
| 478 |
+
if (!files || files.length === 0) return;
|
| 479 |
+
|
| 480 |
+
for (const file of files) {
|
| 481 |
+
const formData = new FormData();
|
| 482 |
+
formData.append('image', file);
|
| 483 |
+
|
| 484 |
+
try {
|
| 485 |
+
const response = await fetch('/creation/api/upload-image', {
|
| 486 |
+
method: 'POST',
|
| 487 |
+
body: formData
|
| 488 |
+
});
|
| 489 |
+
const data = await response.json();
|
| 490 |
+
|
| 491 |
+
if (data.success) {
|
| 492 |
+
uploadedContyImages.push(data.image_url);
|
| 493 |
+
// ์ฒซ ์ด๋ฏธ์ง๋ฉด ์๋ ์ ํ
|
| 494 |
+
if (!selectedContyImageUrl) {
|
| 495 |
+
selectedContyImageUrl = data.image_url;
|
| 496 |
+
}
|
| 497 |
+
} else {
|
| 498 |
+
alert(`์
๋ก๋ ์คํจ: ${data.error}`);
|
| 499 |
+
}
|
| 500 |
+
} catch (error) {
|
| 501 |
+
console.error('Upload error:', error);
|
| 502 |
+
alert('์
๋ก๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
|
| 503 |
+
}
|
| 504 |
+
}
|
| 505 |
+
renderContyGallery();
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
function renderContyGallery() {
|
| 509 |
+
const gallery = document.getElementById('contyGallery');
|
| 510 |
+
if (uploadedContyImages.length === 0) {
|
| 511 |
+
gallery.innerHTML = `
|
| 512 |
+
<div class="text-center w-100 py-4 text-muted border rounded-3 bg-light" style="grid-column: 1/-1;">
|
| 513 |
+
<i class="fas fa-cloud-upload-alt fa-2x mb-2 d-block"></i>
|
| 514 |
+
์
๋ก๋๋ ์ฝํฐ๊ฐ ์์ต๋๋ค. ์ ํ ์ ์์ ๊ธฐ๋ฐ์ด ๋ ๊ทธ๋ฆผ ์ฝํฐ๋ฅผ ์
๋ก๋ํด์ฃผ์ธ์.
|
| 515 |
+
</div>
|
| 516 |
+
`;
|
| 517 |
+
return;
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
gallery.innerHTML = uploadedContyImages.map(url => `
|
| 521 |
+
<div class="conty-item ${url === selectedContyImageUrl ? 'selected' : ''}" onclick="selectContyImage('${url}')">
|
| 522 |
+
<img src="${url}" alt="Conty">
|
| 523 |
+
<div class="conty-selection-badge">์ ํ๋จ</div>
|
| 524 |
+
<button class="conty-remove" onclick="event.stopPropagation(); removeContyImage('${url}')">
|
| 525 |
+
<i class="fas fa-times"></i>
|
| 526 |
+
</button>
|
| 527 |
+
</div>
|
| 528 |
+
`).join('');
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
function selectContyImage(url) {
|
| 532 |
+
selectedContyImageUrl = (selectedContyImageUrl === url) ? null : url;
|
| 533 |
+
renderContyGallery();
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
function removeContyImage(url) {
|
| 537 |
+
uploadedContyImages = uploadedContyImages.filter(u => u !== url);
|
| 538 |
+
if (selectedContyImageUrl === url) {
|
| 539 |
+
selectedContyImageUrl = null;
|
| 540 |
+
}
|
| 541 |
+
renderContyGallery();
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
async function generateLineArt() {
|
| 545 |
+
const prompt = document.getElementById('genPrompt').value.trim();
|
| 546 |
+
const model = document.getElementById('modelSelect').value;
|
| 547 |
+
const btn = document.getElementById('generateBtn');
|
| 548 |
+
const resultArea = document.getElementById('resultArea');
|
| 549 |
+
const imageContainer = document.getElementById('imageContainer');
|
| 550 |
+
const includeCharImages = document.getElementById('includeCharImages').checked;
|
| 551 |
+
|
| 552 |
+
if (!prompt) return alert("ํ๋กฌํํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.");
|
| 553 |
+
if (!model) return alert("๋ชจ๋ธ์ ์ ํํด์ฃผ์ธ์.");
|
| 554 |
+
|
| 555 |
+
btn.disabled = true;
|
| 556 |
+
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>์ ํ ์์ฑ ์ค...';
|
| 557 |
+
|
| 558 |
+
resultArea.classList.remove('d-none');
|
| 559 |
+
imageContainer.innerHTML = `
|
| 560 |
+
<div class="text-center py-5">
|
| 561 |
+
<div class="spinner-border text-primary mb-3" role="status"></div>
|
| 562 |
+
<p class="text-muted mb-0">๊ณ ํ๋ฆฌํฐ ์ ํ๋ฅผ ์์ฑํ๊ณ ์์ต๋๋ค. ์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์...</p>
|
| 563 |
+
</div>
|
| 564 |
+
`;
|
| 565 |
+
|
| 566 |
+
resultArea.scrollIntoView({ behavior: 'smooth' });
|
| 567 |
+
|
| 568 |
+
// ์ด๋ฏธ์ง URL ๋ฆฌ์คํธ ์์ง
|
| 569 |
+
const imageUrls = [];
|
| 570 |
+
|
| 571 |
+
// 1. ์ ํ๋ ๊ทธ๋ฆผ ์ฝํฐ ์ด๋ฏธ์ง๋ฅผ ์ฒซ ๋ฒ์งธ๋ก ์ถ๊ฐ (๊ฐ์ฅ ์ค์)
|
| 572 |
+
if (selectedContyImageUrl) {
|
| 573 |
+
imageUrls.push(selectedContyImageUrl);
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
// 2. ์บ๋ฆญํฐ ์ด๋ฏธ์ง ์ ๋ณด ํฌํจ ์ ์ถ๊ฐ
|
| 577 |
+
if (includeCharImages && window.extractedCharacters && window.extractedCharacters.length > 0) {
|
| 578 |
+
window.extractedCharacters.forEach(c => {
|
| 579 |
+
if (c.image_path) {
|
| 580 |
+
// ์ค๋ณต ๋ฐฉ์งํ๋ฉฐ ์ถ๊ฐ
|
| 581 |
+
const fullPath = `/uploads/${c.image_path}`;
|
| 582 |
+
if (!imageUrls.includes(fullPath)) {
|
| 583 |
+
imageUrls.push(fullPath);
|
| 584 |
+
}
|
| 585 |
+
}
|
| 586 |
+
});
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
try {
|
| 590 |
+
const res = await fetch('/creation/api/generate-image', {
|
| 591 |
+
method: 'POST',
|
| 592 |
+
headers: {'Content-Type': 'application/json'},
|
| 593 |
+
body: JSON.stringify({
|
| 594 |
+
prompt: prompt,
|
| 595 |
+
model_name: model,
|
| 596 |
+
image_urls: imageUrls // ์์ง๋ ๋ชจ๋ ์ด๋ฏธ์ง URL ์ ๋ฌ
|
| 597 |
+
})
|
| 598 |
+
});
|
| 599 |
+
|
| 600 |
+
const data = await res.json();
|
| 601 |
+
if (data.success) {
|
| 602 |
+
imageContainer.innerHTML = `<img src="${data.image_url}" class="result-image" alt="Generated Line Art">`;
|
| 603 |
+
document.getElementById('downloadLink').href = data.image_url;
|
| 604 |
+
} else {
|
| 605 |
+
alert("์์ฑ ์คํจ: " + (data.error || "์ ์ ์๋ ์ค๋ฅ"));
|
| 606 |
+
imageContainer.innerHTML = '<p class="text-danger small p-3">์ด๋ฏธ์ง ์์ฑ์ ์คํจํ์ต๋๋ค.</p>';
|
| 607 |
+
}
|
| 608 |
+
} catch (e) {
|
| 609 |
+
console.error("Generation error:", e);
|
| 610 |
+
alert("ํต์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.");
|
| 611 |
+
imageContainer.innerHTML = '<p class="text-danger small p-3">ํต์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.</p>';
|
| 612 |
+
} finally {
|
| 613 |
+
btn.disabled = false;
|
| 614 |
+
btn.innerHTML = '<i class="fas fa-wand-sparkles me-2"></i> ์ ํ ์ด๋ฏธ์ง ์์ฑ ์์';
|
| 615 |
+
}
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
async function loadGlobalPrompt() {
|
| 619 |
+
try {
|
| 620 |
+
const res = await fetch('/creation/api/line-art-challenge/global-prompt');
|
| 621 |
+
const data = await res.json();
|
| 622 |
+
if (data.prompt) {
|
| 623 |
+
document.getElementById('genPrompt').value = data.prompt;
|
| 624 |
+
}
|
| 625 |
+
} catch (e) {}
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
async function saveGlobalPrompt() {
|
| 629 |
+
const prompt = document.getElementById('genPrompt').value;
|
| 630 |
+
try {
|
| 631 |
+
const res = await fetch('/creation/api/line-art-challenge/global-prompt', {
|
| 632 |
+
method: 'POST',
|
| 633 |
+
headers: {'Content-Type': 'application/json'},
|
| 634 |
+
body: JSON.stringify({ prompt: prompt })
|
| 635 |
+
});
|
| 636 |
+
if (res.ok) alert("์ ํ ๋์ ๊ธฐ๋ณธ ํ๋กฌํํธ๋ก ์ ์ฅ๋์์ต๋๋ค.");
|
| 637 |
+
} catch (e) {
|
| 638 |
+
alert("์ ์ฅ ์คํจ");
|
| 639 |
+
}
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 643 |
+
loadModels();
|
| 644 |
+
loadNovelProjects();
|
| 645 |
+
loadGlobalPrompt();
|
| 646 |
+
});
|
| 647 |
+
</script>
|
| 648 |
+
{% endblock %}
|