openfree commited on
Commit
965f9f4
·
verified ·
1 Parent(s): 547fa82

Update app-BACKUP.py

Browse files
Files changed (1) hide show
  1. app-BACKUP.py +408 -72
app-BACKUP.py CHANGED
@@ -35,6 +35,8 @@ except ImportError:
35
  DOCX_AVAILABLE = False
36
  logger.warning("python-docx not installed. DOCX export will be disabled.")
37
 
 
 
38
  # --- Environment variables and constants ---
39
  FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "")
40
  BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "")
@@ -468,6 +470,12 @@ Provide detailed 40-episode plotline with key events for each episode."""
468
  **이전 내용 요약:**
469
  {previous_content[-1500:] if previous_content else "첫 화입니다"}
470
 
 
 
 
 
 
 
471
  **작성 지침:**
472
  1. **구성**: 3-4개의 주요 장면으로 구성
473
  - 도입부: 이전 화 연결 및 현재 상황
@@ -493,6 +501,7 @@ Provide detailed 40-episode plotline with key events for each episode."""
493
  **참고 후크 예시:**
494
  {random.choice(hooks)}
495
 
 
496
  {episode_num}화를 풍성하고 몰입감 있게 작성하세요. 반드시 400-600단어로 작성하세요.""",
497
 
498
  "English": f"""Write episode {episode_num} of the web novel.
@@ -506,6 +515,12 @@ Provide detailed 40-episode plotline with key events for each episode."""
506
  **Previous content:**
507
  {previous_content[-1500:] if previous_content else "First episode"}
508
 
 
 
 
 
 
 
509
  **Guidelines:**
510
  1. **Structure**: 3-4 major scenes
511
  - Opening: Connect from previous, current situation
@@ -531,6 +546,7 @@ Provide detailed 40-episode plotline with key events for each episode."""
531
  **Hook example:**
532
  {random.choice(hooks)}
533
 
 
534
  Write rich, immersive episode {episode_num}. Must be 400-600 words."""
535
  }
536
 
@@ -759,17 +775,32 @@ Provide specific, practical improvements."""
759
  "writer", language
760
  )
761
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
762
  # Extract hook (last sentence)
763
- sentences = episode_content.split('.')
764
  hook = sentences[-2] + '.' if len(sentences) > 1 else sentences[-1]
765
 
766
- # Save episode
767
  WebNovelDatabase.save_episode(
768
  self.current_session_id, episode_num,
769
- episode_content, hook
770
  )
771
 
772
- accumulated_content += f"\n\n### {episode_num}화\n{episode_content}"
 
773
 
774
  # Quick critique every 5 episodes
775
  if episode_num % 5 == 0:
@@ -791,7 +822,79 @@ Provide specific, practical improvements."""
791
  logger.error(f"Web novel generation error: {e}", exc_info=True)
792
  yield f"❌ 오류 발생: {e}", accumulated_content if 'accumulated_content' in locals() else "", "오류", self.current_session_id
793
 
794
- # --- Random theme generator ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
795
  def generate_random_webnovel_theme(genre: str, language: str) -> str:
796
  """Generate random web novel theme using novel_themes.json and LLM"""
797
  try:
@@ -896,7 +999,7 @@ def generate_random_webnovel_theme(genre: str, language: str) -> str:
896
 
897
  # Create prompt for LLM
898
  if language == "Korean":
899
- prompt = f"""다음 요소들을 활용하여 {genre} 장르의 매력적인 웹소설 테마를 생성하세요:
900
 
901
  【선택된 요소들】
902
  - 핵심 장르: {selected_genre_key}
@@ -914,19 +1017,24 @@ def generate_random_webnovel_theme(genre: str, language: str) -> str:
914
  【독자를 사로잡을 질문들】
915
  {chr(10).join(reader_questions[:2]) if reader_questions else ""}
916
 
917
- 요구사항:
918
- 1. 위 요소들을 자연스럽게 융합하여 하나의 매력적인 스토리로 만드세요
919
- 2. 한국 웹소설 독자들이 좋아할 만한 설정을 추가하세요
920
- 3. 제목이 떠오를 정도로 구체적이면서도 흥미로운 설정을 만드세요
921
- 4. 200-300자 내외로 간결하게 작성하세요
 
 
 
 
 
 
 
922
 
923
- 형식:
924
- 줄의 강렬한 도입문으로 시작하세요.
925
- 그 다음 주인공의 상황과 목표를 명확히 제시하세요.
926
- 마지막으로 독자의 기대감을 자극하는 요소를 추가하세요."""
927
 
928
  else: # English
929
- prompt = f"""Create an engaging web novel theme for {genre} genre using these elements:
930
 
931
  【Selected Elements】
932
  - Core genre: {selected_genre_key}
@@ -941,16 +1049,21 @@ def generate_random_webnovel_theme(genre: str, language: str) -> str:
941
  【Reference Hook】
942
  {selected_hook}
943
 
944
- Requirements:
945
- 1. Naturally blend these elements into one attractive story
946
- 2. Add settings that Korean web novel readers would love
947
- 3. Make it specific and intriguing enough to suggest a title
948
- 4. Keep it concise, around 200-300 characters
 
 
 
 
 
 
 
949
 
