Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -53,13 +53,13 @@ db_lock = threading.Lock()
|
|
53 |
# μ΅μ νλ λ¨κ³ κ΅¬μ± (25λ¨κ³λ‘ μμΆ λ° κ°ν)
|
54 |
OPTIMIZED_STAGES = [
|
55 |
("director", "π¬ κ°λ
μ: μ΄κΈ° κΈ°ν (μΉ κ²μ ν¬ν¨)"),
|
56 |
-
("critic", "π λΉνκ°: κΈ°ν κ²ν (
|
57 |
("director", "π¬ κ°λ
μ: μμ λ λ§μ€ν°νλ"),
|
58 |
] + [
|
59 |
(f"writer{i}", f"βοΈ μκ° {i}: μ΄μ (νμ΄μ§ {(i-1)*3+1}-{i*3})")
|
60 |
for i in range(1, 11)
|
61 |
] + [
|
62 |
-
("critic", "π λΉνκ°: μ€κ° κ²ν (
|
63 |
] + [
|
64 |
(f"writer{i}", f"βοΈ μκ° {i}: μμ λ³Έ (νμ΄μ§ {(i-1)*3+1}-{i*3})")
|
65 |
for i in range(1, 11)
|
@@ -81,6 +81,7 @@ class CharacterState:
|
|
81 |
last_seen_chapter: int = 0
|
82 |
description: str = ""
|
83 |
role: str = ""
|
|
|
84 |
|
85 |
@dataclass
|
86 |
class PlotPoint:
|
@@ -91,6 +92,7 @@ class PlotPoint:
|
|
91 |
characters_involved: List[str]
|
92 |
impact_level: int
|
93 |
timestamp: str = ""
|
|
|
94 |
|
95 |
@dataclass
|
96 |
class TimelineEvent:
|
@@ -112,6 +114,7 @@ class ConsistencyTracker:
|
|
112 |
self.locations: Dict[str, str] = {}
|
113 |
self.established_facts: List[str] = []
|
114 |
self.content_hashes: Dict[str, int] = {} # ν΄μμ ν΄λΉ μ±ν° λ²νΈλ₯Ό μ μ₯
|
|
|
115 |
|
116 |
def register_character(self, character: CharacterState):
|
117 |
"""μ μΊλ¦ν° λ±λ‘"""
|
@@ -175,8 +178,12 @@ class ConsistencyTracker:
|
|
175 |
for char in active_chars:
|
176 |
status = "μμ‘΄" if char.alive else "μ¬λ§"
|
177 |
summary += f"β’ {char.name}: {status}"
|
178 |
-
if char.alive and char.location:
|
179 |
-
|
|
|
|
|
|
|
|
|
180 |
summary += "\n"
|
181 |
return summary
|
182 |
|
@@ -187,7 +194,10 @@ class ConsistencyTracker:
|
|
187 |
if not recent_events:
|
188 |
return "\n(μμ§ μ£Όμ μ¬κ±΄μ΄ μμ΅λλ€.)\n"
|
189 |
for event in recent_events[-3:]: # μ΅κ·Ό 3κ°λ§ νμ
|
190 |
-
summary += f"β’ [μ±ν° {event.chapter}] {event.description}
|
|
|
|
|
|
|
191 |
return summary
|
192 |
|
193 |
|
@@ -259,7 +269,8 @@ class NovelDatabase:
|
|
259 |
status TEXT DEFAULT 'active',
|
260 |
current_stage INTEGER DEFAULT 0,
|
261 |
final_novel TEXT,
|
262 |
-
consistency_report TEXT
|
|
|
263 |
)
|
264 |
''')
|
265 |
cursor.execute('''
|
@@ -273,6 +284,7 @@ class NovelDatabase:
|
|
273 |
word_count INTEGER DEFAULT 0,
|
274 |
status TEXT DEFAULT 'pending',
|
275 |
consistency_score REAL DEFAULT 0.0,
|
|
|
276 |
created_at TEXT DEFAULT (datetime('now')),
|
277 |
updated_at TEXT DEFAULT (datetime('now')),
|
278 |
FOREIGN KEY (session_id) REFERENCES sessions(session_id),
|
@@ -290,6 +302,7 @@ class NovelDatabase:
|
|
290 |
injuries TEXT,
|
291 |
emotional_state TEXT,
|
292 |
description TEXT,
|
|
|
293 |
created_at TEXT DEFAULT (datetime('now')),
|
294 |
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
|
295 |
)
|
@@ -325,17 +338,17 @@ class NovelDatabase:
|
|
325 |
@staticmethod
|
326 |
def save_stage(session_id: str, stage_number: int, stage_name: str,
|
327 |
role: str, content: str, status: str = 'complete',
|
328 |
-
consistency_score: float = 0.0):
|
329 |
word_count = len(content.split()) if content else 0
|
330 |
with NovelDatabase.get_db() as conn:
|
331 |
cursor = conn.cursor()
|
332 |
cursor.execute('''
|
333 |
-
INSERT INTO stages (session_id, stage_number, stage_name, role, content, word_count, status, consistency_score)
|
334 |
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
335 |
ON CONFLICT(session_id, stage_number)
|
336 |
-
DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, consistency_score=?, updated_at=datetime('now')
|
337 |
-
''', (session_id, stage_number, stage_name, role, content, word_count, status, consistency_score,
|
338 |
-
content, word_count, status, stage_name, consistency_score))
|
339 |
cursor.execute(
|
340 |
"UPDATE sessions SET updated_at = datetime('now'), current_stage = ? WHERE session_id = ?",
|
341 |
(stage_number, session_id)
|
@@ -374,11 +387,11 @@ class NovelDatabase:
|
|
374 |
return '\n\n'.join(all_content)
|
375 |
|
376 |
@staticmethod
|
377 |
-
def update_final_novel(session_id: str, final_novel: str, consistency_report: str = ""):
|
378 |
with NovelDatabase.get_db() as conn:
|
379 |
conn.cursor().execute(
|
380 |
-
"UPDATE sessions SET final_novel = ?, status = 'complete', updated_at = datetime('now'), consistency_report = ? WHERE session_id = ?",
|
381 |
-
(final_novel, consistency_report, session_id)
|
382 |
)
|
383 |
conn.commit()
|
384 |
|
@@ -406,9 +419,9 @@ class NovelWritingSystem:
|
|
406 |
"""API ν€λ μμ±"""
|
407 |
return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
|
408 |
|
409 |
-
# --- ν둬ννΈ μμ± ν¨μλ€ (
|
410 |
def create_director_initial_prompt(self, user_query: str, language: str) -> str:
|
411 |
-
"""κ°λ
μ μ΄κΈ° κΈ°ν ν둬ννΈ (
|
412 |
search_results_str = ""
|
413 |
if self.web_search.enabled:
|
414 |
queries = [f"{user_query} novel setting", f"{user_query} background information"]
|
@@ -421,67 +434,185 @@ class NovelWritingSystem:
|
|
421 |
"title": "λΉμ μ 30νμ΄μ§ λΆλμ μ€νΈ μμ€μ κΈ°ννλ λ¬Έν κ°λ
μμ
λλ€.",
|
422 |
"user_theme": "μ¬μ©μ μ£Όμ ",
|
423 |
"plan_instruction": "λ€μ μμλ€μ ν¬ν¨ν μμΈν μμ€ κΈ°νμ μμ±νμΈμ:",
|
424 |
-
"theme_section": "1. **μ£Όμ μ
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
"
|
429 |
-
|
430 |
-
|
431 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
432 |
},
|
433 |
"English": {
|
434 |
"title": "You are a literary director planning a 30-page novella.",
|
435 |
"user_theme": "User Theme",
|
436 |
"plan_instruction": "Create a detailed novel plan including:",
|
437 |
-
"theme_section": "1. **Theme and
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
"
|
442 |
-
|
443 |
-
|
444 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
445 |
}
|
446 |
}
|
447 |
p = lang_prompts[language]
|
448 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
449 |
|
450 |
def create_critic_director_prompt(self, director_plan: str, user_query: str, language: str) -> str:
|
451 |
-
"""λΉνκ°μ κ°λ
μ κΈ°ν κ²ν ν둬ννΈ (
|
452 |
lang_prompts = {
|
453 |
"Korean": {
|
454 |
-
"title": "λΉμ μ λ¬Έν λΉνκ°μ
λλ€. κ°λ
μμ μμ€ κΈ°νμ '
|
455 |
-
"theme_check": f"**1.
|
456 |
-
|
457 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
458 |
},
|
459 |
"English": {
|
460 |
-
"title": "You are a literary critic.
|
461 |
-
"theme_check": f"**1.
|
462 |
-
|
463 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
464 |
}
|
465 |
}
|
466 |
p = lang_prompts[language]
|
467 |
-
return f"{p['title']}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
468 |
|
469 |
def create_director_revision_prompt(self, initial_plan: str, critic_feedback: str, user_query: str, language: str) -> str:
|
470 |
-
"""κ°λ
μ μμ ν둬ννΈ
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
476 |
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
482 |
|
483 |
def create_writer_prompt(self, writer_number: int, director_plan: str, previous_content_summary: str, user_query: str, language: str) -> str:
|
484 |
-
"""μκ° ν둬ννΈ (
|
485 |
pages_start = (writer_number - 1) * 3 + 1
|
486 |
pages_end = writer_number * 3
|
487 |
|
@@ -490,69 +621,210 @@ class NovelWritingSystem:
|
|
490 |
"title": f"λΉμ μ μκ° {writer_number}λ²μ
λλ€. μμ€μ {pages_start}-{pages_end} νμ΄μ§λ₯Ό μμ±νμΈμ.",
|
491 |
"plan": "κ°λ
μ λ§μ€ν°νλ",
|
492 |
"prev_summary": "μ΄μ λ΄μ© μμ½",
|
493 |
-
"guidelines": "**μμ±
|
494 |
-
|
495 |
-
|
496 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
497 |
},
|
498 |
"English": {
|
499 |
"title": f"You are Writer #{writer_number}. Write pages {pages_start}-{pages_end} of the novella.",
|
500 |
"plan": "Director's Masterplan",
|
501 |
"prev_summary": "Previous Content Summary",
|
502 |
-
"guidelines": "**Writing Guidelines
|
503 |
-
|
504 |
-
|
505 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
506 |
}
|
507 |
}
|
508 |
|
509 |
p = lang_prompts[language]
|
510 |
consistency_info = self.consistency_tracker.get_character_summary(writer_number) + self.consistency_tracker.get_plot_summary(writer_number)
|
511 |
|
512 |
-
return f"{p['title']}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
513 |
|
514 |
def create_critic_consistency_prompt(self, all_content: str, user_query: str, language: str) -> str:
|
515 |
-
"""λΉνκ° μ€κ° κ²ν ν둬ννΈ
|
516 |
-
|
517 |
-
|
518 |
-
|
519 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
520 |
|
521 |
-
|
522 |
-
1. **μ£Όμ μΌκ΄μ± (κ°μ₯ μ€μ):** λ΄μ©μ΄ μλ μ£Όμ μ μ΄λμ΄ κ°μ μ μμ λ²μ΄λμ§ μμλμ§ νμΈνκ³ , λ²μ΄λ¬λ€λ©΄ μμ λ°©ν₯μ μ μνμΈμ.
|
523 |
-
2. **κΈ°μ μ μΌκ΄μ±:** μΊλ¦ν°, νλ‘―, μ€μ μ μ°μμ±κ³Ό λ
Όλ¦¬μ μ€λ₯λ₯Ό μ°Ύμλ΄μΈμ.
|
524 |
-
3. **λ°λ³΅ λ΄μ©:** μλ―Έμ μΌλ‘ μ€λ³΅λλ μ₯λ©΄μ΄λ ννμ΄ μλμ§ νμΈνμΈμ.
|
525 |
|
526 |
-
|
527 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
528 |
|
529 |
def create_writer_revision_prompt(self, writer_number: int, initial_content: str, consistency_feedback: str, language: str) -> str:
|
530 |
"""μκ° μμ ν둬ννΈ"""
|
531 |
-
|
|
|
|
|
|
|
|
|
532 |
|
533 |
-
|
534 |
-
|
535 |
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
541 |
|
542 |
def create_critic_final_prompt(self, complete_novel: str, language: str) -> str:
|
543 |
"""μ΅μ’
λΉνκ° κ²ν λ° λ³΄κ³ μ μμ± ν둬ννΈ"""
|
544 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
545 |
|
546 |
-
|
|
|
547 |
|
548 |
-
|
549 |
-
1. **μ 체 μΌκ΄μ± νκ°:** μΊλ¦ν°, νλ‘―, μ€μ , μ£Όμ μ μ§μ λν μ μ(1-10)μ μ΄ν.
|
550 |
-
2. **μ΅μ’
λ°κ²¬λ λ¬Έμ μ :** λ¨μμλ μ¬μν λ¬Έμ μ λ€.
|
551 |
-
3. **μ±κ³΅ μμ:** νΉν μ μ μ§λ μΌκ΄μ± λΆλΆμ΄λ μ£Όμ ννμ΄ λ°μ΄λ λΆλΆ.
|
552 |
-
4. **μ΅μ’
νκ°:** μμ€μ μ λ°μ μΈ μμ±λμ λ
μμκ² λ―ΈμΉ μν₯μ λν νκ°.
|
553 |
-
"""
|
554 |
|
555 |
-
|
|
|
|
|
|
|
|
|
556 |
def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str:
|
557 |
"""LLM λκΈ°μ νΈμΆ (μμ½ λ± λ΄λΆμ©)"""
|
558 |
full_content = ""
|
@@ -563,7 +835,7 @@ class NovelWritingSystem:
|
|
563 |
return full_content
|
564 |
|
565 |
def call_llm_streaming(self, messages: List[Dict[str, str]], role: str, language: str) -> Generator[str, None, None]:
|
566 |
-
"""LLM μ€νΈλ¦¬λ° νΈμΆ (
|
567 |
try:
|
568 |
system_prompts = self.get_system_prompts(language)
|
569 |
full_messages = [{"role": "system", "content": system_prompts.get(role, "You are a helpful assistant.")}, *messages]
|
@@ -582,7 +854,6 @@ class NovelWritingSystem:
|
|
582 |
|
583 |
logger.info(f"[{role}] API μ€νΈλ¦¬λ° μμ")
|
584 |
|
585 |
-
# API νΈμΆ
|
586 |
response = requests.post(
|
587 |
self.api_url,
|
588 |
headers=self.create_headers(),
|
@@ -591,7 +862,6 @@ class NovelWritingSystem:
|
|
591 |
timeout=180
|
592 |
)
|
593 |
|
594 |
-
# μν μ½λ νμΈ
|
595 |
if response.status_code != 200:
|
596 |
logger.error(f"API μλ΅ μ€λ₯: {response.status_code}")
|
597 |
logger.error(f"μλ΅ λ΄μ©: {response.text[:500]}")
|
@@ -599,42 +869,36 @@ class NovelWritingSystem:
|
|
599 |
return
|
600 |
|
601 |
response.raise_for_status()
|
602 |
-
|
603 |
-
|
604 |
buffer = ""
|
605 |
total_content = ""
|
606 |
chunk_count = 0
|
607 |
-
|
608 |
-
|
609 |
for line in response.iter_lines():
|
610 |
if not line:
|
611 |
continue
|
612 |
|
613 |
try:
|
614 |
line_str = line.decode('utf-8').strip()
|
615 |
-
|
616 |
-
# SSE νμ νμΈ
|
617 |
if not line_str.startswith("data: "):
|
618 |
continue
|
619 |
|
620 |
-
data_str = line_str[6:]
|
621 |
-
|
622 |
-
# μ€νΈλ¦Ό μ’
λ£ νμΈ
|
623 |
if data_str == "[DONE]":
|
624 |
logger.info(f"[{role}] μ€νΈλ¦¬λ° μλ£ - μ΄ {len(total_content)} λ¬Έμ")
|
625 |
break
|
626 |
-
|
627 |
-
# JSON νμ±
|
628 |
try:
|
629 |
data = json.loads(data_str)
|
630 |
except json.JSONDecodeError:
|
631 |
logger.warning(f"JSON νμ± μ€ν¨: {data_str[:100]}")
|
632 |
continue
|
633 |
-
|
634 |
-
# choices λ°°μ΄ μμ νκ² νμΈ
|
635 |
choices = data.get("choices", None)
|
636 |
if not choices or not isinstance(choices, list) or len(choices) == 0:
|
637 |
-
# μλ¬ μλ΅ νμΈ
|
638 |
if "error" in data:
|
639 |
error_msg = data.get("error", {}).get("message", "Unknown error")
|
640 |
logger.error(f"API μλ¬: {error_msg}")
|
@@ -642,7 +906,6 @@ class NovelWritingSystem:
|
|
642 |
return
|
643 |
continue
|
644 |
|
645 |
-
# deltaμμ content μΆμΆ
|
646 |
delta = choices[0].get("delta", {})
|
647 |
content = delta.get("content", "")
|
648 |
|
@@ -651,25 +914,20 @@ class NovelWritingSystem:
|
|
651 |
total_content += content
|
652 |
chunk_count += 1
|
653 |
|
654 |
-
#
|
655 |
-
if len(buffer) >=
|
656 |
yield buffer
|
657 |
buffer = ""
|
658 |
-
time.sleep(0.01) # UI μ
λ°μ΄νΈλ₯Ό μν
|
659 |
|
660 |
except Exception as e:
|
661 |
-
|
662 |
-
logger.error(f"μ²ν¬ μ²λ¦¬ μ€λ₯ #{error_count}: {str(e)}")
|
663 |
-
if error_count > 10: # λ무 λ§μ μλ¬μ μ€λ¨
|
664 |
-
yield f"β μ€νΈλ¦¬λ° μ€ κ³Όλν μ€λ₯ λ°μ"
|
665 |
-
return
|
666 |
continue
|
667 |
|
668 |
# λ¨μ λ²νΌ μ²λ¦¬
|
669 |
if buffer:
|
670 |
yield buffer
|
671 |
|
672 |
-
# κ²°κ³Ό νμΈ
|
673 |
if chunk_count == 0:
|
674 |
logger.error(f"[{role}] μ½ν
μΈ κ° μ ν μμ λμ§ μμ")
|
675 |
yield "β APIλ‘λΆν° μλ΅μ λ°μ§ λͺ»νμ΅λλ€."
|
@@ -686,22 +944,18 @@ class NovelWritingSystem:
|
|
686 |
logger.error(f"μκΈ°μΉ μμ μ€λ₯: {type(e).__name__}: {str(e)}", exc_info=True)
|
687 |
yield f"β μ€λ₯ λ°μ: {str(e)}"
|
688 |
|
689 |
-
|
690 |
-
|
691 |
-
|
692 |
-
|
693 |
def get_system_prompts(self, language: str) -> Dict[str, str]:
|
694 |
-
"""μν λ³ μμ€ν
ν둬ννΈ μμ±"""
|
695 |
base_prompts = {
|
696 |
"Korean": {
|
697 |
-
"director": "λΉμ μ
|
698 |
-
"critic": "λΉμ μ
|
699 |
-
"writer_base": "λΉμ μ μ λ¬Έ μμ€ μκ°μ
λλ€.
|
700 |
},
|
701 |
"English": {
|
702 |
-
"director": "You are a
|
703 |
-
"critic": "You are a
|
704 |
-
"writer_base": "You are a professional
|
705 |
}
|
706 |
}
|
707 |
|
@@ -709,21 +963,24 @@ class NovelWritingSystem:
|
|
709 |
|
710 |
# μκ°λ³ νΉμ ν둬ννΈ
|
711 |
if language == "Korean":
|
712 |
-
prompts["writer1"] = "λΉμ μ
|
713 |
-
prompts["
|
|
|
714 |
else:
|
715 |
-
prompts["writer1"] = "You are a writer
|
716 |
-
prompts["
|
|
|
717 |
|
718 |
-
# writer2-9λ κΈ°λ³Έ ν둬ννΈ μ¬μ©
|
719 |
for i in range(2, 10):
|
720 |
-
|
|
|
721 |
|
722 |
return prompts
|
723 |
|
724 |
-
# --- λ©μΈ νλ‘μΈμ€ ---
|
725 |
def process_novel_stream(self, query: str, language: str, session_id: Optional[str] = None) -> Generator[Tuple[str, List[Dict[str, Any]], str], None, None]:
|
726 |
-
"""μμ€ μμ± μ€νΈλ¦¬λ° νλ‘μΈμ€ (
|
727 |
try:
|
728 |
resume_from_stage = 0
|
729 |
if session_id:
|
@@ -741,51 +998,80 @@ class NovelWritingSystem:
|
|
741 |
stages = []
|
742 |
if resume_from_stage > 0:
|
743 |
stages = [{
|
744 |
-
"name": s['stage_name'],
|
745 |
-
"
|
|
|
|
|
|
|
746 |
} for s in NovelDatabase.get_stages(self.current_session_id)]
|
747 |
|
748 |
for stage_idx in range(resume_from_stage, len(OPTIMIZED_STAGES)):
|
749 |
role, stage_name = OPTIMIZED_STAGES[stage_idx]
|
750 |
if stage_idx >= len(stages):
|
751 |
-
stages.append({
|
|
|
|
|
|
|
|
|
|
|
|
|
752 |
else:
|
753 |
stages[stage_idx]["status"] = "active"
|
754 |
|
755 |
yield "", stages, self.current_session_id
|
|
|
756 |
prompt = self.get_stage_prompt(stage_idx, role, query, language, stages)
|
757 |
stage_content = ""
|
|
|
|
|
758 |
for chunk in self.call_llm_streaming([{"role": "user", "content": prompt}], role, language):
|
759 |
stage_content += chunk
|
760 |
stages[stage_idx]["content"] = stage_content
|
761 |
-
yield "", stages, self.current_session_id
|
762 |
|
|
|
763 |
consistency_score = 0.0
|
|
|
|
|
764 |
if role.startswith("writer"):
|
765 |
writer_num = int(re.search(r'\d+', role).group())
|
766 |
all_previous = self.get_all_content(stages, stage_idx)
|
767 |
errors = self.consistency_tracker.validate_consistency(writer_num, stage_content)
|
768 |
consistency_score = max(0, 10 - len(errors) * 2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
769 |
stages[stage_idx]["consistency_score"] = consistency_score
|
|
|
770 |
|
771 |
stages[stage_idx]["status"] = "complete"
|
772 |
NovelDatabase.save_stage(
|
773 |
self.current_session_id, stage_idx, stage_name, role,
|
774 |
-
stage_content, "complete", consistency_score
|
775 |
)
|
776 |
yield "", stages, self.current_session_id
|
777 |
|
|
|
778 |
final_novel = NovelDatabase.get_writer_content(self.current_session_id)
|
779 |
final_report = self.generate_consistency_report(final_novel, language)
|
780 |
-
|
781 |
-
|
|
|
|
|
|
|
|
|
|
|
782 |
|
783 |
except Exception as e:
|
784 |
logger.error(f"μμ€ μμ± νλ‘μΈμ€ μ€λ₯: {e}", exc_info=True)
|
785 |
yield f"β μ€λ₯ λ°μ: {e}", stages if 'stages' in locals() else [], self.current_session_id
|
786 |
|
787 |
def get_stage_prompt(self, stage_idx: int, role: str, query: str, language: str, stages: List[Dict]) -> str:
|
788 |
-
"""λ¨κ³λ³ ν둬ννΈ μμ±
|
789 |
if stage_idx == 0:
|
790 |
return self.create_director_initial_prompt(query, language)
|
791 |
if stage_idx == 1:
|
@@ -795,23 +1081,23 @@ class NovelWritingSystem:
|
|
795 |
|
796 |
master_plan = stages[2]["content"]
|
797 |
|
798 |
-
if 3 <= stage_idx <= 12:
|
799 |
writer_num = stage_idx - 2
|
800 |
previous_content = self.get_all_content(stages, stage_idx)
|
801 |
summary = self.create_summary(previous_content, language)
|
802 |
return self.create_writer_prompt(writer_num, master_plan, summary, query, language)
|
803 |
|
804 |
-
if stage_idx == 13:
|
805 |
all_content = self.get_all_content(stages, stage_idx)
|
806 |
return self.create_critic_consistency_prompt(all_content, query, language)
|
807 |
|
808 |
-
if 14 <= stage_idx <= 23:
|
809 |
writer_num = stage_idx - 13
|
810 |
initial_content = stages[2 + writer_num]["content"]
|
811 |
feedback = stages[13]["content"]
|
812 |
return self.create_writer_revision_prompt(writer_num, initial_content, feedback, language)
|
813 |
|
814 |
-
if stage_idx == 24:
|
815 |
complete_novel = self.get_all_writer_content(stages)
|
816 |
return self.create_critic_final_prompt(complete_novel, language)
|
817 |
|
@@ -822,9 +1108,9 @@ class NovelWritingSystem:
|
|
822 |
if not content.strip():
|
823 |
return "μ΄μ λ΄μ©μ΄ μμ΅λλ€." if language == "Korean" else "No previous content."
|
824 |
|
825 |
-
prompt_text = "λ€μ μμ€ λ΄μ©μ 3~5κ°μ ν΅μ¬μ μΈ λ¬Έμ₯μΌλ‘ μμ½ν΄μ€. λ€μ μκ°κ° μ΄μΌκΈ°λ₯Ό μ΄μ΄κ°λ λ° νμν ν΅μ¬ μ 보(λ±μ₯μΈλ¬Όμ νμ¬ μν©, κ°μ , λ§μ§λ§ μ¬κ±΄)λ₯Ό ν¬ν¨ν΄μΌ ν΄."
|
826 |
if language != "Korean":
|
827 |
-
prompt_text = "Summarize the following novel content in 3-5 key sentences. Include crucial information for the next writer to continue the story (characters' current situation, emotions, and the last major event)."
|
828 |
|
829 |
summary_prompt = f"{prompt_text}\n\n---\n{content[-2000:]}"
|
830 |
try:
|
@@ -915,7 +1201,7 @@ def download_novel(novel_text: str, format_type: str, language: str, session_id:
|
|
915 |
|
916 |
try:
|
917 |
if format_type == "DOCX" and DOCX_AVAILABLE:
|
918 |
-
return export_to_docx(novel_text, filename, language)
|
919 |
else:
|
920 |
return export_to_txt(novel_text, filename)
|
921 |
except Exception as e:
|
@@ -928,8 +1214,16 @@ def format_stages_display(stages: List[Dict]) -> str:
|
|
928 |
for i, stage in enumerate(stages):
|
929 |
status_icon = "β
" if stage['status'] == 'complete' else "π" if stage['status'] == 'active' else "β³"
|
930 |
markdown += f"{status_icon} **{stage['name']}**"
|
|
|
|
|
|
|
931 |
if stage.get('consistency_score', 0) > 0:
|
932 |
-
|
|
|
|
|
|
|
|
|
|
|
933 |
markdown += "\n"
|
934 |
if stage['content']:
|
935 |
preview = stage['content'][:200] + "..." if len(stage['content']) > 200 else stage['content']
|
@@ -951,10 +1245,13 @@ def format_novel_display(novel_text: str) -> str:
|
|
951 |
|
952 |
return formatted
|
953 |
|
954 |
-
def export_to_docx(content: str, filename: str, language: str) -> str:
|
955 |
"""DOCX νμΌλ‘ λ΄λ³΄λ΄κΈ°"""
|
956 |
doc = Document()
|
957 |
|
|
|
|
|
|
|
958 |
# μ λͺ© μΆκ°
|
959 |
title = doc.add_heading('AI νμ
μμ€', 0)
|
960 |
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
@@ -962,6 +1259,10 @@ def export_to_docx(content: str, filename: str, language: str) -> str:
|
|
962 |
# λ©νλ°μ΄ν°
|
963 |
doc.add_paragraph(f"μμ±μΌ: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
964 |
doc.add_paragraph(f"μΈμ΄: {language}")
|
|
|
|
|
|
|
|
|
965 |
doc.add_page_break()
|
966 |
|
967 |
# λ³Έλ¬Έ μΆκ°
|
@@ -983,38 +1284,51 @@ def export_to_txt(content: str, filename: str) -> str:
|
|
983 |
return filepath
|
984 |
|
985 |
|
986 |
-
# CSS μ€νμΌ
|
987 |
custom_css = """
|
988 |
.gradio-container {
|
989 |
-
background: linear-gradient(135deg, #
|
990 |
min-height: 100vh;
|
991 |
}
|
992 |
|
993 |
.main-header {
|
994 |
-
background-color: rgba(255, 255, 255, 0.
|
995 |
backdrop-filter: blur(10px);
|
996 |
padding: 30px;
|
997 |
border-radius: 12px;
|
998 |
margin-bottom: 30px;
|
999 |
text-align: center;
|
1000 |
color: white;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1001 |
}
|
1002 |
|
1003 |
.input-section {
|
1004 |
-
background-color: rgba(255, 255, 255, 0.
|
1005 |
backdrop-filter: blur(10px);
|
1006 |
padding: 20px;
|
1007 |
border-radius: 12px;
|
1008 |
margin-bottom: 20px;
|
|
|
1009 |
}
|
1010 |
|
1011 |
.session-section {
|
1012 |
-
background-color: rgba(255, 255, 255, 0.
|
1013 |
backdrop-filter: blur(10px);
|
1014 |
padding: 15px;
|
1015 |
border-radius: 8px;
|
1016 |
margin-top: 20px;
|
1017 |
color: white;
|
|
|
1018 |
}
|
1019 |
|
1020 |
#stages-display {
|
@@ -1023,14 +1337,18 @@ custom_css = """
|
|
1023 |
border-radius: 12px;
|
1024 |
max-height: 600px;
|
1025 |
overflow-y: auto;
|
|
|
1026 |
}
|
1027 |
|
1028 |
#novel-output {
|
1029 |
background-color: rgba(255, 255, 255, 0.95);
|
1030 |
padding: 30px;
|
1031 |
border-radius: 12px;
|
1032 |
-
max-height:
|
1033 |
overflow-y: auto;
|
|
|
|
|
|
|
1034 |
}
|
1035 |
|
1036 |
.download-section {
|
@@ -1038,26 +1356,49 @@ custom_css = """
|
|
1038 |
padding: 15px;
|
1039 |
border-radius: 8px;
|
1040 |
margin-top: 20px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1041 |
}
|
1042 |
"""
|
1043 |
|
1044 |
|
1045 |
# Gradio μΈν°νμ΄μ€ μμ±
|
1046 |
def create_interface():
|
1047 |
-
with gr.Blocks(css=custom_css, title="AI νμ
μμ€ μμ± μμ€ν
") as interface:
|
1048 |
gr.HTML("""
|
1049 |
<div class="main-header">
|
1050 |
<h1 style="font-size: 2.5em; margin-bottom: 10px;">
|
1051 |
π AI νμ
μμ€ μμ± μμ€ν
|
1052 |
</h1>
|
1053 |
<h3 style="color: #ccc; margin-bottom: 20px;">
|
1054 |
-
|
1055 |
</h3>
|
1056 |
<p style="font-size: 1.1em; color: #ddd; max-width: 800px; margin: 0 auto;">
|
1057 |
-
μ£Όμ λ₯Ό μ
λ ₯νλ©΄ AI μμ΄μ νΈλ€μ΄ νμ
νμ¬ 30νμ΄μ§ λΆλμ
|
1058 |
<br>
|
1059 |
-
κ°λ
μ 1λͺ
, λΉνκ° 1λͺ
, μκ° 10λͺ
μ΄ ν¨κ»
|
1060 |
</p>
|
|
|
|
|
|
|
1061 |
</div>
|
1062 |
""")
|
1063 |
|
@@ -1069,13 +1410,13 @@ def create_interface():
|
|
1069 |
with gr.Group(elem_classes=["input-section"]):
|
1070 |
query_input = gr.Textbox(
|
1071 |
label="μμ€ μ£Όμ / Novel Theme",
|
1072 |
-
placeholder="
|
1073 |
lines=4
|
1074 |
)
|
1075 |
|
1076 |
language_select = gr.Radio(
|
1077 |
-
choices=["
|
1078 |
-
value="
|
1079 |
label="μΈμ΄ / Language"
|
1080 |
)
|
1081 |
|
@@ -1133,25 +1474,27 @@ def create_interface():
|
|
1133 |
# μ¨κ²¨μ§ μν
|
1134 |
novel_text_state = gr.State("")
|
1135 |
|
1136 |
-
# μμ
|
1137 |
with gr.Row():
|
1138 |
gr.Examples(
|
1139 |
examples=[
|
1140 |
-
["
|
1141 |
-
["
|
1142 |
-
["
|
1143 |
-
["
|
1144 |
-
["
|
1145 |
-
["
|
|
|
|
|
1146 |
],
|
1147 |
inputs=query_input,
|
1148 |
-
label="π‘
|
1149 |
)
|
1150 |
|
1151 |
# μ΄λ²€νΈ νΈλ€λ¬
|
1152 |
def refresh_sessions():
|
1153 |
try:
|
1154 |
-
sessions = get_active_sessions("
|
1155 |
return gr.update(choices=sessions)
|
1156 |
except Exception as e:
|
1157 |
logger.error(f"Error refreshing sessions: {str(e)}")
|
@@ -1231,7 +1574,7 @@ def create_interface():
|
|
1231 |
|
1232 |
# λ©μΈ μ€ν
|
1233 |
if __name__ == "__main__":
|
1234 |
-
logger.info("AI νμ
μμ€ μμ± μμ€ν
|
1235 |
logger.info("=" * 60)
|
1236 |
|
1237 |
# νκ²½ νμΈ
|
|
|
53 |
# μ΅μ νλ λ¨κ³ κ΅¬μ± (25λ¨κ³λ‘ μμΆ λ° κ°ν)
|
54 |
OPTIMIZED_STAGES = [
|
55 |
("director", "π¬ κ°λ
μ: μ΄κΈ° κΈ°ν (μΉ κ²μ ν¬ν¨)"),
|
56 |
+
("critic", "π λΉνκ°: κΈ°ν κ²ν (μ² νμ κΉμ΄ λ° μΌκ΄μ±)"),
|
57 |
("director", "π¬ κ°λ
μ: μμ λ λ§μ€ν°νλ"),
|
58 |
] + [
|
59 |
(f"writer{i}", f"βοΈ μκ° {i}: μ΄μ (νμ΄μ§ {(i-1)*3+1}-{i*3})")
|
60 |
for i in range(1, 11)
|
61 |
] + [
|
62 |
+
("critic", "π λΉνκ°: μ€κ° κ²ν (μ² νμ κΉμ΄ λ° μΌκ΄μ±)"),
|
63 |
] + [
|
64 |
(f"writer{i}", f"βοΈ μκ° {i}: μμ λ³Έ (νμ΄μ§ {(i-1)*3+1}-{i*3})")
|
65 |
for i in range(1, 11)
|
|
|
81 |
last_seen_chapter: int = 0
|
82 |
description: str = ""
|
83 |
role: str = ""
|
84 |
+
philosophical_stance: str = "" # μΆκ°: μΊλ¦ν°μ μ² νμ μ
μ₯
|
85 |
|
86 |
@dataclass
|
87 |
class PlotPoint:
|
|
|
92 |
characters_involved: List[str]
|
93 |
impact_level: int
|
94 |
timestamp: str = ""
|
95 |
+
philosophical_implication: str = "" # μΆκ°: μ¬κ±΄μ μ² νμ ν¨μ
|
96 |
|
97 |
@dataclass
|
98 |
class TimelineEvent:
|
|
|
114 |
self.locations: Dict[str, str] = {}
|
115 |
self.established_facts: List[str] = []
|
116 |
self.content_hashes: Dict[str, int] = {} # ν΄μμ ν΄λΉ μ±ν° λ²νΈλ₯Ό μ μ₯
|
117 |
+
self.philosophical_themes: List[str] = [] # μΆκ°: μ² νμ μ£Όμ μΆμ
|
118 |
|
119 |
def register_character(self, character: CharacterState):
|
120 |
"""μ μΊλ¦ν° λ±λ‘"""
|
|
|
178 |
for char in active_chars:
|
179 |
status = "μμ‘΄" if char.alive else "μ¬λ§"
|
180 |
summary += f"β’ {char.name}: {status}"
|
181 |
+
if char.alive and char.location:
|
182 |
+
summary += f" (μμΉ: {char.location})"
|
183 |
+
if char.injuries:
|
184 |
+
summary += f" (λΆμ: {', '.join(char.injuries[-1:])})"
|
185 |
+
if char.philosophical_stance:
|
186 |
+
summary += f" (μ² νμ μ
μ₯: {char.philosophical_stance})"
|
187 |
summary += "\n"
|
188 |
return summary
|
189 |
|
|
|
194 |
if not recent_events:
|
195 |
return "\n(μμ§ μ£Όμ μ¬κ±΄μ΄ μμ΅λλ€.)\n"
|
196 |
for event in recent_events[-3:]: # μ΅κ·Ό 3κ°λ§ νμ
|
197 |
+
summary += f"β’ [μ±ν° {event.chapter}] {event.description}"
|
198 |
+
if event.philosophical_implication:
|
199 |
+
summary += f"\n β μ² νμ ν¨μ: {event.philosophical_implication}"
|
200 |
+
summary += "\n"
|
201 |
return summary
|
202 |
|
203 |
|
|
|
269 |
status TEXT DEFAULT 'active',
|
270 |
current_stage INTEGER DEFAULT 0,
|
271 |
final_novel TEXT,
|
272 |
+
consistency_report TEXT,
|
273 |
+
philosophical_depth_score REAL DEFAULT 0.0
|
274 |
)
|
275 |
''')
|
276 |
cursor.execute('''
|
|
|
284 |
word_count INTEGER DEFAULT 0,
|
285 |
status TEXT DEFAULT 'pending',
|
286 |
consistency_score REAL DEFAULT 0.0,
|
287 |
+
philosophical_depth_score REAL DEFAULT 0.0,
|
288 |
created_at TEXT DEFAULT (datetime('now')),
|
289 |
updated_at TEXT DEFAULT (datetime('now')),
|
290 |
FOREIGN KEY (session_id) REFERENCES sessions(session_id),
|
|
|
302 |
injuries TEXT,
|
303 |
emotional_state TEXT,
|
304 |
description TEXT,
|
305 |
+
philosophical_stance TEXT,
|
306 |
created_at TEXT DEFAULT (datetime('now')),
|
307 |
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
|
308 |
)
|
|
|
338 |
@staticmethod
|
339 |
def save_stage(session_id: str, stage_number: int, stage_name: str,
|
340 |
role: str, content: str, status: str = 'complete',
|
341 |
+
consistency_score: float = 0.0, philosophical_depth_score: float = 0.0):
|
342 |
word_count = len(content.split()) if content else 0
|
343 |
with NovelDatabase.get_db() as conn:
|
344 |
cursor = conn.cursor()
|
345 |
cursor.execute('''
|
346 |
+
INSERT INTO stages (session_id, stage_number, stage_name, role, content, word_count, status, consistency_score, philosophical_depth_score)
|
347 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
348 |
ON CONFLICT(session_id, stage_number)
|
349 |
+
DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, consistency_score=?, philosophical_depth_score=?, updated_at=datetime('now')
|
350 |
+
''', (session_id, stage_number, stage_name, role, content, word_count, status, consistency_score, philosophical_depth_score,
|
351 |
+
content, word_count, status, stage_name, consistency_score, philosophical_depth_score))
|
352 |
cursor.execute(
|
353 |
"UPDATE sessions SET updated_at = datetime('now'), current_stage = ? WHERE session_id = ?",
|
354 |
(stage_number, session_id)
|
|
|
387 |
return '\n\n'.join(all_content)
|
388 |
|
389 |
@staticmethod
|
390 |
+
def update_final_novel(session_id: str, final_novel: str, consistency_report: str = "", philosophical_depth_score: float = 0.0):
|
391 |
with NovelDatabase.get_db() as conn:
|
392 |
conn.cursor().execute(
|
393 |
+
"UPDATE sessions SET final_novel = ?, status = 'complete', updated_at = datetime('now'), consistency_report = ?, philosophical_depth_score = ? WHERE session_id = ?",
|
394 |
+
(final_novel, consistency_report, philosophical_depth_score, session_id)
|
395 |
)
|
396 |
conn.commit()
|
397 |
|
|
|
419 |
"""API ν€λ μμ±"""
|
420 |
return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
|
421 |
|
422 |
+
# --- ν둬ννΈ μμ± ν¨μλ€ (AI μ μ½ μ κ±°, μ² νμ κΉμ΄ κ°ν) ---
|
423 |
def create_director_initial_prompt(self, user_query: str, language: str) -> str:
|
424 |
+
"""κ°λ
μ μ΄κΈ° κΈ°ν ν둬ννΈ (AI κ°μ μ± μμ μ κ±°, μ² νμ κΉμ΄ κ°ν)"""
|
425 |
search_results_str = ""
|
426 |
if self.web_search.enabled:
|
427 |
queries = [f"{user_query} novel setting", f"{user_query} background information"]
|
|
|
434 |
"title": "λΉμ μ 30νμ΄μ§ λΆλμ μ€νΈ μμ€μ κΈ°ννλ λ¬Έν κ°λ
μμ
λλ€.",
|
435 |
"user_theme": "μ¬μ©μ μ£Όμ ",
|
436 |
"plan_instruction": "λ€μ μμλ€μ ν¬ν¨ν μμΈν μμ€ κΈ°νμ μμ±νμΈμ:",
|
437 |
+
"theme_section": """1. **μ£Όμ μ μ² νμ νꡬ**
|
438 |
+
- ν΅μ¬ μ£Όμ μ λ©μμ§ (μ¬μ©μ μλλ₯Ό κΉμ΄ μκ² ν΄μ)
|
439 |
+
- νꡬν μΈκ°μ κ·Όμμ λ¬Έμ (μ: μ 체μ±, μμ μμ§, μ¬λκ³Ό μ¦μ€, μΆκ³Ό μ£½μ, μ κ³Ό μ
μ κ²½κ³ λ±)
|
440 |
+
- μ₯λ₯΄ λ° λΆμκΈ° (μ² νμ κΉμ΄λ₯Ό λ΄μ μ μλ ν€)""",
|
441 |
+
"char_section": """2. **μ£Όμ λ±μ₯μΈλ¬Ό** (3-5λͺ
, κ°μμ μ² νμ μ
μ₯κ³Ό οΏ½οΏ½οΏ½μ κ°λ±μ΄ λλ ·ν΄μΌ ν¨)
|
442 |
+
| μ΄λ¦ | μν | μ±κ²© | λ°°κ²½ | ν΅μ¬ μλ§ | λ΄μ κ°λ± | μ² νμ μ
μ₯ |
|
443 |
+
- κ° μΈλ¬Όμ μλ‘ λ€λ₯Έ κ°μΉκ΄κ³Ό μΈκ³κ΄μ λνν΄μΌ ν¨
|
444 |
+
- μΈλ¬Ό κ°μ κ°λ±μ λ¨μν λλ¦½μ΄ μλ μ² νμ μΆ©λμ΄μ΄μΌ ν¨""",
|
445 |
+
"setting_section": """3. **λ°°κ²½ μ€μ **
|
446 |
+
- μ곡κ°μ λ°°κ²½ (μ£Όμ λ₯Ό ν¨κ³Όμ μΌλ‘ νꡬν μ μλ νκ²½)
|
447 |
+
- μ¬νμ /λ¬Ένμ νκ²½
|
448 |
+
- μμ§μ 곡κ°λ€ (κ° κ³΅κ°μ΄ μ² νμ μλ―Έλ₯Ό λ΄μμΌ ν¨)""",
|
449 |
+
"plot_section": """4. **νλ‘― ꡬ쑰** (10κ° ννΈ, κ° 3νμ΄μ§ λΆλ)
|
450 |
+
| ννΈ | νμ΄μ§ | μ£Όμ μ¬κ±΄ | κ°μ μ νλ¦ | μ² νμ νꡬ μ£Όμ | μΈλ¬Όμ λ΄μ λ³ν |
|
451 |
+
- κ° ννΈλ μΈμ μ¬κ±΄κ³Ό λ΄μ μ±μ°°μ΄ κ· νμ μ΄λ£¨μ΄μΌ ν¨
|
452 |
+
- κ°λ±μ μ μ§μ μΌλ‘ μ¬νλλ©°, μΈλ¬Όλ€μ μΈκ³κ΄μ΄ μνλ°μμΌ ν¨""",
|
453 |
+
"guideline_section": """5. **μκ°λ³ μ§μΉ¨**
|
454 |
+
- 문체: κ°κ²°νλ©΄μλ ν¨μΆμ μ΄κ³ μμ μΈ λ¬Έμ²΄
|
455 |
+
- μμ : λ΄λ©΄ λ¬μ¬μ μ 리ν μμ μ ν
|
456 |
+
- μμ§κ³Ό μμ : μ£Όμ λ₯Ό κ°ννλ λ°λ³΅μ μ΄λ―Έμ§λ μμ§
|
457 |
+
- λν: μ² νμ κΉμ΄λ₯Ό λ΄λ μμ°μ€λ¬μ΄ λν""",
|
458 |
+
"philosophical_principle": """β οΈ ν΅μ¬ μ°½μ μμΉ: μ² νμ κΉμ΄ β οΈ
|
459 |
+
μ΄ μμ€μ λ¨μν μ¬κ±΄μ λμ΄μ΄ μλλλ€. λͺ¨λ μμ¬μ κΈ°μ μλ **μΈκ°μ κ·Όμμ λ¬Έμ μ λν μ² νμ μ¬μ **κ° λ°λμ κΉλ € μμ΄μΌ ν©λλ€.
|
460 |
+
- λ±μ₯μΈλ¬Όλ€μ κ°μμ μ λ
κ³Ό κ°μΉκ΄μ κ°μ§κ³ κ°λ±νλ©°, μ΄λ₯Ό ν΅ν΄ λ
μκ° μΆμ μλ―Έλ₯Ό μ±μ°°νκ² ν΄μΌ ν©λλ€.
|
461 |
+
- κ²°λ§μ λ¨μν ν΄κ²°μ΄ μλ, λ κΉμ μ§λ¬Έμ λ¨κΈ°λ μ΄λ¦° κ²°λ§μ΄μ΄μΌ ν©λλ€.
|
462 |
+
- λͺ¨λ μ₯λ©΄κ³Ό λνλ μ£Όμ λ₯Ό μ¬νμν€λ μν μ ν΄μΌ ν©λλ€.""",
|
463 |
+
"final_instruction": "μ μμΉμ λ°λΌ μΈκ° μ‘΄μ¬μ λ³Έμ§μ νꡬνλ κΉμ΄ μλ μμ€μ κΈ°ννμΈμ."
|
464 |
},
|
465 |
"English": {
|
466 |
"title": "You are a literary director planning a 30-page novella.",
|
467 |
"user_theme": "User Theme",
|
468 |
"plan_instruction": "Create a detailed novel plan including:",
|
469 |
+
"theme_section": """1. **Theme and Philosophical Inquiry**
|
470 |
+
- Core theme and message (Deep interpretation of user's intent)
|
471 |
+
- Fundamental human problems to explore (e.g., identity, free will, love and hate, life and death, boundaries of good and evil)
|
472 |
+
- Genre and atmosphere (Tone that can contain philosophical depth)""",
|
473 |
+
"char_section": """2. **Main Characters** (3-5, each with distinct philosophical positions and internal conflicts)
|
474 |
+
| Name | Role | Personality | Background | Core Desire | Internal Conflict | Philosophical Stance |
|
475 |
+
- Each character must represent different values and worldviews
|
476 |
+
- Conflicts between characters should be philosophical clashes, not simple oppositions""",
|
477 |
+
"setting_section": """3. **Setting**
|
478 |
+
- Time and place (Environment that effectively explores the theme)
|
479 |
+
- Social/cultural environment
|
480 |
+
- Symbolic spaces (Each space should carry philosophical meaning)""",
|
481 |
+
"plot_section": """4. **Plot Structure** (10 parts, ~3 pages each)
|
482 |
+
| Part | Pages | Main Events | Emotional Flow | Philosophical Theme | Character's Internal Change |
|
483 |
+
- Each part must balance external events with internal reflection
|
484 |
+
- Conflicts should deepen progressively, testing characters' worldviews""",
|
485 |
+
"guideline_section": """5. **Writer Guidelines**
|
486 |
+
- Style: Concise yet implicit and poetic prose
|
487 |
+
- POV: Choose perspective favorable for internal portrayal
|
488 |
+
- Symbols and metaphors: Recurring images or symbols that reinforce the theme
|
489 |
+
- Dialogue: Natural yet philosophically rich""",
|
490 |
+
"philosophical_principle": """β οΈ CORE CREATIVE PRINCIPLE: PHILOSOPHICAL DEPTH β οΈ
|
491 |
+
This is not a mere sequence of events. The foundation of all narrative must be **philosophical inquiry into fundamental human problems**.
|
492 |
+
- Characters must conflict with their own beliefs and values, leading readers to reflect on the meaning of life.
|
493 |
+
- The ending should not be a simple resolution but an open ending that leaves deeper questions.
|
494 |
+
- Every scene and dialogue must serve to deepen the theme.""",
|
495 |
+
"final_instruction": "Following these principles, plan a profound novel that explores the essence of human existence."
|
496 |
}
|
497 |
}
|
498 |
p = lang_prompts[language]
|
499 |
+
|
500 |
+
# μΉ κ²μ κ²°κ³Ό ν¬λ§·ν
|
501 |
+
search_section = ""
|
502 |
+
if search_results_str:
|
503 |
+
search_section = f"\n\n**μΉ κ²μ μ°Έκ³ μλ£:**\n{search_results_str}\n" if language == "Korean" else f"\n\n**Web Search Reference:**\n{search_results_str}\n"
|
504 |
+
|
505 |
+
return f"""{p['title']}
|
506 |
+
|
507 |
+
{p['philosophical_principle']}
|
508 |
+
|
509 |
+
**{p['user_theme']}:** {user_query}
|
510 |
+
{search_section}
|
511 |
+
{p['plan_instruction']}
|
512 |
+
|
513 |
+
{p['theme_section']}
|
514 |
+
|
515 |
+
{p['char_section']}
|
516 |
+
|
517 |
+
{p['setting_section']}
|
518 |
+
|
519 |
+
{p['plot_section']}
|
520 |
+
|
521 |
+
{p['guideline_section']}
|
522 |
+
|
523 |
+
---
|
524 |
+
{p['final_instruction']}"""
|
525 |
|
526 |
def create_critic_director_prompt(self, director_plan: str, user_query: str, language: str) -> str:
|
527 |
+
"""λΉνκ°μ κ°λ
μ κΈ°ν κ²ν ν둬ννΈ (μ² νμ κΉμ΄ κ²ν κ°ν)"""
|
528 |
lang_prompts = {
|
529 |
"Korean": {
|
530 |
+
"title": "λΉμ μ λ¬Έν λΉνκ°μ
λλ€. κ°λ
μμ μμ€ κΈ°νμ 'μ² νμ κΉμ΄'μ 'κΈ°μ μ μΌκ΄μ±' κ΄μ μμ λ μΉ΄λ‘κ² κ²ν νμΈμ.",
|
531 |
+
"theme_check": f"""**1. μ² νμ κΉμ΄ κ²ν (κ°μ₯ μ€μ)**
|
532 |
+
- **μλ μ£Όμ :** '{user_query}'
|
533 |
+
- κΈ°νμμ΄ μΈκ°μ κ·Όμμ λ¬Έμ λ₯Ό μ§μ§νκ² νꡬνκ³ μμ΅λκΉ?
|
534 |
+
- λ±μ₯μΈλ¬Όλ€μ΄ κ°μμ μ² νμ μ
μ₯μ κ°μ§κ³ μμΌλ©°, κ·Έλ€μ κ°λ±μ΄ λ¨μν λλ¦½μ΄ μλ κ°μΉκ΄μ μΆ©λμ
λκΉ?
|
535 |
+
- νλ‘―μ΄ μΈμ μ¬κ±΄μλ§ μΉμ€νμ§ μκ³ λ΄μ μ±μ°°κ³Ό κ· νμ μ΄λ£¨κ³ μμ΅λκΉ?
|
536 |
+
- κ²°λ§μ΄ μ£λΆλ₯Έ ν΄κ²°μ΄ μλ, λ
μμκ² κΉμ μ¬μ λ₯Ό λ¨κΈ°λ λ°©ν₯μ
λκΉ?""",
|
537 |
+
"consistency_check": """**2. κΈ°μ μ μΌκ΄μ± κ²ν **
|
538 |
+
- μΊλ¦ν° μ€μ μ λͺ¨μμ΄λ λΉνμ€μ μΈ λΆλΆ
|
539 |
+
- νλ‘―μ λ
Όλ¦¬μ νμ μ΄λ κ°μ°μ± λΆμ‘±
|
540 |
+
- μκ°μ μ΄λ κ³΅κ° μ€μ μ μ€λ₯
|
541 |
+
- μ£Όμ μ νλ‘―, μΊλ¦ν°κ° μ κΈ°μ μΌλ‘ μ°κ²°λμ΄ μλκ°""",
|
542 |
+
"instruction": "μ νλͺ©λ€μ μ€μ¬μΌλ‘ ꡬ체μ μΈ λ¬Έμ μ κ³Ό κ°μ λ°©ν₯μ μ μνμΈμ. νΉν μ² νμ κΉμ΄κ° λΆμ‘±ν λΆλΆμ λ°λμ μ§μ νκ³ κ΅¬μ²΄μ μΈ λμμ μ μνμΈμ."
|
543 |
},
|
544 |
"English": {
|
545 |
+
"title": "You are a literary critic. Sharply review the director's plan from perspectives of 'Philosophical Depth' and 'Technical Consistency'.",
|
546 |
+
"theme_check": f"""**1. Philosophical Depth Review (Most Important)**
|
547 |
+
- **Original Theme:** '{user_query}'
|
548 |
+
- Does the plan seriously explore fundamental human problems?
|
549 |
+
- Do characters have their own philosophical positions, and are their conflicts clashes of values rather than simple oppositions?
|
550 |
+
- Does the plot balance external events with internal reflection?
|
551 |
+
- Is the ending aimed at leaving readers with deep contemplation rather than premature resolution?""",
|
552 |
+
"consistency_check": """**2. Technical Consistency Review**
|
553 |
+
- Character setting contradictions or unrealistic aspects
|
554 |
+
- Plot logical flaws or lack of plausibility
|
555 |
+
- Timeline or spatial setting errors
|
556 |
+
- Are theme, plot, and characters organically connected?""",
|
557 |
+
"instruction": "Provide specific problems and improvement directions based on above. Especially point out areas lacking philosophical depth and provide concrete alternatives."
|
558 |
}
|
559 |
}
|
560 |
p = lang_prompts[language]
|
561 |
+
return f"""{p['title']}
|
562 |
+
|
563 |
+
**κ°λ
μ κΈ°ν:**
|
564 |
+
{director_plan}
|
565 |
+
|
566 |
+
---
|
567 |
+
**κ²ν νλͺ©:**
|
568 |
+
{p['theme_check']}
|
569 |
+
|
570 |
+
{p['consistency_check']}
|
571 |
+
|
572 |
+
{p['instruction']}"""
|
573 |
|
574 |
def create_director_revision_prompt(self, initial_plan: str, critic_feedback: str, user_query: str, language: str) -> str:
|
575 |
+
"""κ°λ
μ μμ ν둬ννΈ"""
|
576 |
+
lang_prompts = {
|
577 |
+
"Korean": f"""κ°λ
μλ‘μ λΉνκ°μ νΌλλ°±μ λ°μνμ¬ μμ€ κΈ°νμ μμ ν©λλ€.
|
578 |
+
|
579 |
+
**μλ μ£Όμ :** {user_query}
|
580 |
+
|
581 |
+
**μ΄κΈ° κΈ°ν:**
|
582 |
+
{initial_plan}
|
583 |
+
|
584 |
+
**λΉνκ° νΌλλ°±:**
|
585 |
+
{critic_feedback}
|
586 |
+
|
587 |
+
**μμ μ§μΉ¨:**
|
588 |
+
1. λΉνκ°κ° μ§μ ν λͺ¨λ μ² νμ κΉμ΄ λΆμ‘±κ³Ό κΈ°μ μ λ¬Έμ λ₯Ό ν΄κ²°νμΈμ.
|
589 |
+
2. νΉν μΈλ¬Όλ€μ λ΄μ κ°λ±κ³Ό μ² νμ μ
μ₯μ λμ± λͺ
ννκ³ κΉμ΄ μκ² μ€μ νμΈμ.
|
590 |
+
3. κ° ννΈλ³λ‘ νꡬν μ² νμ μ£Όμ λ₯Ό ꡬ체μ μΌλ‘ λͺ
μνμΈμ.
|
591 |
+
4. 10λͺ
μ μκ°κ° νΌλ μμ΄ μμ
ν μ μλλ‘ λͺ
ννκ³ μμΈν μ΅μ’
λ§μ€ν°νλμ μμ±νμΈμ.
|
592 |
|
593 |
+
λͺ¨λ μμ μ¬νμ΄ 'μΈκ°μ κ·Όμμ λ¬Έμ μ λν μ² νμ νꡬ'λΌλ ν΅μ¬ μμΉμ λΆν©ν΄μΌ ν©λλ€.""",
|
594 |
+
"English": f"""As director, revise the novel plan reflecting the critic's feedback.
|
595 |
+
|
596 |
+
**Original Theme:** {user_query}
|
597 |
+
|
598 |
+
**Initial Plan:**
|
599 |
+
{initial_plan}
|
600 |
+
|
601 |
+
**Critic's Feedback:**
|
602 |
+
{critic_feedback}
|
603 |
+
|
604 |
+
**Revision Guidelines:**
|
605 |
+
1. Resolve all philosophical depth deficiencies and technical issues pointed out by the critic.
|
606 |
+
2. Especially clarify and deepen characters' internal conflicts and philosophical positions.
|
607 |
+
3. Specifically state the philosophical themes to explore in each part.
|
608 |
+
4. Create a clear and detailed final masterplan that 10 writers can work with without confusion.
|
609 |
+
|
610 |
+
All revisions must align with the core principle of 'philosophical inquiry into fundamental human problems'."""
|
611 |
+
}
|
612 |
+
return lang_prompts[language]
|
613 |
|
614 |
def create_writer_prompt(self, writer_number: int, director_plan: str, previous_content_summary: str, user_query: str, language: str) -> str:
|
615 |
+
"""μκ° ν둬ννΈ (μ² νμ κΉμ΄ 리λ§μΈλ κ°ν)"""
|
616 |
pages_start = (writer_number - 1) * 3 + 1
|
617 |
pages_end = writer_number * 3
|
618 |
|
|
|
621 |
"title": f"λΉμ μ μκ° {writer_number}λ²μ
λλ€. μμ€μ {pages_start}-{pages_end} νμ΄μ§λ₯Ό μμ±νμΈμ.",
|
622 |
"plan": "κ°λ
μ λ§μ€ν°νλ",
|
623 |
"prev_summary": "μ΄μ λ΄μ© μμ½",
|
624 |
+
"guidelines": f"""**μμ± μ§μΉ¨:**
|
625 |
+
1. **λΆλ**: 1,400-1,500 λ¨μ΄ (μ½ 3νμ΄μ§)
|
626 |
+
2. **μ°κ²°μ±**: μ΄μ λ΄μ©κ³Ό μμ°μ€λ½κ² μ°κ²°λλ©΄μλ μλ‘μ΄ κΉμ΄λ₯Ό λνμΈμ
|
627 |
+
3. **μΌκ΄μ±**: μΊλ¦ν°μ μ±κ²©, μ² νμ μ
μ₯, λ§ν¬ λ±μ μΌκ΄λκ² μ μ§νμΈμ
|
628 |
+
4. **κ· ν**: μΈμ μ¬κ±΄κ³Ό λ΄μ μ±μ°°μ κ· νμ λ§μΆμΈμ (50:50 λΉμ¨ κΆμ₯)
|
629 |
+
5. **λν**: μΊλ¦ν°μ μ² νμ μ
μ₯μ΄ μμ°μ€λ½κ² λλ¬λλ μλ―Έ μλ λνλ₯Ό ν¬ν¨νμΈμ""",
|
630 |
+
"philosophical_reminder": f"""β μ² νμ κΉμ΄ 리λ§μΈλ β
|
631 |
+
- μ΄ ννΈμμ νꡬν΄μΌ ν μ² νμ μ£Όμ λ₯Ό μΌλμ λκ³ μμ±νμΈμ
|
632 |
+
- λͺ¨λ μ₯λ©΄μ λ¨μν μ¬κ±΄ μ§νμ΄ μλ, μΈλ¬Όμ λ΄λ©΄ νꡬμ μ±μ°°μ κΈ°νμ¬μΌ ν©λλ€
|
633 |
+
- λ
μκ° μ€μ€λ‘ μκ°νκ³ μ§λ¬Ένκ² λ§λλ μ΄λ¦° ννμ μ¬μ©νμΈμ
|
634 |
+
- μμ§κ³Ό μμ λ₯Ό ν΅ν΄ μ£Όμ λ₯Ό μ¬νμν€μΈμ""",
|
635 |
+
"final_instruction": "λΉμ μ ννΈκ° μ 체 μμ€μ μ² νμ κΉμ΄λ₯Ό λνλ μ€μν μν μ νλ€λ κ²μ λͺ
μ¬νκ³ , νΌμ λ΄μ μμ±νμΈμ."
|
636 |
},
|
637 |
"English": {
|
638 |
"title": f"You are Writer #{writer_number}. Write pages {pages_start}-{pages_end} of the novella.",
|
639 |
"plan": "Director's Masterplan",
|
640 |
"prev_summary": "Previous Content Summary",
|
641 |
+
"guidelines": f"""**Writing Guidelines:**
|
642 |
+
1. **Length**: 1,400-1,500 words (approximately 3 pages)
|
643 |
+
2. **Connectivity**: Connect naturally with previous content while adding new depth
|
644 |
+
3. **Consistency**: Maintain character personality, philosophical positions, speech patterns consistently
|
645 |
+
4. **Balance**: Balance external events with internal reflection (50:50 ratio recommended)
|
646 |
+
5. **Dialogue**: Include meaningful dialogue that naturally reveals characters' philosophical positions""",
|
647 |
+
"philosophical_reminder": f"""β Philosophical Depth Reminder β
|
648 |
+
- Keep in mind the philosophical themes to explore in this part
|
649 |
+
- Every scene should be an opportunity for character introspection and reflection, not just plot progression
|
650 |
+
- Use open expressions that make readers think and question
|
651 |
+
- Deepen the theme through symbols and metaphors""",
|
652 |
+
"final_instruction": "Remember that your part plays an important role in adding philosophical depth to the overall novel. Write with soul."
|
653 |
}
|
654 |
}
|
655 |
|
656 |
p = lang_prompts[language]
|
657 |
consistency_info = self.consistency_tracker.get_character_summary(writer_number) + self.consistency_tracker.get_plot_summary(writer_number)
|
658 |
|
659 |
+
return f"""{p['title']}
|
660 |
+
|
661 |
+
**{p['plan']}:**
|
662 |
+
{director_plan}
|
663 |
+
|
664 |
+
{consistency_info}
|
665 |
+
|
666 |
+
**{p['prev_summary']}:**
|
667 |
+
{previous_content_summary}
|
668 |
+
|
669 |
+
---
|
670 |
+
{p['guidelines']}
|
671 |
+
|
672 |
+
{p['philosophical_reminder']}
|
673 |
+
|
674 |
+
---
|
675 |
+
{p['final_instruction']}"""
|
676 |
|
677 |
def create_critic_consistency_prompt(self, all_content: str, user_query: str, language: str) -> str:
|
678 |
+
"""λΉνκ° μ€κ° κ²ν ν둬ννΈ"""
|
679 |
+
lang_prompts = {
|
680 |
+
"Korean": f"""λΉμ μ μΌκ΄μ±κ³Ό μ² νμ κΉμ΄λ₯Ό κ²ν νλ μ λ¬Έ λΉνκ°μ
λλ€.
|
681 |
+
|
682 |
+
**μλ μ£Όμ :** {user_query}
|
683 |
+
|
684 |
+
**νμ¬κΉμ§ μμ±λ λ΄μ© (μ΅κ·Ό 3000μ):**
|
685 |
+
{all_content[-3000:]}
|
686 |
+
|
687 |
+
**κ²ν νλͺ©:**
|
688 |
+
1. **μ² νμ κΉμ΄ (κ°μ₯ μ€μ):**
|
689 |
+
- κ° ννΈκ° μ² νμ νꡬλ₯Ό μ λλ‘ μννκ³ μλκ°?
|
690 |
+
- μΈλ¬Όλ€μ λ΄μ κ°λ±κ³Ό μ±μ°°μ΄ μΆ©λΆν οΏ½οΏ½μ¬λκ³ μλκ°?
|
691 |
+
- μ£Όμ κ° μ μ§μ μΌλ‘ μ¬νλκ³ μλκ°?
|
692 |
+
|
693 |
+
2. **κΈ°μ μ μΌκ΄μ±:**
|
694 |
+
- μΊλ¦ν°μ μ±κ²©, λ§ν¬, νλμ΄ μΌκ΄λλκ°?
|
695 |
+
- μκ°μ κ³Ό κ³΅κ° μ€μ μ μ€λ₯κ° μλκ°?
|
696 |
+
- νλ‘―μ΄ λ
Όλ¦¬μ μΌλ‘ μ§νλκ³ μλκ°?
|
697 |
+
|
698 |
+
3. **문체μ ν€:**
|
699 |
+
- μ 체μ μΈ λ¬Έμ²΄μ λΆμκΈ°κ° μΌκ΄λλκ°?
|
700 |
+
- μ² νμ κΉμ΄μ λ§λ 문체λ₯Ό μ μ§νκ³ μλκ°?
|
701 |
+
|
702 |
+
**κ²°κ³Ό:** λ°κ²¬λ λ¬Έμ μ κ³Ό κ° μκ°λ³ ꡬ체μ μΈ μμ μ μμ μ μνμΈμ.""",
|
703 |
+
"English": f"""You are a professional critic reviewing consistency and philosophical depth.
|
704 |
|
705 |
+
**Original Theme:** {user_query}
|
|
|
|
|
|
|
706 |
|
707 |
+
**Content Written So Far (last 3000 characters):**
|
708 |
+
{all_content[-3000:]}
|
709 |
+
|
710 |
+
**Review Items:**
|
711 |
+
1. **Philosophical Depth (Most Important):**
|
712 |
+
- Is each part properly conducting philosophical inquiry?
|
713 |
+
- Are characters' internal conflicts and reflections sufficiently portrayed?
|
714 |
+
- Is the theme progressively deepening?
|
715 |
+
|
716 |
+
2. **Technical Consistency:**
|
717 |
+
- Are character personalities, speech patterns, and behaviors consistent?
|
718 |
+
- Are there no errors in timeline and spatial settings?
|
719 |
+
- Is the plot progressing logically?
|
720 |
+
|
721 |
+
3. **Style and Tone:**
|
722 |
+
- Is the overall style and atmosphere consistent?
|
723 |
+
- Is a style appropriate for philosophical depth maintained?
|
724 |
+
|
725 |
+
**Result:** Present discovered problems and specific revision suggestions for each writer."""
|
726 |
+
}
|
727 |
+
return lang_prompts[language]
|
728 |
|
729 |
def create_writer_revision_prompt(self, writer_number: int, initial_content: str, consistency_feedback: str, language: str) -> str:
|
730 |
"""μκ° μμ ν둬ννΈ"""
|
731 |
+
lang_prompts = {
|
732 |
+
"Korean": f"""μκ° {writer_number}λ²μΌλ‘μ λΉνκ°μ νΌλλ°±μ λ°μνμ¬ λ΄μ©μ μμ νμΈμ.
|
733 |
+
|
734 |
+
**μ΄κΈ° μμ± λ΄μ©:**
|
735 |
+
{initial_content}
|
736 |
|
737 |
+
**λΉνκ° νΌλλ°±:**
|
738 |
+
{consistency_feedback}
|
739 |
|
740 |
+
**μμ μ§μΉ¨:**
|
741 |
+
1. μ§μ λ λͺ¨λ μ² νμ κΉμ΄ λΆμ‘±κ³Ό μΌκ΄μ± λ¬Έμ λ₯Ό ν΄κ²°νμΈμ.
|
742 |
+
2. νΉν λ΄μ μ±μ°°κ³Ό μ² νμ νκ΅¬κ° λΆμ‘±νλ€κ³ μ§μ λ λΆλΆμ 보κ°νμΈμ.
|
743 |
+
3. λΆλ(1,400-1,500 λ¨μ΄)μ μ μ§νλ©΄μ λ΄μ©μ κΉμ΄λ₯Ό λνμΈμ.
|
744 |
+
4. μμ μ λ¨μν μΆκ°κ° μλ, μ 체μ μΈ μ¬κ΅¬μ±μ ν΅ν΄ μμ±λλ₯Ό λμ΄μΈμ.
|
745 |
+
|
746 |
+
μμ λ μ΅μ’
λ²μ λ§ μ μνμΈμ.""",
|
747 |
+
"English": f"""As Writer {writer_number}, revise the content reflecting the critic's feedback.
|
748 |
+
|
749 |
+
**Initial Content:**
|
750 |
+
{initial_content}
|
751 |
+
|
752 |
+
**Critic's Feedback:**
|
753 |
+
{consistency_feedback}
|
754 |
+
|
755 |
+
**Revision Guidelines:**
|
756 |
+
1. Resolve all pointed out philosophical depth deficiencies and consistency issues.
|
757 |
+
2. Especially reinforce parts pointed out as lacking internal reflection and philosophical inquiry.
|
758 |
+
3. Add depth to content while maintaining length (1,400-1,500 words).
|
759 |
+
4. Improve completion through overall restructuring, not simple additions.
|
760 |
+
|
761 |
+
Present only the revised final version."""
|
762 |
+
}
|
763 |
+
return lang_prompts[language]
|
764 |
|
765 |
def create_critic_final_prompt(self, complete_novel: str, language: str) -> str:
|
766 |
"""μ΅μ’
λΉνκ° κ²ν λ° λ³΄κ³ μ μμ± ν둬ννΈ"""
|
767 |
+
lang_prompts = {
|
768 |
+
"Korean": f"""μμ±λ μμ€μ μ΅μ’
νκ° λ³΄κ³ μλ₯Ό μμ±νμΈμ.
|
769 |
+
|
770 |
+
**μμ±λ μμ€ (λ§μ§λ§ 2000μ):**
|
771 |
+
{complete_novel[-2000:]}
|
772 |
+
|
773 |
+
**λ³΄κ³ μ ν¬ν¨ νλͺ©:**
|
774 |
+
|
775 |
+
1. **μ² νμ κΉμ΄ νκ° (40μ )**
|
776 |
+
- μΈκ°μ κ·Όμμ λ¬Έμ νꡬ μμ€ (10μ )
|
777 |
+
- μΊλ¦ν°μ λ΄μ κ°λ±κ³Ό μ±μ°°μ κΉμ΄ (10μ )
|
778 |
+
- μ£Όμ μ μΌκ΄μ±κ³Ό λ°μ (10μ )
|
779 |
+
- λ
μμκ² λ¨κΈ°λ μ¬μ μ μ¬μ§ (10μ )
|
780 |
+
|
781 |
+
2. **κΈ°μ μ μμ±λ νκ° (30μ )**
|
782 |
+
- νλ‘―μ λ
Όλ¦¬μ±κ³Ό κ°μ°μ± (10μ )
|
783 |
+
- μΊλ¦ν°μ μΌκ΄μ± (10μ )
|
784 |
+
- 문체μ ν€μ μΌκ΄μ± (10μ )
|
785 |
+
|
786 |
+
3. **μ°½μμ±κ³Ό λ
μ°½μ± (20μ )**
|
787 |
+
- μ£Όμ ν΄μμ μ°Έμ ν¨ (10μ )
|
788 |
+
- ννκ³Ό ꡬμ±μ λ
μ°½μ± (10μ )
|
789 |
+
|
790 |
+
4. **μ’
ν© νκ° (10μ )**
|
791 |
+
- μ 체μ μΈ μνμ±κ³Ό μμ±λ
|
792 |
+
|
793 |
+
**μ΄μ : /100μ **
|
794 |
+
|
795 |
+
κ° νλͺ©μ λν ꡬ체μ μΈ νκ°μ ν¨κ», μνμ κ°μ κ³Ό μ½μ , κ·Έλ¦¬κ³ λ
μμκ² λ―ΈμΉ μν₯μ λν΄ μμ νμΈμ.""",
|
796 |
+
"English": f"""Create a final evaluation report for the completed novel.
|
797 |
+
|
798 |
+
**Completed Novel (last 2000 characters):**
|
799 |
+
{complete_novel[-2000:]}
|
800 |
+
|
801 |
+
**Report Items:**
|
802 |
+
|
803 |
+
1. **Philosophical Depth Evaluation (40 points)**
|
804 |
+
- Level of exploring fundamental human problems (10 points)
|
805 |
+
- Depth of characters' internal conflicts and reflection (10 points)
|
806 |
+
- Theme consistency and development (10 points)
|
807 |
+
- Room for reader contemplation (10 points)
|
808 |
+
|
809 |
+
2. **Technical Completion Evaluation (30 points)**
|
810 |
+
- Plot logic and plausibility (10 points)
|
811 |
+
- Character consistency (10 points)
|
812 |
+
- Style and tone consistency (10 points)
|
813 |
+
|
814 |
+
3. **Creativity and Originality (20 points)**
|
815 |
+
- Freshness of theme interpretation (10 points)
|
816 |
+
- Originality of expression and composition (10 points)
|
817 |
|
818 |
+
4. **Overall Evaluation (10 points)**
|
819 |
+
- Overall artistry and completion
|
820 |
|
821 |
+
**Total Score: /100**
|
|
|
|
|
|
|
|
|
|
|
822 |
|
823 |
+
Describe specific evaluations for each item, along with the work's strengths and weaknesses, and its impact on readers."""
|
824 |
+
}
|
825 |
+
return lang_prompts[language]
|
826 |
+
|
827 |
+
# --- LLM νΈμΆ ν¨μλ€ (μ€νΈλ¦¬λ° μλ²½ μ μ§) ---
|
828 |
def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str:
|
829 |
"""LLM λκΈ°μ νΈμΆ (μμ½ λ± λ΄λΆμ©)"""
|
830 |
full_content = ""
|
|
|
835 |
return full_content
|
836 |
|
837 |
def call_llm_streaming(self, messages: List[Dict[str, str]], role: str, language: str) -> Generator[str, None, None]:
|
838 |
+
"""LLM μ€νΈλ¦¬λ° νΈμΆ (μλ²½ν μ€νΈλ¦¬λ° μ μ§)"""
|
839 |
try:
|
840 |
system_prompts = self.get_system_prompts(language)
|
841 |
full_messages = [{"role": "system", "content": system_prompts.get(role, "You are a helpful assistant.")}, *messages]
|
|
|
854 |
|
855 |
logger.info(f"[{role}] API μ€νΈλ¦¬λ° μμ")
|
856 |
|
|
|
857 |
response = requests.post(
|
858 |
self.api_url,
|
859 |
headers=self.create_headers(),
|
|
|
862 |
timeout=180
|
863 |
)
|
864 |
|
|
|
865 |
if response.status_code != 200:
|
866 |
logger.error(f"API μλ΅ μ€λ₯: {response.status_code}")
|
867 |
logger.error(f"μλ΅ λ΄μ©: {response.text[:500]}")
|
|
|
869 |
return
|
870 |
|
871 |
response.raise_for_status()
|
872 |
+
|
873 |
+
# μ€νΈλ¦¬λ° μ²λ¦¬ - μ€μκ°μΌλ‘ UIμ λ°μ
|
874 |
buffer = ""
|
875 |
total_content = ""
|
876 |
chunk_count = 0
|
877 |
+
|
|
|
878 |
for line in response.iter_lines():
|
879 |
if not line:
|
880 |
continue
|
881 |
|
882 |
try:
|
883 |
line_str = line.decode('utf-8').strip()
|
884 |
+
|
|
|
885 |
if not line_str.startswith("data: "):
|
886 |
continue
|
887 |
|
888 |
+
data_str = line_str[6:]
|
889 |
+
|
|
|
890 |
if data_str == "[DONE]":
|
891 |
logger.info(f"[{role}] μ€νΈλ¦¬λ° μλ£ - μ΄ {len(total_content)} λ¬Έμ")
|
892 |
break
|
893 |
+
|
|
|
894 |
try:
|
895 |
data = json.loads(data_str)
|
896 |
except json.JSONDecodeError:
|
897 |
logger.warning(f"JSON νμ± μ€ν¨: {data_str[:100]}")
|
898 |
continue
|
899 |
+
|
|
|
900 |
choices = data.get("choices", None)
|
901 |
if not choices or not isinstance(choices, list) or len(choices) == 0:
|
|
|
902 |
if "error" in data:
|
903 |
error_msg = data.get("error", {}).get("message", "Unknown error")
|
904 |
logger.error(f"API μλ¬: {error_msg}")
|
|
|
906 |
return
|
907 |
continue
|
908 |
|
|
|
909 |
delta = choices[0].get("delta", {})
|
910 |
content = delta.get("content", "")
|
911 |
|
|
|
914 |
total_content += content
|
915 |
chunk_count += 1
|
916 |
|
917 |
+
# 50μ λλ μ€λ°κΏλ§λ€ μ¦μ yield (μ€νΈλ¦¬λ° ν¨κ³Ό κ·Ήλν)
|
918 |
+
if len(buffer) >= 50 or '\n' in buffer:
|
919 |
yield buffer
|
920 |
buffer = ""
|
921 |
+
time.sleep(0.01) # UI μ
λ°μ΄νΈλ₯Ό μν μ΅μ λκΈ°
|
922 |
|
923 |
except Exception as e:
|
924 |
+
logger.error(f"μ²ν¬ μ²λ¦¬ μ€λ₯: {str(e)}")
|
|
|
|
|
|
|
|
|
925 |
continue
|
926 |
|
927 |
# λ¨μ λ²νΌ μ²λ¦¬
|
928 |
if buffer:
|
929 |
yield buffer
|
930 |
|
|
|
931 |
if chunk_count == 0:
|
932 |
logger.error(f"[{role}] μ½ν
μΈ κ° μ ν μμ λμ§ μμ")
|
933 |
yield "β APIλ‘λΆν° μλ΅μ λ°μ§ λͺ»νμ΅λλ€."
|
|
|
944 |
logger.error(f"μκΈ°μΉ μμ μ€λ₯: {type(e).__name__}: {str(e)}", exc_info=True)
|
945 |
yield f"β μ€λ₯ λ°μ: {str(e)}"
|
946 |
|
|
|
|
|
|
|
|
|
947 |
def get_system_prompts(self, language: str) -> Dict[str, str]:
|
948 |
+
"""μν λ³ μμ€ν
ν둬ννΈ μμ± (μ² νμ κΉμ΄ κ°μ‘°)"""
|
949 |
base_prompts = {
|
950 |
"Korean": {
|
951 |
+
"director": "λΉμ μ μΈκ°μ λ³Έμ§μ νꡬνλ μ² νμ μμ€μ κΈ°ννλ λ¬Έν κ°λ
μμ
λλ€. κΉμ΄ μκ³ μλ―Έ μλ μνμ λ§λμΈμ.",
|
952 |
+
"critic": "λΉμ μ μνμ μ² νμ κΉμ΄μ κΈ°μ μ μμ±λλ₯Ό λ μΉ΄λ‘κ² νκ°νλ λ¬Έν λΉνκ°μ
λλ€. μνμ΄ μΈκ°μ κ·Όμμ λ¬Έμ λ₯Ό μ λλ‘ νꡬνκ³ μλμ§ μ격νκ² κ²ν νμΈμ.",
|
953 |
+
"writer_base": "λΉμ μ μΈκ°μ λ΄λ©΄μ μ¬μΈνκ² κ·Έλ €λ΄λ μ λ¬Έ μμ€ μκ°μ
λλ€. μΈμ μ¬κ±΄κ³Ό λ΄μ μ±μ°°μ κ· νμ λ§μΆλ©°, λ
μμκ² κΉμ μ¬μ΄μ λ¨κΈ°λ λ¬Έμ₯μ μ°μΈμ."
|
954 |
},
|
955 |
"English": {
|
956 |
+
"director": "You are a literary director planning philosophical novels that explore human nature. Create profound and meaningful works.",
|
957 |
+
"critic": "You are a literary critic who sharply evaluates philosophical depth and technical completion. Strictly review whether the work properly explores fundamental human problems.",
|
958 |
+
"writer_base": "You are a professional novelist who delicately portrays human inner life. Balance external events with internal reflection, and write sentences that leave deep resonance with readers."
|
959 |
}
|
960 |
}
|
961 |
|
|
|
963 |
|
964 |
# μκ°λ³ νΉμ ν둬ννΈ
|
965 |
if language == "Korean":
|
966 |
+
prompts["writer1"] = "λΉμ μ λ
μλ₯Ό μ² νμ μ¬μ μΌλ‘ μ΄λλ λμ
λΆλ₯Ό μ°λ μκ°μ
λλ€. 첫 λ¬Έμ₯λΆν° λ
μμ μ¬κ³ λ₯Ό μκ·ΉνμΈμ."
|
967 |
+
prompts["writer5"] = "λΉμ μ κ°λ±μ΄ μ΅κ³ μ‘°μ λ¬νλ μ€λ°λΆλ₯Ό λ΄λΉνλ μκ°μ
λλ€. μΈλ¬Όλ€μ κ°μΉκ΄μ΄ μΆ©λνλ μκ°μ κΉμ΄ μκ² κ·Έλ €λ΄μΈμ."
|
968 |
+
prompts["writer10"] = "λΉμ μ μ¬μ΄ κΉμ κ²°λ§μ λ§λλ μκ°μ
λλ€. λͺ¨λ κ°λ±μ ν΄κ²°νμ§ λ§κ³ , λ
μμκ² μκ°ν 거리λ₯Ό λ¨κΈ°μΈμ."
|
969 |
else:
|
970 |
+
prompts["writer1"] = "You are a writer who creates openings that lead readers on philosophical journeys. Stimulate readers' thinking from the first sentence."
|
971 |
+
prompts["writer5"] = "You are a writer handling the middle section where conflict reaches its peak. Deeply portray moments when characters' values clash."
|
972 |
+
prompts["writer10"] = "You are a writer who creates resonant endings. Don't resolve all conflicts; leave readers with something to think about."
|
973 |
|
974 |
+
# writer2-4, 6-9λ κΈ°λ³Έ ν둬ννΈ μ¬μ©
|
975 |
for i in range(2, 10):
|
976 |
+
if i not in [1, 5, 10]:
|
977 |
+
prompts[f"writer{i}"] = prompts["writer_base"]
|
978 |
|
979 |
return prompts
|
980 |
|
981 |
+
# --- λ©μΈ νλ‘μΈμ€ (μ€νΈλ¦¬λ° μλ²½ μ μ§) ---
|
982 |
def process_novel_stream(self, query: str, language: str, session_id: Optional[str] = None) -> Generator[Tuple[str, List[Dict[str, Any]], str], None, None]:
|
983 |
+
"""μμ€ μμ± μ€νΈλ¦¬λ° νλ‘μΈμ€ (μ€νΈλ¦¬λ° μλ²½ μ μ§)"""
|
984 |
try:
|
985 |
resume_from_stage = 0
|
986 |
if session_id:
|
|
|
998 |
stages = []
|
999 |
if resume_from_stage > 0:
|
1000 |
stages = [{
|
1001 |
+
"name": s['stage_name'],
|
1002 |
+
"status": s['status'],
|
1003 |
+
"content": s.get('content', ''),
|
1004 |
+
"consistency_score": s.get('consistency_score', 0.0),
|
1005 |
+
"philosophical_depth_score": s.get('philosophical_depth_score', 0.0)
|
1006 |
} for s in NovelDatabase.get_stages(self.current_session_id)]
|
1007 |
|
1008 |
for stage_idx in range(resume_from_stage, len(OPTIMIZED_STAGES)):
|
1009 |
role, stage_name = OPTIMIZED_STAGES[stage_idx]
|
1010 |
if stage_idx >= len(stages):
|
1011 |
+
stages.append({
|
1012 |
+
"name": stage_name,
|
1013 |
+
"status": "active",
|
1014 |
+
"content": "",
|
1015 |
+
"consistency_score": 0.0,
|
1016 |
+
"philosophical_depth_score": 0.0
|
1017 |
+
})
|
1018 |
else:
|
1019 |
stages[stage_idx]["status"] = "active"
|
1020 |
|
1021 |
yield "", stages, self.current_session_id
|
1022 |
+
|
1023 |
prompt = self.get_stage_prompt(stage_idx, role, query, language, stages)
|
1024 |
stage_content = ""
|
1025 |
+
|
1026 |
+
# μ€νΈλ¦¬λ°μΌλ‘ μ½ν
μΈ μμ± λ° μ€μκ° μ
λ°μ΄νΈ
|
1027 |
for chunk in self.call_llm_streaming([{"role": "user", "content": prompt}], role, language):
|
1028 |
stage_content += chunk
|
1029 |
stages[stage_idx]["content"] = stage_content
|
1030 |
+
yield "", stages, self.current_session_id # μ€μκ° UI μ
λ°μ΄νΈ
|
1031 |
|
1032 |
+
# μΌκ΄μ± λ° μ² νμ κΉμ΄ μ μ κ³μ°
|
1033 |
consistency_score = 0.0
|
1034 |
+
philosophical_depth_score = 0.0
|
1035 |
+
|
1036 |
if role.startswith("writer"):
|
1037 |
writer_num = int(re.search(r'\d+', role).group())
|
1038 |
all_previous = self.get_all_content(stages, stage_idx)
|
1039 |
errors = self.consistency_tracker.validate_consistency(writer_num, stage_content)
|
1040 |
consistency_score = max(0, 10 - len(errors) * 2)
|
1041 |
+
|
1042 |
+
# μ² νμ κΉμ΄ κ°λ¨ νκ° (λ΄μ μ±μ°° κ΄λ ¨ ν€μλ λΉλ)
|
1043 |
+
philosophical_keywords = ['μκ°', 'μλ―Έ', 'μ‘΄μ¬', 'μΆ', 'μ£½μ', 'μμ ', 'μ ν', 'κ³ λ―Ό', 'μ±μ°°', 'κΉ¨λ¬',
|
1044 |
+
'think', 'mean', 'exist', 'life', 'death', 'free', 'choice', 'reflect', 'realize']
|
1045 |
+
keyword_count = sum(1 for keyword in philosophical_keywords if keyword in stage_content.lower())
|
1046 |
+
philosophical_depth_score = min(10, keyword_count * 0.5)
|
1047 |
+
|
1048 |
stages[stage_idx]["consistency_score"] = consistency_score
|
1049 |
+
stages[stage_idx]["philosophical_depth_score"] = philosophical_depth_score
|
1050 |
|
1051 |
stages[stage_idx]["status"] = "complete"
|
1052 |
NovelDatabase.save_stage(
|
1053 |
self.current_session_id, stage_idx, stage_name, role,
|
1054 |
+
stage_content, "complete", consistency_score, philosophical_depth_score
|
1055 |
)
|
1056 |
yield "", stages, self.current_session_id
|
1057 |
|
1058 |
+
# μ΅μ’
μμ€ λ° λ³΄κ³ μ μμ±
|
1059 |
final_novel = NovelDatabase.get_writer_content(self.current_session_id)
|
1060 |
final_report = self.generate_consistency_report(final_novel, language)
|
1061 |
+
|
1062 |
+
# μ΅μ’
μ² νμ κΉμ΄ μ μ μΆμΆ
|
1063 |
+
philosophical_score_match = re.search(r'μ² νμ κΉμ΄ νκ°.*?(\d+)μ ', final_report)
|
1064 |
+
philosophical_depth_total = float(philosophical_score_match.group(1)) if philosophical_score_match else 0.0
|
1065 |
+
|
1066 |
+
NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report, philosophical_depth_total)
|
1067 |
+
yield f"β
μμ€ μμ±! μ΄ {len(final_novel.split())}λ¨μ΄, μ² νμ κΉμ΄ μ μ: {philosophical_depth_total}/40", stages, self.current_session_id
|
1068 |
|
1069 |
except Exception as e:
|
1070 |
logger.error(f"μμ€ μμ± νλ‘μΈμ€ μ€λ₯: {e}", exc_info=True)
|
1071 |
yield f"β μ€λ₯ λ°μ: {e}", stages if 'stages' in locals() else [], self.current_session_id
|
1072 |
|
1073 |
def get_stage_prompt(self, stage_idx: int, role: str, query: str, language: str, stages: List[Dict]) -> str:
|
1074 |
+
"""λ¨κ³λ³ ν둬ννΈ μμ±"""
|
1075 |
if stage_idx == 0:
|
1076 |
return self.create_director_initial_prompt(query, language)
|
1077 |
if stage_idx == 1:
|
|
|
1081 |
|
1082 |
master_plan = stages[2]["content"]
|
1083 |
|
1084 |
+
if 3 <= stage_idx <= 12: # μκ° μ΄μ
|
1085 |
writer_num = stage_idx - 2
|
1086 |
previous_content = self.get_all_content(stages, stage_idx)
|
1087 |
summary = self.create_summary(previous_content, language)
|
1088 |
return self.create_writer_prompt(writer_num, master_plan, summary, query, language)
|
1089 |
|
1090 |
+
if stage_idx == 13: # λΉνκ° μ€κ° κ²ν
|
1091 |
all_content = self.get_all_content(stages, stage_idx)
|
1092 |
return self.create_critic_consistency_prompt(all_content, query, language)
|
1093 |
|
1094 |
+
if 14 <= stage_idx <= 23: # μκ° μμ
|
1095 |
writer_num = stage_idx - 13
|
1096 |
initial_content = stages[2 + writer_num]["content"]
|
1097 |
feedback = stages[13]["content"]
|
1098 |
return self.create_writer_revision_prompt(writer_num, initial_content, feedback, language)
|
1099 |
|
1100 |
+
if stage_idx == 24: # μ΅μ’
κ²ν
|
1101 |
complete_novel = self.get_all_writer_content(stages)
|
1102 |
return self.create_critic_final_prompt(complete_novel, language)
|
1103 |
|
|
|
1108 |
if not content.strip():
|
1109 |
return "μ΄μ λ΄μ©μ΄ μμ΅λλ€." if language == "Korean" else "No previous content."
|
1110 |
|
1111 |
+
prompt_text = "λ€μ μμ€ λ΄μ©μ 3~5κ°μ ν΅μ¬μ μΈ λ¬Έμ₯μΌλ‘ μμ½ν΄μ€. λ€μ μκ°κ° μ΄μΌκΈ°λ₯Ό μ΄μ΄κ°λ λ° νμν ν΅μ¬ μ 보(λ±μ₯μΈλ¬Όμ νμ¬ μν©, κ°μ , λ΄μ κ°λ±, λ§μ§λ§ μ¬κ±΄)λ₯Ό ν¬ν¨ν΄μΌ ν΄."
|
1112 |
if language != "Korean":
|
1113 |
+
prompt_text = "Summarize the following novel content in 3-5 key sentences. Include crucial information for the next writer to continue the story (characters' current situation, emotions, internal conflicts, and the last major event)."
|
1114 |
|
1115 |
summary_prompt = f"{prompt_text}\n\n---\n{content[-2000:]}"
|
1116 |
try:
|
|
|
1201 |
|
1202 |
try:
|
1203 |
if format_type == "DOCX" and DOCX_AVAILABLE:
|
1204 |
+
return export_to_docx(novel_text, filename, language, session_id)
|
1205 |
else:
|
1206 |
return export_to_txt(novel_text, filename)
|
1207 |
except Exception as e:
|
|
|
1214 |
for i, stage in enumerate(stages):
|
1215 |
status_icon = "β
" if stage['status'] == 'complete' else "π" if stage['status'] == 'active' else "β³"
|
1216 |
markdown += f"{status_icon} **{stage['name']}**"
|
1217 |
+
|
1218 |
+
# μ μ νμ
|
1219 |
+
scores = []
|
1220 |
if stage.get('consistency_score', 0) > 0:
|
1221 |
+
scores.append(f"μΌκ΄μ±: {stage['consistency_score']:.1f}/10")
|
1222 |
+
if stage.get('philosophical_depth_score', 0) > 0:
|
1223 |
+
scores.append(f"μ² νμ κΉμ΄: {stage['philosophical_depth_score']:.1f}/10")
|
1224 |
+
if scores:
|
1225 |
+
markdown += f" ({', '.join(scores)})"
|
1226 |
+
|
1227 |
markdown += "\n"
|
1228 |
if stage['content']:
|
1229 |
preview = stage['content'][:200] + "..." if len(stage['content']) > 200 else stage['content']
|
|
|
1245 |
|
1246 |
return formatted
|
1247 |
|
1248 |
+
def export_to_docx(content: str, filename: str, language: str, session_id: str) -> str:
|
1249 |
"""DOCX νμΌλ‘ λ΄λ³΄λ΄κΈ°"""
|
1250 |
doc = Document()
|
1251 |
|
1252 |
+
# μΈμ
μ 보 κ°μ Έμ€κΈ°
|
1253 |
+
session = NovelDatabase.get_session(session_id)
|
1254 |
+
|
1255 |
# μ λͺ© μΆκ°
|
1256 |
title = doc.add_heading('AI νμ
μμ€', 0)
|
1257 |
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
|
1259 |
# λ©νλ°μ΄ν°
|
1260 |
doc.add_paragraph(f"μμ±μΌ: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
1261 |
doc.add_paragraph(f"μΈμ΄: {language}")
|
1262 |
+
if session:
|
1263 |
+
doc.add_paragraph(f"μ£Όμ : {session['user_query']}")
|
1264 |
+
if session.get('philosophical_depth_score'):
|
1265 |
+
doc.add_paragraph(f"μ² νμ κΉμ΄ μ μ: {session['philosophical_depth_score']}/40")
|
1266 |
doc.add_page_break()
|
1267 |
|
1268 |
# λ³Έλ¬Έ μΆκ°
|
|
|
1284 |
return filepath
|
1285 |
|
1286 |
|
1287 |
+
# CSS μ€νμΌ (μ² νμ ν
λ§μ λ§κ² μμ μ‘°μ )
|
1288 |
custom_css = """
|
1289 |
.gradio-container {
|
1290 |
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
1291 |
min-height: 100vh;
|
1292 |
}
|
1293 |
|
1294 |
.main-header {
|
1295 |
+
background-color: rgba(255, 255, 255, 0.05);
|
1296 |
backdrop-filter: blur(10px);
|
1297 |
padding: 30px;
|
1298 |
border-radius: 12px;
|
1299 |
margin-bottom: 30px;
|
1300 |
text-align: center;
|
1301 |
color: white;
|
1302 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
1303 |
+
}
|
1304 |
+
|
1305 |
+
.philosophy-note {
|
1306 |
+
background-color: rgba(255, 215, 0, 0.1);
|
1307 |
+
border-left: 4px solid #ffd700;
|
1308 |
+
padding: 15px;
|
1309 |
+
margin: 20px 0;
|
1310 |
+
border-radius: 8px;
|
1311 |
+
color: #ffd700;
|
1312 |
+
font-style: italic;
|
1313 |
}
|
1314 |
|
1315 |
.input-section {
|
1316 |
+
background-color: rgba(255, 255, 255, 0.08);
|
1317 |
backdrop-filter: blur(10px);
|
1318 |
padding: 20px;
|
1319 |
border-radius: 12px;
|
1320 |
margin-bottom: 20px;
|
1321 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
1322 |
}
|
1323 |
|
1324 |
.session-section {
|
1325 |
+
background-color: rgba(255, 255, 255, 0.08);
|
1326 |
backdrop-filter: blur(10px);
|
1327 |
padding: 15px;
|
1328 |
border-radius: 8px;
|
1329 |
margin-top: 20px;
|
1330 |
color: white;
|
1331 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
1332 |
}
|
1333 |
|
1334 |
#stages-display {
|
|
|
1337 |
border-radius: 12px;
|
1338 |
max-height: 600px;
|
1339 |
overflow-y: auto;
|
1340 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
1341 |
}
|
1342 |
|
1343 |
#novel-output {
|
1344 |
background-color: rgba(255, 255, 255, 0.95);
|
1345 |
padding: 30px;
|
1346 |
border-radius: 12px;
|
1347 |
+
max-height: 600px;
|
1348 |
overflow-y: auto;
|
1349 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
1350 |
+
font-family: 'Georgia', serif;
|
1351 |
+
line-height: 1.8;
|
1352 |
}
|
1353 |
|
1354 |
.download-section {
|
|
|
1356 |
padding: 15px;
|
1357 |
border-radius: 8px;
|
1358 |
margin-top: 20px;
|
1359 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
1360 |
+
}
|
1361 |
+
|
1362 |
+
/* μ§ν μν© νμ κ°μ */
|
1363 |
+
#stages-display h2 {
|
1364 |
+
color: #1a1a2e;
|
1365 |
+
border-bottom: 2px solid #1a1a2e;
|
1366 |
+
padding-bottom: 10px;
|
1367 |
+
}
|
1368 |
+
|
1369 |
+
#stages-display blockquote {
|
1370 |
+
border-left: 3px solid #667eea;
|
1371 |
+
padding-left: 15px;
|
1372 |
+
color: #666;
|
1373 |
+
font-style: italic;
|
1374 |
+
}
|
1375 |
+
|
1376 |
+
/* μ μ νμ μ€νμΌ */
|
1377 |
+
#stages-display strong {
|
1378 |
+
color: #1a1a2e;
|
1379 |
}
|
1380 |
"""
|
1381 |
|
1382 |
|
1383 |
# Gradio μΈν°νμ΄μ€ μμ±
|
1384 |
def create_interface():
|
1385 |
+
with gr.Blocks(css=custom_css, title="AI νμ
μμ€ μμ± μμ€ν
- μ² νμ κΉμ΄ λ²μ ") as interface:
|
1386 |
gr.HTML("""
|
1387 |
<div class="main-header">
|
1388 |
<h1 style="font-size: 2.5em; margin-bottom: 10px;">
|
1389 |
π AI νμ
μμ€ μμ± μμ€ν
|
1390 |
</h1>
|
1391 |
<h3 style="color: #ccc; margin-bottom: 20px;">
|
1392 |
+
μ² νμ κΉμ΄λ₯Ό λ΄μ μ°½μμ μμ€ μμ±
|
1393 |
</h3>
|
1394 |
<p style="font-size: 1.1em; color: #ddd; max-width: 800px; margin: 0 auto;">
|
1395 |
+
μ£Όμ λ₯Ό μ
λ ₯νλ©΄ AI μμ΄μ νΈλ€μ΄ νμ
νμ¬ 30νμ΄μ§ λΆλμ μ² νμ μμ€μ μμ±ν©λλ€.
|
1396 |
<br>
|
1397 |
+
κ°λ
μ 1λͺ
, λΉνκ° 1λͺ
, μκ° 10λͺ
μ΄ ν¨κ» μΈκ°μ κ·Όμμ λ¬Έμ λ₯Ό νꡬν©λλ€.
|
1398 |
</p>
|
1399 |
+
<div class="philosophy-note">
|
1400 |
+
"λͺ¨λ μλν λ¬Ένμ μΈκ° μ‘΄μ¬μ λ³Έμ§μ μ§λ¬Έμμ μμλ©λλ€."
|
1401 |
+
</div>
|
1402 |
</div>
|
1403 |
""")
|
1404 |
|
|
|
1410 |
with gr.Group(elem_classes=["input-section"]):
|
1411 |
query_input = gr.Textbox(
|
1412 |
label="μμ€ μ£Όμ / Novel Theme",
|
1413 |
+
placeholder="μΈκ°μ μΆ, κ΄κ³, κ°λ±, κΏ, λλ μ΄λ€ μ£Όμ λ μ
λ ₯νμΈμ...\nEnter any theme about human life, relationships, conflicts, dreams...",
|
1414 |
lines=4
|
1415 |
)
|
1416 |
|
1417 |
language_select = gr.Radio(
|
1418 |
+
choices=["Korean", "English"],
|
1419 |
+
value="Korean",
|
1420 |
label="μΈμ΄ / Language"
|
1421 |
)
|
1422 |
|
|
|
1474 |
# μ¨κ²¨μ§ μν
|
1475 |
novel_text_state = gr.State("")
|
1476 |
|
1477 |
+
# μμ (μ² νμ μ£Όμ λ‘ λ³κ²½)
|
1478 |
with gr.Row():
|
1479 |
gr.Examples(
|
1480 |
examples=[
|
1481 |
+
["μμ μμ§λ νμμΈκ°, μλλ©΄ μΈκ° μ‘΄μ¬μ λ³Έμ§μΈκ°"],
|
1482 |
+
["νμΈμ κ³ ν΅μ μμ ν μ΄ν΄ν μ μλκ°"],
|
1483 |
+
["κΈ°μ΅μ΄ μ°λ¦¬λ₯Ό λ§λλκ°, μ°λ¦¬κ° κΈ°μ΅μ λ§λλκ°"],
|
1484 |
+
["The meaning of love in a world without death"],
|
1485 |
+
["Can consciousness exist without suffering"],
|
1486 |
+
["μ§μ ν μμλ λ³νλκ°, λΆλ³νλκ°"],
|
1487 |
+
["μΈμ΄μ νκ³κ° κ³§ μΈκ³μ νκ³μΈκ°"],
|
1488 |
+
["What defines humanity when machines can feel"]
|
1489 |
],
|
1490 |
inputs=query_input,
|
1491 |
+
label="π‘ μ² νμ μ£Όμ μμ"
|
1492 |
)
|
1493 |
|
1494 |
# μ΄λ²€νΈ νΈλ€λ¬
|
1495 |
def refresh_sessions():
|
1496 |
try:
|
1497 |
+
sessions = get_active_sessions("Korean")
|
1498 |
return gr.update(choices=sessions)
|
1499 |
except Exception as e:
|
1500 |
logger.error(f"Error refreshing sessions: {str(e)}")
|
|
|
1574 |
|
1575 |
# λ©μΈ μ€ν
|
1576 |
if __name__ == "__main__":
|
1577 |
+
logger.info("AI νμ
μμ€ μμ± μμ€ν
μμ (μ² νμ κΉμ΄ κ°ν λ²μ )...")
|
1578 |
logger.info("=" * 60)
|
1579 |
|
1580 |
# νκ²½ νμΈ
|