openfree commited on
Commit
90af475
·
verified ·
1 Parent(s): e1b181e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +559 -510
app.py CHANGED
@@ -473,6 +473,8 @@ Provide detailed 40-episode plotline centered on the input story setting. Specif
473
  **전체 플롯에서 {episode_num}화 내용:**
474
  {self._extract_episode_plan(plot_outline, episode_num)}
475
 
 
 
476
  **이전 내용 요약:**
477
  {previous_content[-1500:] if previous_content else "첫 화입니다"}
478
 
@@ -490,6 +492,7 @@ Provide detailed 40-episode plotline centered on the input story setting. Specif
490
  - 후크: 다음 화 예고
491
 
492
  2. **필수 요소:**
 
493
  - 생생한 대화와 행동 묘사
494
  - 캐릭터 감정과 내면 갈등
495
  - 장면 전환과 템포 조절
@@ -507,8 +510,7 @@ Provide detailed 40-episode plotline centered on the input story setting. Specif
507
  **참고 후크 예시:**
508
  {random.choice(hooks)}
509
 
510
- 소제목은 이번 화의 핵심 사건이나 전환점을 암시하는 매력적인 문구로 작성하세요.
511
- {episode_num}화를 풍성하고 몰입감 있게 작성하세요. 반드시 400-600단어로 작성하세요.""",
512
 
513
  "English": f"""Write episode {episode_num} of the web novel.
514
 
@@ -518,6 +520,8 @@ Provide detailed 40-episode plotline centered on the input story setting. Specif
518
  **Episode {episode_num} from plot:**
519
  {self._extract_episode_plan(plot_outline, episode_num)}
520
 
 
 
521
  **Previous content:**
522
  {previous_content[-1500:] if previous_content else "First episode"}
523
 
@@ -535,6 +539,7 @@ Episode {episode_num}. [Attractive subtitle that captures the essence of this ep
535
  - Hook: Next episode teaser
536
 
537
  2. **Essential elements:**
 
538
  - Vivid dialogue and action
539
  - Character emotions and conflicts
540
  - Scene transitions and pacing
@@ -552,8 +557,7 @@ Episode {episode_num}. [Attractive subtitle that captures the essence of this ep
552
  **Hook example:**
553
  {random.choice(hooks)}
554
 
555
- Create an attractive subtitle that hints at key events or turning points.
556
- Write rich, immersive episode {episode_num}. Must be 400-600 words."""
557
  }
558
 
559
  return lang_prompts.get(language, lang_prompts["Korean"])
@@ -603,18 +607,39 @@ Provide specific improvements."""
603
 
604
  patterns = [
605
  f"{episode_num}화:", f"Episode {episode_num}:",
606
- f"제{episode_num}화:", f"EP{episode_num}:"
 
 
 
 
 
 
 
 
607
  ]
608
 
609
  for line in lines:
 
610
  if any(pattern in line for pattern in patterns):
611
  capturing = True
612
- elif capturing and any(f"{episode_num+1}" in line for pattern in patterns):
 
 
613
  break
614
  elif capturing:
615
  episode_section.append(line)
616
-
617
- return '\n'.join(episode_section) if episode_section else "플롯을 참고하여 작성"
 
 
 
 
 
 
 
 
 
 
618
 
619
  # --- LLM call functions ---
620
  def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str:
@@ -701,38 +726,48 @@ Provide specific improvements."""
701
  독자를 중독시키는 플롯과 전개를 설계합니다.
702
  장르별 관습과 독자 기대를 정확히 파악합니다.
703
  40화 완결 구조로 완벽한 기승전결을 만듭니다.
704
- 각 화마다 충실한 내용과 전개를 계획합니다.""",
 
 
705
 
706
  "writer": """당신은 독자를 사로잡는 웹소설 작가입니다.
707
  풍부하고 몰입감 있는 문체를 구사합니다.
708
  각 화를 400-600단어로 충실하게 작성합니다.
709
  여러 장면과 전환을 통해 이야기를 전개합니다.
710
  대화, 행동, 내면 묘사를 균형있게 배치합니다.
711
- 매 화 끝에 강력한 후크로 다음 화를 기다리게 만듭니다.""",
 
 
712
 
713
  "critic": """당신은 웹소설 독자의 마음을 읽는 평론가입니다.
714
  재미와 몰입감을 최우선으로 평가합니다.
715
  장르적 쾌감과 독자 만족도를 분석합니다.
716
- 구체적이고 실용적인 개선안을 제시합니다."""
 
717
  },
718
  "English": {
719
  "planner": """You perfectly understand the Korean web novel market.
720
  Design addictive plots and developments.
721
  Accurately grasp genre conventions and reader expectations.
722
  Create perfect story structure in 40 episodes.
723
- Plan substantial content and development for each episode.""",
 
 
724
 
725
  "writer": """You are a web novelist who captivates readers.
726
  Use rich and immersive writing style.
727
  Write each episode with 400-600 words faithfully.
728
  Develop story through multiple scenes and transitions.
729
  Balance dialogue, action, and inner descriptions.
730
- End each episode with powerful hook for next.""",
 
 
731
 
732
  "critic": """You read web novel readers' minds.
733
  Prioritize fun and immersion in evaluation.
734
  Analyze genre satisfaction and reader enjoyment.
735
- Provide specific, practical improvements."""
 
736
  }
737
  }
738
 
@@ -753,6 +788,8 @@ Provide specific, practical improvements."""
753
  self.current_session_id = WebNovelDatabase.create_session(query, genre, language)
754
  self.tracker.set_genre(genre)
755
  logger.info(f"Created new session: {self.current_session_id}")
 
 
756
 
757
  # Generate plot outline first
758
  if resume_from_episode == 0:
@@ -764,6 +801,9 @@ Provide specific, practical improvements."""
764
  "planner", language
765
  )
766
 
 
 
 
767
  yield "✅ 플롯 구성 완료!", "", f"40화 구성 완료", self.current_session_id
768
 
769
  # Generate episodes
@@ -772,12 +812,21 @@ Provide specific, practical improvements."""
772
  # Write episode
773
  yield f"✍️ {episode_num}화 집필 중...", accumulated_content, f"진행률: {episode_num}/{TARGET_EPISODES}화", self.current_session_id
774
 
 
775
  episode_prompt = self.create_episode_prompt(
776
  episode_num, plot_outline, accumulated_content, genre, language
777
  )
778
 
 
 
 
 
 
 
 
 
779
  episode_content = self.call_llm_sync(
780
- [{"role": "user", "content": episode_prompt}],
781
  "writer", language
782
  )
783
 
@@ -1105,9 +1154,9 @@ def generate_random_webnovel_theme(genre: str, language: str) -> str:
1105
  [세계관과 배경 설정을 3-4줄로 설명]
1106
 
1107
  👥 **주요 캐릭터:**
1108
- 주인공: [이름] - [간단한 설명]
1109
- 주요인물1: [이름] - [간단한 설명]
1110
- 주요인물2: [이름] - [간단한 설명]
1111
 
1112
  📝 **작품소개:**
1113
  [독자의 흥미를 끄는 3-4줄의 작품 소개. 주인공의 상황, 목표, 핵심 갈등을 포함]"""
@@ -1140,9 +1189,9 @@ Format exactly as follows:
1140
  [World and background setting in 3-4 lines]
1141
 
1142
  👥 **Main Characters:**
1143
- Protagonist: [Name] - [Brief description]
1144
- Key Character 1: [Name] - [Brief description]
1145
- Key Character 2: [Name] - [Brief description]
1146
 
1147
  📝 **Synopsis:**