950
- Format:
951
- Start with one powerful opening line.
952
- Then clearly present the protagonist's situation and goal.
953
- Finally add elements that spark reader anticipation."""
954
 
955
  # Call LLM to generate theme
956
  messages = [{"role": "user", "content": prompt}]
@@ -967,44 +1080,205 @@ def generate_fallback_theme(genre: str, language: str) -> str:
967
  templates = {
968
  "로맨스": {
969
  "themes": [
970
- "계약결혼 3개월, 이혼 직전 재벌 남편이 기억을 잃었다. '당신이 내 첫사랑이야.'",
971
- "냉혈 검사와 미운정 가득한 이혼 전문 변호사. 법정에서 만날 때마다 불꽃이 튄다."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
972
  ]
973
  },
974
  "로판": {
975
  "themes": [
976
- "악녀로 빙의했는데 이미 처형 직전. 살려면 북부 전쟁광 공작과 계약결혼해야 한다.",
977
- "회귀한 황녀, 이번 생은 언니 대신 폐태자와 정략결혼하기로 했다."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
978
  ]
979
  },
980
  "판타지": {
981
  "themes": [
982
- "F급 각성자, 하지만 내 스킬은 죽은 보스 몬스터를 부활시키는 SSS급 네크로맨서.",
983
- "탑 등반 중 사망, 튜토리얼로 회귀했다. 이번엔 혼자 모든 층을 정복한다."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
984
  ]
985
  },
986
  "현판": {
987
  "themes": [
988
- "게이트 출현 10년 후, 무능력자였던 내게 시스템 창이 떴다. [유일무이 직업: 아이템 제작사]",
989
- "헌터 사관학교 꼴찌, 졸업식 날 S급 게이트가 열렸다. 그리고 나만 살아남았다."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
990
  ]
991
  },
992
  "무협": {
993
  "themes": [
994
- "천하제일 문파의 폐급 막내, 우연히 마교 교주의 비급을 습득했다.",
995
- "정파 최고 문파의 장문인으로 회귀. 이번 생은 마교와 손잡고 천하를 뒤집는다."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
996
  ]
997
  },
998
  "미스터리": {
999
  "themes": [
1000
- "폐쇄된 학교, 연쇄 실종 사건. 나만 범인의 패턴을 알고 있다.",
1001
- "완벽한 알리바이, 하지만 나는 그가 범인임을 안다. 타임루프에 갇혔으니까."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1002
  ]
1003
  },
1004
  "라이트노벨": {
1005
  "themes": [
1006
- "게임 최종보스가 현실에 나타났다. '날 키운 건 너니까, 책임져.'",
1007
- "평범한 고등학생인 나, 어느 날 전학 온 미소녀가 말했다. '전 사신입니다.'"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1008
  ]
1009
  }
1010
  }
@@ -1012,11 +1286,7 @@ def generate_fallback_theme(genre: str, language: str) -> str:
1012
  genre_themes = templates.get(genre, templates["로맨스"])
1013
  selected = random.choice(genre_themes["themes"])
1014
 
1015
- if language == "Korean":
1016
- return selected
1017
- else:
1018
- # Simple translation logic
1019
- return selected.replace("회귀", "regression").replace("빙의", "transmigration")
1020
 
1021
  def generate_theme_with_llm_only(genre: str, language: str) -> str:
1022
  """Generate theme using only LLM when JSON is not available or has errors"""
@@ -1057,43 +1327,49 @@ def generate_theme_with_llm_only(genre: str, language: str) -> str:
1057
  genre_info = genre_prompts.get(genre, genre_prompts["로맨스"])
1058
 
1059
  if language == "Korean":
1060
- prompt = f"""한국 웹소설 {genre} 장르의 중독성 있는 테마를 생성하세요.
1061
 
1062
  다음 인기 요소들을 참고하세요:
1063
  - 핵심 요소: {', '.join(genre_info['elements'])}
1064
  - 인기 훅: {', '.join(genre_info['hooks'])}
1065
 
1066
- 요구사항:
1067
- 1. 200자 내외의 짧고 임팩트 있는 설정
1068
- 2. 첫 문장은 독자를 사로잡는 강렬한 훅
1069
- 3. 주인공의 특별한 능력이나 상황 명시
1070
- 4. 갈등과 목표가 명확히 드러나도록
1071
- 5. 제목이 떠오를 정도로 구체적이면서 흥미로운 설정
1072
-
1073
- 예시 형식:
1074
- "[강렬한 문장]"
1075
- [주인공 소개와 특별한 상황]
1076
- [핵심 갈등과 목표]
1077
- [독자의 기대감을 자극하는 마무리]"""
 
 
 
1078
  else:
1079
- prompt = f"""Generate an addictive Korean web novel theme for {genre} genre.
1080
 
1081
  Reference these popular elements:
1082
  - Core elements: {', '.join(genre_info['elements'])}
1083
  - Popular hooks: {', '.join(genre_info['hooks'])}
1084
 
