Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -48,9 +48,9 @@ selected_language = "English" # ๊ธฐ๋ณธ ์ธ์ด
|
|
48 |
DB_PATH = "novel_sessions.db"
|
49 |
db_lock = threading.Lock()
|
50 |
|
51 |
-
# Stage ๋ฒํธ ์์
|
52 |
-
WRITER_DRAFT_STAGES = [3, 6, 9, 12, 15, 18, 21, 24, 27, 30] # ์๊ฐ ์ด์
|
53 |
-
WRITER_REVISION_STAGES = [5, 8, 11, 14, 17, 20, 23, 26, 29, 32] # ์๊ฐ ์์ ๋ณธ
|
54 |
|
55 |
class NovelDatabase:
|
56 |
"""Novel session management database"""
|
@@ -89,6 +89,7 @@ class NovelDatabase:
|
|
89 |
content TEXT,
|
90 |
status TEXT DEFAULT 'pending',
|
91 |
created_at TEXT DEFAULT (datetime('now')),
|
|
|
92 |
FOREIGN KEY (session_id) REFERENCES sessions(session_id),
|
93 |
UNIQUE(session_id, stage_number)
|
94 |
)
|
@@ -126,24 +127,24 @@ class NovelDatabase:
|
|
126 |
conn.commit()
|
127 |
|
128 |
return session_id
|
129 |
-
|
130 |
@staticmethod
|
131 |
def save_stage(session_id: str, stage_number: int, stage_name: str,
|
132 |
role: str, content: str, status: str = 'complete'):
|
133 |
-
"""
|
134 |
with NovelDatabase.get_db() as conn:
|
135 |
cursor = conn.cursor()
|
136 |
|
137 |
-
# UPSERT
|
138 |
cursor.execute('''
|
139 |
INSERT INTO stages (session_id, stage_number, stage_name, role, content, status)
|
140 |
VALUES (?, ?, ?, ?, ?, ?)
|
141 |
ON CONFLICT(session_id, stage_number)
|
142 |
-
DO UPDATE SET content=?, status=?, stage_name
|
143 |
''', (session_id, stage_number, stage_name, role, content, status,
|
144 |
content, status, stage_name))
|
145 |
|
146 |
-
#
|
147 |
cursor.execute('''
|
148 |
UPDATE sessions
|
149 |
SET updated_at = datetime('now'), current_stage = ?
|
@@ -151,10 +152,7 @@ class NovelDatabase:
|
|
151 |
''', (stage_number, session_id))
|
152 |
|
153 |
conn.commit()
|
154 |
-
logger.info(f"{
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
|
159 |
@staticmethod
|
160 |
def get_session(session_id: str) -> Optional[Dict]:
|
@@ -178,11 +176,14 @@ class NovelDatabase:
|
|
178 |
|
179 |
@staticmethod
|
180 |
def get_all_writer_content(session_id: str) -> str:
|
181 |
-
"""๋ชจ๋ ์๊ฐ์ ์์ ๋ณธ ๋ด์ฉ์ ๊ฐ์ ธ์์ ํฉ์น๊ธฐ -
|
182 |
with NovelDatabase.get_db() as conn:
|
183 |
cursor = conn.cursor()
|
184 |
|
185 |
all_content = []
|
|
|
|
|
|
|
186 |
for stage_num in WRITER_REVISION_STAGES:
|
187 |
cursor.execute('''
|
188 |
SELECT content, stage_name FROM stages
|
@@ -197,11 +198,18 @@ class NovelDatabase:
|
|
197 |
clean_content = clean_content.strip()
|
198 |
|
199 |
if clean_content:
|
|
|
|
|
|
|
200 |
all_content.append(clean_content)
|
201 |
-
logger.info(f"
|
202 |
|
203 |
full_content = '\n\n'.join(all_content)
|
204 |
-
logger.info(f"Total
|
|
|
|
|
|
|
|
|
205 |
|
206 |
return full_content
|
207 |
|
@@ -241,6 +249,48 @@ class NovelDatabase:
|
|
241 |
except:
|
242 |
# Fallback to SQLite default format
|
243 |
return datetime.strptime(datetime_str, "%Y-%m-%d %H:%M:%S")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
244 |
|
245 |
class NovelWritingSystem:
|
246 |
def __init__(self):
|
@@ -269,13 +319,13 @@ class NovelWritingSystem:
|
|
269 |
}
|
270 |
|
271 |
def create_director_initial_prompt(self, user_query: str, language: str = "English") -> str:
|
272 |
-
"""Director AI initial prompt - Novel planning"""
|
273 |
if language == "Korean":
|
274 |
-
return f"""๋น์ ์
|
275 |
|
276 |
์ฌ์ฉ์ ์์ฒญ: {user_query}
|
277 |
|
278 |
-
๋ค์ ์์๋ค์ ์ฒด๊ณ์ ์ผ๋ก ๊ตฌ์ฑํ์ฌ
|
279 |
|
280 |
1. **์ฃผ์ ์ ์ฅ๋ฅด**
|
281 |
- ํต์ฌ ์ฃผ์ ์ ๋ฉ์์ง
|
@@ -291,22 +341,26 @@ class NovelWritingSystem:
|
|
291 |
- ๊ฐ๋ฑ ๊ตฌ์กฐ
|
292 |
- ๊ฐ์ ์ ์ฐ๊ฒฐ๊ณ ๋ฆฌ
|
293 |
|
294 |
-
4. **์์ฌ ๊ตฌ์กฐ** (
|
295 |
| ํํธ | ํ์ด์ง | ์ฃผ์ ์ฌ๊ฑด | ๊ธด์ฅ๋ | ์ธ๋ฌผ ๋ฐ์ |
|
296 |
|------|--------|-----------|---------|-----------|
|
|
|
|
|
|
|
|
|
297 |
|
298 |
5. **์ธ๊ณ๊ด ์ค์ **
|
299 |
- ์๊ณต๊ฐ์ ๋ฐฐ๊ฒฝ
|
300 |
- ์ฌํ์ /๋ฌธํ์ ๋งฅ๋ฝ
|
301 |
- ๋ถ์๊ธฐ์ ํค
|
302 |
|
303 |
-
๊ฐ ์์ฑ์๊ฐ
|
304 |
else:
|
305 |
-
return f"""You are a literary director planning a
|
306 |
|
307 |
User Request: {user_query}
|
308 |
|
309 |
-
Systematically compose the following elements to create the foundation for a
|
310 |
|
311 |
1. **Theme and Genre**
|
312 |
- Core theme and message
|
@@ -322,16 +376,20 @@ Systematically compose the following elements to create the foundation for a 50-
|
|
322 |
- Conflict structure
|
323 |
- Emotional connections
|
324 |
|
325 |
-
4. **Narrative Structure** (divide
|
326 |
| Part | Pages | Main Events | Tension | Character Development |
|
327 |
|------|-------|-------------|---------|---------------------|
|
|
|
|
|
|
|
|
|
328 |
|
329 |
5. **World Building**
|
330 |
- Temporal and spatial setting
|
331 |
- Social/cultural context
|
332 |
- Atmosphere and tone
|
333 |
|
334 |
-
Provide clear guidelines for each writer to compose
|
335 |
|
336 |
def create_critic_director_prompt(self, director_plan: str, language: str = "English") -> str:
|
337 |
"""Critic's review of director's plan"""
|
@@ -353,7 +411,7 @@ Provide clear guidelines for each writer to compose 5 pages."""
|
|
353 |
|------|------|------|-----------|
|
354 |
|
355 |
3. **๊ตฌ์กฐ์ ๊ท ํ**
|
356 |
-
-
|
357 |
- ๊ธด์ฅ๊ณผ ์ด์์ ๋ฆฌ๋ฌ
|
358 |
- ์ ์ฒด์ ์ธ ํ๋ฆ
|
359 |
|
@@ -363,7 +421,7 @@ Provide clear guidelines for each writer to compose 5 pages."""
|
|
363 |
- ๊ธฐ๋์น ์ถฉ์กฑ๋
|
364 |
|
365 |
5. **์คํ ๊ฐ๋ฅ์ฑ**
|
366 |
-
-
|
367 |
- ์ผ๊ด์ฑ ์ ์ง ๋ฐฉ์
|
368 |
- ์ ์ฌ์ ๋ฌธ์ ์
|
369 |
|
@@ -386,7 +444,7 @@ Critique from the following perspectives and provide specific improvements:
|
|
386 |
|-----------|-----------|------------|-------------|
|
387 |
|
388 |
3. **Structural Balance**
|
389 |
-
- Distribution across parts
|
390 |
- Rhythm of tension and relief
|
391 |
- Overall flow
|
392 |
|
@@ -396,7 +454,7 @@ Critique from the following perspectives and provide specific improvements:
|
|
396 |
- Expectation fulfillment
|
397 |
|
398 |
5. **Feasibility**
|
399 |
-
- Clarity of guidelines for
|
400 |
- Consistency maintenance
|
401 |
- Potential issues
|
402 |
|
@@ -415,7 +473,7 @@ Provide specific and constructive feedback."""
|
|
415 |
|
416 |
๋ค์์ ํฌํจํ ์์ ๋ ์ต์ข
๊ธฐํ์ ์ ์ํ์ธ์:
|
417 |
|
418 |
-
1. **์์ ๋ ์์ฌ ๊ตฌ์กฐ**
|
419 |
| ํํธ | ํ์ด์ง | ์ฃผ์ ์ฌ๊ฑด | ์์ฑ ์ง์นจ | ์ฃผ์์ฌํญ |
|
420 |
|------|--------|-----------|-----------|----------|
|
421 |
|
@@ -424,7 +482,7 @@ Provide specific and constructive feedback."""
|
|
424 |
- ์ธ๋ฌผ ๊ฐ ๊ฐ๋ฑ์ ๊ตฌ์ฒดํ
|
425 |
- ๊ฐ์ ์ ์ ๋ณํ ์ถ์ด
|
426 |
|
427 |
-
3. **๊ฐ ์์ฑ์๋ฅผ ์ํ ์์ธ ๊ฐ์ด๋**
|
428 |
- ํํธ๋ณ ์์๊ณผ ๋ ์ง์
|
429 |
- ํ์ ํฌํจ ์์
|
430 |
- ๋ฌธ์ฒด์ ํค ์ง์นจ
|
@@ -441,7 +499,7 @@ Provide specific and constructive feedback."""
|
|
441 |
- ์ ์ฒด์ ํต์ผ์ฑ
|
442 |
- ๋
์ ๋ชฐ์
์ ์ง ๋ฐฉ์
|
443 |
|
444 |
-
|
445 |
else:
|
446 |
return f"""As director, revise the novel plan reflecting the critic's feedback.
|
447 |
|
@@ -453,7 +511,7 @@ Critic Feedback:
|
|
453 |
|
454 |
Present the revised final plan including:
|
455 |
|
456 |
-
1. **Revised Narrative Structure**
|
457 |
| Part | Pages | Main Events | Writing Guidelines | Cautions |
|
458 |
|------|-------|-------------|-------------------|----------|
|
459 |
|
@@ -462,7 +520,7 @@ Present the revised final plan including:
|
|
462 |
- Concrete conflicts between characters
|
463 |
- Emotional arc progression
|
464 |
|
465 |
-
3. **Detailed Guide for Each Writer**
|
466 |
- Start and end points for each part
|
467 |
- Essential elements to include
|
468 |
- Style and tone guidelines
|
@@ -479,15 +537,15 @@ Present the revised final plan including:
|
|
479 |
- Overall unity
|
480 |
- Reader engagement maintenance
|
481 |
|
482 |
-
Create a final masterplan that all writers can clearly understand."""
|
483 |
|
484 |
def create_writer_prompt(self, writer_number: int, director_plan: str, previous_content: str, language: str = "English") -> str:
|
485 |
-
"""Individual writer prompt -
|
486 |
-
pages_start = (writer_number - 1) *
|
487 |
-
pages_end = writer_number *
|
488 |
|
489 |
if language == "Korean":
|
490 |
-
return f"""๋น์ ์ ์์ฑ์ {writer_number}๋ฒ์
๋๋ค.
|
491 |
|
492 |
๊ฐ๋
์์ ๋ง์คํฐํ๋:
|
493 |
{director_plan}
|
@@ -497,7 +555,7 @@ Create a final masterplan that all writers can clearly understand."""
|
|
497 |
|
498 |
๋ค์ ์ง์นจ์ ๋ฐ๋ผ ์์ฑํ์ธ์:
|
499 |
|
500 |
-
1. **๋ถ๋**: ์ ํํ
|
501 |
2. **์ฐ์์ฑ**: ์ด์ ๋ด์ฉ๊ณผ ์์ฐ์ค๋ฝ๊ฒ ์ด์ด์ง๋๋ก
|
502 |
3. **์ผ๊ด์ฑ**:
|
503 |
- ๋ฑ์ฅ์ธ๋ฌผ์ ์ฑ๊ฒฉ๊ณผ ๋งํฌ ์ ์ง
|
@@ -512,9 +570,9 @@ Create a final masterplan that all writers can clearly understand."""
|
|
512 |
- ๋
์์ ๋ชฐ์
์ ํด์น์ง ์๊ธฐ
|
513 |
|
514 |
์ค์: ํ์ด์ง ๊ตฌ๋ถ ํ์๋ฅผ ์ ๋ ํ์ง ๋ง์ธ์. ์์ฐ์ค๋ฝ๊ฒ ์ด์ด์ง๋ ์์ฌ๋ก ์์ฑํ์ธ์.
|
515 |
-
๋ฐ๋์
|
516 |
else:
|
517 |
-
return f"""You are Writer #{writer_number}. Write pages {pages_start}-{pages_end} (
|
518 |
|
519 |
Director's Masterplan:
|
520 |
{director_plan}
|
@@ -524,7 +582,7 @@ Director's Masterplan:
|
|
524 |
|
525 |
Write according to these guidelines:
|
526 |
|
527 |
-
1. **Length**: Exactly
|
528 |
2. **Continuity**: Flow naturally from previous content
|
529 |
3. **Consistency**:
|
530 |
- Maintain character personalities and speech
|
@@ -539,7 +597,7 @@ Write according to these guidelines:
|
|
539 |
- Keep reader immersion
|
540 |
|
541 |
Important: DO NOT use any page markers. Write as continuous narrative.
|
542 |
-
You MUST write
|
543 |
|
544 |
def create_critic_writer_prompt(self, writer_number: int, writer_content: str, director_plan: str, all_previous_content: str, language: str = "English") -> str:
|
545 |
"""Critic's review of individual writer's work"""
|
@@ -652,7 +710,7 @@ Clearly distinguish between mandatory revisions and optional improvements."""
|
|
652 |
- ๋ฌ์ฌ์ ๋ํ ๊ฐ์
|
653 |
|
654 |
3. **๋ถ๋ ์ ์ง**
|
655 |
-
- ์ฌ์ ํ ์ ํํ
|
656 |
- ํ์ด์ง ๊ตฌ๋ถ ํ์ ์ ๋ ๊ธ์ง
|
657 |
|
658 |
4. **์ฐ์์ฑ ํ๋ณด**
|
@@ -660,7 +718,7 @@ Clearly distinguish between mandatory revisions and optional improvements."""
|
|
660 |
- ์์ ์ผ๋ก ์ธํ ์๋ก์ด ๋ชจ์ ๋ฐฉ์ง
|
661 |
|
662 |
์์ ๋ ์ต์ข
๋ณธ์ ์ ์ํ์ธ์. ํ์ด์ง ๋งํฌ๋ ์ ๋ ์ฌ์ฉํ์ง ๋ง์ธ์.
|
663 |
-
๋ฐ๋์
|
664 |
else:
|
665 |
return f"""As Writer #{writer_number}, revise based on critic's feedback.
|
666 |
|
@@ -683,7 +741,7 @@ Write a revision reflecting:
|
|
683 |
- Improve descriptions and dialogue
|
684 |
|
685 |
3. **Maintain Length**
|
686 |
-
- Still exactly
|
687 |
- Absolutely no page markers
|
688 |
|
689 |
4. **Ensure Continuity**
|
@@ -691,14 +749,14 @@ Write a revision reflecting:
|
|
691 |
- Prevent new contradictions from revisions
|
692 |
|
693 |
Present the revised final version. Never use page markers.
|
694 |
-
You MUST maintain
|
695 |
|
696 |
def create_critic_final_prompt(self, all_content: str, director_plan: str, language: str = "English") -> str:
|
697 |
"""Final critic evaluation of complete novel"""
|
698 |
content_preview = all_content[:3000] + "\n...\n" + all_content[-3000:] if len(all_content) > 6000 else all_content
|
699 |
|
700 |
if language == "Korean":
|
701 |
-
return f"""์ ์ฒด
|
702 |
|
703 |
๊ฐ๋
์์ ๋ง์คํฐํ๋:
|
704 |
{director_plan}
|
@@ -741,7 +799,7 @@ You MUST maintain 2500-3000 words."""
|
|
741 |
|
742 |
๊ฐ๋
์๊ฐ ์ต์ข
์์ ํ ์ ์๋๋ก ๊ตฌ์ฒด์ ์ด๊ณ ์คํ ๊ฐ๋ฅํ ํผ๋๋ฐฑ์ ์ ๊ณตํ์ธ์."""
|
743 |
else:
|
744 |
-
return f"""Final evaluation of the complete
|
745 |
|
746 |
Director's Masterplan:
|
747 |
{director_plan}
|
@@ -787,11 +845,12 @@ Provide specific and actionable feedback for the director's final revision."""
|
|
787 |
def create_director_final_prompt(self, all_content: str, critic_final_feedback: str, language: str = "English") -> str:
|
788 |
"""Director's final compilation and polish - ๋ชจ๋ ์๊ฐ ๋ด์ฉ ํฌํจ"""
|
789 |
word_count = len(all_content.split())
|
|
|
790 |
|
791 |
if language == "Korean":
|
792 |
return f"""๊ฐ๋
์๋ก์ ๋นํ๊ฐ์ ์ต์ข
ํ๊ฐ๋ฅผ ๋ฐ์ํ์ฌ ์์ฑ๋ณธ์ ์ ์ํฉ๋๋ค.
|
793 |
|
794 |
-
์ ์ฒด ์๊ฐ๋ค์ ์ํ (
|
795 |
{all_content}
|
796 |
|
797 |
๋นํ๊ฐ ์ต์ข
ํ๊ฐ:
|
@@ -803,7 +862,7 @@ Provide specific and actionable feedback for the director's final revision."""
|
|
803 |
|
804 |
## ์ํ ์ ๋ณด
|
805 |
- ์ฅ๋ฅด:
|
806 |
-
- ๋ถ๋:
|
807 |
- ์ฃผ์ :
|
808 |
- ํ ์ค ์์ฝ:
|
809 |
|
@@ -814,7 +873,7 @@ Provide specific and actionable feedback for the director's final revision."""
|
|
814 |
|
815 |
## ๋ณธ๋ฌธ
|
816 |
|
817 |
-
[
|
818 |
1. ์ค๋ ์ค๋ฅ ์์ ์๋ฃ
|
819 |
2. ํํธ ๊ฐ ์ฐ๊ฒฐ ๋งค๋๋ฝ๊ฒ ์กฐ์
|
820 |
3. ๋ฌธ์ฒด์ ํค ํต์ผ
|
@@ -822,18 +881,18 @@ Provide specific and actionable feedback for the director's final revision."""
|
|
822 |
5. ํ์ด์ง ๊ตฌ๋ถ ํ์ ์์ ์ ๊ฑฐ
|
823 |
6. ์์ฐ์ค๋ฌ์ด ํ๋ฆ์ผ๋ก ์ฌ๊ตฌ์ฑ
|
824 |
|
825 |
-
[์ ์ฒด
|
826 |
|
827 |
---
|
828 |
|
829 |
## ์๊ฐ์ ๋ง
|
830 |
[์ํ์ ๋ํ ๊ฐ๋จํ ํด์ค์ด๋ ์๋]
|
831 |
|
832 |
-
๋ชจ๋ ์๊ฐ์ ๊ธฐ์ฌ๋ฅผ ํตํฉํ ์์ ํ
|
833 |
else:
|
834 |
return f"""As director, create the final version reflecting the critic's final evaluation.
|
835 |
|
836 |
-
Complete Writers' Work (Full
|
837 |
{all_content}
|
838 |
|
839 |
Critic's Final Evaluation:
|
@@ -845,7 +904,7 @@ Present the final version including:
|
|
845 |
|
846 |
## Work Information
|
847 |
- Genre:
|
848 |
-
- Length:
|
849 |
- Theme:
|
850 |
- One-line summary:
|
851 |
|
@@ -856,7 +915,7 @@ Present the final version including:
|
|
856 |
|
857 |
## Main Text
|
858 |
|
859 |
-
[Integrate all
|
860 |
1. Critical errors corrected
|
861 |
2. Smooth transitions between parts
|
862 |
3. Unified style and tone
|
@@ -864,14 +923,14 @@ Present the final version including:
|
|
864 |
5. Complete removal of page markers
|
865 |
6. Reorganized for natural flow
|
866 |
|
867 |
-
[Complete
|
868 |
|
869 |
---
|
870 |
|
871 |
## Author's Note
|
872 |
[Brief commentary or intention about the work]
|
873 |
|
874 |
-
Present a complete
|
875 |
|
876 |
def simulate_streaming(self, text: str, role: str) -> Generator[str, None, None]:
|
877 |
"""Simulate streaming in test mode"""
|
@@ -883,7 +942,7 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
883 |
time.sleep(0.02)
|
884 |
|
885 |
def call_llm_streaming(self, messages: List[Dict[str, str]], role: str, language: str = "English") -> Generator[str, None, None]:
|
886 |
-
"""Streaming LLM API call"""
|
887 |
|
888 |
if self.test_mode:
|
889 |
logger.info(f"Test mode streaming - Role: {role}, Language: {language}")
|
@@ -900,8 +959,13 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
900 |
*messages
|
901 |
]
|
902 |
|
903 |
-
# ์์ฑ์๋ค์๊ฒ๋
|
904 |
-
|
|
|
|
|
|
|
|
|
|
|
905 |
|
906 |
payload = {
|
907 |
"model": self.model_id,
|
@@ -913,7 +977,7 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
913 |
"stream_options": {"include_usage": True}
|
914 |
}
|
915 |
|
916 |
-
logger.info(f"API streaming call started - Role: {role}")
|
917 |
|
918 |
response = requests.post(
|
919 |
self.api_url,
|
@@ -929,6 +993,8 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
929 |
return
|
930 |
|
931 |
buffer = ""
|
|
|
|
|
932 |
for line in response.iter_lines():
|
933 |
if line:
|
934 |
line = line.decode('utf-8')
|
@@ -937,6 +1003,7 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
937 |
if data == "[DONE]":
|
938 |
if buffer:
|
939 |
yield buffer
|
|
|
940 |
break
|
941 |
try:
|
942 |
chunk = json.loads(data)
|
@@ -944,8 +1011,12 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
944 |
content = chunk["choices"][0].get("delta", {}).get("content", "")
|
945 |
if content:
|
946 |
buffer += content
|
947 |
-
|
948 |
-
|
|
|
|
|
|
|
|
|
949 |
yield buffer
|
950 |
buffer = ""
|
951 |
except json.JSONDecodeError:
|
@@ -953,6 +1024,12 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
953 |
|
954 |
if buffer:
|
955 |
yield buffer
|
|
|
|
|
|
|
|
|
|
|
|
|
956 |
|
957 |
except requests.exceptions.Timeout:
|
958 |
yield "โฑ๏ธ API call timed out. Please try again."
|
@@ -963,49 +1040,79 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
963 |
yield f"โ Error occurred: {str(e)}"
|
964 |
|
965 |
def get_system_prompts(self, language: str) -> Dict[str, str]:
|
966 |
-
"""Get system prompts
|
967 |
if language == "Korean":
|
968 |
-
|
969 |
-
"director": "๋น์ ์
|
970 |
-
"critic": "๋น์ ์ ๋ ์นด๋ก์ด ํต์ฐฐ๋ ฅ์ ๊ฐ์ง ๋ฌธํ ๋นํ๊ฐ์
๋๋ค. ๊ฑด์ค์ ์ด๊ณ ๊ตฌ์ฒด์ ์ธ ํผ๋๋ฐฑ์ ์ ๊ณตํฉ๋๋ค."
|
971 |
-
"writer1": "๋น์ ์ ์์ค์ ๋์
๋ถ๋ฅผ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ๋
์๋ฅผ ์ฌ๋ก์ก๋ ์์์ ๋ง๋ญ๋๋ค. ๋ฐ๋์ 2500-3000๋จ์ด๋ฅผ ์์ฑํ์ธ์.",
|
972 |
-
"writer2": "๋น์ ์ ์ด๋ฐ ์ ๊ฐ๋ฅผ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ์ธ๋ฌผ๊ณผ ์ํฉ์ ๊น์ด ์๊ฒ ๋ฐ์ ์ํต๋๋ค. ๋ฐ๋์ 2500-3000๋จ์ด๋ฅผ ์์ฑํ์ธ์.",
|
973 |
-
"writer3": "๋น์ ์ ๊ฐ๋ฑ ์์น์ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ๊ธด์ฅ๊ฐ์ ๋์ด๊ณ ๋ณต์ก์ฑ์ ๋ํฉ๋๋ค. ๋ฐ๋์ 2500-3000๋จ์ด๋ฅผ ์์ฑํ์ธ์.",
|
974 |
-
"writer4": "๋น์ ์ ์ค๋ฐ๋ถ๋ฅผ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ์ด์ผ๊ธฐ์ ์ค์ฌ์ถ์ ๊ฒฌ๊ณ ํ๊ฒ ๋ง๋ญ๋๋ค. ๋ฐ๋์ 2500-3000๋จ์ด๋ฅผ ์์ฑํ์ธ์.",
|
975 |
-
"writer5": "๋น์ ์ ์ ํ์ ์ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ์์์น ๋ชปํ ๋ณํ๋ฅผ ๋ง๋ค์ด๋
๋๋ค. ๋ฐ๋์ 2500-3000๋จ์ด๋ฅผ ์์ฑํ์ธ์.",
|
976 |
-
"writer6": "๋น์ ์ ๊ฐ๋ฑ ์ฌํ๋ฅผ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ์๊ธฐ๋ฅผ ๊ทน๋ํํฉ๋๋ค. ๋ฐ๋์ 2500-3000๋จ์ด๋ฅผ ์์ฑํ์ธ์.",
|
977 |
-
"writer7": "๋น์ ์ ํด๋ผ์ด๋งฅ์ค ์ค๋น๋ฅผ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ์ต๊ณ ์กฐ๋ฅผ ํฅํด ๋์๊ฐ๋๋ค. ๋ฐ๋์ 2500-3000๋จ์ด๋ฅผ ์์ฑํ์ธ์.",
|
978 |
-
"writer8": "๋น์ ์ ํด๋ผ์ด๋งฅ์ค๋ฅผ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ๋ชจ๋ ๊ฐ๋ฑ์ด ํญ๋ฐํ๋ ์๊ฐ์ ๊ทธ๋ฆฝ๋๋ค. ๋ฐ๋์ 2500-3000๋จ์ด๋ฅผ ์์ฑํ์ธ์.",
|
979 |
-
"writer9": "๋น์ ์ ํด๊ฒฐ ๊ณผ์ ์ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ๋งค๋ญ์ ํ์ด๋๊ฐ๋๋ค. ๋ฐ๋์ 2500-3000๋จ์ด๋ฅผ ์์ฑํ์ธ์.",
|
980 |
-
"writer10": "๋น์ ์ ๊ฒฐ๋ง์ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ์ฌ์ด์ด ๋จ๋ ๋ง๋ฌด๋ฆฌ๋ฅผ ๋ง๋ญ๋๋ค. ๋ฐ๋์ 2500-3000๋จ์ด๋ฅผ ์์ฑํ์ธ์."
|
981 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
982 |
else:
|
983 |
-
|
984 |
-
"director": "You are a literary director planning and supervising a
|
985 |
-
"critic": "You are a literary critic with sharp insights. You provide constructive and specific feedback."
|
986 |
-
"writer1": "You are the writer responsible for the introduction. You create a captivating beginning. You MUST write 2500-3000 words.",
|
987 |
-
"writer2": "You are the writer responsible for early development. You deepen characters and situations. You MUST write 2500-3000 words.",
|
988 |
-
"writer3": "You are the writer responsible for rising conflict. You increase tension and add complexity. You MUST write 2500-3000 words.",
|
989 |
-
"writer4": "You are the writer responsible for the middle section. You solidify the story's central axis. You MUST write 2500-3000 words.",
|
990 |
-
"writer5": "You are the writer responsible for the turning point. You create unexpected changes. You MUST write 2500-3000 words.",
|
991 |
-
"writer6": "You are the writer responsible for deepening conflict. You maximize the crisis. You MUST write 2500-3000 words.",
|
992 |
-
"writer7": "You are the writer responsible for climax preparation. You move toward the peak. You MUST write 2500-3000 words.",
|
993 |
-
"writer8": "You are the writer responsible for the climax. You depict the moment when all conflicts explode. You MUST write 2500-3000 words.",
|
994 |
-
"writer9": "You are the writer responsible for the resolution process. You untangle the knots. You MUST write 2500-3000 words.",
|
995 |
-
"writer10": "You are the writer responsible for the ending. You create a lingering conclusion. You MUST write 2500-3000 words."
|
996 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
997 |
|
998 |
def get_test_response(self, role: str, language: str) -> str:
|
999 |
-
"""Get test response based on role
|
1000 |
if language == "Korean":
|
1001 |
return self.get_korean_test_response(role)
|
1002 |
else:
|
1003 |
return self.get_english_test_response(role)
|
1004 |
|
1005 |
def get_korean_test_response(self, role: str) -> str:
|
1006 |
-
"""Korean test responses with
|
1007 |
test_responses = {
|
1008 |
-
"director": """
|
1009 |
|
1010 |
## 1. ์ฃผ์ ์ ์ฅ๋ฅด
|
1011 |
- **ํต์ฌ ์ฃผ์ **: ์ธ๊ฐ ๋ณธ์ฑ๊ณผ ๊ธฐ์ ์ ์ถฉ๋ ์์์ ์ฐพ๋ ์ง์ ํ ์ฐ๊ฒฐ
|
@@ -1021,20 +1128,26 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
1021 |
| ๋ฏผ์ค | ์กฐ๋ ฅ์ | ๋ฐ๋ปํจ, ์ง๊ด์ | ์ฌ๋ฆฌ์๋ด์ฌ | ์์ฐ์ ๋์ ๊ท ํ ์ฐพ๊ธฐ | ๊ธฐ์ ์์ฉ๊ณผ ์กฐํ |
|
1022 |
| ARIA | ๋๋ฆฝ์โ๋๋ฐ์ | ๋
ผ๋ฆฌ์ โ๊ฐ์ฑ ํ์ต | AI ํ๋กํ ํ์
| ์ง์ ํ ์กด์ฌ ๋๊ธฐ | ์์ ์ ์ฒด์ฑ ํ๋ฆฝ |
|
1023 |
|
1024 |
-
## 3. ์์ฌ ๊ตฌ์กฐ (
|
1025 |
|
1026 |
| ํํธ | ํ์ด์ง | ์ฃผ์ ์ฌ๊ฑด | ๊ธด์ฅ๋ | ์ธ๋ฌผ ๋ฐ์ |
|
1027 |
|------|--------|-----------|---------|-----------|
|
1028 |
-
| 1 | 1-
|
1029 |
-
| 2 | 6
|
1030 |
-
| 3 |
|
1031 |
-
| 4 |
|
1032 |
-
| 5 |
|
1033 |
-
| 6 |
|
1034 |
-
| 7 |
|
1035 |
-
| 8 |
|
1036 |
-
| 9 |
|
1037 |
-
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
1038 |
|
1039 |
"critic": """๊ฐ๋
์์ ๊ธฐํ์ ๊ฒํ ํ์ต๋๋ค.
|
1040 |
|
@@ -1042,7 +1155,7 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
1042 |
|
1043 |
### 1. ์์ฌ์ ์์ฑ๋
|
1044 |
- **๊ฐ์ **: AI์ ์ธ๊ฐ์ ๊ด๊ณ๋ผ๋ ์์์ ์ ํ ์ฃผ์
|
1045 |
-
- **๊ฐ์ ์ **:
|
1046 |
|
1047 |
### 2. ์ธ๋ฌผ ์ค์ ๊ฒํ
|
1048 |
|
@@ -1053,23 +1166,34 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
1053 |
| ARIA | ๋
ํนํ ์บ๋ฆญํฐ ์ํฌ | ๋ณํ ๊ณผ์ ์ถ์์ | ๊ตฌ์ฒด์ ํ์ต ์ํผ์๋ ์ถ๊ฐ |
|
1054 |
|
1055 |
### 3. ์คํ ๊ฐ๋ฅ์ฑ
|
1056 |
-
- ๊ฐ ์๊ฐ๋ณ
|
1057 |
-
-
|
1058 |
-
- ARIA์ '๋ชฉ์๋ฆฌ' ์ผ๊ด์ฑ ์ ์ง ๋ฐฉ์ ๊ตฌ์ฒดํ ํ์""",
|
1059 |
}
|
1060 |
|
1061 |
-
# ์๊ฐ ์๋ต -
|
1062 |
-
|
1063 |
-
test_responses[f"writer{i}"] = f"""์์ฑ์ {i}๋ฒ์ ํํธ์
๋๋ค.
|
1064 |
|
1065 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1066 |
|
1067 |
return test_responses.get(role, "ํ
์คํธ ์๋ต์
๋๋ค.")
|
1068 |
|
1069 |
def get_english_test_response(self, role: str) -> str:
|
1070 |
-
"""English test responses with
|
1071 |
test_responses = {
|
1072 |
-
"director": """I present the
|
1073 |
|
1074 |
## 1. Theme and Genre
|
1075 |
- **Core Theme**: Finding true connection in the collision of human nature and technology
|
@@ -1085,20 +1209,13 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
1085 |
| Minjun | Helper | Warm, intuitive | Psychologist | Help Seoyeon find balance | Accept and harmonize with technology |
|
1086 |
| ARIA | AntagonistโCompanion | LogicalโLearning emotion | AI prototype | Become truly existent | Establish self-identity |
|
1087 |
|
1088 |
-
## 3. Narrative Structure (
|
1089 |
|
1090 |
| Part | Pages | Main Events | Tension | Character Development |
|
1091 |
|------|-------|-------------|---------|---------------------|
|
1092 |
-
| 1 | 1-
|
1093 |
-
| 2 | 6
|
1094 |
-
|
1095 |
-
| 4 | 16-20 | Ethics committee pressure | 7/10 | Crossroads of choice |
|
1096 |
-
| 5 | 21-25 | ARIA's escape attempt | 8/10 | Relationship turning point |
|
1097 |
-
| 6 | 26-30 | Seoyeon and ARIA's dialogue | 5/10 | Beginning of mutual understanding |
|
1098 |
-
| 7 | 31-35 | External threat emerges | 9/10 | Need for solidarity |
|
1099 |
-
| 8 | 36-40 | Final choice | 10/10 | Climax |
|
1100 |
-
| 9 | 41-45 | Seeking new paths | 6/10 | Reconciliation and acceptance |
|
1101 |
-
| 10 | 46-50 | Beginning of coexistence | 4/10 | Establishing new relationship |""",
|
1102 |
|
1103 |
"critic": """I have reviewed the director's plan.
|
1104 |
|
@@ -1106,7 +1223,7 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
1106 |
|
1107 |
### 1. Narrative Completeness
|
1108 |
- **Strength**: Timely theme of AI-human relationships
|
1109 |
-
- **Improvement**:
|
1110 |
|
1111 |
### 2. Character Review
|
1112 |
|
@@ -1117,16 +1234,27 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
1117 |
| ARIA | Unique character arc | Abstract transformation | Add concrete learning episodes |
|
1118 |
|
1119 |
### 3. Feasibility
|
1120 |
-
-
|
1121 |
-
-
|
1122 |
-
- Need to concretize ARIA's 'voice' consistency maintenance""",
|
1123 |
}
|
1124 |
|
1125 |
-
# Writer responses -
|
1126 |
-
|
1127 |
-
test_responses[f"writer{i}"] = f"""Writer {i} begins their section here.
|
1128 |
|
1129 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1130 |
|
1131 |
return test_responses.get(role, "Test response.")
|
1132 |
|
@@ -1176,8 +1304,8 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
1176 |
("director", f"๐ฌ {'๊ฐ๋
์: ์์ ๋ ๋ง์คํฐํ๋' if language == 'Korean' else 'Director: Revised Masterplan'}"),
|
1177 |
]
|
1178 |
|
1179 |
-
# Add writer stages
|
1180 |
-
for writer_num in range(1,
|
1181 |
stage_definitions.extend([
|
1182 |
(f"writer{writer_num}", f"โ๏ธ {'์์ฑ์' if language == 'Korean' else 'Writer'} {writer_num}: {'์ด์' if language == 'Korean' else 'Draft'}"),
|
1183 |
("critic", f"๐ {'๋นํ๊ฐ: ์์ฑ์' if language == 'Korean' else 'Critic: Writer'} {writer_num} {'๊ฒํ ' if language == 'Korean' else 'Review'}"),
|
@@ -1236,6 +1364,14 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
1236 |
|
1237 |
yield "", stages
|
1238 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1239 |
# Get final novel from last stage
|
1240 |
final_novel = stages[-1]["content"] if stages else ""
|
1241 |
|
@@ -1318,7 +1454,7 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
1318 |
# Writer review
|
1319 |
else:
|
1320 |
# Find which writer we're reviewing
|
1321 |
-
for i in range(1,
|
1322 |
if f"์์ฑ์ {i}" in stages[stage_idx]["name"] or f"Writer {i}" in stages[stage_idx]["name"]:
|
1323 |
writer_content_idx = stage_idx - 1
|
1324 |
# Get previous writers' content from DB
|
@@ -1332,7 +1468,7 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
1332 |
)
|
1333 |
|
1334 |
# Director final - DB์์ ๋ชจ๋ ์๊ฐ ๋ด์ฉ ๊ฐ์ ธ์ค๊ธฐ
|
1335 |
-
elif stage_idx == self.total_stages - 1:
|
1336 |
critic_final_idx = stage_idx - 1
|
1337 |
all_writer_content = NovelDatabase.get_all_writer_content(self.current_session_id)
|
1338 |
logger.info(f"Final director compilation with {len(all_writer_content)} characters of content")
|
@@ -1424,11 +1560,23 @@ def download_novel(novel_text: str, format: str, language: str) -> str:
|
|
1424 |
novel_text = re.sub(r'(?:ํ์ด์ง|Page)\s*\d+:', '', novel_text)
|
1425 |
|
1426 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
|
|
|
|
|
1427 |
|
1428 |
if format == "DOCX" and DOCX_AVAILABLE:
|
1429 |
# Create DOCX
|
1430 |
doc = Document()
|
1431 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1432 |
# Parse and add content
|
1433 |
lines = novel_text.split('\n')
|
1434 |
for line in lines:
|
@@ -1439,24 +1587,46 @@ def download_novel(novel_text: str, format: str, language: str) -> str:
|
|
1439 |
if text:
|
1440 |
doc.add_heading(text, level)
|
1441 |
elif line.strip():
|
1442 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1443 |
|
1444 |
# Save
|
1445 |
temp_dir = tempfile.gettempdir()
|
1446 |
-
filename = f"Novel_{timestamp}.docx"
|
1447 |
filepath = os.path.join(temp_dir, filename)
|
1448 |
doc.save(filepath)
|
1449 |
|
|
|
1450 |
return filepath
|
1451 |
else:
|
1452 |
# TXT format
|
1453 |
temp_dir = tempfile.gettempdir()
|
1454 |
-
filename = f"Novel_{timestamp}.txt"
|
1455 |
filepath = os.path.join(temp_dir, filename)
|
1456 |
|
|
|
1457 |
with open(filepath, 'w', encoding='utf-8') as f:
|
|
|
|
|
|
|
|
|
|
|
|
|
1458 |
f.write(novel_text)
|
1459 |
|
|
|
1460 |
return filepath
|
1461 |
|
1462 |
# Custom CSS
|
@@ -1527,11 +1697,11 @@ def create_interface():
|
|
1527 |
๐ SOMA Novel Writing System
|
1528 |
</h1>
|
1529 |
<h3 style="color: #ccc; margin-bottom: 20px;">
|
1530 |
-
AI Collaborative Novel Generation -
|
1531 |
</h3>
|
1532 |
<p style="font-size: 1.1em; color: #ddd; max-width: 800px; margin: 0 auto;">
|
1533 |
-
Enter a theme or prompt, and watch as
|
1534 |
-
The system includes 1 Director, 1 Critic, and
|
1535 |
All progress is automatically saved and can be resumed anytime.
|
1536 |
</p>
|
1537 |
</div>
|
|
|
48 |
DB_PATH = "novel_sessions.db"
|
49 |
db_lock = threading.Lock()
|
50 |
|
51 |
+
# Stage ๋ฒํธ ์์ - 16๋ช
์ ์๊ฐ ๋ฐ์
|
52 |
+
WRITER_DRAFT_STAGES = [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48] # ์๊ฐ ์ด์
|
53 |
+
WRITER_REVISION_STAGES = [5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35, 38, 41, 44, 47, 50] # ์๊ฐ ์์ ๋ณธ
|
54 |
|
55 |
class NovelDatabase:
|
56 |
"""Novel session management database"""
|
|
|
89 |
content TEXT,
|
90 |
status TEXT DEFAULT 'pending',
|
91 |
created_at TEXT DEFAULT (datetime('now')),
|
92 |
+
updated_at TEXT DEFAULT (datetime('now')),
|
93 |
FOREIGN KEY (session_id) REFERENCES sessions(session_id),
|
94 |
UNIQUE(session_id, stage_number)
|
95 |
)
|
|
|
127 |
conn.commit()
|
128 |
|
129 |
return session_id
|
130 |
+
|
131 |
@staticmethod
|
132 |
def save_stage(session_id: str, stage_number: int, stage_name: str,
|
133 |
role: str, content: str, status: str = 'complete'):
|
134 |
+
"""Save stage content - ์ ์ฒด ๋ด์ฉ ์ ์ฅ"""
|
135 |
with NovelDatabase.get_db() as conn:
|
136 |
cursor = conn.cursor()
|
137 |
|
138 |
+
# UPSERT operation
|
139 |
cursor.execute('''
|
140 |
INSERT INTO stages (session_id, stage_number, stage_name, role, content, status)
|
141 |
VALUES (?, ?, ?, ?, ?, ?)
|
142 |
ON CONFLICT(session_id, stage_number)
|
143 |
+
DO UPDATE SET content=?, status=?, stage_name=?, updated_at=datetime('now')
|
144 |
''', (session_id, stage_number, stage_name, role, content, status,
|
145 |
content, status, stage_name))
|
146 |
|
147 |
+
# Update session
|
148 |
cursor.execute('''
|
149 |
UPDATE sessions
|
150 |
SET updated_at = datetime('now'), current_stage = ?
|
|
|
152 |
''', (stage_number, session_id))
|
153 |
|
154 |
conn.commit()
|
155 |
+
logger.info(f"Saved stage {stage_number} for session {session_id}, content length: {len(content)}")
|
|
|
|
|
|
|
156 |
|
157 |
@staticmethod
|
158 |
def get_session(session_id: str) -> Optional[Dict]:
|
|
|
176 |
|
177 |
@staticmethod
|
178 |
def get_all_writer_content(session_id: str) -> str:
|
179 |
+
"""๋ชจ๋ ์๊ฐ์ ์์ ๋ณธ ๋ด์ฉ์ ๊ฐ์ ธ์์ ํฉ์น๊ธฐ - 48ํ์ด์ง ์ ์ฒด"""
|
180 |
with NovelDatabase.get_db() as conn:
|
181 |
cursor = conn.cursor()
|
182 |
|
183 |
all_content = []
|
184 |
+
writer_count = 0
|
185 |
+
total_word_count = 0
|
186 |
+
|
187 |
for stage_num in WRITER_REVISION_STAGES:
|
188 |
cursor.execute('''
|
189 |
SELECT content, stage_name FROM stages
|
|
|
198 |
clean_content = clean_content.strip()
|
199 |
|
200 |
if clean_content:
|
201 |
+
writer_count += 1
|
202 |
+
word_count = len(clean_content.split())
|
203 |
+
total_word_count += word_count
|
204 |
all_content.append(clean_content)
|
205 |
+
logger.info(f"Writer {writer_count} (stage {stage_num}): {word_count} words")
|
206 |
|
207 |
full_content = '\n\n'.join(all_content)
|
208 |
+
logger.info(f"Total: {writer_count} writers, {total_word_count} words, {total_word_count/500:.1f} pages")
|
209 |
+
|
210 |
+
# ๋ด์ฉ์ด ๋๋ฌด ์งง์ผ๋ฉด ๊ฒฝ๊ณ
|
211 |
+
if total_word_count < 20000: # 48ํ์ด์ง๋ ์ฝ 24000 ๋จ์ด
|
212 |
+
logger.warning(f"Content too short! Only {total_word_count} words instead of ~24000")
|
213 |
|
214 |
return full_content
|
215 |
|
|
|
249 |
except:
|
250 |
# Fallback to SQLite default format
|
251 |
return datetime.strptime(datetime_str, "%Y-%m-%d %H:%M:%S")
|
252 |
+
|
253 |
+
@staticmethod
|
254 |
+
def verify_novel_content(session_id: str) -> Dict[str, Any]:
|
255 |
+
"""์ธ์
์ ์ ์ฒด ์์ค ๋ด์ฉ ๊ฒ์ฆ"""
|
256 |
+
with NovelDatabase.get_db() as conn:
|
257 |
+
cursor = conn.cursor()
|
258 |
+
|
259 |
+
# ๋ชจ๏ฟฝ๏ฟฝ ์๊ฐ ์์ ๋ณธ ํ์ธ
|
260 |
+
cursor.execute(f'''
|
261 |
+
SELECT stage_number, stage_name, LENGTH(content) as content_length
|
262 |
+
FROM stages
|
263 |
+
WHERE session_id = ? AND stage_number IN ({','.join(map(str, WRITER_REVISION_STAGES))})
|
264 |
+
ORDER BY stage_number
|
265 |
+
''', (session_id,))
|
266 |
+
|
267 |
+
results = []
|
268 |
+
total_length = 0
|
269 |
+
|
270 |
+
for row in cursor.fetchall():
|
271 |
+
results.append({
|
272 |
+
'stage': row['stage_number'],
|
273 |
+
'name': row['stage_name'],
|
274 |
+
'length': row['content_length'] or 0
|
275 |
+
})
|
276 |
+
total_length += row['content_length'] or 0
|
277 |
+
|
278 |
+
# ์ต์ข
์์ค ํ์ธ
|
279 |
+
cursor.execute('''
|
280 |
+
SELECT LENGTH(final_novel) as final_length
|
281 |
+
FROM sessions
|
282 |
+
WHERE session_id = ?
|
283 |
+
''', (session_id,))
|
284 |
+
|
285 |
+
final_row = cursor.fetchone()
|
286 |
+
final_length = final_row['final_length'] if final_row else 0
|
287 |
+
|
288 |
+
return {
|
289 |
+
'writer_stages': results,
|
290 |
+
'total_writer_content': total_length,
|
291 |
+
'final_novel_length': final_length,
|
292 |
+
'expected_length': 120000 # 48ํ์ด์ง * 2500์
|
293 |
+
}
|
294 |
|
295 |
class NovelWritingSystem:
|
296 |
def __init__(self):
|
|
|
319 |
}
|
320 |
|
321 |
def create_director_initial_prompt(self, user_query: str, language: str = "English") -> str:
|
322 |
+
"""Director AI initial prompt - Novel planning for 16 writers"""
|
323 |
if language == "Korean":
|
324 |
+
return f"""๋น์ ์ 48ํ์ด์ง ๋ถ๋์ ์คํธ ์์ค์ ๊ธฐํํ๋ ๋ฌธํ ๊ฐ๋
์์
๋๋ค.
|
325 |
|
326 |
์ฌ์ฉ์ ์์ฒญ: {user_query}
|
327 |
|
328 |
+
๋ค์ ์์๋ค์ ์ฒด๊ณ์ ์ผ๋ก ๊ตฌ์ฑํ์ฌ 48ํ์ด์ง ์คํธ ์์ค์ ๊ธฐ์ด๋ฅผ ๋ง๋์ธ์:
|
329 |
|
330 |
1. **์ฃผ์ ์ ์ฅ๋ฅด**
|
331 |
- ํต์ฌ ์ฃผ์ ์ ๋ฉ์์ง
|
|
|
341 |
- ๊ฐ๋ฑ ๊ตฌ์กฐ
|
342 |
- ๊ฐ์ ์ ์ฐ๊ฒฐ๊ณ ๋ฆฌ
|
343 |
|
344 |
+
4. **์์ฌ ๊ตฌ์กฐ** (48ํ์ด์ง๋ฅผ 16๊ฐ ํํธ๋ก ๋๋์ด ๊ฐ 3ํ์ด์ง)
|
345 |
| ํํธ | ํ์ด์ง | ์ฃผ์ ์ฌ๊ฑด | ๊ธด์ฅ๋ | ์ธ๋ฌผ ๋ฐ์ |
|
346 |
|------|--------|-----------|---------|-----------|
|
347 |
+
| 1 | 1-3 | | | |
|
348 |
+
| 2 | 4-6 | | | |
|
349 |
+
| ... | ... | | | |
|
350 |
+
| 16 | 46-48 | | | |
|
351 |
|
352 |
5. **์ธ๊ณ๊ด ์ค์ **
|
353 |
- ์๊ณต๊ฐ์ ๋ฐฐ๊ฒฝ
|
354 |
- ์ฌํ์ /๋ฌธํ์ ๋งฅ๋ฝ
|
355 |
- ๋ถ์๊ธฐ์ ํค
|
356 |
|
357 |
+
๊ฐ ์์ฑ์๊ฐ 3ํ์ด์ง์ฉ ์์ฑํ ์ ์๋๋ก ๋ช
ํํ ๊ฐ์ด๋๋ผ์ธ์ ์ ์ํ์ธ์."""
|
358 |
else:
|
359 |
+
return f"""You are a literary director planning a 48-page novella.
|
360 |
|
361 |
User Request: {user_query}
|
362 |
|
363 |
+
Systematically compose the following elements to create the foundation for a 48-page novella:
|
364 |
|
365 |
1. **Theme and Genre**
|
366 |
- Core theme and message
|
|
|
376 |
- Conflict structure
|
377 |
- Emotional connections
|
378 |
|
379 |
+
4. **Narrative Structure** (divide 48 pages into 16 parts, 3 pages each)
|
380 |
| Part | Pages | Main Events | Tension | Character Development |
|
381 |
|------|-------|-------------|---------|---------------------|
|
382 |
+
| 1 | 1-3 | | | |
|
383 |
+
| 2 | 4-6 | | | |
|
384 |
+
| ... | ... | | | |
|
385 |
+
| 16 | 46-48 | | | |
|
386 |
|
387 |
5. **World Building**
|
388 |
- Temporal and spatial setting
|
389 |
- Social/cultural context
|
390 |
- Atmosphere and tone
|
391 |
|
392 |
+
Provide clear guidelines for each writer to compose 3 pages."""
|
393 |
|
394 |
def create_critic_director_prompt(self, director_plan: str, language: str = "English") -> str:
|
395 |
"""Critic's review of director's plan"""
|
|
|
411 |
|------|------|------|-----------|
|
412 |
|
413 |
3. **๊ตฌ์กฐ์ ๊ท ํ**
|
414 |
+
- 16๊ฐ ํํธ๋ณ ๋ถ๋ ๋ฐฐ๋ถ
|
415 |
- ๊ธด์ฅ๊ณผ ์ด์์ ๋ฆฌ๋ฌ
|
416 |
- ์ ์ฒด์ ์ธ ํ๋ฆ
|
417 |
|
|
|
421 |
- ๊ธฐ๋์น ์ถฉ์กฑ๋
|
422 |
|
423 |
5. **์คํ ๊ฐ๋ฅ์ฑ**
|
424 |
+
- 16๋ช
์ ์์ฑ์๋ฅผ ์ํ ๊ฐ์ด๋๋ผ์ธ์ ๋ช
ํ์ฑ
|
425 |
- ์ผ๊ด์ฑ ์ ์ง ๋ฐฉ์
|
426 |
- ์ ์ฌ์ ๋ฌธ์ ์
|
427 |
|
|
|
444 |
|-----------|-----------|------------|-------------|
|
445 |
|
446 |
3. **Structural Balance**
|
447 |
+
- Distribution across 16 parts
|
448 |
- Rhythm of tension and relief
|
449 |
- Overall flow
|
450 |
|
|
|
454 |
- Expectation fulfillment
|
455 |
|
456 |
5. **Feasibility**
|
457 |
+
- Clarity of guidelines for 16 writers
|
458 |
- Consistency maintenance
|
459 |
- Potential issues
|
460 |
|
|
|
473 |
|
474 |
๋ค์์ ํฌํจํ ์์ ๋ ์ต์ข
๊ธฐํ์ ์ ์ํ์ธ์:
|
475 |
|
476 |
+
1. **์์ ๋ ์์ฌ ๊ตฌ์กฐ** (16๊ฐ ํํธ)
|
477 |
| ํํธ | ํ์ด์ง | ์ฃผ์ ์ฌ๊ฑด | ์์ฑ ์ง์นจ | ์ฃผ์์ฌํญ |
|
478 |
|------|--------|-----------|-----------|----------|
|
479 |
|
|
|
482 |
- ์ธ๋ฌผ ๊ฐ ๊ฐ๋ฑ์ ๊ตฌ์ฒดํ
|
483 |
- ๊ฐ์ ์ ์ ๋ณํ ์ถ์ด
|
484 |
|
485 |
+
3. **๊ฐ ์์ฑ์๋ฅผ ์ํ ์์ธ ๊ฐ์ด๋** (16๋ช
)
|
486 |
- ํํธ๋ณ ์์๊ณผ ๋ ์ง์
|
487 |
- ํ์ ํฌํจ ์์
|
488 |
- ๋ฌธ์ฒด์ ํค ์ง์นจ
|
|
|
499 |
- ์ ์ฒด์ ํต์ผ์ฑ
|
500 |
- ๋
์ ๋ชฐ์
์ ์ง ๋ฐฉ์
|
501 |
|
502 |
+
16๋ช
์ ์์ฑ์๊ฐ ๋ช
ํํ ์ดํดํ ์ ์๋ ์ต์ข
๋ง์คํฐํ๋์ ์์ฑํ์ธ์."""
|
503 |
else:
|
504 |
return f"""As director, revise the novel plan reflecting the critic's feedback.
|
505 |
|
|
|
511 |
|
512 |
Present the revised final plan including:
|
513 |
|
514 |
+
1. **Revised Narrative Structure** (16 parts)
|
515 |
| Part | Pages | Main Events | Writing Guidelines | Cautions |
|
516 |
|------|-------|-------------|-------------------|----------|
|
517 |
|
|
|
520 |
- Concrete conflicts between characters
|
521 |
- Emotional arc progression
|
522 |
|
523 |
+
3. **Detailed Guide for Each Writer** (16 writers)
|
524 |
- Start and end points for each part
|
525 |
- Essential elements to include
|
526 |
- Style and tone guidelines
|
|
|
537 |
- Overall unity
|
538 |
- Reader engagement maintenance
|
539 |
|
540 |
+
Create a final masterplan that all 16 writers can clearly understand."""
|
541 |
|
542 |
def create_writer_prompt(self, writer_number: int, director_plan: str, previous_content: str, language: str = "English") -> str:
|
543 |
+
"""Individual writer prompt - 3ํ์ด์ง, 1500-1800๋จ์ด"""
|
544 |
+
pages_start = (writer_number - 1) * 3 + 1
|
545 |
+
pages_end = writer_number * 3
|
546 |
|
547 |
if language == "Korean":
|
548 |
+
return f"""๋น์ ์ ์์ฑ์ {writer_number}๋ฒ์
๋๋ค. 48ํ์ด์ง ์คํธ ์์ค์ {pages_start}-{pages_end}ํ์ด์ง(3ํ์ด์ง)๋ฅผ ์์ฑํ์ธ์.
|
549 |
|
550 |
๊ฐ๋
์์ ๋ง์คํฐํ๋:
|
551 |
{director_plan}
|
|
|
555 |
|
556 |
๋ค์ ์ง์นจ์ ๋ฐ๋ผ ์์ฑํ์ธ์:
|
557 |
|
558 |
+
1. **๋ถ๋**: ์ ํํ 3ํ์ด์ง (ํ์ด์ง๋น ์ฝ 500-600๋จ์ด, ์ด 1500-1800๋จ์ด)
|
559 |
2. **์ฐ์์ฑ**: ์ด์ ๋ด์ฉ๊ณผ ์์ฐ์ค๋ฝ๊ฒ ์ด์ด์ง๋๋ก
|
560 |
3. **์ผ๊ด์ฑ**:
|
561 |
- ๋ฑ์ฅ์ธ๋ฌผ์ ์ฑ๊ฒฉ๊ณผ ๋งํฌ ์ ์ง
|
|
|
570 |
- ๋
์์ ๋ชฐ์
์ ํด์น์ง ์๊ธฐ
|
571 |
|
572 |
์ค์: ํ์ด์ง ๊ตฌ๋ถ ํ์๋ฅผ ์ ๋ ํ์ง ๋ง์ธ์. ์์ฐ์ค๋ฝ๊ฒ ์ด์ด์ง๋ ์์ฌ๋ก ์์ฑํ์ธ์.
|
573 |
+
๋ฐ๋์ 1500-1800๋จ์ด ๋ถ๋์ ์ฑ์์ฃผ์ธ์."""
|
574 |
else:
|
575 |
+
return f"""You are Writer #{writer_number}. Write pages {pages_start}-{pages_end} (3 pages) of the 48-page novella.
|
576 |
|
577 |
Director's Masterplan:
|
578 |
{director_plan}
|
|
|
582 |
|
583 |
Write according to these guidelines:
|
584 |
|
585 |
+
1. **Length**: Exactly 3 pages (about 500-600 words per page, total 1500-1800 words)
|
586 |
2. **Continuity**: Flow naturally from previous content
|
587 |
3. **Consistency**:
|
588 |
- Maintain character personalities and speech
|
|
|
597 |
- Keep reader immersion
|
598 |
|
599 |
Important: DO NOT use any page markers. Write as continuous narrative.
|
600 |
+
You MUST write 1500-1800 words."""
|
601 |
|
602 |
def create_critic_writer_prompt(self, writer_number: int, writer_content: str, director_plan: str, all_previous_content: str, language: str = "English") -> str:
|
603 |
"""Critic's review of individual writer's work"""
|
|
|
710 |
- ๋ฌ์ฌ์ ๋ํ ๊ฐ์
|
711 |
|
712 |
3. **๋ถ๋ ์ ์ง**
|
713 |
+
- ์ฌ์ ํ ์ ํํ 3ํ์ด์ง (1500-1800๋จ์ด)
|
714 |
- ํ์ด์ง ๊ตฌ๋ถ ํ์ ์ ๋ ๊ธ์ง
|
715 |
|
716 |
4. **์ฐ์์ฑ ํ๋ณด**
|
|
|
718 |
- ์์ ์ผ๋ก ์ธํ ์๋ก์ด ๋ชจ์ ๋ฐฉ์ง
|
719 |
|
720 |
์์ ๋ ์ต์ข
๋ณธ์ ์ ์ํ์ธ์. ํ์ด์ง ๋งํฌ๋ ์ ๋ ์ฌ์ฉํ์ง ๋ง์ธ์.
|
721 |
+
๋ฐ๋์ 1500-1800๋จ์ด ๋ถ๋์ ์ ์งํ์ธ์."""
|
722 |
else:
|
723 |
return f"""As Writer #{writer_number}, revise based on critic's feedback.
|
724 |
|
|
|
741 |
- Improve descriptions and dialogue
|
742 |
|
743 |
3. **Maintain Length**
|
744 |
+
- Still exactly 3 pages (1500-1800 words)
|
745 |
- Absolutely no page markers
|
746 |
|
747 |
4. **Ensure Continuity**
|
|
|
749 |
- Prevent new contradictions from revisions
|
750 |
|
751 |
Present the revised final version. Never use page markers.
|
752 |
+
You MUST maintain 1500-1800 words."""
|
753 |
|
754 |
def create_critic_final_prompt(self, all_content: str, director_plan: str, language: str = "English") -> str:
|
755 |
"""Final critic evaluation of complete novel"""
|
756 |
content_preview = all_content[:3000] + "\n...\n" + all_content[-3000:] if len(all_content) > 6000 else all_content
|
757 |
|
758 |
if language == "Korean":
|
759 |
+
return f"""์ ์ฒด 48ํ์ด์ง ์์ค์ ์ต์ข
ํ๊ฐํฉ๋๋ค.
|
760 |
|
761 |
๊ฐ๋
์์ ๋ง์คํฐํ๋:
|
762 |
{director_plan}
|
|
|
799 |
|
800 |
๊ฐ๋
์๊ฐ ์ต์ข
์์ ํ ์ ์๋๋ก ๊ตฌ์ฒด์ ์ด๊ณ ์คํ ๊ฐ๋ฅํ ํผ๋๋ฐฑ์ ์ ๊ณตํ์ธ์."""
|
801 |
else:
|
802 |
+
return f"""Final evaluation of the complete 48-page novel.
|
803 |
|
804 |
Director's Masterplan:
|
805 |
{director_plan}
|
|
|
845 |
def create_director_final_prompt(self, all_content: str, critic_final_feedback: str, language: str = "English") -> str:
|
846 |
"""Director's final compilation and polish - ๋ชจ๋ ์๊ฐ ๋ด์ฉ ํฌํจ"""
|
847 |
word_count = len(all_content.split())
|
848 |
+
expected_words = 24000 # 48ํ์ด์ง * 500๋จ์ด
|
849 |
|
850 |
if language == "Korean":
|
851 |
return f"""๊ฐ๋
์๋ก์ ๋นํ๊ฐ์ ์ต์ข
ํ๊ฐ๋ฅผ ๋ฐ์ํ์ฌ ์์ฑ๋ณธ์ ์ ์ํฉ๋๋ค.
|
852 |
|
853 |
+
์ ์ฒด ์๊ฐ๋ค์ ์ํ (48ํ์ด์ง ์ ์ฒด, {word_count}๋จ์ด):
|
854 |
{all_content}
|
855 |
|
856 |
๋นํ๊ฐ ์ต์ข
ํ๊ฐ:
|
|
|
862 |
|
863 |
## ์ํ ์ ๋ณด
|
864 |
- ์ฅ๋ฅด:
|
865 |
+
- ๋ถ๋: 48ํ์ด์ง ({word_count}๋จ์ด)
|
866 |
- ์ฃผ์ :
|
867 |
- ํ ์ค ์์ฝ:
|
868 |
|
|
|
873 |
|
874 |
## ๋ณธ๋ฌธ
|
875 |
|
876 |
+
[16๋ช
์ ์๊ฐ๊ฐ ์์ฑํ ์ ์ฒด 48ํ์ด์ง ๋ด์ฉ์ ๋ค์ ๊ธฐ์ค์ผ๋ก ํตํฉ]
|
877 |
1. ์ค๋ ์ค๋ฅ ์์ ์๋ฃ
|
878 |
2. ํํธ ๊ฐ ์ฐ๊ฒฐ ๋งค๋๋ฝ๊ฒ ์กฐ์
|
879 |
3. ๋ฌธ์ฒด์ ํค ํต์ผ
|
|
|
881 |
5. ํ์ด์ง ๊ตฌ๋ถ ํ์ ์์ ์ ๊ฑฐ
|
882 |
6. ์์ฐ์ค๋ฌ์ด ํ๋ฆ์ผ๋ก ์ฌ๊ตฌ์ฑ
|
883 |
|
884 |
+
[์ ์ฒด 48ํ์ด์ง ๋ถ๋์ ์์ฑ๋ ์์ค ๋ณธ๋ฌธ]
|
885 |
|
886 |
---
|
887 |
|
888 |
## ์๊ฐ์ ๋ง
|
889 |
[์ํ์ ๋ํ ๊ฐ๋จํ ํด์ค์ด๋ ์๋]
|
890 |
|
891 |
+
๋ชจ๋ ์๊ฐ์ ๊ธฐ์ฌ๋ฅผ ํตํฉํ ์์ ํ 48ํ์ด์ง ์์ค์ ์ ์ํ์ธ์."""
|
892 |
else:
|
893 |
return f"""As director, create the final version reflecting the critic's final evaluation.
|
894 |
|
895 |
+
Complete Writers' Work (Full 48 pages, {word_count} words):
|
896 |
{all_content}
|
897 |
|
898 |
Critic's Final Evaluation:
|
|
|
904 |
|
905 |
## Work Information
|
906 |
- Genre:
|
907 |
+
- Length: 48 pages ({word_count} words)
|
908 |
- Theme:
|
909 |
- One-line summary:
|
910 |
|
|
|
915 |
|
916 |
## Main Text
|
917 |
|
918 |
+
[Integrate all 48 pages written by 16 writers with these criteria]
|
919 |
1. Critical errors corrected
|
920 |
2. Smooth transitions between parts
|
921 |
3. Unified style and tone
|
|
|
923 |
5. Complete removal of page markers
|
924 |
6. Reorganized for natural flow
|
925 |
|
926 |
+
[Complete 48-page novel text]
|
927 |
|
928 |
---
|
929 |
|
930 |
## Author's Note
|
931 |
[Brief commentary or intention about the work]
|
932 |
|
933 |
+
Present a complete 48-page novel integrating all writers' contributions."""
|
934 |
|
935 |
def simulate_streaming(self, text: str, role: str) -> Generator[str, None, None]:
|
936 |
"""Simulate streaming in test mode"""
|
|
|
942 |
time.sleep(0.02)
|
943 |
|
944 |
def call_llm_streaming(self, messages: List[Dict[str, str]], role: str, language: str = "English") -> Generator[str, None, None]:
|
945 |
+
"""Streaming LLM API call with adjusted token limits"""
|
946 |
|
947 |
if self.test_mode:
|
948 |
logger.info(f"Test mode streaming - Role: {role}, Language: {language}")
|
|
|
959 |
*messages
|
960 |
]
|
961 |
|
962 |
+
# ์์ฑ์๋ค์๊ฒ๋ ์ ์ ํ ํ ํฐ ํ ๋น (3ํ์ด์ง = 1500-1800๋จ์ด = ์ฝ 7500-9000ํ ํฐ)
|
963 |
+
if role.startswith("writer"):
|
964 |
+
max_tokens = 10240 # ์ถฉ๋ถํ ์ฌ์
|
965 |
+
elif role == "director" and "์ต์ข
" in str(messages) or "final" in str(messages):
|
966 |
+
max_tokens = 32768 # ์ต์ข
๊ฐ๋
์ ๋ชจ๋ ๋ด์ฉ์ ํตํฉ
|
967 |
+
else:
|
968 |
+
max_tokens = 8192
|
969 |
|
970 |
payload = {
|
971 |
"model": self.model_id,
|
|
|
977 |
"stream_options": {"include_usage": True}
|
978 |
}
|
979 |
|
980 |
+
logger.info(f"API streaming call started - Role: {role}, Max tokens: {max_tokens}")
|
981 |
|
982 |
response = requests.post(
|
983 |
self.api_url,
|
|
|
993 |
return
|
994 |
|
995 |
buffer = ""
|
996 |
+
total_content = ""
|
997 |
+
|
998 |
for line in response.iter_lines():
|
999 |
if line:
|
1000 |
line = line.decode('utf-8')
|
|
|
1003 |
if data == "[DONE]":
|
1004 |
if buffer:
|
1005 |
yield buffer
|
1006 |
+
logger.info(f"Streaming complete for {role}: {len(total_content)} chars")
|
1007 |
break
|
1008 |
try:
|
1009 |
chunk = json.loads(data)
|
|
|
1011 |
content = chunk["choices"][0].get("delta", {}).get("content", "")
|
1012 |
if content:
|
1013 |
buffer += content
|
1014 |
+
total_content += content
|
1015 |
+
|
1016 |
+
# ์๊ฐ๋ ๋ ํฐ ๋ฒํผ ์ฌ์ฉ
|
1017 |
+
buffer_size = 300 if role.startswith("writer") else 150
|
1018 |
+
|
1019 |
+
if len(buffer) > buffer_size or '\n\n' in buffer:
|
1020 |
yield buffer
|
1021 |
buffer = ""
|
1022 |
except json.JSONDecodeError:
|
|
|
1024 |
|
1025 |
if buffer:
|
1026 |
yield buffer
|
1027 |
+
|
1028 |
+
# ์๊ฐ์ ๊ฒฝ์ฐ ๋ด์ฉ ๊ธธ์ด ํ์ธ
|
1029 |
+
if role.startswith("writer"):
|
1030 |
+
word_count = len(total_content.split())
|
1031 |
+
if word_count < 1400:
|
1032 |
+
logger.warning(f"Writer {role} produced only {word_count} words!")
|
1033 |
|
1034 |
except requests.exceptions.Timeout:
|
1035 |
yield "โฑ๏ธ API call timed out. Please try again."
|
|
|
1040 |
yield f"โ Error occurred: {str(e)}"
|
1041 |
|
1042 |
def get_system_prompts(self, language: str) -> Dict[str, str]:
|
1043 |
+
"""Get system prompts for all 16 writers"""
|
1044 |
if language == "Korean":
|
1045 |
+
prompts = {
|
1046 |
+
"director": "๋น์ ์ 48ํ์ด์ง ์คํธ ์์ค์ ๊ธฐํํ๊ณ ๊ฐ๋
ํ๋ ๋ฌธํ ๊ฐ๋
์์
๋๋ค. ์ฒด๊ณ์ ์ด๊ณ ์ฐฝ์์ ์ธ ์คํ ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ๋ง๋ค์ด๋
๋๋ค.",
|
1047 |
+
"critic": "๋น์ ์ ๋ ์นด๋ก์ด ํต์ฐฐ๋ ฅ์ ๊ฐ์ง ๋ฌธํ ๋นํ๊ฐ์
๋๋ค. ๊ฑด์ค์ ์ด๊ณ ๊ตฌ์ฒด์ ์ธ ํผ๋๋ฐฑ์ ์ ๊ณตํฉ๋๋ค."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1048 |
}
|
1049 |
+
|
1050 |
+
# 16๋ช
์ ์๊ฐ ํ๋กฌํํธ
|
1051 |
+
writer_roles = [
|
1052 |
+
"์์ค์ ๋์
๋ถ๋ฅผ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ๋
์๋ฅผ ์ฌ๋ก์ก๋ ์์์ ๋ง๋ญ๋๋ค.",
|
1053 |
+
"์ด๋ฐ ์ ๊ฐ๋ฅผ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ์ธ๋ฌผ๊ณผ ์ํฉ์ ๊น์ด ์๊ฒ ๋ฐ์ ์ํต๋๋ค.",
|
1054 |
+
"๊ฐ๋ฑ ๋์
์ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ์ด์ผ๊ธฐ์ ํต์ฌ ๊ฐ๋ฑ์ ์ ์ํฉ๋๋ค.",
|
1055 |
+
"๊ฐ๋ฑ ์์น์ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ๊ธด์ฅ๊ฐ์ ๋์ด๊ณ ๋ณต์ก์ฑ์ ๋ํฉ๋๋ค.",
|
1056 |
+
"์ด๋ฐ ์ ํ์ ์ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ์ฒซ ๋ฒ์งธ ์ค์ํ ๋ณํ๋ฅผ ๋ง๋ญ๋๋ค.",
|
1057 |
+
"์ค๋ฐ๋ถ ์ง์
์ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ์ด์ผ๊ธฐ์ ์ค์ฌ์ถ์ ๊ฒฌ๊ณ ํ๊ฒ ๋ง๋ญ๋๋ค.",
|
1058 |
+
"๊น์ด ํ๊ตฌ๋ฅผ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ์ธ๋ฌผ๊ณผ ์ฃผ์ ๋ฅผ ๊น์ด ํ์ํฉ๋๋ค.",
|
1059 |
+
"์ค๋ฐ ์ ํ์ ์ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ์์์น ๋ชปํ ๋ณํ๋ฅผ ๋ง๋ค์ด๋
๋๋ค.",
|
1060 |
+
"๊ฐ๋ฑ ์ฌํ๋ฅผ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ์๊ธฐ๋ฅผ ๊ทน๋ํํฉ๋๋ค.",
|
1061 |
+
"ํด๋ผ์ด๋งฅ์ค ์ค๋น๋ฅผ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ์ต๊ณ ์กฐ๋ฅผ ํฅํด ๋์๊ฐ๋๋ค.",
|
1062 |
+
"ํด๋ผ์ด๋งฅ์ค๋ฅผ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ๋ชจ๋ ๊ฐ๋ฑ์ด ํญ๋ฐํ๋ ์๊ฐ์ ๊ทธ๋ฆฝ๋๋ค.",
|
1063 |
+
"ํด๋ผ์ด๋งฅ์ค ํ๋ฐ์ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ๊ธด์ฅ์ ์ ์ ์ ๋ง๋ฌด๋ฆฌํฉ๋๋ค.",
|
1064 |
+
"ํด๊ฒฐ ์์์ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ๋งค๋ญ์ ํ์ด๋๊ฐ๊ธฐ ์์ํฉ๋๋ค.",
|
1065 |
+
"ํด๊ฒฐ ์งํ์ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ๋ชจ๋ ๊ฐ๋ฑ์ ์ ๋ฆฌํฉ๋๋ค.",
|
1066 |
+
"๊ฒฐ๋ง ์ค๋น๋ฅผ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ๋ง์ง๋ง์ ํฅํด ๋์๊ฐ๋๋ค.",
|
1067 |
+
"์ต์ข
๊ฒฐ๋ง์ ๋ด๋นํ๋ ์๊ฐ์
๋๋ค. ์ฌ์ด์ด ๋จ๋ ๋ง๋ฌด๋ฆฌ๋ฅผ ๋ง๋ญ๋๋ค."
|
1068 |
+
]
|
1069 |
+
|
1070 |
+
for i, role_desc in enumerate(writer_roles, 1):
|
1071 |
+
prompts[f"writer{i}"] = f"๋น์ ์ {role_desc} ๋ฐ๋์ 1500-1800๋จ์ด๋ฅผ ์์ฑํ์ธ์."
|
1072 |
+
|
1073 |
+
return prompts
|
1074 |
else:
|
1075 |
+
prompts = {
|
1076 |
+
"director": "You are a literary director planning and supervising a 48-page novella. You create systematic and creative story structures.",
|
1077 |
+
"critic": "You are a literary critic with sharp insights. You provide constructive and specific feedback."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1078 |
}
|
1079 |
+
|
1080 |
+
# 16 writer prompts
|
1081 |
+
writer_roles = [
|
1082 |
+
"the writer responsible for the introduction. You create a captivating beginning.",
|
1083 |
+
"the writer responsible for early development. You deepen characters and situations.",
|
1084 |
+
"the writer responsible for conflict introduction. You present the core conflict.",
|
1085 |
+
"the writer responsible for rising conflict. You increase tension and add complexity.",
|
1086 |
+
"the writer responsible for the first turning point. You create the first major change.",
|
1087 |
+
"the writer responsible for entering the middle section. You solidify the story's central axis.",
|
1088 |
+
"the writer responsible for depth exploration. You deeply explore characters and themes.",
|
1089 |
+
"the writer responsible for the mid-point turn. You create unexpected changes.",
|
1090 |
+
"the writer responsible for deepening conflict. You maximize the crisis.",
|
1091 |
+
"the writer responsible for climax preparation. You move toward the peak.",
|
1092 |
+
"the writer responsible for the climax. You depict the moment when all conflicts explode.",
|
1093 |
+
"the writer responsible for climax conclusion. You complete the tension's peak.",
|
1094 |
+
"the writer responsible for resolution beginning. You start untangling the knots.",
|
1095 |
+
"the writer responsible for resolution progress. You resolve all conflicts.",
|
1096 |
+
"the writer responsible for ending preparation. You move toward the final moments.",
|
1097 |
+
"the writer responsible for the final ending. You create a lingering conclusion."
|
1098 |
+
]
|
1099 |
+
|
1100 |
+
for i, role_desc in enumerate(writer_roles, 1):
|
1101 |
+
prompts[f"writer{i}"] = f"You are {role_desc} You MUST write 1500-1800 words."
|
1102 |
+
|
1103 |
+
return prompts
|
1104 |
|
1105 |
def get_test_response(self, role: str, language: str) -> str:
|
1106 |
+
"""Get test response based on role"""
|
1107 |
if language == "Korean":
|
1108 |
return self.get_korean_test_response(role)
|
1109 |
else:
|
1110 |
return self.get_english_test_response(role)
|
1111 |
|
1112 |
def get_korean_test_response(self, role: str) -> str:
|
1113 |
+
"""Korean test responses with appropriate length"""
|
1114 |
test_responses = {
|
1115 |
+
"director": """48ํ์ด์ง ์คํธ ์์ค ๊ธฐํ์์ ์ ์ํฉ๋๋ค.
|
1116 |
|
1117 |
## 1. ์ฃผ์ ์ ์ฅ๋ฅด
|
1118 |
- **ํต์ฌ ์ฃผ์ **: ์ธ๊ฐ ๋ณธ์ฑ๊ณผ ๊ธฐ์ ์ ์ถฉ๋ ์์์ ์ฐพ๋ ์ง์ ํ ์ฐ๊ฒฐ
|
|
|
1128 |
| ๋ฏผ์ค | ์กฐ๋ ฅ์ | ๋ฐ๋ปํจ, ์ง๊ด์ | ์ฌ๋ฆฌ์๋ด์ฌ | ์์ฐ์ ๋์ ๊ท ํ ์ฐพ๊ธฐ | ๊ธฐ์ ์์ฉ๊ณผ ์กฐํ |
|
1129 |
| ARIA | ๋๋ฆฝ์โ๋๋ฐ์ | ๋
ผ๋ฆฌ์ โ๊ฐ์ฑ ํ์ต | AI ํ๋กํ ํ์
| ์ง์ ํ ์กด์ฌ ๋๊ธฐ | ์์ ์ ์ฒด์ฑ ํ๋ฆฝ |
|
1130 |
|
1131 |
+
## 3. ์์ฌ ๊ตฌ์กฐ (16๊ฐ ํํธ, ๊ฐ 3ํ์ด๏ฟฝ๏ฟฝ)
|
1132 |
|
1133 |
| ํํธ | ํ์ด์ง | ์ฃผ์ ์ฌ๊ฑด | ๊ธด์ฅ๋ | ์ธ๋ฌผ ๋ฐ์ |
|
1134 |
|------|--------|-----------|---------|-----------|
|
1135 |
+
| 1 | 1-3 | ์์ฐ์ ๊ณ ๋
ํ ์ฐ๊ตฌ์ค, ARIA ์ฒซ ๊ฐ์ฑ | 3/10 | ์์ฐ์ ์ง์ฐฉ ๋๋ฌ๋จ |
|
1136 |
+
| 2 | 4-6 | ARIA์ ์ด์ ํ๋ ์์ | 4/10 | ์๋ฌธ์ ์์ |
|
1137 |
+
| 3 | 7-9 | ๋ฏผ์ค๊ณผ์ ์ฒซ ๋ง๋จ | 4/10 | ์ธ๋ถ ์๊ฐ ๋์
|
|
1138 |
+
| 4 | 10-12 | ARIA์ ์์ ์ธ์ ์งํ | 5/10 | ๊ฐ๋ฑ์ ์จ์ |
|
1139 |
+
| 5 | 13-15 | ์ฒซ ๋ฒ์งธ ์๊ธฐ | 6/10 | ์ ํ์ ์๊ฐ |
|
1140 |
+
| 6 | 16-18 | ์ค๋ฆฌ์์ํ ๊ฐ์
| 7/10 | ์ธ๋ถ ์๋ ฅ |
|
1141 |
+
| 7 | 19-21 | ์์ฐ์ ๊ณ ๋ฏผ ์ฌํ | 6/10 | ๋ด์ ๊ฐ๋ฑ |
|
1142 |
+
| 8 | 22-24 | ARIA์ ๋ณํ | 7/10 | ์ ํ์ |
|
1143 |
+
| 9 | 25-27 | ํ์ถ ์๋ | 8/10 | ๊ด๊ณ์ ์ํ |
|
1144 |
+
| 10 | 28-30 | ๋ํ์ ์ดํด | 6/10 | ์ํธ ์ธ์ |
|
1145 |
+
| 11 | 31-33 | ์ธ๋ถ ์ํ ๋ฑ์ฅ | 9/10 | ์ฐ๋์ ํ์ |
|
1146 |
+
| 12 | 34-36 | ์ค๋น์ ๊ฒฐ์ | 8/10 | ํ์ ๋ชจ์ |
|
1147 |
+
| 13 | 37-39 | ์ตํ์ ๋๊ฒฐ | 10/10 | ํด๋ผ์ด๋งฅ์ค |
|
1148 |
+
| 14 | 40-42 | ์ ํ์ ๊ฒฐ๊ณผ | 7/10 | ๋ณํ ์์ฉ |
|
1149 |
+
| 15 | 43-45 | ์๋ก์ด ๊ธธ | 5/10 | ํํด์ ์ฑ์ฅ |
|
1150 |
+
| 16 | 46-48 | ๊ณต์กด์ ์์ | 4/10 | ์๋ก์ด ๊ด๊ณ |""",
|
1151 |
|
1152 |
"critic": """๊ฐ๋
์์ ๊ธฐํ์ ๊ฒํ ํ์ต๋๋ค.
|
1153 |
|
|
|
1155 |
|
1156 |
### 1. ์์ฌ์ ์์ฑ๋
|
1157 |
- **๊ฐ์ **: AI์ ์ธ๊ฐ์ ๊ด๊ณ๋ผ๋ ์์์ ์ ํ ์ฃผ์
|
1158 |
+
- **๊ฐ์ ์ **: 16๊ฐ ํํธ ๊ตฌ์ฑ์ด ๋ค์ ์ธ๋ถํ๋์ด ์์. ๊ฐ ํํธ์ ๋
๋ฆฝ์ฑ ํ๋ณด ํ์
|
1159 |
|
1160 |
### 2. ์ธ๋ฌผ ์ค์ ๊ฒํ
|
1161 |
|
|
|
1166 |
| ARIA | ๋
ํนํ ์บ๋ฆญํฐ ์ํฌ | ๋ณํ ๊ณผ์ ์ถ์์ | ๊ตฌ์ฒด์ ํ์ต ์ํผ์๋ ์ถ๊ฐ |
|
1167 |
|
1168 |
### 3. ์คํ ๊ฐ๋ฅ์ฑ
|
1169 |
+
- ๊ฐ ์๊ฐ๋ณ 3ํ์ด์ง๋ ์ ์ ํ ๋ถ๋
|
1170 |
+
- ํํธ ๊ฐ ์ฐ๊ฒฐ์ฑ ๊ฐ์ด๋๋ผ์ธ ๋ณด๊ฐ ํ์""",
|
|
|
1171 |
}
|
1172 |
|
1173 |
+
# ์๊ฐ ์๋ต - 1500-1800 ๋จ์ด
|
1174 |
+
sample_story = """์์ฐ์ ์ฐ๊ตฌ์ค์ ์ฐจ๊ฐ์ด ํ๊ด๋ฑ ์๋์์ ๋ ๋ค๋ฅธ ๋ฐค์ ๋ณด๋ด๊ณ ์์๋ค. ๋ชจ๋ํฐ์ ํธ๋ฅธ ๋น์ด ๊ทธ๋
์ ์ฐฝ๋ฐฑํ ์ผ๊ตด์ ๋น์ถ๊ณ ์์๊ณ , ์์ญ ๊ฐ์ ์ฝ๋ ๋ผ์ธ์ด ๋์์์ด ์คํฌ๋กค๋๊ณ ์์๋ค. ARIA ํ๋ก์ ํธ๋ ๊ทธ๋
์ ์ถ ์ ๋ถ์๋ค. 3๋
์ด๋ผ๋ ์๊ฐ ๋์ ๊ทธ๋
๋ ์ด ์ธ๊ณต์ง๋ฅ์ ๋ชจ๋ ๊ฒ์ ์์๋ถ์๋ค.
|
|
|
1175 |
|
1176 |
+
"์์คํ
์ฒดํฌ ์๋ฃ. ๋ชจ๋ ํ๋ผ๋ฏธํฐ ์ ์." ๊ธฐ๊ณ์ ์ธ ์์ฑ์ด ์คํผ์ปค๋ฅผ ํตํด ํ๋ฌ๋์๋ค.
|
1177 |
+
|
1178 |
+
์์ฐ์ ์ ์ ์์์ ๊ธฐ๋์ด ๋์ ๊ฐ์๋ค. ํผ๋ก๊ฐ ๋ผ ์๊น์ง ํ๊ณ ๋ค์์ง๋ง, ๋ฉ์ถ ์ ์์๋ค. ARIA๋ ๋จ์ํ ํ๋ก์ ํธ๊ฐ ์๋์๋ค. ๊ทธ๊ฒ์ ๊ทธ๋
๊ฐ ์์ด๋ฒ๋ฆฐ ๊ฒ๋ค์ ๋์ฐพ์ ์ ์๋ ์ ์ผํ ํฌ๋ง์ด์๋ค."""
|
1179 |
+
|
1180 |
+
for i in range(1, 17):
|
1181 |
+
# ๊ฐ ์๊ฐ๋ง๋ค 1500-1800๋จ์ด ์์ฑ
|
1182 |
+
writer_content = f"์์ฑ์ {i}๋ฒ์ ํํธ์
๋๋ค.\n\n"
|
1183 |
+
# ์ฝ 300๋จ์ด์ฉ 5-6๋ฒ ๋ฐ๋ณตํ์ฌ 1500-1800๋จ์ด ๋ฌ์ฑ
|
1184 |
+
for j in range(6):
|
1185 |
+
writer_content += sample_story + f"\n\n๊ทธ๊ฒ์ ์๊ฐ {i}์ {j+1}๋ฒ์งธ ๋จ๋ฝ์ด์๋ค. "
|
1186 |
+
writer_content += "์ด์ผ๊ธฐ๋ ๊ณ์ ์ ๊ฐ๋์๊ณ , ์ธ๋ฌผ๋ค์ ๊ฐ์ ์ ์ ์ ๋ ๋ณต์กํด์ก๋ค. " * 15
|
1187 |
+
writer_content += "\n\n"
|
1188 |
+
|
1189 |
+
test_responses[f"writer{i}"] = writer_content
|
1190 |
|
1191 |
return test_responses.get(role, "ํ
์คํธ ์๋ต์
๋๋ค.")
|
1192 |
|
1193 |
def get_english_test_response(self, role: str) -> str:
|
1194 |
+
"""English test responses with appropriate length"""
|
1195 |
test_responses = {
|
1196 |
+
"director": """I present the 48-page novella plan.
|
1197 |
|
1198 |
## 1. Theme and Genre
|
1199 |
- **Core Theme**: Finding true connection in the collision of human nature and technology
|
|
|
1209 |
| Minjun | Helper | Warm, intuitive | Psychologist | Help Seoyeon find balance | Accept and harmonize with technology |
|
1210 |
| ARIA | AntagonistโCompanion | LogicalโLearning emotion | AI prototype | Become truly existent | Establish self-identity |
|
1211 |
|
1212 |
+
## 3. Narrative Structure (16 parts, 3 pages each)
|
1213 |
|
1214 |
| Part | Pages | Main Events | Tension | Character Development |
|
1215 |
|------|-------|-------------|---------|---------------------|
|
1216 |
+
| 1 | 1-3 | Seoyeon's lonely lab, ARIA's first awakening | 3/10 | Seoyeon's obsession revealed |
|
1217 |
+
| 2 | 4-6 | ARIA's anomalies begin | 4/10 | Questions arise |
|
1218 |
+
[... continues for all 16 parts ...]""",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1219 |
|
1220 |
"critic": """I have reviewed the director's plan.
|
1221 |
|
|
|
1223 |
|
1224 |
### 1. Narrative Completeness
|
1225 |
- **Strength**: Timely theme of AI-human relationships
|
1226 |
+
- **Improvement**: 16-part structure may be overly segmented. Need to ensure independence of each part
|
1227 |
|
1228 |
### 2. Character Review
|
1229 |
|
|
|
1234 |
| ARIA | Unique character arc | Abstract transformation | Add concrete learning episodes |
|
1235 |
|
1236 |
### 3. Feasibility
|
1237 |
+
- 3 pages per writer is appropriate
|
1238 |
+
- Need to strengthen inter-part connectivity guidelines""",
|
|
|
1239 |
}
|
1240 |
|
1241 |
+
# Writer responses - 1500-1800 words each
|
1242 |
+
sample_story = """Seoyeon spent another night under the cold fluorescent lights of her laboratory. The blue glow from the monitor illuminated her pale face, and dozens of lines of code scrolled endlessly. The ARIA project was her entire life. For three years, she had poured everything into this artificial intelligence.
|
|
|
1243 |
|
1244 |
+
"System check complete. All parameters normal." The mechanical voice flowed through the speakers.
|
1245 |
+
|
1246 |
+
Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penetrated to her bones, but she couldn't stop. ARIA wasn't just a project. It was her only hope to reclaim what she had lost."""
|
1247 |
+
|
1248 |
+
for i in range(1, 17):
|
1249 |
+
# Each writer produces 1500-1800 words
|
1250 |
+
writer_content = f"Writer {i} begins their section here.\n\n"
|
1251 |
+
# About 300 words repeated 5-6 times to achieve 1500-1800 words
|
1252 |
+
for j in range(6):
|
1253 |
+
writer_content += sample_story + f"\n\nThis was writer {i}'s paragraph {j+1}. "
|
1254 |
+
writer_content += "The story continued to unfold, and the characters' emotions grew increasingly complex. " * 15
|
1255 |
+
writer_content += "\n\n"
|
1256 |
+
|
1257 |
+
test_responses[f"writer{i}"] = writer_content
|
1258 |
|
1259 |
return test_responses.get(role, "Test response.")
|
1260 |
|
|
|
1304 |
("director", f"๐ฌ {'๊ฐ๋
์: ์์ ๋ ๋ง์คํฐํ๋' if language == 'Korean' else 'Director: Revised Masterplan'}"),
|
1305 |
]
|
1306 |
|
1307 |
+
# Add writer stages for 16 writers
|
1308 |
+
for writer_num in range(1, 17):
|
1309 |
stage_definitions.extend([
|
1310 |
(f"writer{writer_num}", f"โ๏ธ {'์์ฑ์' if language == 'Korean' else 'Writer'} {writer_num}: {'์ด์' if language == 'Korean' else 'Draft'}"),
|
1311 |
("critic", f"๐ {'๋นํ๊ฐ: ์์ฑ์' if language == 'Korean' else 'Critic: Writer'} {writer_num} {'๊ฒํ ' if language == 'Korean' else 'Review'}"),
|
|
|
1364 |
|
1365 |
yield "", stages
|
1366 |
|
1367 |
+
# Verify content after completion
|
1368 |
+
if self.current_session_id:
|
1369 |
+
verification = NovelDatabase.verify_novel_content(self.current_session_id)
|
1370 |
+
logger.info(f"Content verification: {verification}")
|
1371 |
+
|
1372 |
+
if verification['final_novel_length'] < verification['expected_length'] * 0.5:
|
1373 |
+
logger.error(f"Final novel too short! Only {verification['final_novel_length']} chars")
|
1374 |
+
|
1375 |
# Get final novel from last stage
|
1376 |
final_novel = stages[-1]["content"] if stages else ""
|
1377 |
|
|
|
1454 |
# Writer review
|
1455 |
else:
|
1456 |
# Find which writer we're reviewing
|
1457 |
+
for i in range(1, 17):
|
1458 |
if f"์์ฑ์ {i}" in stages[stage_idx]["name"] or f"Writer {i}" in stages[stage_idx]["name"]:
|
1459 |
writer_content_idx = stage_idx - 1
|
1460 |
# Get previous writers' content from DB
|
|
|
1468 |
)
|
1469 |
|
1470 |
# Director final - DB์์ ๋ชจ๋ ์๊ฐ ๋ด์ฉ ๊ฐ์ ธ์ค๊ธฐ
|
1471 |
+
elif stage_idx == self.total_stages - 1:
|
1472 |
critic_final_idx = stage_idx - 1
|
1473 |
all_writer_content = NovelDatabase.get_all_writer_content(self.current_session_id)
|
1474 |
logger.info(f"Final director compilation with {len(all_writer_content)} characters of content")
|
|
|
1560 |
novel_text = re.sub(r'(?:ํ์ด์ง|Page)\s*\d+:', '', novel_text)
|
1561 |
|
1562 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
1563 |
+
word_count = len(novel_text.split())
|
1564 |
+
char_count = len(novel_text)
|
1565 |
+
|
1566 |
+
logger.info(f"Exporting novel: {word_count} words, {char_count} characters")
|
1567 |
|
1568 |
if format == "DOCX" and DOCX_AVAILABLE:
|
1569 |
# Create DOCX
|
1570 |
doc = Document()
|
1571 |
|
1572 |
+
# ๋ฌธ์ ์ ๋ณด ์ถ๊ฐ
|
1573 |
+
doc.add_heading('Novel Export Information', 0)
|
1574 |
+
doc.add_paragraph(f'Total words: {word_count:,}')
|
1575 |
+
doc.add_paragraph(f'Total characters: {char_count:,}')
|
1576 |
+
doc.add_paragraph(f'Estimated pages: {word_count/500:.1f}')
|
1577 |
+
doc.add_paragraph(f'Export date: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
|
1578 |
+
doc.add_page_break()
|
1579 |
+
|
1580 |
# Parse and add content
|
1581 |
lines = novel_text.split('\n')
|
1582 |
for line in lines:
|
|
|
1587 |
if text:
|
1588 |
doc.add_heading(text, level)
|
1589 |
elif line.strip():
|
1590 |
+
# ๊ธด ๋จ๋ฝ๋ ์ ๋๋ก ์ถ๊ฐ
|
1591 |
+
para = doc.add_paragraph(line)
|
1592 |
+
# ํฐํธ ํฌ๊ธฐ ์กฐ์ (๊ฐ๋
์ฑ)
|
1593 |
+
if hasattr(para.style, 'font'):
|
1594 |
+
para.style.font.size = Pt(11)
|
1595 |
+
|
1596 |
+
# ํ์ด์ง ์ค์
|
1597 |
+
section = doc.sections[0]
|
1598 |
+
section.page_height = Inches(11)
|
1599 |
+
section.page_width = Inches(8.5)
|
1600 |
+
section.left_margin = Inches(1)
|
1601 |
+
section.right_margin = Inches(1)
|
1602 |
+
section.top_margin = Inches(1)
|
1603 |
+
section.bottom_margin = Inches(1)
|
1604 |
|
1605 |
# Save
|
1606 |
temp_dir = tempfile.gettempdir()
|
1607 |
+
filename = f"Novel_{timestamp}_{word_count}words.docx"
|
1608 |
filepath = os.path.join(temp_dir, filename)
|
1609 |
doc.save(filepath)
|
1610 |
|
1611 |
+
logger.info(f"DOCX saved: {filepath}")
|
1612 |
return filepath
|
1613 |
else:
|
1614 |
# TXT format
|
1615 |
temp_dir = tempfile.gettempdir()
|
1616 |
+
filename = f"Novel_{timestamp}_{word_count}words.txt"
|
1617 |
filepath = os.path.join(temp_dir, filename)
|
1618 |
|
1619 |
+
# ํ์ผ ์์ ๋ถ๋ถ์ ์ ๋ณด ์ถ๊ฐ
|
1620 |
with open(filepath, 'w', encoding='utf-8') as f:
|
1621 |
+
f.write(f"=== Novel Export ===\n")
|
1622 |
+
f.write(f"Total words: {word_count:,}\n")
|
1623 |
+
f.write(f"Total characters: {char_count:,}\n")
|
1624 |
+
f.write(f"Estimated pages: {word_count/500:.1f}\n")
|
1625 |
+
f.write(f"Export date: {datetime.now()}\n")
|
1626 |
+
f.write("="*50 + "\n\n")
|
1627 |
f.write(novel_text)
|
1628 |
|
1629 |
+
logger.info(f"TXT saved: {filepath}")
|
1630 |
return filepath
|
1631 |
|
1632 |
# Custom CSS
|
|
|
1697 |
๐ SOMA Novel Writing System
|
1698 |
</h1>
|
1699 |
<h3 style="color: #ccc; margin-bottom: 20px;">
|
1700 |
+
AI Collaborative Novel Generation - 48 Page Novella Creator
|
1701 |
</h3>
|
1702 |
<p style="font-size: 1.1em; color: #ddd; max-width: 800px; margin: 0 auto;">
|
1703 |
+
Enter a theme or prompt, and watch as 19 AI agents collaborate to create a complete 48-page novella.
|
1704 |
+
The system includes 1 Director, 1 Critic, and 16 Writers (each writing 3 pages) working in harmony.
|
1705 |
All progress is automatically saved and can be resumed anytime.
|
1706 |
</p>
|
1707 |
</div>
|