1148
  [3-4 lines that hook readers. Include protagonist's situation, goal, and core conflict]"""
@@ -1169,9 +1218,9 @@ def generate_fallback_theme(genre: str, language: str) -> str:
1169
  현대 서울, 대기업 본사와 강남의 펜트하우스가 주 무대. 3개월 계약결혼 만료 직전, 남편이 교통사고로 기억을 잃고 아내를 첫사랑으로 착각하는 상황.
1170
 
1171
  👥 **주요 캐릭터:**
1172
- 주인공: 서연우(28) - 평범한 회사원, 부모님 병원비를 위해 계약결혼
1173
- 남주: 강준혁(32) - 냉혈 재벌 3세, 기억상실 후 순정남으로 변신
1174
- 조연: 한소영(30) - 준혁의 전 약혼녀, 복수를 계획 중
1175
 
1176
  📝 **작품소개:**
1177
  "당신이 내 첫사랑이야." 이혼 서류에 도장을 찍으려던 순간, 교통사고를 당한 냉혈 재벌 남편이 나를 운명의 상대로 착각한다. 3개월간 연기했던 가짜 부부에서 진짜 사랑이 시작되는데...""",
@@ -1182,235 +1231,235 @@ def generate_fallback_theme(genre: str, language: str) -> str:
1182
  서울중앙지법과 검찰청이 주 무대. 냉혈 검사와 이혼 전문 변호사가 법정에서 대립하며 티격태격하는 법정 로맨스.
1183
 
1184
  👥 **주요 캐릭터:**
1185
- 주인공: 오지원(30) - 승률 100% 이혼 전문 변호사
1186
- 남주: 민시준(33) - 원칙주의 엘리트 검사
1187
- 조연: 박세진(35) - 지원의 전 남편이자 시준의 선배 검사
1188
 
1189
  📝 **작품소개:**
1190
  "변호사님, 법정에서만 만나기로 했잖아요." 하필 전 남편의 불륜 소송을 맡은 날, 상대 검사가 나타났다. 법정에선 적, 밖에선 연인. 우리의 관계는 대체 뭘까?"""
1191
- ]
1192
- },
1193
- "로판": {
1194
- "themes": [
1195
- """📖 **제목:** 악녀는 이번 생에서 도망친다
1196
 
1197
  🌍 **설정:**
1198
  마법이 존재하는 제국, 1년 후 처형당할 운명의 악녀 공작 영애로 빙의. 북부 변방의 전쟁광 공작과의 계약결혼이 유일한 생존루트.
1199
 
1200
  👥 **주요 캐릭터:**
1201
- 주인공: 아델라이드(20) - 빙의한 악녀, 원작 지식 보유
1202
- 남주: 카시우스(25) - 북부의 전쟁광 공작, 숨겨진 순정남
1203
- 악역: 황태자 레온(23) - 여주에게 집착하는 얀데레
1204
 
1205
  📝 **작품소개:**
1206
  소설 속 악녀로 빙의했는데 이미 처형 선고를 받은 상태? 살려면 원작에 없던 북부 공작과 계약결혼해야 한다. "1년만 함께해주세요. 그 후엔 자유를 드리겠습니다." 하지만 계약 기간이 끝나도 그가 날 놓아주지 않는다.""",
1207
-
1208
- """📖 **제목:** 회귀한 황녀는 버려진 왕자를 택한다
1209
 
1210
  🌍 **설정:**
1211
  제국력 892년으로 회귀한 황녀. 전생에서 자신을 배신한 황태자 대신, 버려진 서자 왕자와 손을 잡고 제국을 뒤집으려 한다.
1212
 
1213
  👥 **주요 캐릭터:**
1214
- 주인공: 로젤린(22) - 회귀한 황녀, 미래를 아는 전략가
1215
- 남주: 다미안(24) - 버려진 서자 왕자, 숨겨진 흑막
1216
- 악역: 황태자 세바스찬(26) - 전생의 배신자
1217
 
1218
  📝 **작품소개:**
1219
  독살당해 회귀한 황녀, 이번엔 다르게 살겠다. 모두가 무시하는 서자 왕자의 손을 잡았다. "저와 함께 제국을 뒤집으시겠습니까?" 하지만 그는 내가 아는 것보다 훨씬 ��험한 남자였다."""
1220
- ]
1221
- },
1222
- "판타지": {
1223
- "themes": [
1224
- """📖 **제목:** F급 헌터, SSS급 네크로맨서가 되다
1225
 
1226
  🌍 **설정:**
1227
  게이트와 던전이 출현한 지 10년 후의 한국. F급 헌터가 우연히 얻은 스킬로 죽은 보스 몬스터를 부활시켜 부리는 유일무이 네크로맨서가 된다.
1228
 
1229
  👥 **주요 캐릭터:**
1230
- 주인공: 김도현(24) - F급에서 SSS급 네크로맨서로 각성
1231
- 조력자: 리치 왕(???) - 첫 번째 언데드, 전설의 대마법사
1232
- 라이벌: 최강훈(26) - S급 길드 마스터, 주인공을 경계
1233
 
1234
  📝 **작품소개:**
1235
  "F급 주제에 무슨 헛소리야?" 모두가 비웃었다. 하지만 첫 번째 보스를 쓰러뜨린 순간, 시스템 메시지가 떴다. [SSS급 히든 클래스: 네크로맨서 각성] 이제 죽은 보스들이 내 부하가 된다.""",
1236
-
1237
- """📖 **제목:** 탑을 역주행하는 회귀자
1238
 
1239
  🌍 **설정:**
1240
  100층 탑 정상에서 죽은 후 튜토리얼로 회귀. 하지만 이번엔 100층부터 거꾸로 내려가며 모든 층을 정복하는 역주행 시스템이 열렸다.
1241
 
1242
  👥 **주요 캐릭터:**
1243
- 주인공: 이성진(28) - 유일한 역주행 회귀자
1244
- 조력자: 관리자(???) - 탑의 시스템 AI, 주인공에게 호의적
1245
- 라이벌: 성하윤(25) - 이번 회차 최강 신인
1246
 
1247
  📝 **작품소개:**
1248
  100층에서 죽었다. 눈을 떠보니 튜토리얼이었다. [역주행 시스템이 개방되었습니다] "뭐? 100층부터 시작한다고?" 최강자의 기억을 가진 채 정상에서부터 내려가는 전무후무한 공략이 시작된다."""
1249
- ]
1250
- },
1251
- "현판": {
1252
- "themes": [
1253
- """📖 **제목:** 무능력자의 SSS급 아이템 제작
1254
 
1255
  🌍 **설정:**
1256
  게이트 출현 10년, 전 국민의 70%가 각성한 한국. 무능력자로 살던 주인공에게 갑자기 아이템 제작 시스템이 열린다.
1257
 
1258
  👥 **주요 캐릭터:**
1259
- 주인공: 박준서(25) - 무능력자에서 유일무이 아이템 제작사로
1260
- 의뢰인: 강하늘(27) - S급 헌터, 첫 번째 고객
1261
- 라이벌: 대기업 '아르테미스' - 아이템 독점 기업
1262
 
1263
  📝 **작품소개:**
1264
  "각성 등급: 없음" 10년째 무능력자로 살았다. 그런데 오늘, 이상한 시스템 창이 떴다. [SSS급 생산직: 아이템 크래프터] 이제 내가 만든 아이템이 세계를 바꾼다.""",
1265
-
1266
- """📖 **제목:** 헌터 사관학교의 숨겨진 최강자
1267
 
1268
  🌍 **설정:**
1269
  한국 최고의 헌터 사관학교. 입학시험 꼴찌로 들어온 주인공이 사실은 능력을 숨기고 있는 특급 요원.
1270
 
1271
  👥 **주요 캐릭터:**
1272
- 주인공: 윤시우(20) - 꼴찌로 위장한 특급 헌터
1273
- 히로인: 차유진(20) - 학년 수석, 재벌가 영애
1274
- 교관: 한태성(35) - 전설의 헌터, 주인공의 정체를 의심
1275
 
1276
  📝 **작품소개:**
1277
  "측정 불가? 그럼 F급이네." 일부러 힘을 숨기고 꼴찌로 입학했다. 하지만 S급 게이트가 학교에 열리면서 정체를 숨길 수 없게 됐다. "너... 대체 누구야?"라는 물음에 어떻게 답해야 할까."""
1278
- ]
1279
- },
1280
- "무협": {
1281
- "themes": [
1282
- """📖 **제목:** 천하제일문 폐급제자의 마교 비급
1283
 
1284
  🌍 **설정:**
1285
  정파 무림의 중원. 천하제일문의 폐급 막내제자가 우연히 마교 교주의 비급을 습득하고 정마를 아우르는 절대무공을 익힌다.
1286
 
1287
  👥 **주요 캐릭터:**
1288
- 주인공: 진천(18) - 폐급에서 절대고수로
1289
- 스승: 혈마노조(???) - 비급에 깃든 마교 전설
1290
- 라이벌: 남궁세가 소가주 - 정파 제일 천재
1291
 
1292
  📝 **작품소개:**
1293
  "하찮은 것이 감히!" 모두가 무시하던 막내제자. 하지만 떨어진 절벽에서 발견한 것은 전설로만 전해지던 천마신공. "이제부터가 진짜 시작이다." 정파와 마교를 뒤흔들 폐급의 반란이 시작된다.""",
1294
-
1295
- """📖 **제목:** 화산파 장문인으로 회귀하다
1296
 
1297
  🌍 **설정:**
1298
  100년 전 화산파가 최고 문파이던 시절로 회귀. 미래를 아는 장문인이 되어 문파를 지키고 무림을 재편한다.
1299
 
1300
  👥 **주요 캐릭터:**
1301
- 주인공: 청운진인(45→25) - 회귀한 화산파 장문인
1302
- 제자: 백무진(15) - 미래의 화산파 배신자
1303
- 맹우: 마교 성녀 - 전생의 적, 이생의 동료
1304
 
1305
  📝 **작품소개:**
1306
  멸문 직전에 회귀했다. 이번엔 다르다. "앞으로 화산파는 정파의 규율을 벗어난다." 미래를 아는 장문인의 파격적인 결정. 마교와 손잡고 무림의 판도를 뒤집는다."""
1307
- ]
1308
- },
1309
- "미스터리": {
1310
- "themes": [
1311
- """📖 **제목:** 폐교에 갇힌 7명, 그리고 나
1312
 
1313
  🌍 **설정:**
1314
  폐쇄된 산골 학교, 동창회를 위해 모인 8명이 갇힌다. 하나씩 사라지는 동창들. 범인은 이 안에 있다.
1315
 
1316
  👥 **주요 캐릭터:**
1317
- 주인공: 서민준(28) - 프로파일러 출신 교사
1318
- 용의자1: 김태희(28) - 실종된 친구의 전 연인
1319
- 용의자2: 박진우(28) - 10년 전 사건의 목격자
1320
 
1321
  📝 **작품소개:**
1322
  "10년 전 그날처럼..." 폐교에서 열린 동창회, 하지만 출구는 봉쇄됐다. 한 명씩 사라지는 친구들. 10년 전 묻어둔 비밀이 되살아난다. 살인자는 우리 중 한 명이다.""",
1323
-
1324
- """📖 **제목:** 타임루프 속 연쇄살인마를 찾아라
1325
 
1326
  🌍 **설정:**
1327
  같은 하루가 반복되는 타임루프. 매번 다른 방법으로 살인이 일어나지만 범인은 동일인. 루프를 깨려면 범인을 찾아야 한다.
1328
 
1329
  👥 **주요 캐릭터:**
1330
- 주인공: 강해인(30) - 타임루프에 갇힌 형사
1331
- 희생자: 이수연(25) - 매번 죽는 카페 알바생
1332
- 용의자들: 카페 단골 5명 - 각자의 비밀을 숨기고 있음
1333
 
1334
  📝 **작품소개:**
1335
  "또 오늘이야..." 49번째 같은 아침. 오후 3시 33분, 카페에서 살인이 일어난다. 범인을 잡아야 내일이 온다. 하지만 범인은 매번 완벽한 알리바이를 만든다. 과연 50번째 오늘은 다를까?"""
1336
- ]
1337
- },
1338
- "라이트노벨": {
1339
- "themes": [
1340
- """📖 **제목:** 내 여자친구가 사실은 마왕이었다
1341
 
1342
  🌍 **설정:**
1343
  평범한 고등학교, 하지만 학생과 교사 중 일부는 이세계에서 온 존재들. 주인공만 모르는 학교의 비밀.
1344
 
1345
  👥 **주요 캐릭터:**
1346
- 주인공: 김태양(17) - 평범한 고등학생(?)
1347
- 히로인: 루시퍼(17) - 마왕이자 여자친구
1348
- 라이벌: 미카엘(17) - 천사이자 학생회장
1349
 
1350
  📝 **작품소개:**
1351
  "선배, 사실 저... 마왕이에요!" 1년째 사귄 여자친구의 충격 고백. 근데 학생회장은 천사고, 담임은 드래곤이라고? 평범한 줄 알았던 우리 학교의 정체가 밝혀진다. "그래서... 우리 헤어져야 해?"라고 묻자 그녀가 울기 시작했다.""",
1352
-
1353
- """📖 **제목:** 게�� 아이템이 현실에 떨어진다
1354
 
1355
  🌍 **설정:**
1356
  모바일 게임과 현실이 연동되기 시작한 세계. 게임에서 얻은 아이템이 현실에 나타나면서 벌어지는 학원 코미디.
1357
 
1358
  👥 **주요 캐릭터:**
1359
- 주인공: 박도윤(18) - 게임 폐인 고등학생
1360
- 히로인: 최서연(18) - 전교 1등, 의외로 게임 고수
1361
- 친구: 장민혁(18) - 현질 전사, 개그 담당
1362
 
1363
  📝 **작품소개:**
1364
  "어? 이거 내 SSR 무기잖아?" 핸드폰 게임에서 뽑은 아이템이 책상 위에 나타났다. 문제는 학교에 몬스터도 나타나기 시작했다는 것. "야, 수능보다 레이드가 더 중요해진 것 같은데?"라며 웃는 친구들과 함께하는 좌충우돌 학원 판타지."""
1365
- ]
1366
- }
1367
- }
1368
-
1369
- genre_themes = templates.get(genre, templates["로맨스"])
1370
- selected = random.choice(genre_themes["themes"])
1371
-
1372
- return selected
1373
 