1085
- Requirements:
1086
- 1. Around 200 characters, short and impactful
1087
- 2. First sentence must be a powerful hook
1088
- 3. Clear special ability or situation for protagonist
1089
- 4. Clear conflict and goal
1090
- 5. Specific and intriguing enough to suggest a title
1091
-
1092
- Example format:
1093
- "[Powerful opening sentence]"
1094
- [Protagonist introduction and special situation]
1095
- [Core conflict and goal]
1096
- [Ending that sparks reader anticipation]"""
 
 
 
1097
 
1098
  messages = [{"role": "user", "content": prompt}]
1099
  generated_theme = system.call_llm_sync(messages, "writer", language)
@@ -1156,8 +1432,18 @@ def format_webnovel_display(episodes: List[Dict], genre: str) -> str:
1156
  ep_num = ep.get('episode_number', 0)
1157
  content = ep.get('content', '')
1158
 
1159
- formatted += f"## 제{ep_num}화\n\n"
1160
- formatted += f"{content}\n\n"
 
 
 
 
 
 
 
 
 
 
1161
 
1162
  if ep_num < len(episodes): # Not last episode
1163
  formatted += "➡️ *다음 화에 계속...*\n\n"
@@ -1272,6 +1558,46 @@ def create_interface():
1272
  def handle_random_theme(genre, language):
1273
  return generate_random_webnovel_theme(genre, language)
1274
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1275
  # Connect events
1276
  submit_btn.click(
1277
  fn=process_query,
@@ -1285,6 +1611,16 @@ def create_interface():
1285
  outputs=[query_input]
1286
  )
1287
 
 
 
 
 
 
 
 
 
 
 
1288
  # Examples
1289
  gr.Examples(
1290
  examples=[
 
35
  DOCX_AVAILABLE = False
36
  logger.warning("python-docx not installed. DOCX export will be disabled.")
37
 
38
+ import io # Add io import for DOCX export
39
+
40
  # --- Environment variables and constants ---
41
  FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "")
42
  BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "")
 
470
  **이전 내용 요약:**
471
  {previous_content[-1500:] if previous_content else "첫 화입니다"}
472
 
473
+ **작성 형식:**
474
+ 반드시 다음 형식으로 시작하세요:
475
+ {episode_num}화. [이번 화의 핵심을 담은 매력적인 소제목]
476
+
477
+ (한 줄 띄우고 본문 시작)
478
+
479
  **작성 지침:**
480
  1. **구성**: 3-4개의 주요 장면으로 구성
481
  - 도입부: 이전 화 연결 및 현재 상황
 
501
  **참고 후크 예시:**
502
  {random.choice(hooks)}
503
 
504
+ 소제목은 이번 화의 핵심 사건이나 전환점을 암시하는 매력적인 문구로 작성하세요.
505
  {episode_num}화를 풍성하고 몰입감 있게 작성하세요. 반드시 400-600단어로 작성하세요.""",
506
 
507
  "English": f"""Write episode {episode_num} of the web novel.
 
515
  **Previous content:**
516
  {previous_content[-1500:] if previous_content else "First episode"}
517
 
518
+ **Format:**
519
+ Must start with:
520
+ Episode {episode_num}. [Attractive subtitle that captures the essence of this episode]
521
+
522
+ (blank line then start main text)
523
+
524
  **Guidelines:**
525
  1. **Structure**: 3-4 major scenes
526
  - Opening: Connect from previous, current situation
 
546
  **Hook example:**
547
  {random.choice(hooks)}
548
 
549
+ Create an attractive subtitle that hints at key events or turning points.
550
  Write rich, immersive episode {episode_num}. Must be 400-600 words."""
551
  }
552
 
 
775
  "writer", language
776
  )
777
 
778
+ # Extract episode title and content
779
+ lines = episode_content.strip().split('\n')
780
+ episode_title = ""
781
+ actual_content = episode_content
782
+
783
+ # Check if first line contains episode number and title
784
+ if lines and (f"{episode_num}화." in lines[0] or f"Episode {episode_num}." in lines[0]):
785
+ episode_title = lines[0]
786
+ # Join the rest as content (excluding the title line and empty line after it)
787
+ actual_content = '\n'.join(lines[2:] if len(lines) > 2 and lines[1].strip() == "" else lines[1:])
788
+ else:
789
+ # If no title format found, generate a default title
790
+ episode_title = f"{episode_num}화. 제{episode_num}화"
791
+
792
  # Extract hook (last sentence)
793
+ sentences = actual_content.split('.')
794
  hook = sentences[-2] + '.' if len(sentences) > 1 else sentences[-1]
795
 
796
+ # Save episode with title
797
  WebNovelDatabase.save_episode(
798
  self.current_session_id, episode_num,
799
+ actual_content, hook
800
  )
801
 
802
+ # Add to accumulated content with title
803
+ accumulated_content += f"\n\n### {episode_title}\n{actual_content}"
804
 
805
  # Quick critique every 5 episodes
806
  if episode_num % 5 == 0:
 
822
  logger.error(f"Web novel generation error: {e}", exc_info=True)
823
  yield f"❌ 오류 발생: {e}", accumulated_content if 'accumulated_content' in locals() else "", "오류", self.current_session_id
824
 
