Spaces:
Running
Running
Update app.py
Browse files
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 |
-
|
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 |
-
|
|
|
|
|
613 |
break
|
614 |
elif capturing:
|
615 |
episode_section.append(line)
|
616 |
-
|
617 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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":
|
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 |
-
|
1110 |
-
|
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 |
-
|
1144 |
-
|
1145 |
-
|
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 |
-
|
1173 |
-
|
1174 |
-
|
1175 |
|
1176 |
📝 **작품소개:**
|
1177 |
"당신이 내 첫사랑이야." 이혼 서류에 도장을 찍으려던 순간, 교통사고를 당한 냉혈 재벌 남편이 나를 운명의 상대로 착각한다. 3개월간 연기했던 가짜 부부에서 진짜 사랑이 시작되는데...""",
|
@@ -1182,235 +1231,235 @@ def generate_fallback_theme(genre: str, language: str) -> str:
|
|
1182 |
서울중앙지법과 검찰청이 주 무대. 냉혈 검사와 이혼 전문 변호사가 법정에서 대립하며 티격태격하는 법정 로맨스.
|
1183 |
|
1184 |
👥 **주요 캐릭터:**
|
1185 |
-
|
1186 |
-
|
1187 |
-
|
1188 |
|
1189 |
📝 **작품소개:**
|
1190 |
"변호사님, 법정에서만 만나기로 했잖아요." 하필 전 남편의 불륜 소송을 맡은 날, 상대 검사가 나타났다. 법정에선 적, 밖에선 연인. 우리의 관계는 대체 뭘까?"""
|
1191 |
-
|
1192 |
-
|
1193 |
-
|
1194 |
-
|
1195 |
-
|
1196 |
|
1197 |
🌍 **설정:**
|
1198 |
마법이 존재하는 제국, 1년 후 처형당할 운명의 악녀 공작 영애로 빙의. 북부 변방의 전쟁광 공작과의 계약결혼이 유일한 생존루트.
|
1199 |
|
1200 |
👥 **주요 캐릭터:**
|
1201 |
-
|
1202 |
-
|
1203 |
-
|
1204 |
|
1205 |
📝 **작품소개:**
|
1206 |
소설 속 악녀로 빙의했는데 이미 처형 선고를 받은 상태? 살려면 원작에 없던 북부 공작과 계약결혼해야 한다. "1년만 함께해주세요. 그 후엔 자유를 드리겠습니다." 하지만 계약 기간이 끝나도 그가 날 놓아주지 않는다.""",
|
1207 |
-
|
1208 |
-
|
1209 |
|
1210 |
🌍 **설정:**
|
1211 |
제국력 892년으로 회귀한 황녀. 전생에서 자신을 배신한 황태자 대신, 버려진 서자 왕자와 손을 잡고 제국을 뒤집으려 한다.
|
1212 |
|
1213 |
👥 **주요 캐릭터:**
|
1214 |
-
|
1215 |
-
|
1216 |
-
|
1217 |
|
1218 |
📝 **작품소개:**
|
1219 |
독살당해 회귀한 황녀, 이번엔 다르게 살겠다. 모두가 무시하는 서자 왕자의 손을 잡았다. "저와 함께 제국을 뒤집으시겠습니까?" 하지만 그는 내가 아는 것보다 훨씬 ��험한 남자였다."""
|
1220 |
-
|
1221 |
-
|
1222 |
-
|
1223 |
-
|
1224 |
-
|
1225 |
|
1226 |
🌍 **설정:**
|
1227 |
게이트와 던전이 출현한 지 10년 후의 한국. F급 헌터가 우연히 얻은 스킬로 죽은 보스 몬스터를 부활시켜 부리는 유일무이 네크로맨서가 된다.
|
1228 |
|
1229 |
👥 **주요 캐릭터:**
|
1230 |
-
|
1231 |
-
|
1232 |
-
|
1233 |
|
1234 |
📝 **작품소개:**
|
1235 |
"F급 주제에 무슨 헛소리야?" 모두가 비웃었다. 하지만 첫 번째 보스를 쓰러뜨린 순간, 시스템 메시지가 떴다. [SSS급 히든 클래스: 네크로맨서 각성] 이제 죽은 보스들이 내 부하가 된다.""",
|
1236 |
-
|
1237 |
-
|
1238 |
|
1239 |
🌍 **설정:**
|
1240 |
100층 탑 정상에서 죽은 후 튜토리얼로 회귀. 하지만 이번엔 100층부터 거꾸로 내려가며 모든 층을 정복하는 역주행 시스템이 열렸다.
|
1241 |
|
1242 |
👥 **주요 캐릭터:**
|
1243 |
-
|
1244 |
-
|
1245 |
-
|
1246 |
|
1247 |
📝 **작품소개:**
|
1248 |
100층에서 죽었다. 눈을 떠보니 튜토리얼이었다. [역주행 시스템이 개방되었습니다] "뭐? 100층부터 시작한다고?" 최강자의 기억을 가진 채 정상에서부터 내려가는 전무후무한 공략이 시작된다."""
|
1249 |
-
|
1250 |
-
|
1251 |
-
|
1252 |
-
|
1253 |
-
|
1254 |
|
1255 |
🌍 **설정:**
|
1256 |
게이트 출현 10년, 전 국민의 70%가 각성한 한국. 무능력자로 살던 주인공에게 갑자기 아이템 제작 시스템이 열린다.
|
1257 |
|
1258 |
👥 **주요 캐릭터:**
|
1259 |
-
|
1260 |
-
|
1261 |
-
|
1262 |
|
1263 |
📝 **작품소개:**
|
1264 |
"각성 등급: 없음" 10년째 무능력자로 살았다. 그런데 오늘, 이상한 시스템 창이 떴다. [SSS급 생산직: 아이템 크래프터] 이제 내가 만든 아이템이 세계를 바꾼다.""",
|
1265 |
-
|
1266 |
-
|
1267 |
|
1268 |
🌍 **설정:**
|
1269 |
한국 최고의 헌터 사관학교. 입학시험 꼴찌로 들어온 주인공이 사실은 능력을 숨기고 있는 특급 요원.
|
1270 |
|
1271 |
👥 **주요 캐릭터:**
|
1272 |
-
|
1273 |
-
|
1274 |
-
|
1275 |
|
1276 |
📝 **작품소개:**
|
1277 |
"측정 불가? 그럼 F급이네." 일부러 힘을 숨기고 꼴찌로 입학했다. 하지만 S급 게이트가 학교에 열리면서 정체를 숨길 수 없게 됐다. "너... 대체 누구야?"라는 물음에 어떻게 답해야 할까."""
|
1278 |
-
|
1279 |
-
|
1280 |
-
|
1281 |
-
|
1282 |
-
|
1283 |
|
1284 |
🌍 **설정:**
|
1285 |
정파 무림의 중원. 천하제일문의 폐급 막내제자가 우연히 마교 교주의 비급을 습득하고 정마를 아우르는 절대무공을 익힌다.
|
1286 |
|
1287 |
👥 **주요 캐릭터:**
|
1288 |
-
|
1289 |
-
|
1290 |
-
|
1291 |
|
1292 |
📝 **작품소개:**
|
1293 |
"하찮은 것이 감히!" 모두가 무시하던 막내제자. 하지만 떨어진 절벽에서 발견한 것은 전설로만 전해지던 천마신공. "이제부터가 진짜 시작이다." 정파와 마교를 뒤흔들 폐급의 반란이 시작된다.""",
|
1294 |
-
|
1295 |
-
|
1296 |
|
1297 |
🌍 **설정:**
|
1298 |
100년 전 화산파가 최고 문파이던 시절로 회귀. 미래를 아는 장문인이 되어 문파를 지키고 무림을 재편한다.
|
1299 |
|
1300 |
👥 **주요 캐릭터:**
|
1301 |
-
|
1302 |
-
|
1303 |
-
|
1304 |
|
1305 |
📝 **작품소개:**
|
1306 |
멸문 직전에 회귀했다. 이번엔 다르다. "앞으로 화산파는 정파의 규율을 벗어난다." 미래를 아는 장문인의 파격적인 결정. 마교와 손잡고 무림의 판도를 뒤집는다."""
|
1307 |
-
|
1308 |
-
|
1309 |
-
|
1310 |
-
|
1311 |
-
|
1312 |
|
1313 |
🌍 **설정:**
|
1314 |
폐쇄된 산골 학교, 동창회를 위해 모인 8명이 갇힌다. 하나씩 사라지는 동창들. 범인은 이 안에 있다.
|
1315 |
|
1316 |
👥 **주요 캐릭터:**
|
1317 |
-
|
1318 |
-
|
1319 |
-
|
1320 |
|
1321 |
📝 **작품소개:**
|
1322 |
"10년 전 그날처럼..." 폐교에서 열린 동창회, 하지만 출구는 봉쇄됐다. 한 명씩 사라지는 친구들. 10년 전 묻어둔 비밀이 되살아난다. 살인자는 우리 중 한 명이다.""",
|
1323 |
-
|
1324 |
-
|
1325 |
|
1326 |
🌍 **설정:**
|
1327 |
같은 하루가 반복되는 타임루프. 매번 다른 방법으로 살인이 일어나지만 범인은 동일인. 루프를 깨려면 범인을 찾아야 한다.
|
1328 |
|
1329 |
👥 **주요 캐릭터:**
|
1330 |
-
|
1331 |
-
|
1332 |
-
|
1333 |
|
1334 |
📝 **작품소개:**
|
1335 |
"또 오늘이야..." 49번째 같은 아침. 오후 3시 33분, 카페에서 살인이 일어난다. 범인을 잡아야 내일이 온다. 하지만 범인은 매번 완벽한 알리바이를 만든다. 과연 50번째 오늘은 다를까?"""
|
1336 |
-
|
1337 |
-
|
1338 |
-
|
1339 |
-
|
1340 |
-
|
1341 |
|
1342 |
🌍 **설정:**
|
1343 |
평범한 고등학교, 하지만 학생과 교사 중 일부는 이세계에서 온 존재들. 주인공만 모르는 학교의 비밀.
|
1344 |
|
1345 |
👥 **주요 캐릭터:**
|
1346 |
-
|
1347 |
-
|
1348 |
-
|
1349 |
|
1350 |
📝 **작품소개:**
|
1351 |
"선배, 사실 저... 마왕이에요!" 1년째 사귄 여자친구의 충격 고백. 근데 학생회장은 천사고, 담임은 드래곤이라고? 평범한 줄 알았던 우리 학교의 정체가 밝혀진다. "그래서... 우리 헤어져야 해?"라고 묻자 그녀가 울기 시작했다.""",
|
1352 |
-
|
1353 |
-
|
1354 |
|
1355 |
🌍 **설정:**
|
1356 |
모바일 게임과 현실이 연동되기 시작한 세계. 게임에서 얻은 아이템이 현실에 나타나면서 벌어지는 학원 코미디.
|
1357 |
|
1358 |
👥 **주요 캐릭터:**
|
1359 |
-
|
1360 |
-
|
1361 |
-
|
1362 |
|
1363 |
📝 **작품소개:**
|
1364 |
"어? 이거 내 SSR 무기잖아?" 핸드폰 게임에서 뽑은 아이템이 책상 위에 나타났다. 문제는 학교에 몬스터도 나타나기 시작했다는 것. "야, 수능보다 레이드가 더 중요해진 것 같은데?"라며 웃는 친구들과 함께하는 좌충우돌 학원 판타지."""
|
1365 |
-
|
1366 |
-
|
1367 |
-
|
1368 |
-
|
1369 |
-
|
1370 |
-
|
1371 |
-
|
1372 |
-
|
1373 |
|
1374 |
def generate_theme_with_llm_only(genre: str, language: str) -> str:
|
1375 |
-
|
1376 |
-
|
1377 |
-
|
1378 |
-
|
1379 |
-
|
1380 |
-
|
1381 |
-
|
1382 |
-
|
1383 |
-
|
1384 |
-
|
1385 |
-
|
1386 |
-
|
1387 |
-
|
1388 |
-
|
1389 |
-
|
1390 |
-
|
1391 |
-
|
1392 |
-
|
1393 |
-
|
1394 |
-
|
1395 |
-
|
1396 |
-
|
1397 |
-
|
1398 |
-
|
1399 |
-
|
1400 |
-
|
1401 |
-
|
1402 |
-
|
1403 |
-
|
1404 |
-
|
1405 |
-
|
1406 |
-
|
1407 |
-
|
1408 |
-
|
1409 |
-
|
1410 |
-
|
1411 |
-
|
1412 |
-
|
1413 |
-
|
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 |
-
|
1430 |
-
|
1431 |
|
1432 |
📝 **작품소개:**
|
1433 |
[3-4줄로 작품의 핵심 갈등과 매력을 소개. 첫 문장은 강한 훅으로 시작하고, 주인공의 목표와 장애물을 명확히 제시]"""
|
1434 |
-
|
1435 |
-
|
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 |
-
|
1451 |
-
|
1452 |
-
|
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 |
-
|
1458 |
-
|
1459 |
-
|
1460 |
-
|
1461 |
|
1462 |
# --- UI functions ---
|
1463 |
def format_episodes_display(episodes: List[Dict], current_episode: int = 0) -> str:
|
1464 |
-
|
1465 |
-
|
1466 |
-
|
1467 |
-
|
1468 |
-
|
1469 |
-
|
1470 |
-
|
1471 |
-
|
1472 |
-
|
1473 |
-
|
1474 |
-
|
1475 |
-
|
1476 |
-
|
1477 |
-
|
1478 |
-
|
1479 |
-
|
1480 |
-
|
1481 |
-
|
1482 |
-
|
1483 |
-
|
1484 |
-
|
1485 |
-
|
1486 |
-
|
1487 |
-
|
1488 |
-
|
1489 |
-
|
1490 |
-
|
1491 |
-
|
1492 |
-
|
1493 |
-
|
1494 |
-
|
1495 |
-
|
1496 |
-
|
1497 |
-
|
1498 |
-
|
1499 |
-
|
1500 |
|
1501 |
def format_webnovel_display(episodes: List[Dict], genre: str) -> str:
|
1502 |
-
|
1503 |
-
|
1504 |
-
|
1505 |
-
|
1506 |
-
|
1507 |
-
|
1508 |
-
|
1509 |
-
|
1510 |
-
|
1511 |
-
|
1512 |
-
|
1513 |
-
|
1514 |
-
|
1515 |
-
|
1516 |
-
|
1517 |
-
|
1518 |
-
|
1519 |
-
|
1520 |
-
|
1521 |
-
|
1522 |
-
|
1523 |
-
|
1524 |
-
|
1525 |
-
|
1526 |
-
|
1527 |
-
|
1528 |
-
|
1529 |
-
|
1530 |
-
|
1531 |
-
|
1532 |
-
|
1533 |
|
1534 |
# --- Gradio interface ---
|
1535 |
def create_interface():
|
1536 |
-
|
1537 |
-
|
1538 |
-
|
1539 |
-
|
1540 |
-
|
1541 |
-
|
1542 |
-
|
1543 |
-
|
1544 |
-
|
1545 |
-
|
1546 |
-
|
1547 |
-
|
1548 |
-
|
1549 |
-
|
1550 |
-
|
1551 |
-
|
1552 |
-
|
1553 |
-
|
1554 |
-
|
1555 |
-
|
1556 |
-
|
1557 |
-
|
1558 |
-
|
1559 |
-
|
1560 |
-
|
1561 |
-
|
1562 |
-
|
1563 |
-
|
1564 |
-
|
1565 |
-
|
1566 |
-
|
1567 |
-
|
1568 |
-
|
1569 |
-
|
1570 |
-
|
1571 |
-
|
1572 |
-
|
1573 |
-
|
1574 |
-
|
1575 |
-
|
1576 |
-
|
1577 |
-
|
1578 |
-
|
1579 |
-
|
1580 |
-
|
1581 |
-
|
1582 |
-
|
1583 |
-
|
1584 |
-
|
1585 |
-
|
1586 |
-
|
1587 |
-
|
1588 |
-
|
1589 |
-
|
1590 |
-
|
1591 |
-
|
1592 |
-
|
1593 |
-
|
1594 |
-
|
1595 |
-
|
1596 |
-
|
1597 |
-
|
1598 |
-
|
1599 |
-
|
1600 |
-
|
1601 |
-
|
1602 |
-
|
1603 |
-
|
1604 |
-
|
1605 |
-
|
1606 |
-
|
1607 |
-
|
1608 |
-
|
1609 |
-
|
1610 |
-
|
1611 |
-
|
1612 |
-
|
1613 |
-
|
1614 |
-
|
1615 |
-
|
1616 |
-
|
1617 |
-
|
1618 |
-
|
1619 |
-
|
1620 |
-
|
1621 |
-
|
1622 |
-
|
1623 |
-
|
1624 |
-
|
1625 |
-
|
1626 |
-
|
1627 |
-
|
1628 |
-
|
1629 |
-
|
1630 |
-
|
1631 |
-
|
1632 |
-
|
1633 |
-
|
1634 |
-
|
1635 |
-
|
1636 |
-
|
1637 |
-
|
1638 |
-
|
1639 |
-
|
1640 |
-
|
1641 |
-
|
1642 |
-
|
1643 |
-
|
1644 |
-
|
1645 |
-
|
1646 |
-
|
1647 |
-
|
1648 |
-
|
1649 |
-
|
1650 |
-
|
1651 |
-
|
1652 |
-
|
1653 |
-
|
1654 |
-
|
1655 |
-
|
1656 |
-
|
1657 |
-
|
1658 |
-
|
1659 |
-
|
1660 |
-
|
1661 |
-
|
1662 |
-
|
1663 |
-
|
1664 |
-
|
1665 |
-
|
1666 |
-
|
1667 |
-
|
1668 |
-
|
1669 |
-
|
1670 |
-
|
1671 |
-
|
1672 |
-
|
1673 |
-
|
1674 |
-
|
1675 |
-
|
1676 |
-
|
1677 |
-
|
1678 |
-
|
1679 |
-
|
1680 |
-
|
1681 |
-
|
1682 |
-
|
1683 |
-
|
1684 |
-
|
1685 |
-
|
1686 |
-
|
1687 |
-
|
1688 |
-
|
1689 |
-
|
1690 |
-
|
1691 |
-
|
1692 |
-
|
1693 |
-
|
1694 |
-
|
1695 |
-
|
1696 |
-
|
1697 |
-
|
1698 |
-
|
1699 |
-
|
1700 |
-
|
1701 |
-
|
1702 |
-
|
1703 |
-
|
1704 |
-
|
1705 |
-
|
1706 |
-
|
1707 |
-
|
1708 |
-
|
1709 |
-
|
1710 |
-
|
1711 |
-
|
1712 |
-
|
1713 |
-
|
1714 |
-
|
1715 |
-
|
1716 |
-
|
1717 |
-
|
1718 |
-
|
1719 |
-
|
1720 |
-
|
1721 |
-
|
1722 |
-
|
1723 |
-
|
1724 |
-
|
1725 |
-
|
1726 |
-
|
1727 |
-
|
1728 |
-
|
1729 |
-
|
1730 |
-
|
1731 |
-
|
1732 |
-
|
1733 |
-
|
1734 |
-
|
1735 |
-
|
1736 |
-
|
1737 |
-
|
1738 |
-
|
1739 |
-
|
1740 |
-
|
1741 |
-
|
1742 |
-
|
1743 |
-
|
1744 |
-
|
1745 |
-
|
1746 |
-
|
1747 |
-
|
1748 |
-
|
1749 |
-
|
1750 |
-
|
1751 |
-
|
1752 |
-
|
1753 |
-
|
1754 |
-
|
1755 |
-
|
1756 |
-
|
1757 |
-
|
1758 |
-
|
1759 |
-
|
1760 |
-
|
1761 |
-
|
1762 |
-
|
1763 |
-
|
1764 |
-
|
1765 |
-
|
1766 |
-
|
1767 |
-
|
1768 |
-
|
1769 |
-
|
1770 |
-
|
1771 |
-
|
1772 |
-
|
1773 |
-
|
1774 |
-
|
1775 |
-
|
1776 |
-
|
1777 |
-
|
1778 |
-
|
1779 |
-
|
1780 |
-
|
1781 |
-
|
1782 |
-
|
1783 |
-
|
1784 |
-
|
1785 |
-
|
1786 |
-
|
1787 |
-
|
1788 |
-
|
1789 |
-
|
1790 |
-
|
1791 |
-
|
1792 |
|
1793 |
# Main
|
1794 |
if __name__ == "__main__":
|
1795 |
-
|
1796 |
-
|
1797 |
-
|
1798 |
-
|
1799 |
-
|
1800 |
-
|
1801 |
-
|
1802 |
-
|
1803 |
-
|
1804 |
-
|
1805 |
-
|
1806 |
-
|
1807 |
-
|
1808 |
-
|
1809 |
-
|
1810 |
-
|
1811 |
-
|
1812 |
-
|
1813 |
-
|
1814 |
-
|
1815 |
-
|
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 |
+
)
|