1374
  def generate_theme_with_llm_only(genre: str, language: str) -> str:
1375
- """Generate theme using only LLM when JSON is not available or has errors"""
1376
- system = WebNovelSystem()
1377
-
1378
- # Genre-specific prompts based on popular web novel trends
1379
- genre_prompts = {
1380
- "로맨스": {
1381
- "elements": ["계약결혼", "재벌", "이혼", "첫사랑", "운명적 만남", "오해와 화해"],
1382
- "hooks": ["기억상실", "정체 숨기기", "가짜 연인", "원나잇 후 재회"]
1383
- },
1384
- "로판": {
1385
- "elements": ["빙의", "회귀", "악녀", "황녀", "공작", "원작 파괴"],
1386
- "hooks": ["처형 직전", "파혼 선언", "독살 시도", "폐위 위기"]
1387
- },
1388
- "판타지": {
1389
- "elements": ["시스템", "각성", "던전", "회귀", "탑 등반", "SSS급"],
1390
- "hooks": ["F급에서 시작", "숨겨진 클래스", "유일무이 스킬", "죽음 후 각성"]
1391
- },
1392
- "현판": {
1393
- "elements": ["헌터", "게이트", "각성자", "길드", "아이템", "랭킹"],
1394
- "hooks": ["늦은 각성", "재능 재평가", "S급 게이트", "시스템 오류"]
1395
- },
1396
- "무협": {
1397
- "elements": ["회귀", "천재", "마교", "비급", "복수", "환생"],
1398
- "hooks": ["폐급에서 최강", "배신 후 각성", "숨겨진 혈통", "기연 획득"]
1399
- },
1400
- "미스터리": {
1401
- "elements": ["탐정", "연쇄살인", "타임루프", "초능력", "과거의 비밀"],
1402
- "hooks": ["밀실 살인", "예고 살인", "기억 조작", "시간 역행"]
1403
- },
1404
- "라이트노벨": {
1405
- "elements": ["학원", "이세계", "히로인", "게임", "일상", "판타지"],
1406
- "hooks": ["전학생 정체", "게임 현실화", "평행세계", "숨겨진 능력"]
1407
- }
1408
- }
1409
-
1410
- genre_info = genre_prompts.get(genre, genre_prompts["로맨스"])
1411
-
1412
- if language == "Korean":
1413
- prompt = f"""한국 웹소설 {genre} 장르의 중독성 있는 작품을 기획하세요.
1414
 
1415
  다음 인기 요소들을 참고하세요:
1416
  - 핵심 요소: {', '.join(genre_info['elements'])}
@@ -1425,14 +1474,14 @@ def generate_theme_with_llm_only(genre: str, language: str) -> str:
1425
  [세계관과 배경을 3-4줄로 설명. 시대, 장소, 핵심 설정 포함]
1426
 
1427
  👥 **주요 캐릭터:**
1428
- 주인공: [이름(나이)] - [직업/신분, 핵심 특징]
1429
- 주요인물1: [이름(나이)] - [관계/역할, 특징]
1430
- 주요인물2: [이름(나이)] - [관계/역할, 특징]
1431
 
1432
  📝 **작품소개:**
1433
  [3-4줄로 작품의 핵심 갈등과 매력을 소개. 첫 문장은 강한 훅으로 시작하고, 주인공의 목표와 장애물을 명확히 제시]"""
1434
- else:
1435
- prompt = f"""Generate an addictive Korean web novel for {genre} genre.
1436
 
1437
  Reference these popular elements:
1438
  - Core elements: {', '.join(genre_info['elements'])}
@@ -1447,370 +1496,370 @@ Format exactly as follows:
1447
  [World and background in 3-4 lines. Include era, location, core settings]
1448
 
1449
  👥 **Main Characters:**
1450
- Protagonist: [Name(Age)] - [Job/Status, key traits]
1451
- Key Character 1: [Name(Age)] - [Relationship/Role, traits]
1452
- Key Character 2: [Name(Age)] - [Relationship/Role, traits]
1453
 
1454
  📝 **Synopsis:**
1455
  [3-4 lines introducing core conflict and appeal. Start with strong hook, clearly present protagonist's goal and obstacles]"""
1456
-
1457
- messages = [{"role": "user", "content": prompt}]
1458
- generated_theme = system.call_llm_sync(messages, "writer", language)
1459
-
1460
- return generated_theme
1461
 
1462
  # --- UI functions ---
1463
  def format_episodes_display(episodes: List[Dict], current_episode: int = 0) -> str:
1464
- """Format episodes for display"""
1465
- markdown = "## 📚 웹소설 연재 현황\n\n"
1466
-
1467
- if not episodes:
1468
- return markdown + "*아직 작성된 에피소드가 없습니다.*"
1469
-
1470
- # Stats
1471
- total_episodes = len(episodes)
1472
- total_words = sum(ep.get('word_count', 0) for ep in episodes)
1473
- avg_engagement = sum(ep.get('reader_engagement', 0) for ep in episodes) / len(episodes) if episodes else 0
1474
-
1475
- markdown += f"**진행 상황:** {total_episodes} / {TARGET_EPISODES}화\n"
1476
- markdown += f"**총 단어 수:** {total_words:,} / {TARGET_WORDS:,}\n"
1477
- markdown += f"**평균 몰입도:** ⭐ {avg_engagement:.1f} / 10\n\n"
1478
- markdown += "---\n\n"
1479
-
1480
- # Episode list
1481
- for ep in episodes[-5:]: # Show last 5 episodes
1482
- ep_num = ep.get('episode_number', 0)
1483
- word_count = ep.get('word_count', 0)
1484
-
1485
- markdown += f"### 📖 {ep_num}화\n"
1486
- markdown += f"*{word_count}단어*\n\n"
1487
-
1488
- content = ep.get('content', '')
1489
- if content:
1490
- preview = content[:200] + "..." if len(content) > 200 else content
1491
- markdown += f"{preview}\n\n"
1492
-
1493
- hook = ep.get('hook', '')
1494
- if hook:
1495
- markdown += f"**🪝 후크:** *{hook}*\n\n"
1496
-
1497
- markdown += "---\n\n"
1498
-
1499
- return markdown
1500
 
1501
  def format_webnovel_display(episodes: List[Dict], genre: str) -> str:
1502
- """Format complete web novel for display"""
1503
- if not episodes:
1504
- return "아직 완성된 웹소설이 없습니다."
1505
-
1506
- formatted = f"# 🎭 {genre} 웹소설\n\n"
1507
-
1508
- # Novel stats
1509
- total_words = sum(ep.get('word_count', 0) for ep in episodes)
1510
- formatted += f"**총 {len(episodes)}화 완결 | {total_words:,}단어**\n\n"
1511
- formatted += "---\n\n"
1512
-
1513
- # Episodes
1514
- for idx, ep in enumerate(episodes):
1515
- ep_num = ep.get('episode_number', 0)
1516
- content = ep.get('content', '')
1517
-
1518
- # Content already includes the title, so display as is
1519
- formatted += f"## {content.split(chr(10))[0] if content else f'{ep_num}화'}\n\n"
1520
-
1521
- # Get the actual content (skip title and empty line)
1522
- lines = content.split('\n')
1523
- if len(lines) > 1:
1524
- actual_content = '\n'.join(lines[2:] if len(lines) > 2 and lines[1].strip() == "" else lines[1:])
1525
- formatted += f"{actual_content}\n\n"
1526
-
1527
- if idx < len(episodes) - 1: # Not last episode
1528
- formatted += "➡️ *다음 화에 계속...*\n\n"
1529
-
1530
- formatted += "---\n\n"
1531
-
1532
- return formatted
1533
 
1534
  # --- Gradio interface ---
1535
  def create_interface():