825
+ # --- Export functions ---
826
+ def export_to_txt(episodes: List[Dict], genre: str, title: str = "") -> str:
827
+ """Export web novel to TXT format"""
828
+ content = f"{'=' * 50}\n"
829
+ content += f"{title if title else genre + ' 웹소설'}\n"
830
+ content += f"{'=' * 50}\n\n"
831
+ content += f"총 {len(episodes)}화 완결\n"
832
+ content += f"총 단어 수: {sum(ep.get('word_count', 0) for ep in episodes):,}\n"
833
+ content += f"{'=' * 50}\n\n"
834
+
835
+ for ep in episodes:
836
+ ep_num = ep.get('episode_number', 0)
837
+ ep_content = ep.get('content', '')
838
+
839
+ # Extract title if exists in content
840
+ lines = ep_content.strip().split('\n')
841
+ if lines and (f"{ep_num}화." in lines[0] or f"Episode {ep_num}." in lines[0]):
842
+ content += f"\n{lines[0]}\n"
843
+ content += f"{'-' * 40}\n\n"
844
+ actual_content = '\n'.join(lines[2:] if len(lines) > 2 and lines[1].strip() == "" else lines[1:])
845
+ content += actual_content
846
+ else:
847
+ content += f"\n{ep_num}화\n"
848
+ content += f"{'-' * 40}\n\n"
849
+ content += ep_content
850
+
851
+ content += f"\n\n{'=' * 50}\n"
852
+
853
+ return content
854
+
855
+ def export_to_docx(episodes: List[Dict], genre: str, title: str = "") -> bytes:
856
+ """Export web novel to DOCX format"""
857
+ if not DOCX_AVAILABLE:
858
+ raise Exception("python-docx is not installed")
859
+
860
+ doc = Document()
861
+
862
+ # Title
863
+ doc.add_heading(title if title else f"{genre} 웹소설", 0)
864
+
865
+ # Stats
866
+ doc.add_paragraph(f"총 {len(episodes)}화 완결")
867
+ doc.add_paragraph(f"총 단어 수: {sum(ep.get('word_count', 0) for ep in episodes):,}")
868
+ doc.add_page_break()
869
+
870
+ # Episodes
871
+ for ep in episodes:
872
+ ep_num = ep.get('episode_number', 0)
873
+ ep_content = ep.get('content', '')
874
+
875
+ # Extract title if exists
876
+ lines = ep_content.strip().split('\n')
877
+ if lines and (f"{ep_num}화." in lines[0] or f"Episode {ep_num}." in lines[0]):
878
+ doc.add_heading(lines[0], 1)
879
+ actual_content = '\n'.join(lines[2:] if len(lines) > 2 and lines[1].strip() == "" else lines[1:])
880
+ else:
881
+ doc.add_heading(f"{ep_num}화", 1)
882
+ actual_content = ep_content
883
+
884
+ # Add content paragraphs
885
+ for paragraph in actual_content.split('\n'):
886
+ if paragraph.strip():
887
+ doc.add_paragraph(paragraph.strip())
888
+
889
+ if ep_num < len(episodes):
890
+ doc.add_page_break()
891
+
892
+ # Save to bytes
893
+ import io
894
+ bytes_io = io.BytesIO()
895
+ doc.save(bytes_io)
896
+ bytes_io.seek(0)
897
+ return bytes_io.getvalue()
898
  def generate_random_webnovel_theme(genre: str, language: str) -> str:
899
  """Generate random web novel theme using novel_themes.json and LLM"""
900
  try:
 
999
 
1000
  # Create prompt for LLM
1001
  if language == "Korean":
1002
+ prompt = f"""다음 요소들을 활용하여 {genre} 장르의 매력적인 웹소설을 기획하세요:
1003
 
1004
  【선택된 요소들】
1005
  - 핵심 장르: {selected_genre_key}
 
1017
  【독자를 사로잡을 질문들】
1018
  {chr(10).join(reader_questions[:2]) if reader_questions else ""}
1019
 
1020
+ 다음 형식으로 정확히 작성하세요:
1021
+
1022
+ 📖 **제목:**
1023
+ [매력적이고 기억에 남는 제목]
1024
+
1025
+ 🌍 **설정:**
1026
+ [세계관과 배경 설정을 3-4줄로 설명]
1027
+
1028
+ 👥 **주요 캐릭터:**
1029
+ • 주인공: [이름] - [간단한 설명]
1030
+ • 주요인물1: [이름] - [간단한 설명]
1031
+ • 주요인물2: [이름] - [간단한 설명]
1032
 
1033
+ 📝 **작품소개:**
1034
+ [독자의 흥미를 끄는 3-4줄의 작품 소개. 주인공의 상황, 목표, 핵심 갈등을 포함]"""
 
 
1035
 
1036
  else: # English
1037
+ prompt = f"""Create an engaging web novel for {genre} genre using these elements:
1038
 
1039
  【Selected Elements】
1040
  - Core genre: {selected_genre_key}
 
1049
  【Reference Hook】
1050
  {selected_hook}
1051
 
1052
+ Format exactly as follows:
1053
+
1054
+ 📖 **Title:**
1055
+ [Attractive and memorable title]
1056
+
1057
+ 🌍 **Setting:**
1058
+ [World and background setting in 3-4 lines]
1059
+
1060
+ 👥 **Main Characters:**
1061
+ • Protagonist: [Name] - [Brief description]
1062
+ • Key Character 1: [Name] - [Brief description]
1063
+ • Key Character 2: [Name] - [Brief description]
1064
 
1065
+ 📝 **Synopsis:**
1066
+ [3-4 lines that hook readers. Include protagonist's situation, goal, and core conflict]"""
 
 
1067
 
1068
  # Call LLM to generate theme
1069
  messages = [{"role": "user", "content": prompt}]
 