1536
- with gr.Blocks(theme=gr.themes.Soft(), title="K-WebNovel Generator") as interface:
1537
- gr.HTML("""
1538
- <style>
1539
- .main-header {
1540
- text-align: center;
1541
- margin-bottom: 2rem;
1542
- }
1543
-
1544
- .header-title {
1545
- font-size: 3rem;
1546
- margin-bottom: 1rem;
1547
- }
1548
-
1549
- .header-subtitle {
1550
- font-size: 1.2rem;
1551
- margin-bottom: 0.5rem;
1552
- }
1553
-
1554
- .header-description {
1555
- margin-bottom: 1.5rem;
1556
- }
1557
-
1558
- .badges-container {
1559
- display: flex;
1560
- justify-content: center;
1561
- align-items: center;
1562
- gap: 8px;
1563
- flex-wrap: wrap;
1564
- margin-top: 12px;
1565
- }
1566
-
1567
- .badges-container a img {
1568
- height: 28px;
1569
- transition: transform 0.2s ease;
1570
- }
1571
-
1572
- .badges-container a:hover img {
1573
- transform: scale(1.05);
1574
- }
1575
-
1576
- @media (max-width: 768px) {
1577
- .header-title {
1578
- font-size: 2.5rem;
1579
- }
1580
-
1581
- .header-subtitle {
1582
- font-size: 1.1rem;
1583
- }
1584
-
1585
- .badges-container {
1586
- gap: 6px;
1587
- }
1588
-
1589
- .badges-container a img {
1590
- height: 24px;
1591
- }
1592
- }
1593
- </style>
1594
-
1595
- <div class="main-header">
1596
- <h1 class="header-title">📚 K-WebNovel Generator</h1>
1597
-
1598
- <div class="badges-container">
1599
- <a href="https://huggingface.co/spaces/fantaxy/AGI-LEADERBOARD" target="_blank">
1600
- <img src="https://img.shields.io/static/v1?label=HF&message=AGI-LEADERBOARD&color=%23d4a574&labelColor=%238b6239&logo=HUGGINGFACE&logoColor=%23faf8f5&style=for-the-badge" alt="badge">
1601
- </a>
1602
- <a href="https://huggingface.co/spaces/openfree/AGI-NOVEL" target="_blank">
1603
- <img src="https://img.shields.io/static/v1?label=HF&message=AGI-NOVEL&color=%23d4a574&labelColor=%235a3e28&logo=huggingface&logoColor=%23faf8f5&style=for-the-badge" alt="badge">
1604
- </a>
1605
- <a href="https://huggingface.co/spaces/openfree/AGI-Screenplay" target="_blank">
1606
- <img src="https://img.shields.io/static/v1?label=HF&message=AGI-Screenplay&color=%23b8956f&labelColor=%23745940&logo=HUGGINGFACE&logoColor=%23faf8f5&style=for-the-badge" alt="badge">
1607
- </a>
1608
- <a href="https://huggingface.co/spaces/openfree/AGI-WebNovel" target="_blank">
1609
- <img src="https://img.shields.io/static/v1?label=HF&message=AGI-WebNovel&color=%23c7a679&labelColor=%236b5036&logo=HUGGINGFACE&logoColor=%23faf8f5&style=for-the-badge" alt="badge">
1610
- </a>
1611
- <a href="https://discord.gg/openfreeai" target="_blank">
1612
- <img src="https://img.shields.io/static/v1?label=Discord&message=Openfree%20AI&color=%23c19656&labelColor=%236d4e31&logo=discord&logoColor=white&style=for-the-badge" alt="badge">
1613
- </a>
1614
- </div>
1615
-
1616
- <p class="header-subtitle">한국형 웹소설 자동 생성 시스템</p>
1617
- <p class="header-description">장르별 맞춤형 40화 완결 웹소설을 생성합니다</p>
1618
- </div>
1619
- """)
1620
-
1621
- # State
1622
- current_session_id = gr.State(None)
1623
-
1624
- with gr.Tab("✍️ 웹소설 쓰기"):
1625
- with gr.Group():
1626
- gr.Markdown("### 🎯 웹소설 설정")
1627
-
1628
- with gr.Row():
1629
- with gr.Column(scale=2):
1630
- genre_select = gr.Radio(
1631
- choices=list(WEBNOVEL_GENRES.keys()),
1632
- value="로맨스",
1633
- label="장르 선택",
1634
- info="원하는 장르를 선택하세요"
1635
- )
1636
-
1637
- query_input = gr.Textbox(
1638
- label="스토리 테마",
1639
- placeholder="웹소설의 기본 설정이나 주제를 입력하세요...",
1640
- lines=3
1641
- )
1642
-
1643
- with gr.Row():
1644
- random_btn = gr.Button("🎲 랜덤 테마", variant="secondary")
1645
- submit_btn = gr.Button("📝 연재 시작", variant="primary", size="lg")
1646
-
1647
- with gr.Column(scale=1):
1648
- language_select = gr.Radio(
1649
- choices=["Korean", "English"],
1650
- value="Korean",
1651
- label="언어"
1652
- )
1653
-
1654
- gr.Markdown("""
1655
- **장르별 특징:**
1656
- - 로맨스: 달달한 사랑 이야기
1657
- - 로판: 회귀/빙의 판타지
1658
- - 판타지: 성장과 모험
1659
- - 현판: 현대 배경 능력자
1660
- - 무협: 무공과 강호
1661
- - 미스터리: 추리와 반전
1662
- - 라노벨: 가벼운 일상물
1663
- """)
1664
-
1665
- status_text = gr.Textbox(
1666
- label="진행 상황",
1667
- interactive=False,
1668
- value="장르를 선택하고 테마를 입력하세요"
1669
- )
1670
-
1671
- # Output
1672
- with gr.Row():
1673
- with gr.Column():
1674
- episodes_display = gr.Markdown("*연재 진행 상황이 여기에 표시됩니다*")
1675
-
1676
- with gr.Column():
1677
- novel_display = gr.Markdown("*완성된 웹소설이 여기에 표시됩니다*")
1678
-
1679
- with gr.Row():
1680
- download_format = gr.Radio(
1681
- choices=["TXT", "DOCX"],
1682
- value="TXT",
1683
- label="다운로드 형식"
1684
- )
1685
- download_btn = gr.Button("📥 다운로드", variant="secondary")
1686
-
1687
- download_file = gr.File(visible=False)
1688
-
1689
- with gr.Tab("📚 테마 라이브러리"):
1690
- gr.Markdown("### 인기 웹소설 테마")
1691
-
1692
- library_genre = gr.Radio(
1693
- choices=["전체"] + list(WEBNOVEL_GENRES.keys()),
1694
- value="전체",
1695
- label="장르 필터"
1696
- )
1697
-
1698
- theme_library = gr.HTML("<p>테마 라이브러리 로딩 중...</p>")
1699
-
1700
- refresh_library_btn = gr.Button("🔄 새로고침")
1701
-
1702
- # Event handlers
1703
- def process_query(query, genre, language, session_id):
1704
- system = WebNovelSystem()
1705
- episodes = ""
1706
- novel = ""
1707
-
1708
- for ep_display, novel_display, status, new_session_id in system.process_webnovel_stream(query, genre, language, session_id):
1709
- episodes = ep_display
1710
- novel = novel_display
1711
- yield episodes, novel, status, new_session_id
1712
-
1713
- def handle_random_theme(genre, language):
1714
- return generate_random_webnovel_theme(genre, language)
1715
-
1716
- def handle_download(download_format, session_id, genre):
1717
- """Handle download request"""
1718
- if not session_id:
1719
- return None
1720
-
1721
- try:
1722
- episodes = WebNovelDatabase.get_episodes(session_id)
1723
- if not episodes:
1724
- return None
1725
-
1726
- # Get title from first episode or generate default
1727
- title = f"{genre} 웹소설"
1728
-
1729
- if download_format == "TXT":
1730
- content = export_to_txt(episodes, genre, title)
1731
-
1732
- # Save to temporary file
1733
- with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
1734
- suffix='.txt', delete=False) as f:
1735
- f.write(content)
1736
- return f.name
1737
-
1738
- elif download_format == "DOCX":
1739
- if not DOCX_AVAILABLE:
1740
- gr.Warning("DOCX export requires python-docx library")
1741
- return None
1742
-
1743
- content = export_to_docx(episodes, genre, title)
1744
-
1745
- # Save to temporary file
1746
- with tempfile.NamedTemporaryFile(mode='wb', suffix='.docx',
1747
- delete=False) as f:
1748
- f.write(content)
1749
- return f.name
1750
-
1751
- except Exception as e:
1752
- logger.error(f"Download error: {e}")
1753
- gr.Warning(f"다운로드 중 오류 발생: {str(e)}")
1754
- return None
1755
-
1756
- # Connect events
1757
- submit_btn.click(
1758
- fn=process_query,
1759
- inputs=[query_input, genre_select, language_select, current_session_id],
1760
- outputs=[episodes_display, novel_display, status_text, current_session_id]
1761
- )
1762
-
1763
- random_btn.click(
1764
- fn=handle_random_theme,
1765
- inputs=[genre_select, language_select],
1766
- outputs=[query_input]
1767
- )
1768
-
1769
- download_btn.click(
1770
- fn=handle_download,
1771
- inputs=[download_format, current_session_id, genre_select],
1772
- outputs=[download_file]
1773
- ).then(
1774
- fn=lambda x: gr.update(visible=True) if x else gr.update(visible=False),
1775
- inputs=[download_file],
1776
- outputs=[download_file]
1777
- )
1778
-
1779
- # Examples
1780
- gr.Examples(
1781
- examples=[
1782
- ["계약결혼한 재벌 3세와 평범한 회사원의 로맨스", "로맨스"],
1783
- ["회귀한 천재 마법사의 복수극", "로판"],
1784
- ["F급 헌터에서 SSS급 각성자가 되는 이야기", "현판"],
1785
- ["폐급에서 천하제일이 되는 무공 천재", "무협"],
1786
- ["평범한 고등학생이 이세계 용사가 되는 이야기", "라이트노벨"]
1787
- ],
1788
- inputs=[query_input, genre_select]
1789
- )
1790
-
1791
- return interface
1792
 
1793
  # Main
1794
  if __name__ == "__main__":
1795
- logger.info("K-WebNovel Generator Starting...")
1796
- logger.info("=" * 60)
1797
-
1798
- # Environment check
1799
- logger.info(f"API Endpoint: {API_URL}")
1800
- logger.info(f"Target: {TARGET_EPISODES} episodes, {TARGET_WORDS:,} words")
1801
- logger.info("Genres: " + ", ".join(WEBNOVEL_GENRES.keys()))
1802
-
1803
- logger.info("=" * 60)
1804
-
1805
- # Initialize database
1806
- logger.info("Initializing database...")
1807
- WebNovelDatabase.init_db()
1808
- logger.info("Database ready.")
1809
-
1810
- # Launch interface
1811
- interface = create_interface()
1812
- interface.launch(
1813
- server_name="0.0.0.0",
1814
- server_port=7860,
1815
- share=False
1816
- )
 
473
  **전체 플롯에서 {episode_num}화 내용:**
474
  {self._extract_episode_plan(plot_outline, episode_num)}
475
 
476
+ ⚠️ **중요**: 위의 플롯 내용을 반드시 충실히 따라 작성하세요. 플롯에서 벗어나지 마세요.
477
+
478
  **이전 내용 요약:**
479
  {previous_content[-1500:] if previous_content else "첫 화입니다"}
480
 
 
492
  - 후크: 다음 화 예고
493
 
494
  2. **필수 요소:**
495
+ - 플롯에 제시된 내용을 충실히 구현
496
  - 생생한 대화와 행동 묘사
497
  - 캐릭터 감정과 내면 갈등
498
  - 장면 전환과 템포 조절
 
510
  **참고 후크 예시:**
511
  {random.choice(hooks)}
512
 
513
+ 플롯에 충실하면서도 몰입감 있게 작성하세요. 반드시 400-600단어로 작성하세요.""",
 
514
 
515
  "English": f"""Write episode {episode_num} of the web novel.
516
 
 
520
  **Episode {episode_num} from plot:**
521
  {self._extract_episode_plan(plot_outline, episode_num)}
522
 
523
+ ⚠️ **IMPORTANT**: You MUST faithfully follow the plot content above. Do not deviate from the plot.
524
+
525
  **Previous content:**
526
  {previous_content[-1500:] if previous_content else "First episode"}
527
 
 
539
  - Hook: Next episode teaser
540
 
541
  2. **Essential elements:**
542
+ - Faithfully implement the plot content
543
  - Vivid dialogue and action
544
  - Character emotions and conflicts
545
  - Scene transitions and pacing
 
557
  **Hook example:**
558
  {random.choice(hooks)}
559
 
560
+ Write faithfully to the plot while being immersive. Must be 400-600 words."""
 
561
  }
562
 
563
  return lang_prompts.get(language, lang_prompts["Korean"])
 
607
 
608
  patterns = [
609
  f"{episode_num}화:", f"Episode {episode_num}:",
610
+ f"제{episode_num}화:", f"EP{episode_num}:",
611
+ f"{episode_num}.", f"[{episode_num}]"
612
+ ]
613
+
614
+ # Also check for next episode patterns
615
+ next_patterns = [
616
+ f"{episode_num+1}화:", f"Episode {episode_num+1}:",
617
+ f"제{episode_num+1}화:", f"EP{episode_num+1}:",
618
+ f"{episode_num+1}.", f"[{episode_num+1}]"
619
  ]
620
 
621
  for line in lines:
622
+ # Start capturing when we find the episode number
623
  if any(pattern in line for pattern in patterns):
624
  capturing = True
625
+ episode_section.append(line)
626
+ # Stop capturing when we find the next episode number
627
+ elif capturing and any(pattern in line for pattern in next_patterns):
628
  break
629
  elif capturing:
630
  episode_section.append(line)
631
+
632
+ # If we found episode content, return it
633
+ if episode_section:
634
+ return '\n'.join(episode_section)
635
+
636
+ # If no specific episode found, provide more context
637
+ logger.warning(f"Could not find specific plan for episode {episode_num}")
638
+ return f"""에피소드 {episode_num}에 대한 구체적인 플롯을 찾을 수 없습니다.
639
+ 전체 플롯을 참고하여 {episode_num}화를 작성하되, 반드시 사용자가 제공한 원본 스토리 설정을 따르세요.
640
+
641
+ 참고: 전체 플롯 일부
642
+ {plot_outline[:1000]}..."""
643
 
644
  # --- LLM call functions ---
645
  def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str:
 
726
  독자를 중독시키는 플롯과 전개를 설계합니다.
727
  장르별 관습과 독자 기대를 정확히 파악합니다.
728
  40화 완결 구조로 완벽한 기승전결을 만듭니다.
729
+ 각 화마다 충실한 내용과 전개를 계획합니다.
730
+
731
+ ⚠️ 가장 중요한 원칙: 사용자가 제공한 스토리 설정을 절대적으로 우선시하고, 이를 중심으로 모든 플롯을 구성합니다. 장르 관습보다 사용자의 구체적 설정이 항상 우선입니다.""",
732
 
733
  "writer": """당신은 독자를 사로잡는 웹소설 작가입니다.
734
  풍부하고 몰입감 있는 문체를 구사합니다.
735
  각 화를 400-600단어로 충실하게 작성합니다.
736
  여러 장면과 전환을 통해 이야기를 전개합니다.
737
  대화, 행동, 내면 묘사를 균형있게 배치합니다.
738
+ 매 화 끝에 강력한 후크로 다음 화를 기다리게 만듭니다.
739
+
740
+ ⚠️ 가장 중요한 원칙: 제공된 플롯 아웃라인을 정확히 따르고, 절대 임의로 내용을 변경하거나 추가하지 않습니다. 플롯에 명시된 내용만을 충실히 구현합니다.""",
741
 
742
  "critic": """당신은 웹소설 독자의 마음을 읽는 평론가입니다.
743
  재미와 몰입감을 최우선으로 평가합니다.
744
  장르적 쾌감과 독자 만족도를 분석합니다.
745
+ 구체적이고 실용적인 개선안을 제시합니다.
746
+ 플롯 충실도와 일관성을 중요하게 평가합니다."""
747
  },
748
  "English": {
749
  "planner": """You perfectly understand the Korean web novel market.
750
  Design addictive plots and developments.
751
  Accurately grasp genre conventions and reader expectations.
752
  Create perfect story structure in 40 episodes.
753
+ Plan substantial content and development for each episode.
754
+
755
+ ⚠️ Most important principle: Absolutely prioritize the user's story setting and build all plots around it. User's specific settings always take precedence over genre conventions.""",
756
 
757
  "writer": """You are a web novelist who captivates readers.
758
  Use rich and immersive writing style.
759
  Write each episode with 400-600 words faithfully.
760
  Develop story through multiple scenes and transitions.
761
  Balance dialogue, action, and inner descriptions.
762
+ End each episode with powerful hook for next.
763
+
764
+ ⚠️ Most important principle: Follow the provided plot outline exactly and never arbitrarily change or add content. Faithfully implement only what is specified in the plot.""",
765
 
766
  "critic": """You read web novel readers' minds.
767
  Prioritize fun and immersion in evaluation.
768
  Analyze genre satisfaction and reader enjoyment.
769
+ Provide specific, practical improvements.
770
+ Evaluate plot fidelity and consistency as important factors."""
771
  }
772
  }
773
 
 
788
  self.current_session_id = WebNovelDatabase.create_session(query, genre, language)
789
  self.tracker.set_genre(genre)
790
  logger.info(f"Created new session: {self.current_session_id}")
791
+ # Store the original query for reference
792
+ self.original_query = query
793
 
794
  # Generate plot outline first
795
  if resume_from_episode == 0:
 
801
  "planner", language
802
  )
803
 
804
+ # Store plot outline for debugging
805
+ self.plot_outline = plot_outline
806
+
807
  yield "✅ 플롯 구성 완료!", "", f"40화 구성 완료", self.current_session_id
808
 
809
  # Generate episodes
 
812
  # Write episode
813
  yield f"✍️ {episode_num}화 집필 중...", accumulated_content, f"진행률: {episode_num}/{TARGET_EPISODES}화", self.current_session_id
814
 
815
+ # Create enhanced episode prompt with original query reminder
816
  episode_prompt = self.create_episode_prompt(
817
  episode_num, plot_outline, accumulated_content, genre, language
818
  )
819
 
820
+ # Add reminder about original query before calling LLM
821
+ enhanced_prompt = f"""⚠️ 필수 확인사항:
822
+ 원본 스토리 설정: {query}
823
+
824
+ 이 설정을 반드시 반영하여 작성하세요.
825
+
826
+ {episode_prompt}"""
827
+
828
  episode_content = self.call_llm_sync(
829
+ [{"role": "user", "content": enhanced_prompt}],
830
  "writer", language
831
  )
832
 
 
1154
  [세계관과 배경 설정을 3-4줄로 설명]
1155
 
1156
  👥 **주요 캐릭터:**
1157
+ - 주인공: [이름] - [간단한 설명]
1158
+ - 주요인물1: [이름] - [간단한 설명]
1159
+ - 주요인물2: [이름] - [간단한 설명]
1160
 
1161
  📝 **작품소개:**
1162
  [독자의 흥미를 끄는 3-4줄의 작품 소개. 주인공의 상황, 목표, 핵심 갈등을 포함]"""
 
1189
  [World and background setting in 3-4 lines]
1190
 
1191
  👥 **Main Characters:**
1192
+ - Protagonist: [Name] - [Brief description]
1193
+ - Key Character 1: [Name] - [Brief description]
1194
+ - Key Character 2: [Name] - [Brief description]
1195
 
1196
  📝 **Synopsis:**
1197
  [3-4 lines that hook readers. Include protagonist's situation, goal, and core conflict]"""
 
1218
  현대 서울, 대기업 본사와 강남의 펜트하우스가 주 무대. 3개월 계약결혼 만료 직전, 남편이 교통사고로 기억을 잃고 아내를 첫사랑으로 착각하는 상황.
1219
 
1220
  👥 **주요 캐릭터:**
1221
+ - 주인공: 서연우(28) - 평범한 회사원, 부모님 병원비를 위해 계약결혼
1222
+ - 남주: 강준혁(32) - 냉혈 재벌 3세, 기억상실 후 순정남으로 변신
1223
+ - 조연: 한소영(30) - 준혁의 전 약혼녀, 복수를 계획 중
1224
 
1225
  📝 **작품소개:**
1226
  "당신이 내 첫사랑이야." 이혼 서류에 도장을 찍으려던 순간, 교통사고를 당한 냉혈 재벌 남편이 나를 운명의 상대로 착각한다. 3개월간 연기했던 가짜 부부에서 진짜 사랑이 시작되는데...""",
 
1231
  서울중앙지법과 검찰청이 주 무대. 냉혈 검사와 이혼 전문 변호사가 법정에서 대립하며 티격태격하는 법정 로맨스.
1232
 
1233
  👥 **주요 캐릭터:**
1234
+ - 주인공: 오지원(30) - 승률 100% 이혼 전문 변호사
1235
+ - 남주: 민시준(33) - 원칙주의 엘리트 검사
1236
+ - 조연: 박세진(35) - 지원의 전 남편이자 시준의 선배 검사
1237
 
1238
  📝 **작품소개:**
1239
  "변호사님, 법정에서만 만나기로 했잖아요." 하필 전 남편의 불륜 소송을 맡은 날, 상대 검사가 나타났다. 법정에선 적, 밖에선 연인. 우리의 관계는 대체 뭘까?"""