1080
  templates = {
1081
  "로맨스": {
1082
  "themes": [
1083
+ """📖 **제목:** 계약결혼 365일, 기억을 잃은 재벌 남편
1084
+
1085
+ 🌍 **설정:**
1086
+ 현대 서울, 대기업 본사와 강남의 펜트하우스가 주 무대. 3개월 계약결혼 만료 직전, 남편이 교통사고로 기억을 잃고 아내를 첫사랑으로 착각하는 상황.
1087
+
1088
+ 👥 **주요 캐릭터:**
1089
+ • 주인공: 서연우(28) - 평범한 회사원, 부모님 병원비를 위해 계약결혼
1090
+ • 남주: 강준혁(32) - 냉혈 재벌 3세, 기억상실 후 순정남으로 변신
1091
+ • 조연: 한소영(30) - 준혁의 전 약혼녀, 복수를 계획 중
1092
+
1093
+ 📝 **작품소개:**
1094
+ "당신이 내 첫사랑이야." 이혼 서류에 도장을 찍으려던 순간, 교통사고를 당한 냉혈 재벌 남편이 나를 운명의 상대로 착각한다. 3개월간 연기했던 가짜 부부에서 진짜 사랑이 시작되는데...""",
1095
+
1096
+ """📖 **제목:** 검사님, 이혼 소송은 제가 맡을게요
1097
+
1098
+ 🌍 **설정:**
1099
+ 서울중앙지법과 검찰청이 주 무대. 냉혈 검사와 이혼 전문 변호사가 법정에서 대립하며 티격태격하는 법정 로맨스.
1100
+
1101
+ 👥 **주요 캐릭터:**
1102
+ • 주인공: 오지원(30) - 승률 100% 이혼 전문 변호사
1103
+ • 남주: 민시준(33) - 원칙주의 엘리트 검사
1104
+ • 조연: 박세진(35) - 지원의 전 남편이자 시준의 선배 검사
1105
+
1106
+ 📝 **작품소개:**
1107
+ "변호사님, 법정에서만 만나기로 했잖아요." 하필 전 남편의 불륜 소송을 맡은 날, 상대 검사가 나타났다. 법정에선 적, 밖에선 연인. 우리의 관계는 대체 뭘까?"""
1108
  ]
1109
  },
1110
  "로판": {
1111
  "themes": [
1112
+ """📖 **제목:** 악녀는 이번 생에서 도망친다
1113
+
1114
+ 🌍 **설정:**
1115
+ 마법이 존재하는 제국, 1년 후 처형당할 운명의 악녀 공작 영애로 빙의. 북부 변방의 전쟁광 공작과의 계약결혼이 유일한 생존루트.
1116
+
1117
+ 👥 **주요 캐릭터:**
1118
+ • 주인공: 아델라이드(20) - 빙의한 악녀, 원작 지식 보유
1119
+ • 남주: 카시우스(25) - 북부의 전쟁광 공작, 숨겨진 순정남
1120
+ • 악역: 황태자 레온(23) - 여주에게 집착하는 얀데레
1121
+
1122
+ 📝 **작품소개:**
1123
+ 소설 속 악녀로 빙의했는데 이미 처형 선고를 받은 상태? 살려면 원작에 없던 북부 공작과 계약결혼해야 한다. "1년만 함께해주세요. 그 후엔 자유를 드리겠습니다." 하지만 계약 기간이 끝나도 그가 날 놓아주지 않는다.""",
1124
+
1125
+ """📖 **제목:** 회귀한 황녀는 버려진 왕자를 택한다
1126
+
1127
+ 🌍 **설정:**
1128
+ 제국력 892년으로 회귀한 황녀. 전생에서 자신을 배신한 황태자 대신, 버려진 서자 왕자와 손을 잡고 제국을 뒤집으려 한다.
1129
+
1130
+ 👥 **주요 캐릭터:**
1131
+ • 주인공: 로젤린(22) - 회귀한 황녀, 미래를 아는 전략가
1132
+ • 남주: 다미안(24) - 버려진 서자 왕자, 숨겨진 흑막
1133
+ • 악역: 황태자 세바스찬(26) - 전생의 배신자
1134
+
1135
+ 📝 **작품소개:**
1136
+ 독살당해 회귀한 황녀, 이번엔 다르게 살겠다. 모두가 무시하는 서자 왕자의 손을 잡았다. "저와 함께 제국을 뒤집으시겠습니까?" 하지만 그는 내가 아는 것보다 훨씬 위험한 남자였다."""
1137
  ]
1138
  },
1139
  "판타지": {
1140
  "themes": [
1141
+ """📖 **제목:** F급 헌터, SSS급 네크로맨서가 되다
1142
+
1143
+ 🌍 **설정:**
1144
+ 게이트와 던전이 출현한 지 10년 후의 한국. F급 헌터가 우연히 얻은 스킬로 죽은 보스 몬스터를 부활시켜 부리는 유일무이 네크로맨서가 된다.
1145
+
1146
+ 👥 **주요 캐릭터:**
1147
+ • 주인공: 김도현(24) - F급에서 SSS급 네크로맨서로 각성
1148
+ • 조력자: 리치 왕(???) - 첫 번째 언데드, 전설의 대마법사
1149
+ • 라이벌: 최강훈(26) - S급 길드 마스터, 주인공을 경계
1150
+
1151
+ 📝 **작품소개:**
1152
+ "F급 주제에 무슨 헛소리야?" 모두가 비웃었다. 하지만 첫 번째 보스를 쓰러뜨린 순간, 시스템 메시지가 떴다. [SSS급 히�� 클래스: 네크로맨서 각성] 이제 죽은 보스들이 내 부하가 된다.""",
1153
+
1154
+ """📖 **제목:** 탑을 역주행하는 회귀자
1155
+
1156
+ 🌍 **설정:**
1157
+ 100층 탑 정상에서 죽은 후 튜토리얼로 회귀. 하지만 이번엔 100층부터 거꾸로 내려가며 모든 층을 정복하는 역주행 시스템이 열렸다.
1158
+
1159
+ 👥 **주요 캐릭터:**
1160
+ • 주인공: 이성진(28) - 유일한 역주행 회귀자
1161
+ • 조력자: 관리자(???) - 탑의 시스템 AI, 주인공에게 호의적
1162
+ • 라이벌: 성하윤(25) - 이번 회차 최강 신인
1163
+
1164
+ 📝 **작품소개:**
1165
+ 100층에서 죽었다. 눈을 떠보니 튜토리얼이었다. [역주행 시스템이 개방되었습니다] "뭐? 100층부터 시작한다고?" 최강자의 기억을 가진 채 정상에서부터 내려가는 전무후무한 공략이 시작된다."""
1166
  ]
1167
  },
1168
  "현판": {
1169
  "themes": [
1170
+ """📖 **제목:** 무능력자의 SSS급 아이템 제작
1171
+
1172
+ 🌍 **설정:**
1173
+ 게이트 출현 10년, 전 국민의 70%가 각성한 한국. 무능력자로 살던 주인공에게 갑자기 아이템 제작 시스템이 열린다.
1174
+
1175
+ 👥 **주요 캐릭터:**
1176
+ • 주인공: 박준서(25) - 무능력자에서 유일무이 아이템 제작사로
1177
+ • 의뢰인: 강하늘(27) - S급 헌터, 첫 번째 고객
1178
+ • 라이벌: 대기업 '아르테미스' - 아이템 독점 기업
1179
+
1180
+ 📝 **작품소개:**
1181
+ "각성 등급: 없음" 10년째 무능력자로 살았다. 그런데 오늘, 이상한 시스템 창이 떴다. [SSS급 생산직: 아이템 크래프터] 이제 내가 만든 아이템이 세계를 바꾼다.""",
1182
+
1183
+ """📖 **제목:** 헌터 사관학교의 숨겨진 최강자
1184
+
1185
+ 🌍 **설정:**
1186
+ 한국 최고의 헌터 사관학교. 입학시험 꼴찌로 들어온 주인공이 사실은 능력을 숨기고 있는 특급 요원.
1187
+
1188
+ 👥 **주요 캐릭터:**
1189
+ • 주인공: 윤시우(20) - 꼴찌로 위장한 특급 헌터
1190
+ • 히로인: 차유진(20) - 학년 수석, 재벌가 영애
1191
+ • 교관: 한태성(35) - 전설의 헌터, 주인공의 정체를 의심
1192
+
1193
+ 📝 **작품소개:**
1194
+ "측정 불가? 그럼 F급이네." 일부러 힘을 숨기고 꼴찌로 입학했다. 하지만 S급 게이트가 학교에 열리면서 정체를 숨길 수 없게 됐다. "너... 대체 누구야?"라는 물음에 어떻게 답해야 할까."""
1195
  ]
1196
  },
1197
  "무협": {
1198
  "themes": [
1199
+ """📖 **제목:** 천하제일문 폐급제자의 마교 비급
1200
+
1201
+ 🌍 **설정:**
1202
+ 정파 무림의 중원. 천하제일문의 폐급 막내제자가 우연히 마교 교주의 비급을 습득하고 정마를 아우르는 절대무공을 익힌다.
1203
+
1204
+ 👥 **주요 캐릭터:**
1205
+ • 주인공: 진천(18) - 폐급에서 절대고수로
1206
+ • 스승: 혈마노조(???) - 비급에 깃든 마교 전설
1207
+ • 라이벌: 남궁세가 소가주 - 정파 제일 천재
1208
+
1209
+ 📝 **작품소개:**
1210
+ "하찮은 것이 감히!" 모두가 무시하던 막내제자. 하지만 떨어진 절벽에서 발견한 것은 전설로만 전해지던 천마신공. "이제부터가 진짜 시작이다." 정파와 마교를 뒤흔들 폐급의 반란이 시작된다.""",
1211
+
1212
+ """📖 **제목:** 화산파 장문인으로 회귀하다
1213
+
1214
+ 🌍 **설정:**
1215
+ 100년 전 화산파가 최고 문파이던 시절로 회귀. 미래를 아는 장문인이 되어 문파를 지키고 무림을 재편한다.
1216
+
1217
+ 👥 **주요 캐릭터:**
1218
+ • 주인공: 청운진인(45→25) - 회귀한 화산파 장문인
1219
+ • 제자: 백무진(15) - 미래의 화산파 배신자
1220
+ • 맹우: 마교 성녀 - 전생의 적, 이생의 동료
1221
+
1222
+ 📝 **작품소개:**
1223
+ 멸문 직전에 회귀했다. 이번엔 다르다. "앞으로 화산파는 정파의 규율을 벗어난다." 미래를 아는 장문인의 파격적인 결정. 마교와 손잡고 무림의 판도를 뒤집는다."""
1224
  ]
1225
  },
1226
  "미스터리": {
1227
  "themes": [
1228
+ """📖 **제목:** 폐교에 갇힌 7명, 그리고
1229
+
1230
+ 🌍 **설정:**
1231
+ 폐쇄된 산골 학교, 동창회를 위해 모인 8명이 갇힌다. 하나씩 사라지는 동창들. 범인은 이 안에 있다.
1232
+
1233
+ 👥 **주요 캐릭터:**
1234
+ • 주인공: 서민준(28) - 프로파일러 출신 교사
1235
+ • 용의자1: 김태희(28) - 실종된 친구의 전 연인
1236
+ • 용의자2: 박진우(28) - 10년 전 사건의 목격자
1237
+
1238
+ 📝 **작품소개:**
1239
+ "10년 전 그날처럼..." 폐교에서 열린 동창회, 하지만 출구는 봉쇄됐다. 한 명씩 사라지는 친구들. 10년 전 묻어둔 비밀이 되살아난다. 살인자는 우리 중 한 명이다.""",
1240
+
1241
+ """📖 **제목:** 타임루프 속 연쇄살인마를 찾아라
1242
+
1243
+ 🌍 **설정:**
1244
+ 같은 하루가 반복되는 타임루프. 매번 다른 방법으로 살인이 일어나지만 범인은 동일인. 루프를 깨려면 범인을 찾아야 한다.
1245
+
1246
+ 👥 **주요 캐릭터:**
1247
+ • 주인공: 강해인(30) - 타임루프에 갇힌 형사
1248
+ • 희생자: 이수연(25) - 매번 죽는 카페 알바생
1249
+ • 용의자들: 카페 단골 5명 - 각자의 비밀을 숨기고 있음
1250
+
1251
+ 📝 **작품소개:**
1252
+ "또 오늘이야..." 49번째 같은 아침. 오후 3시 33분, 카페에서 살인이 일어난다. 범인을 잡아야 내일이 온다. 하지만 범인은 매번 완벽한 알리바이를 만든다. 과연 50번째 오늘은 다를까?"""
1253
  ]
1254
  },
1255
  "라이트노벨": {
1256
  "themes": [
1257
+ """📖 **제목:** 여자친구가 사실은 마왕이었다
1258
+
1259
+ 🌍 **설정:**
1260
+ 평범한 고등학교, 하지만 학생과 교사 중 일부는 이세계에서 온 존재들. 주인공만 모르는 학교의 비밀.
1261
+
1262
+ 👥 **주요 캐릭터:**
1263
+ • 주인공: 김태양(17) - 평범한 고등학생(?)
1264
+ • 히로인: 루시퍼(17) - 마왕이자 여자친구
1265
+ • 라이벌: 미카엘(17) - 천사이자 학생회장
1266
+
1267
+ 📝 **작품소개:**
1268
+ "선배, 사실 저... 마왕이에요!" 1년째 사귄 여자친구의 충격 고백. 근데 학생회장은 천사고, 담임은 드래곤이라고? 평범한 줄 알았던 우리 학교의 정체가 밝혀진다. "그래서... 우리 헤어져야 해?"라고 묻자 그녀가 울기 시작했다.""",
1269
+
1270
+ """📖 **제목:** 게임 아이템이 현실에 떨어진다
1271
+
1272
+ 🌍 **설정:**
1273
+ 모바일 게임과 현실이 연동되기 시작한 세계. 게임에서 얻은 아이템이 현실에 나타나면서 벌어지는 학원 코미디.
1274
+
1275
+ 👥 **주요 캐릭터:**
1276
+ • 주인공: 박도윤(18) - 게임 폐인 고등학생
1277
+ • 히로인: 최서연(18) - 전교 1등, 의외로 게임 고수
1278
+ • 친구: 장민혁(18) - 현질 전사, 개그 담당
1279
+
1280
+ 📝 **작품소개:**
1281
+ "어? 이거 내 SSR 무기잖아?" 핸드폰 게임에서 뽑은 아이템이 책상 위에 나타났다. 문제는 학교에 몬스터도 나타나기 시작했다는 것. "야, 수능보다 레이드가 더 중요해진 것 같은데?"라며 웃는 친구들과 함께하는 좌충우돌 학원 판타지."""
1282
  ]
1283
  }
1284
  }
 