1240
+ ]
1241
+ },
1242
+ "로판": {
1243
+ "themes": [
1244
+ """📖 **제목:** 악녀는 이번 생에서 도망친다
1245
 
1246
  🌍 **설정:**
1247
  마법이 존재하는 제국, 1년 후 처형당할 운명의 악녀 공작 영애로 빙의. 북부 변방의 전쟁광 공작과의 계약결혼이 유일한 생존루트.
1248
 
1249
  👥 **주요 캐릭터:**
1250
+ - 주인공: 아델라이드(20) - 빙의한 악녀, 원작 지식 보유
1251
+ - 남주: 카시우스(25) - 북부의 전쟁광 공작, 숨겨진 순정남
1252
+ - 악역: 황태자 레온(23) - 여주에게 집착하는 얀데레
1253
 
1254
  📝 **작품소개:**
1255
  소설 속 악녀로 빙의했는데 이미 처형 선고를 받은 상태? 살려면 원작에 없던 북부 공작과 계약결혼해야 한다. "1년만 함께해주세요. 그 후엔 자유를 드리겠습니다." 하지만 계약 기간이 끝나도 그가 날 놓아주지 않는다.""",
1256
+
1257
+ """📖 **제목:** 회귀한 황녀는 버려진 왕자를 택한다
1258
 
1259
  🌍 **설정:**
1260
  제국력 892년으로 회귀한 황녀. 전생에서 자신을 배신한 황태자 대신, 버려진 서자 왕자와 손을 잡고 제국을 뒤집으려 한다.
1261
 
1262
  👥 **주요 캐릭터:**
1263
+ - 주인공: 로젤린(22) - 회귀한 황녀, 미래를 아는 전략가
1264
+ - 남주: 다미안(24) - 버려진 서자 왕자, 숨겨진 흑막
1265
+ - 악역: 황태자 세바스찬(26) - 전생의 배신자
1266
 
1267
  📝 **작품소개:**
1268
  독살당해 회귀한 황녀, 이번엔 다르게 살겠다. 모두가 무시하는 서자 왕자의 손을 잡았다. "저와 함께 제국을 뒤집으시겠습니까?" 하지만 그는 내가 아는 것보다 훨씬 ��험한 남자였다."""
1269
+ ]
1270
+ },
1271
+ "판타지": {
1272
+ "themes": [
1273
+ """📖 **제목:** F급 헌터, SSS급 네크로맨서가 되다
1274
 
1275
  🌍 **설정:**
1276
  게이트와 던전이 출현한 지 10년 후의 한국. F급 헌터가 우연히 얻은 스킬로 죽은 보스 몬스터를 부활시켜 부리는 유일무이 네크로맨서가 된다.
1277
 
1278
  👥 **주요 캐릭터:**
1279
+ - 주인공: 김도현(24) - F급에서 SSS급 네크로맨서로 각성
1280
+ - 조력자: 리치 왕(???) - 첫 번째 언데드, 전설의 대마법사
1281
+ - 라이벌: 최강훈(26) - S급 길드 마스터, 주인공을 경계
1282
 
1283
  📝 **작품소개:**
1284
  "F급 주제에 무슨 헛소리야?" 모두가 비웃었다. 하지만 첫 번째 보스를 쓰러뜨린 순간, 시스템 메시지가 떴다. [SSS급 히든 클래스: 네크로맨서 각성] 이제 죽은 보스들이 내 부하가 된다.""",
1285
+
1286
+ """📖 **제목:** 탑을 역주행하는 회귀자
1287
 
1288
  🌍 **설정:**
1289
  100층 탑 정상에서 죽은 후 튜토리얼로 회귀. 하지만 이번엔 100층부터 거꾸로 내려가며 모든 층을 정복하는 역주행 시스템이 열렸다.
1290
 
1291
  👥 **주요 캐릭터:**
1292
+ - 주인공: 이성진(28) - 유일한 역주행 회귀자
1293
+ - 조력자: 관리자(???) - 탑의 시스템 AI, 주인공에게 호의적
1294
+ - 라이벌: 성하윤(25) - 이번 회차 최강 신인
1295
 
1296
  📝 **작품소개:**
1297
  100층에서 죽었다. 눈을 떠보니 튜토리얼이었다. [역주행 시스템이 개방되었습니다] "뭐? 100층부터 시작한다고?" 최강자의 기억을 가진 채 정상에서부터 내려가는 전무후무한 공략이 시작된다."""
1298
+ ]
1299
+ },
1300
+ "현판": {
1301
+ "themes": [
1302
+ """📖 **제목:** 무능력자의 SSS급 아이템 제작
1303
 
1304
  🌍 **설정:**
1305
  게이트 출현 10년, 전 국민의 70%가 각성한 한국. 무능력자로 살던 주인공에게 갑자기 아이템 제작 시스템이 열린다.
1306
 
1307
  👥 **주요 캐릭터:**
1308
+ - 주인공: 박준서(25) - 무능력자에서 유일무이 아이템 제작사로
1309
+ - 의뢰인: 강하늘(27) - S급 헌터, 첫 번째 고객
1310
+ - 라이벌: 대기업 '아르테미스' - 아이템 독점 기업
1311
 
1312
  📝 **작품소개:**
1313
  "각성 등급: 없음" 10년째 무능력자로 살았다. 그런데 오늘, 이상한 시스템 창이 떴다. [SSS급 생산직: 아이템 크래프터] 이제 내가 만든 아이템이 세계를 바꾼다.""",
1314
+
1315
+ """📖 **제목:** 헌터 사관학교의 숨겨진 최강자
1316
 
1317
  🌍 **설정:**
1318
  한국 최고의 헌터 사관학교. 입학시험 꼴찌로 들어온 주인공이 사실은 능력을 숨기고 있는 특급 요원.
1319
 
1320
  👥 **주요 캐릭터:**
1321
+ - 주인공: 윤시우(20) - 꼴찌로 위장한 특급 헌터
1322
+ - 히로인: 차유진(20) - 학년 수석, 재벌가 영애
1323
+ - 교관: 한태성(35) - 전설의 헌터, 주인공의 정체를 의심
1324
 
1325
  📝 **작품소개:**
1326
  "측정 불가? 그럼 F급이네." 일부러 힘을 숨기고 꼴찌로 입학했다. 하지만 S급 게이트가 학교에 열리면서 정체를 숨길 수 없게 됐다. "너... 대체 누구야?"라는 물음에 어떻게 답해야 할까."""
1327
+ ]
1328
+ },
1329
+ "무협": {
1330
+ "themes": [
1331
+ """📖 **제목:** 천하제일문 폐급제자의 마교 비급
1332
 
1333
  🌍 **설정:**
1334
  정파 무림의 중원. 천하제일문의 폐급 막내제자가 우연히 마교 교주의 비급을 습득하고 정마를 아우르는 절대무공을 익힌다.
1335
 
1336
  👥 **주요 캐릭터:**
1337
+ - 주인공: 진천(18) - 폐급에서 절대고수로
1338
+ - 스승: 혈마노조(???) - 비급에 깃든 마교 전설
1339
+ - 라이벌: 남궁세가 소가주 - 정파 제일 천재
1340
 
1341
  📝 **작품소개:**
1342
  "하찮은 것이 감히!" 모두가 무시하던 막내제자. 하지만 떨어진 절벽에서 발견한 것은 전설로만 전해지던 천마신공. "이제부터가 진짜 시작이다." 정파와 마교를 뒤흔들 폐급의 반란이 시작된다.""",
1343
+
1344
+ """📖 **제목:** 화산파 장문인으로 회귀하다
1345
 
1346
  🌍 **설정:**
1347
  100년 전 화산파가 최고 문파이던 시절로 회귀. 미래를 아는 장문인이 되어 문파를 지키고 무림을 재편한다.
1348
 
1349
  👥 **주요 캐릭터:**
1350
+ - 주인공: 청운진인(45→25) - 회귀한 화산파 장문인
1351
+ - 제자: 백무진(15) - 미래의 화산파 배신자
1352
+ - 맹우: 마교 성녀 - 전생의 적, 이생의 동료
1353
 
1354
  📝 **작품소개:**
1355
  멸문 직전에 회귀했다. 이번엔 다르다. "앞으로 화산파는 정파의 규율을 벗어난다." 미래를 아는 장문인의 파격적인 결정. 마교와 손잡고 무림의 판도를 뒤집는다."""
1356
+ ]
1357
+ },
1358
+ "미스터리": {
1359
+ "themes": [
1360
+ """📖 **제목:** 폐교에 갇힌 7명, 그리고 나
1361
 
1362
  🌍 **설정:**
1363
  폐쇄된 산골 학교, 동창회를 위해 모인 8명이 갇힌다. 하나씩 사라지는 동창들. 범인은 이 안에 있다.
1364
 
1365
  👥 **주요 캐릭터:**
1366
+ - 주인공: 서민준(28) - 프로파일러 출신 교사
1367
+ - 용의자1: 김태희(28) - 실종된 친구의 전 연인
1368
+ - 용의자2: 박진우(28) - 10년 전 사건의 목격자
1369
 
1370
  📝 **작품소개:**
1371
  "10년 전 그날처럼..." 폐교에서 열린 동창회, 하지만 출구는 봉쇄됐다. 한 명씩 사라지는 친구들. 10년 전 묻어둔 비밀이 되살아난다. 살인자는 우리 중 한 명이다.""",
1372
+
1373
+ """📖 **제목:** 타임루프 속 연쇄살인마를 찾아라
1374
 
1375
  🌍 **설정:**
1376
  같은 하루가 반복되는 타임루프. 매번 다른 방법으로 살인이 일어나지만 범인은 동일인. 루프를 깨려면 범인을 찾아야 한다.
1377
 
1378
  👥 **주요 캐릭터:**
1379
+ - 주인공: 강해인(30) - 타임루프에 갇힌 형사
1380
+ - 희생자: 이수연(25) - 매번 죽는 카페 알바생
1381
+ - 용의자들: 카페 단골 5명 - 각자의 비밀을 숨기고 있음
1382
 
1383
  📝 **작품소개:**
1384
  "또 오늘이야..." 49번째 같은 아침. 오후 3시 33분, 카페에서 살인이 일어난다. 범인을 잡아야 내일이 온다. 하지만 범인은 매번 완벽한 알리바이를 만든다. 과연 50번째 오늘은 다를까?"""
1385
+ ]
1386
+ },
1387
+ "라이트노벨": {
1388
+ "themes": [
1389
+ """📖 **제목:** 내 여자친구가 사실은 마왕이었다
1390
 
1391
  🌍 **설정:**
1392
  평범한 고등학교, 하지만 학생과 교사 중 일부는 이세계에서 온 존재들. 주인공만 모르는 학교의 비밀.
1393
 
1394
  👥 **주요 캐릭터:**
1395
+ - 주인공: 김태양(17) - 평범한 고등학생(?)
1396
+ - 히로인: 루시퍼(17) - 마왕이자 여자친구
1397
+ - 라이벌: 미카엘(17) - 천사이자 학생회장
1398
 
1399
  📝 **작품소개:**
1400
  "선배, 사실 저... 마왕이에요!" 1년째 사귄 여자친구의 충격 고백. 근데 학생회장은 천사고, 담임은 드래곤이라고? 평범한 줄 알았던 우리 학교의 정체가 밝혀진다. "그래서... 우리 헤어져야 해?"라고 묻자 그녀가 울기 시작했다.""",
1401
+
1402
+ """📖 **제목:** 게임 아이템이 현실에 떨어진다
1403
 
1404
  🌍 **설정:**
1405
  모바일 게임과 현실이 연동되기 시작한 세계. 게임에서 얻은 아이템이 현실에 나타나면서 벌어지는 학원 코미디.
1406
 
1407
  👥 **주요 캐릭터:**
1408
+ - 주인공: 박도윤(18) - 게임 폐인 고등학생
1409
+ - 히로인: 최서연(18) - 전교 1등, 의외로 게임 고수
1410
+ - 친구: 장민혁(18) - 현질 전사, 개그 담당
1411
 
1412
  📝 **작품소개:**
1413
  "어? 이거 내 SSR 무기잖아?" 핸드폰 게임에서 뽑은 아이템이 책상 위에 나타났다. 문제는 학교에 몬스터도 나타나기 시작했다는 것. "야, 수능보다 레이드가 더 중요해진 것 같은데?"라며 웃는 친구들과 함께하는 좌충우돌 학원 판타지."""
1414
+ ]
1415
+ }
1416
+ }
1417
+
1418
+ genre_themes = templates.get(genre, templates["로맨스"])
1419
+ selected = random.choice(genre_themes["themes"])
1420
+
1421
+ return selected
1422
 