1286
  genre_themes = templates.get(genre, templates["로맨스"])
1287
  selected = random.choice(genre_themes["themes"])
1288
 
1289
+ return selected
 
 
 
 
1290
 
1291
  def generate_theme_with_llm_only(genre: str, language: str) -> str:
1292
  """Generate theme using only LLM when JSON is not available or has errors"""
 
1327
  genre_info = genre_prompts.get(genre, genre_prompts["로맨스"])
1328
 
1329
  if language == "Korean":
1330
+ prompt = f"""한국 웹소설 {genre} 장르의 중독성 있는 작품을 기획하세요.
1331
 
1332
  다음 인기 요소들을 참고하세요:
1333
  - 핵심 요소: {', '.join(genre_info['elements'])}
1334
  - 인기 훅: {', '.join(genre_info['hooks'])}
1335
 
1336
+ 다음 형식으로 정확히 작성하세요:
1337
+
1338
+ 📖 **제목:**
1339
+ [매력적이고 기억하기 쉬운 제목]
1340
+
1341
+ 🌍 **설정:**
1342
+ [세계관과 배경을 3-4줄로 설명. 시대, 장소, 핵심 설정 포함]
1343
+
1344
+ 👥 **주요 캐릭터:**
1345
+ • 주인공: [이름(나이)] - [직업/신분, 핵심 특징]
1346
+ • 주요인물1: [이름(나이)] - [관계/역할, 특��]
1347
+ • 주요인물2: [이름(나이)] - [관계/역할, 특징]
1348
+
1349
+ 📝 **작품소개:**
1350
+ [3-4줄로 작품의 핵심 갈등과 매력을 소개. 첫 문장은 강한 훅으로 시작하고, 주인공의 목표와 장애물을 명확히 제시]"""
1351
  else:
1352
+ prompt = f"""Generate an addictive Korean web novel for {genre} genre.
1353
 
1354
  Reference these popular elements:
1355
  - Core elements: {', '.join(genre_info['elements'])}
1356
  - Popular hooks: {', '.join(genre_info['hooks'])}
1357
 
1358
+ Format exactly as follows:
1359
+
1360
+ 📖 **Title:**
1361
+ [Attractive and memorable title]
1362
+
1363
+ 🌍 **Setting:**
1364
+ [World and background in 3-4 lines. Include era, location, core settings]
1365
+
1366
+ 👥 **Main Characters:**
1367
+ Protagonist: [Name(Age)] - [Job/Status, key traits]
1368
+ • Key Character 1: [Name(Age)] - [Relationship/Role, traits]
1369
+ Key Character 2: [Name(Age)] - [Relationship/Role, traits]
1370
+
1371
+ 📝 **Synopsis:**
1372
+ [3-4 lines introducing core conflict and appeal. Start with strong hook, clearly present protagonist's goal and obstacles]"""
1373
 
1374
  messages = [{"role": "user", "content": prompt}]
1375
  generated_theme = system.call_llm_sync(messages, "writer", language)
 
1432
  ep_num = ep.get('episode_number', 0)
1433
  content = ep.get('content', '')
1434
 
1435
+ # Check if content already has episode title
1436
+ lines = content.strip().split('\n')
1437
+ if lines and (f"{ep_num}화." in lines[0] or f"Episode {ep_num}." in lines[0]):
1438
+ # Use the existing title
1439
+ formatted += f"## {lines[0]}\n\n"
1440
+ # Use the rest as content
1441
+ actual_content = '\n'.join(lines[2:] if len(lines) > 2 and lines[1].strip() == "" else lines[1:])
1442
+ formatted += f"{actual_content}\n\n"
1443
+ else:
1444
+ # No title found, use default
1445
+ formatted += f"## 제{ep_num}화\n\n"
1446
+ formatted += f"{content}\n\n"
1447
 
1448
  if ep_num < len(episodes): # Not last episode
1449
  formatted += "➡️ *다음 화에 계속...*\n\n"
 
1558
  def handle_random_theme(genre, language):
1559
  return generate_random_webnovel_theme(genre, language)
1560
 
1561
+ def handle_download(download_format, session_id, genre):
1562
+ """Handle download request"""
1563
+ if not session_id:
1564
+ return None
1565
+
1566
+ try:
1567
+ episodes = WebNovelDatabase.get_episodes(session_id)
1568
+ if not episodes:
1569
+ return None
1570
+
1571
+ # Get title from first episode or generate default
1572
+ title = f"{genre} 웹소설"
1573
+
1574
+ if download_format == "TXT":
1575
+ content = export_to_txt(episodes, genre, title)
1576
+
1577
+ # Save to temporary file
1578
+ with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
1579
+ suffix='.txt', delete=False) as f:
1580
+ f.write(content)
1581
+ return f.name
1582
+
1583
+ elif download_format == "DOCX":
1584
+ if not DOCX_AVAILABLE:
1585
+ gr.Warning("DOCX export requires python-docx library")
1586
+ return None
1587
+
1588
+ content = export_to_docx(episodes, genre, title)
1589
+
1590
+ # Save to temporary file
1591
+ with tempfile.NamedTemporaryFile(mode='wb', suffix='.docx',
1592
+ delete=False) as f:
1593
+ f.write(content)
1594
+ return f.name
1595
+
1596
+ except Exception as e:
1597
+ logger.error(f"Download error: {e}")
1598
+ gr.Warning(f"다운로드 중 오류 발생: {str(e)}")
1599
+ return None
1600
+
1601
  # Connect events
1602
  submit_btn.click(
1603
  fn=process_query,
 
1611
  outputs=[query_input]
1612
  )
1613
 
1614
+ download_btn.click(
1615
+ fn=handle_download,
1616
+ inputs=[download_format, current_session_id, genre_select],
1617
+ outputs=[download_file]
1618
+ ).then(
1619
+ fn=lambda x: gr.update(visible=True) if x else gr.update(visible=False),
1620
+ inputs=[download_file],
1621
+ outputs=[download_file]
1622
+ )
1623
+
1624
  # Examples
1625
  gr.Examples(
1626
  examples=[