1423
  def generate_theme_with_llm_only(genre: str, language: str) -> str:
1424
+ """Generate theme using only LLM when JSON is not available or has errors"""
1425
+ system = WebNovelSystem()
1426
+
1427
+ # Genre-specific prompts based on popular web novel trends
1428
+ genre_prompts = {
1429
+ "로맨스": {
1430
+ "elements": ["계약결혼", "재벌", "이혼", "첫사랑", "운명적 만남", "오해와 화해"],
1431
+ "hooks": ["기억상실", "정체 숨기기", "가짜 연인", "원나잇 후 재회"]
1432
+ },
1433
+ "로판": {
1434
+ "elements": ["빙의", "회귀", "악녀", "황녀", "공작", "원작 파괴"],
1435
+ "hooks": ["처형 직전", "파혼 선언", "독살 시도", "폐위 위기"]
1436
+ },
1437
+ "판타지": {
1438
+ "elements": ["시스템", "각성", "던전", "회귀", "탑 등반", "SSS급"],
1439
+ "hooks": ["F급에서 시작", "숨겨진 클래스", "유일무이 스킬", "죽음 후 각성"]
1440
+ },
1441
+ "현판": {
1442
+ "elements": ["헌터", "게이트", "각성자", "길드", "아이템", "랭킹"],
1443
+ "hooks": ["늦은 각성", "재능 재평가", "S급 게이트", "시스템 오류"]
1444
+ },
1445
+ "무협": {
1446
+ "elements": ["회귀", "천재", "마교", "비급", "복수", "환생"],
1447
+ "hooks": ["폐급에서 최강", "배신 후 각성", "숨겨진 혈통", "기연 획득"]
1448
+ },
1449
+ "미스터리": {
1450
+ "elements": ["탐정", "연쇄살인", "타임루프", "초능력", "과거의 비밀"],
1451
+ "hooks": ["밀실 살인", "예고 살인", "기억 조작", "시간 역행"]
1452
+ },
1453
+ "라이트노벨": {
1454
+ "elements": ["학원", "이세계", "히로인", "게임", "일상", "판타지"],
1455
+ "hooks": ["전학생 정체", "게임 현실화", "평행세계", "숨겨진 능력"]
1456
+ }
1457
+ }
1458
+
1459
+ genre_info = genre_prompts.get(genre, genre_prompts["로맨스"])
1460
+
1461
+ if language == "Korean":
1462
+ prompt = f"""한국 웹소설 {genre} 장르의 중독성 있는 작품을 기획하세요.
1463
 
1464
  다음 인기 요소들을 참고하세요:
1465
  - 핵심 요소: {', '.join(genre_info['elements'])}
 
1474
  [세계관과 배경을 3-4줄로 설명. 시대, 장소, 핵심 설정 포함]
1475
 
1476
  👥 **주요 캐릭터:**
1477
+ - 주인공: [이름(나이)] - [직업/신분, 핵심 특징]
1478
+ - 주요인물1: [이름(나이)] - [관계/역할, 특징]
1479
+ - 주요인물2: [이름(나이)] - [관계/역할, 특징]
1480
 
1481
  📝 **작품소개:**
1482
  [3-4줄로 작품의 핵심 갈등과 매력을 소개. 첫 문장은 강한 훅으로 시작하고, 주인공의 목표와 장애물을 명확히 제시]"""
1483
+ else:
1484
+ prompt = f"""Generate an addictive Korean web novel for {genre} genre.
1485
 
1486
  Reference these popular elements:
1487
  - Core elements: {', '.join(genre_info['elements'])}
 
1496
  [World and background in 3-4 lines. Include era, location, core settings]
1497
 
1498
  👥 **Main Characters:**
1499
+ - Protagonist: [Name(Age)] - [Job/Status, key traits]
1500
+ - Key Character 1: [Name(Age)] - [Relationship/Role, traits]
1501
+ - Key Character 2: [Name(Age)] - [Relationship/Role, traits]
1502
 
1503
  📝 **Synopsis:**
1504
  [3-4 lines introducing core conflict and appeal. Start with strong hook, clearly present protagonist's goal and obstacles]"""
1505
+
1506
+ messages = [{"role": "user", "content": prompt}]
1507
+ generated_theme = system.call_llm_sync(messages, "writer", language)
1508
+
1509
+ return generated_theme
1510
 
1511
  # --- UI functions ---
1512
  def format_episodes_display(episodes: List[Dict], current_episode: int = 0) -> str:
1513
+ """Format episodes for display"""
1514
+ markdown = "## 📚 웹소설 연재 현황\n\n"
1515
+
1516
+ if not episodes:
1517
+ return markdown + "*아직 작성된 에피소드가 없습니다.*"
1518
+
1519
+ # Stats
1520
+ total_episodes = len(episodes)
1521
+ total_words = sum(ep.get('word_count', 0) for ep in episodes)
1522
+ avg_engagement = sum(ep.get('reader_engagement', 0) for ep in episodes) / len(episodes) if episodes else 0
1523
+
1524
+ markdown += f"**진행 상황:** {total_episodes} / {TARGET_EPISODES}화\n"
1525
+ markdown += f"**총 단어 수:** {total_words:,} / {TARGET_WORDS:,}\n"
1526
+ markdown += f"**평균 몰입도:** ⭐ {avg_engagement:.1f} / 10\n\n"
1527
+ markdown += "---\n\n"
1528
+
1529
+ # Episode list
1530
+ for ep in episodes[-5:]: # Show last 5 episodes
1531
+ ep_num = ep.get('episode_number', 0)
1532
+ word_count = ep.get('word_count', 0)
1533
+
1534
+ markdown += f"### 📖 {ep_num}화\n"
1535
+ markdown += f"*{word_count}단어*\n\n"
1536
+
1537
+ content = ep.get('content', '')
1538
+ if content:
1539
+ preview = content[:200] + "..." if len(content) > 200 else content
1540
+ markdown += f"{preview}\n\n"
1541
+
1542
+ hook = ep.get('hook', '')
1543
+ if hook:
1544
+ markdown += f"**🪝 후크:** *{hook}*\n\n"
1545
+
1546
+ markdown += "---\n\n"
1547
+
1548
+ return markdown
1549
 
1550
  def format_webnovel_display(episodes: List[Dict], genre: str) -> str:
1551
+ """Format complete web novel for display"""
1552
+ if not episodes:
1553
+ return "아직 완성된 웹소설이 없습니다."
1554
+
1555
+ formatted = f"# 🎭 {genre} 웹소설\n\n"
1556
+
1557
+ # Novel stats
1558
+ total_words = sum(ep.get('word_count', 0) for ep in episodes)
1559
+ formatted += f"**총 {len(episodes)}화 완결 | {total_words:,}단어**\n\n"
1560
+ formatted += "---\n\n"
1561
+
1562
+ # Episodes
1563
+ for idx, ep in enumerate(episodes):
1564
+ ep_num = ep.get('episode_number', 0)
1565
+ content = ep.get('content', '')
1566
+
1567
+ # Content already includes the title, so display as is
1568
+ formatted += f"## {content.split(chr(10))[0] if content else f'{ep_num}화'}\n\n"
1569
+
1570
+ # Get the actual content (skip title and empty line)
1571
+ lines = content.split('\n')
1572
+ if len(lines) > 1:
1573
+ actual_content = '\n'.join(lines[2:] if len(lines) > 2 and lines[1].strip() == "" else lines[1:])
1574
+ formatted += f"{actual_content}\n\n"
1575
+
1576
+ if idx < len(episodes) - 1: # Not last episode
1577
+ formatted += "➡️ *다음 화에 계속...*\n\n"
1578
+
1579
+ formatted += "---\n\n"
1580
+
1581
+ return formatted
1582
 
1583
  # --- Gradio interface ---
1584
  def create_interface():
1585
+ with gr.Blocks(theme=gr.themes.Soft(), title="K-WebNovel Generator") as interface:
1586
+ gr.HTML("""
1587
+ <style>
1588
+ .main-header {
1589
+ text-align: center;
1590
+ margin-bottom: 2rem;
1591
+ }
1592
+
1593
+ .header-title {
1594
+ font-size: 3rem;
1595
+ margin-bottom: 1rem;
1596
+ }
1597
+
1598
+ .header-subtitle {
1599
+ font-size: 1.2rem;
1600
+ margin-bottom: 0.5rem;
1601
+ }
1602
+
1603
+ .header-description {
1604
+ margin-bottom: 1.5rem;
1605
+ }
1606
+
1607
+ .badges-container {
1608
+ display: flex;
1609
+ justify-content: center;
1610
+ align-items: center;
1611
+ gap: 8px;
1612
+ flex-wrap: wrap;
1613
+ margin-top: 12px;
1614
+ }
1615
+
1616
+ .badges-container a img {
1617
+ height: 28px;
1618
+ transition: transform 0.2s ease;
1619
+ }
1620
+
1621
+ .badges-container a:hover img {
1622
+ transform: scale(1.05);
1623
+ }
1624
+
1625
+ @media (max-width: 768px) {
1626
+ .header-title {
1627
+ font-size: 2.5rem;
1628
+ }
1629
+
1630
+ .header-subtitle {
1631
+ font-size: 1.1rem;
1632
+ }
1633
+
1634
+ .badges-container {
1635
+ gap: 6px;
1636
+ }
1637
+
1638
+ .badges-container a img {
1639
+ height: 24px;
1640
+ }
1641
+ }
1642
+ </style>
1643
+
1644
+ <div class="main-header">
1645
+ <h1 class="header-title">📚 K-WebNovel Generator</h1>
1646
+
1647
+ <div class="badges-container">
1648
+ <a href="https://huggingface.co/spaces/fantaxy/AGI-LEADERBOARD" target="_blank">
1649
+ <img src="https://img.shields.io/static/v1?label=HF&message=AGI-LEADERBOARD&color=%23d4a574&labelColor=%238b6239&logo=HUGGINGFACE&logoColor=%23faf8f5&style=for-the-badge" alt="badge">
1650
+ </a>
1651
+ <a href="https://huggingface.co/spaces/openfree/AGI-NOVEL" target="_blank">
1652
+ <img src="https://img.shields.io/static/v1?label=HF&message=AGI-NOVEL&color=%23d4a574&labelColor=%235a3e28&logo=huggingface&logoColor=%23faf8f5&style=for-the-badge" alt="badge">
1653
+ </a>
1654
+ <a href="https://huggingface.co/spaces/openfree/AGI-Screenplay" target="_blank">
1655
+ <img src="https://img.shields.io/static/v1?label=HF&message=AGI-Screenplay&color=%23b8956f&labelColor=%23745940&logo=HUGGINGFACE&logoColor=%23faf8f5&style=for-the-badge" alt="badge">
1656
+ </a>
1657
+ <a href="https://huggingface.co/spaces/openfree/AGI-WebNovel" target="_blank">
1658
+ <img src="https://img.shields.io/static/v1?label=HF&message=AGI-WebNovel&color=%23c7a679&labelColor=%236b5036&logo=HUGGINGFACE&logoColor=%23faf8f5&style=for-the-badge" alt="badge">
1659
+ </a>
1660
+ <a href="https://discord.gg/openfreeai" target="_blank">
1661
+ <img src="https://img.shields.io/static/v1?label=Discord&message=Openfree%20AI&color=%23c19656&labelColor=%236d4e31&logo=discord&logoColor=white&style=for-the-badge" alt="badge">
1662
+ </a>
1663
+ </div>
1664
+
1665
+ <p class="header-subtitle">한국형 웹소설 자동 생성 시스템</p>
1666
+ <p class="header-description">장르별 맞춤형 40화 완결 웹소설을 생성합니다</p>
1667
+ </div>
1668
+ """)
1669
+
1670
+ # State
1671
+ current_session_id = gr.State(None)
1672
+
1673
+ with gr.Tab("✍️ 웹소설 쓰기"):
1674
+ with gr.Group():
1675
+ gr.Markdown("### 🎯 웹소설 설정")
1676
+
1677
+ with gr.Row():
1678
+ with gr.Column(scale=2):
1679
+ genre_select = gr.Radio(
1680
+ choices=list(WEBNOVEL_GENRES.keys()),
1681
+ value="로맨스",
1682
+ label="장르 선택",
1683
+ info="원하는 장르를 선택하세요"
1684
+ )
1685
+
1686
+ query_input = gr.Textbox(
1687
+ label="스토리 테마",
1688
+ placeholder="웹소설의 기본 설정이나 주제를 입력하세요...",
1689
+ lines=3
1690
+ )
1691
+
1692
+ with gr.Row():
1693
+ random_btn = gr.Button("🎲 랜덤 테마", variant="secondary")
1694
+ submit_btn = gr.Button("📝 연재 시작", variant="primary", size="lg")
1695
+
1696
+ with gr.Column(scale=1):
1697
+ language_select = gr.Radio(
1698
+ choices=["Korean", "English"],
1699
+ value="Korean",
1700
+ label="언어"
1701
+ )
1702
+
1703
+ gr.Markdown("""
1704
+ **장르별 특징:**
1705
+ - 로맨스: 달달한 사랑 이야기
1706
+ - 로판: 회귀/빙의 판타지
1707
+ - 판타지: 성장과 모험
1708
+ - 현판: 현대 배경 능력자
1709
+ - 무협: 무공과 강호
1710
+ - 미스터리: 추리와 반전
1711
+ - 라노벨: 가벼운 일상물
1712
+ """)
1713
+
1714
+ status_text = gr.Textbox(
1715
+ label="진행 상황",
1716
+ interactive=False,
1717
+ value="장르를 선택하고 테마를 입력하세요"
1718
+ )
1719
+
1720
+ # Output
1721
+ with gr.Row():
1722
+ with gr.Column():
1723
+ episodes_display = gr.Markdown("*연재 진행 상황이 여기에 표시됩니다*")
1724
+
1725
+ with gr.Column():
1726
+ novel_display = gr.Markdown("*완성된 웹소설이 여기에 표시됩니다*")
1727
+
1728
+ with gr.Row():
1729
+ download_format = gr.Radio(
1730
+ choices=["TXT", "DOCX"],
1731
+ value="TXT",
1732
+ label="다운로드 형식"
1733
+ )
1734
+ download_btn = gr.Button("📥 다운로드", variant="secondary")
1735
+
1736
+ download_file = gr.File(visible=False)
1737
+
1738
+ with gr.Tab("📚 테마 라이브러리"):
1739
+ gr.Markdown("### 인기 웹소설 테마")
1740
+
1741
+ library_genre = gr.Radio(
1742
+ choices=["전체"] + list(WEBNOVEL_GENRES.keys()),
1743
+ value="전체",
1744
+ label="장르 필터"
1745
+ )
1746
+
1747
+ theme_library = gr.HTML("<p>테마 라이브러리 로딩 중...</p>")
1748
+
1749
+ refresh_library_btn = gr.Button("🔄 새로고침")
1750
+
1751
+ # Event handlers
1752
+ def process_query(query, genre, language, session_id):
1753
+ system = WebNovelSystem()
1754
+ episodes = ""
1755
+ novel = ""
1756
+
1757
+ for ep_display, novel_display, status, new_session_id in system.process_webnovel_stream(query, genre, language, session_id):
1758
+ episodes = ep_display
1759
+ novel = novel_display
1760
+ yield episodes, novel, status, new_session_id
1761
+
1762
+ def handle_random_theme(genre, language):
1763
+ return generate_random_webnovel_theme(genre, language)
1764
+
1765
+ def handle_download(download_format, session_id, genre):
1766
+ """Handle download request"""
1767
+ if not session_id:
1768
+ return None
1769
+
1770
+ try:
1771
+ episodes = WebNovelDatabase.get_episodes(session_id)
1772
+ if not episodes:
1773
+ return None
1774
+
1775
+ # Get title from first episode or generate default
1776
+ title = f"{genre} 웹소설"
1777
+
1778
+ if download_format == "TXT":
1779
+ content = export_to_txt(episodes, genre, title)
1780
+
1781
+ # Save to temporary file
1782
+ with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
1783
+ suffix='.txt', delete=False) as f:
1784
+ f.write(content)
1785
+ return f.name
1786
+
1787
+ elif download_format == "DOCX":
1788
+ if not DOCX_AVAILABLE:
1789
+ gr.Warning("DOCX export requires python-docx library")
1790
+ return None
1791
+
1792
+ content = export_to_docx(episodes, genre, title)
1793
+
1794
+ # Save to temporary file
1795
+ with tempfile.NamedTemporaryFile(mode='wb', suffix='.docx',
1796
+ delete=False) as f:
1797
+ f.write(content)
1798
+ return f.name
1799
+
1800
+ except Exception as e:
1801
+ logger.error(f"Download error: {e}")
1802
+ gr.Warning(f"다운로드 중 오류 발생: {str(e)}")
1803
+ return None
1804
+
1805
+ # Connect events
1806
+ submit_btn.click(
1807
+ fn=process_query,
1808
+ inputs=[query_input, genre_select, language_select, current_session_id],
1809
+ outputs=[episodes_display, novel_display, status_text, current_session_id]
1810
+ )
1811
+
1812
+ random_btn.click(
1813
+ fn=handle_random_theme,
1814
+ inputs=[genre_select, language_select],
1815
+ outputs=[query_input]
1816
+ )
1817
+
1818
+ download_btn.click(
1819
+ fn=handle_download,
1820
+ inputs=[download_format, current_session_id, genre_select],
1821
+ outputs=[download_file]
1822
+ ).then(
1823
+ fn=lambda x: gr.update(visible=True) if x else gr.update(visible=False),
1824
+ inputs=[download_file],
1825
+ outputs=[download_file]
1826
+ )
1827
+
1828
+ # Examples
1829
+ gr.Examples(
1830
+ examples=[
1831
+ ["계약결혼한 재벌 3세와 평범한 회사원의 로맨스", "로맨스"],
1832
+ ["회귀한 천재 마법사의 복수극", "로판"],
1833
+ ["F급 헌터에서 SSS급 각성자가 되는 이야기", "현판"],
1834
+ ["폐급에서 천하제일이 되는 무공 천재", "무협"],
1835
+ ["평범한 고등학생이 이세계 용사가 되는 이야기", "라이트노벨"]
1836
+ ],
1837
+ inputs=[query_input, genre_select]
1838
+ )
1839
+
1840
+ return interface
1841
 
1842
  # Main
1843
  if __name__ == "__main__":
1844
+ logger.info("K-WebNovel Generator Starting...")
1845
+ logger.info("=" * 60)
1846
+
1847
+ # Environment check
1848
+ logger.info(f"API Endpoint: {API_URL}")
1849
+ logger.info(f"Target: {TARGET_EPISODES} episodes, {TARGET_WORDS:,} words")
1850
+ logger.info("Genres: " + ", ".join(WEBNOVEL_GENRES.keys()))
1851
+
1852
+ logger.info("=" * 60)
1853
+
1854
+ # Initialize database
1855
+ logger.info("Initializing database...")
1856
+ WebNovelDatabase.init_db()
1857
+ logger.info("Database ready.")
1858
+
1859
+ # Launch interface
1860
+ interface = create_interface()
1861
+ interface.launch(
1862
+ server_name="0.0.0.0",
1863
+ server_port=7860,
1864
+ share=False
1865
+ )