Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -894,10 +894,12 @@ def export_to_docx(episodes: List[Dict], genre: str, title: str = "") -> bytes:
|
|
894 |
doc.save(bytes_io)
|
895 |
bytes_io.seek(0)
|
896 |
return bytes_io.getvalue()
|
|
|
|
|
897 |
def generate_random_webnovel_theme(genre: str, language: str) -> str:
|
898 |
"""Generate random web novel theme using novel_themes.json and LLM"""
|
899 |
try:
|
900 |
-
# Load novel_themes.json with error handling
|
901 |
json_path = Path("novel_themes.json")
|
902 |
if not json_path.exists():
|
903 |
logger.warning("novel_themes.json not found, using fallback")
|
@@ -906,93 +908,165 @@ def generate_random_webnovel_theme(genre: str, language: str) -> str:
|
|
906 |
try:
|
907 |
with open(json_path, 'r', encoding='utf-8') as f:
|
908 |
content = f.read()
|
909 |
-
|
910 |
-
|
911 |
-
content = re.sub(r'
|
912 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
913 |
themes_data = json.loads(content)
|
|
|
|
|
914 |
except json.JSONDecodeError as e:
|
915 |
-
logger.error(f"JSON parsing error: {e}")
|
916 |
-
|
917 |
-
|
|
|
|
|
|
|
|
|
918 |
return generate_theme_with_llm_only(genre, language)
|
919 |
|
920 |
-
# Map genres to theme data
|
921 |
genre_mapping = {
|
922 |
-
"๋ก๋งจ์ค": ["romance_fantasy_villainess", "villainess_wants_to_be_lazy", "chaebol_family_intrigue"],
|
923 |
-
"๋กํ": ["romance_fantasy_villainess", "BL_novel_transmigration", "regression_childcare"],
|
924 |
-
"ํํ์ง": ["system_constellation_hunter", "tower_ascension_challenger", "necromancer_solo_leveling"],
|
925 |
-
"ํํ": ["system_constellation_hunter", "chaebol_family_intrigue", "post_apocalypse_survival"],
|
926 |
-
"๋ฌดํ": ["regression_revenge_pro", "necromancer_solo_leveling"],
|
927 |
-
"
|
928 |
-
"๋ผ์ดํธ๋
ธ๋ฒจ": ["BL_novel_transmigration", "villainess_wants_to_be_lazy"]
|
929 |
}
|
930 |
|
931 |
# Get relevant core genres for selected genre
|
932 |
relevant_genres = genre_mapping.get(genre, ["regression_revenge_pro"])
|
933 |
-
selected_genre_key = random.choice(relevant_genres)
|
934 |
|
935 |
-
#
|
936 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
937 |
compatible_elements = core_genre.get("compatible_elements", {})
|
938 |
|
939 |
-
# Select random elements
|
940 |
character_keys = compatible_elements.get("characters", [])
|
|
|
|
|
|
|
|
|
|
|
941 |
selected_character_key = random.choice(character_keys) if character_keys else "betrayed_protagonist"
|
942 |
|
943 |
-
# Get character
|
944 |
-
|
|
|
945 |
character_variations = character_data.get("variations", [])
|
946 |
-
character_desc = random.choice(character_variations) if character_variations else ""
|
947 |
-
character_traits = character_data.get("traits", [])
|
948 |
|
949 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
950 |
settings = compatible_elements.get("settings", [])
|
951 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
952 |
|
953 |
-
|
954 |
-
all_settings = themes_data.get("settings", {})
|
955 |
-
setting_details = []
|
956 |
-
for setting_list in all_settings.values():
|
957 |
-
setting_details.extend(setting_list)
|
958 |
-
specific_setting = random.choice(setting_details) if setting_details else selected_setting
|
959 |
|
960 |
-
# Get mechanics
|
961 |
-
|
962 |
-
|
963 |
-
|
964 |
-
plot_points = mechanic_data.get("plot_points", [])
|
965 |
-
reader_questions = mechanic_data.get("reader_questions", [])
|
966 |
|
967 |
-
|
968 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
969 |
selected_hook_type = random.choice(hook_types) if hook_types else "introduction"
|
970 |
-
hooks = themes_data["episode_hooks"].get(selected_hook_type, [])
|
971 |
-
selected_hook = random.choice(hooks) if hooks else ""
|
972 |
|
973 |
-
|
974 |
-
|
975 |
-
|
976 |
-
|
977 |
-
|
978 |
-
|
979 |
-
|
980 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
981 |
|
982 |
-
# Get plot twists
|
983 |
-
|
|
|
|
|
984 |
if twist_categories:
|
985 |
selected_twist_cat = random.choice(twist_categories)
|
986 |
-
twists =
|
987 |
-
|
988 |
-
|
989 |
-
selected_twist = ""
|
990 |
|
991 |
# Check for fusion genres
|
992 |
fusion_genres = themes_data.get("fusion_genres", {})
|
993 |
-
fusion_options =
|
994 |
selected_fusion = random.choice(fusion_options) if fusion_options and random.random() > 0.7 else ""
|
995 |
|
|
|
|
|
|
|
996 |
# Now use LLM to create a coherent theme from these elements
|
997 |
system = WebNovelSystem()
|
998 |
|
@@ -1001,11 +1075,11 @@ def generate_random_webnovel_theme(genre: str, language: str) -> str:
|
|
1001 |
prompt = f"""๋ค์ ์์๋ค์ ํ์ฉํ์ฌ {genre} ์ฅ๋ฅด์ ๋งค๋ ฅ์ ์ธ ์น์์ค์ ๊ธฐํํ์ธ์:
|
1002 |
|
1003 |
ใ์ ํ๋ ์์๋คใ
|
1004 |
-
- ํต์ฌ ์ฅ๋ฅด: {selected_genre_key}
|
1005 |
- ์บ๋ฆญํฐ: {character_desc}
|
1006 |
- ์บ๋ฆญํฐ ํน์ฑ: {', '.join(character_traits[:3])}
|
1007 |
-
- ๋ฐฐ๊ฒฝ: {
|
1008 |
-
- ํต์ฌ ๋ฉ์ปค๋์ฆ: {selected_mechanic}
|
1009 |
{"- ์์ดํ
: " + selected_item if selected_item else ""}
|
1010 |
{"- ๋ฐ์ ์์: " + selected_twist if selected_twist else ""}
|
1011 |
{"- ํจ์ ์ค์ : " + selected_fusion if selected_fusion else ""}
|
@@ -1014,7 +1088,7 @@ def generate_random_webnovel_theme(genre: str, language: str) -> str:
|
|
1014 |
{selected_hook}
|
1015 |
|
1016 |
ใ๋
์๋ฅผ ์ฌ๋ก์ก์ ์ง๋ฌธ๋คใ
|
1017 |
-
{chr(10).join(
|
1018 |
|
1019 |
๋ค์ ํ์์ผ๋ก ์ ํํ ์์ฑํ์ธ์:
|
1020 |
|
@@ -1036,11 +1110,11 @@ def generate_random_webnovel_theme(genre: str, language: str) -> str:
|
|
1036 |
prompt = f"""Create an engaging web novel for {genre} genre using these elements:
|
1037 |
|
1038 |
ใSelected Elementsใ
|
1039 |
-
- Core genre: {selected_genre_key}
|
1040 |
- Character: {character_desc}
|
1041 |
- Character traits: {', '.join(character_traits[:3])}
|
1042 |
-
- Setting: {
|
1043 |
-
- Core mechanism: {selected_mechanic}
|
1044 |
{"- Item: " + selected_item if selected_item else ""}
|
1045 |
{"- Twist: " + selected_twist if selected_twist else ""}
|
1046 |
{"- Fusion: " + selected_fusion if selected_fusion else ""}
|
@@ -1048,6 +1122,9 @@ def generate_random_webnovel_theme(genre: str, language: str) -> str:
|
|
1048 |
ใReference Hookใ
|
1049 |
{selected_hook}
|
1050 |
|
|
|
|
|
|
|
1051 |
Format exactly as follows:
|
1052 |
|
1053 |
๐ **Title:**
|
@@ -1068,10 +1145,11 @@ Format exactly as follows:
|
|
1068 |
messages = [{"role": "user", "content": prompt}]
|
1069 |
generated_theme = system.call_llm_sync(messages, "writer", language)
|
1070 |
|
|
|
1071 |
return generated_theme
|
1072 |
|
1073 |
except Exception as e:
|
1074 |
-
logger.error(f"Error generating theme from JSON: {e}")
|
1075 |
return generate_fallback_theme(genre, language)
|
1076 |
|
1077 |
def generate_fallback_theme(genre: str, language: str) -> str:
|
|
|
894 |
doc.save(bytes_io)
|
895 |
bytes_io.seek(0)
|
896 |
return bytes_io.getvalue()
|
897 |
+
|
898 |
+
|
899 |
def generate_random_webnovel_theme(genre: str, language: str) -> str:
|
900 |
"""Generate random web novel theme using novel_themes.json and LLM"""
|
901 |
try:
|
902 |
+
# Load novel_themes.json with better error handling
|
903 |
json_path = Path("novel_themes.json")
|
904 |
if not json_path.exists():
|
905 |
logger.warning("novel_themes.json not found, using fallback")
|
|
|
908 |
try:
|
909 |
with open(json_path, 'r', encoding='utf-8') as f:
|
910 |
content = f.read()
|
911 |
+
|
912 |
+
# Remove comments from JSON (/* */ style)
|
913 |
+
content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL)
|
914 |
+
# Remove single line comments (// style)
|
915 |
+
content = re.sub(r'//.*$', '', content, flags=re.MULTILINE)
|
916 |
+
# Remove trailing commas before } or ]
|
917 |
+
content = re.sub(r',\s*([}\]])', r'\1', content)
|
918 |
+
|
919 |
+
# Handle all variations of placeholder patterns
|
920 |
+
content = re.sub(r'\.\.\.\s*\(๊ธฐ์กด.*?\)\s*\.\.\.', '[]', content)
|
921 |
+
content = re.sub(r'\.\.\.\(๊ธฐ์กด.*?\)\.\.\.', '[]', content)
|
922 |
+
content = re.sub(r'\{ \.\.\. \(๊ธฐ์กด ๊ทธ๋๋ก\) \}', '{}', content)
|
923 |
+
content = re.sub(r'\{\s*\.\.\.\s*\(๊ธฐ์กด ๊ทธ๋๋ก\)\s*\}', '{}', content)
|
924 |
+
content = re.sub(r'\{ \.\.\. \}', '{}', content)
|
925 |
+
content = re.sub(r'\{\s*\.\.\.\s*\}', '{}', content)
|
926 |
+
content = re.sub(r'\[ \.\.\. \]', '[]', content)
|
927 |
+
content = re.sub(r'\[\s*\.\.\.\s*\]', '[]', content)
|
928 |
+
|
929 |
+
# Handle ellipsis in strings
|
930 |
+
content = re.sub(r'"[^"]*\.\.\.[^"]*"', '""', content)
|
931 |
+
|
932 |
+
# Debug: save cleaned JSON for inspection
|
933 |
+
with open('novel_themes_cleaned.json', 'w', encoding='utf-8') as debug_file:
|
934 |
+
debug_file.write(content)
|
935 |
+
|
936 |
+
# Parse JSON
|
937 |
themes_data = json.loads(content)
|
938 |
+
logger.info("Successfully parsed novel_themes.json")
|
939 |
+
|
940 |
except json.JSONDecodeError as e:
|
941 |
+
logger.error(f"JSON parsing error at line {e.lineno}, column {e.colno}: {e.msg}")
|
942 |
+
if hasattr(e, 'pos'):
|
943 |
+
error_context = content[max(0, e.pos-100):e.pos+100]
|
944 |
+
logger.error(f"Context around error: ...{error_context}...")
|
945 |
+
# Save problematic content for debugging
|
946 |
+
with open('novel_themes_error.json', 'w', encoding='utf-8') as error_file:
|
947 |
+
error_file.write(content)
|
948 |
return generate_theme_with_llm_only(genre, language)
|
949 |
|
950 |
+
# Map genres to theme data - updated mapping
|
951 |
genre_mapping = {
|
952 |
+
"๋ก๋งจ์ค": ["romance_fantasy_villainess", "villainess_wants_to_be_lazy", "office_romance_rivals", "chaebol_family_intrigue"],
|
953 |
+
"๋กํ": ["romance_fantasy_villainess", "BL_novel_transmigration", "regression_childcare", "saeguk_court_intrigue"],
|
954 |
+
"ํํ์ง": ["system_constellation_hunter", "tower_ascension_challenger", "necromancer_solo_leveling", "ai_dungeon_masters"],
|
955 |
+
"ํํ": ["system_constellation_hunter", "chaebol_family_intrigue", "post_apocalypse_survival", "esports_king_prodigy", "vr_streamer_ranker"],
|
956 |
+
"๋ฌดํ": ["regression_revenge_pro", "necromancer_solo_leveling", "exorcist_k_cult"],
|
957 |
+
"๏ฟฝ๏ฟฝ๏ฟฝ์คํฐ๋ฆฌ": ["post_apocalypse_survival", "tower_ascension_challenger", "survival_reality_show"],
|
958 |
+
"๋ผ์ดํธ๋
ธ๋ฒจ": ["BL_novel_transmigration", "villainess_wants_to_be_lazy", "vr_streamer_ranker", "healing_cafe_fantasy", "idol_regression_superstar"]
|
959 |
}
|
960 |
|
961 |
# Get relevant core genres for selected genre
|
962 |
relevant_genres = genre_mapping.get(genre, ["regression_revenge_pro"])
|
|
|
963 |
|
964 |
+
# Filter out genres that might not exist in the JSON
|
965 |
+
available_genres = []
|
966 |
+
core_genres = themes_data.get("core_genres", {})
|
967 |
+
|
968 |
+
# Debug log available genres
|
969 |
+
logger.debug(f"Available core genres: {list(core_genres.keys())}")
|
970 |
+
|
971 |
+
for genre_key in relevant_genres:
|
972 |
+
if genre_key in core_genres:
|
973 |
+
available_genres.append(genre_key)
|
974 |
+
|
975 |
+
if not available_genres:
|
976 |
+
logger.warning(f"No matching genres found for {genre}, available: {list(core_genres.keys())[:5]}...")
|
977 |
+
# Try to use any available genre
|
978 |
+
available_genres = list(core_genres.keys())[:3]
|
979 |
+
|
980 |
+
selected_genre_key = random.choice(available_genres)
|
981 |
+
logger.debug(f"Selected genre key: {selected_genre_key}")
|
982 |
+
|
983 |
+
# Get genre data safely
|
984 |
+
core_genre = core_genres.get(selected_genre_key, {})
|
985 |
compatible_elements = core_genre.get("compatible_elements", {})
|
986 |
|
987 |
+
# Select random elements with fallbacks
|
988 |
character_keys = compatible_elements.get("characters", [])
|
989 |
+
if not character_keys:
|
990 |
+
# Get any available characters
|
991 |
+
all_characters = list(themes_data.get("characters", {}).keys())
|
992 |
+
character_keys = all_characters[:4] if all_characters else ["betrayed_protagonist"]
|
993 |
+
|
994 |
selected_character_key = random.choice(character_keys) if character_keys else "betrayed_protagonist"
|
995 |
|
996 |
+
# Get character data safely
|
997 |
+
characters_data = themes_data.get("characters", {})
|
998 |
+
character_data = characters_data.get(selected_character_key, {})
|
999 |
character_variations = character_data.get("variations", [])
|
|
|
|
|
1000 |
|
1001 |
+
# Filter out empty or placeholder variations
|
1002 |
+
valid_variations = [v for v in character_variations if v and not v.startswith("...") and len(v) > 10]
|
1003 |
+
character_desc = random.choice(valid_variations) if valid_variations else "์ฃผ์ธ๊ณต์ ํน๋ณํ ์ด๋ช
์ ํ๊ณ ๋ฌ๋ค."
|
1004 |
+
|
1005 |
+
character_traits = character_data.get("traits", ["๊ฒฐ๋จ๋ ฅ", "์ฑ์ฅํ", "๋งค๋ ฅ์ "])
|
1006 |
+
|
1007 |
+
# Get settings safely
|
1008 |
settings = compatible_elements.get("settings", [])
|
1009 |
+
if not settings:
|
1010 |
+
# Try to get from general settings
|
1011 |
+
all_settings_categories = themes_data.get("settings", {})
|
1012 |
+
for category_name, category_settings in all_settings_categories.items():
|
1013 |
+
if isinstance(category_settings, list):
|
1014 |
+
valid_settings = [s for s in category_settings if s and not s.startswith("...") and len(s) > 5]
|
1015 |
+
settings.extend(valid_settings)
|
1016 |
|
1017 |
+
selected_setting = random.choice(settings) if settings else "ํ๋ ๋์"
|
|
|
|
|
|
|
|
|
|
|
1018 |
|
1019 |
+
# Get mechanics safely
|
1020 |
+
mechanics_data = themes_data.get("core_mechanics", {})
|
1021 |
+
mechanics_keys = [k for k in mechanics_data.keys() if k]
|
1022 |
+
selected_mechanic = random.choice(mechanics_keys) if mechanics_keys else "regression_loop_mastery"
|
|
|
|
|
1023 |
|
1024 |
+
mechanic_info = mechanics_data.get(selected_mechanic, {})
|
1025 |
+
plot_points = mechanic_info.get("plot_points", [])
|
1026 |
+
reader_questions = mechanic_info.get("reader_questions", [])
|
1027 |
+
|
1028 |
+
# Filter valid plot points and questions
|
1029 |
+
valid_plot_points = [p for p in plot_points if p and not p.startswith("...") and len(p) > 10]
|
1030 |
+
valid_questions = [q for q in reader_questions if q and not q.startswith("...") and len(q) > 10]
|
1031 |
+
|
1032 |
+
# Get hooks safely
|
1033 |
+
hooks_data = themes_data.get("episode_hooks", {})
|
1034 |
+
hook_types = list(hooks_data.keys())
|
1035 |
selected_hook_type = random.choice(hook_types) if hook_types else "introduction"
|
|
|
|
|
1036 |
|
1037 |
+
hooks = hooks_data.get(selected_hook_type, [])
|
1038 |
+
valid_hooks = [h for h in hooks if h and not h.startswith("...") and len(h) > 10]
|
1039 |
+
selected_hook = random.choice(valid_hooks) if valid_hooks else "์ด๋ช
์ ์ธ ๋ง๋จ์ด ์์๋์๋ค."
|
1040 |
+
|
1041 |
+
# Get items/artifacts for certain genres
|
1042 |
+
selected_item = ""
|
1043 |
+
if genre in ["ํํ์ง", "ํํ", "๋ฌดํ"]:
|
1044 |
+
items_data = themes_data.get("key_items_and_artifacts", {})
|
1045 |
+
item_categories = list(items_data.keys())
|
1046 |
+
if item_categories:
|
1047 |
+
selected_category = random.choice(item_categories)
|
1048 |
+
items = items_data.get(selected_category, [])
|
1049 |
+
valid_items = [i for i in items if i and not i.startswith("...") and len(i) > 10]
|
1050 |
+
selected_item = random.choice(valid_items) if valid_items else ""
|
1051 |
|
1052 |
+
# Get plot twists safely
|
1053 |
+
twists_data = themes_data.get("plot_twists_and_cliches", {})
|
1054 |
+
twist_categories = list(twists_data.keys())
|
1055 |
+
selected_twist = ""
|
1056 |
if twist_categories:
|
1057 |
selected_twist_cat = random.choice(twist_categories)
|
1058 |
+
twists = twists_data.get(selected_twist_cat, [])
|
1059 |
+
valid_twists = [t for t in twists if t and not t.startswith("...") and len(t) > 10]
|
1060 |
+
selected_twist = random.choice(valid_twists) if valid_twists else ""
|
|
|
1061 |
|
1062 |
# Check for fusion genres
|
1063 |
fusion_genres = themes_data.get("fusion_genres", {})
|
1064 |
+
fusion_options = [v for v in fusion_genres.values() if v and not v.startswith("...") and len(v) > 10]
|
1065 |
selected_fusion = random.choice(fusion_options) if fusion_options and random.random() > 0.7 else ""
|
1066 |
|
1067 |
+
# Log selected elements for debugging
|
1068 |
+
logger.debug(f"Selected elements - Genre: {selected_genre_key}, Character: {selected_character_key}, Mechanic: {selected_mechanic}")
|
1069 |
+
|
1070 |
# Now use LLM to create a coherent theme from these elements
|
1071 |
system = WebNovelSystem()
|
1072 |
|
|
|
1075 |
prompt = f"""๋ค์ ์์๋ค์ ํ์ฉํ์ฌ {genre} ์ฅ๋ฅด์ ๋งค๋ ฅ์ ์ธ ์น์์ค์ ๊ธฐํํ์ธ์:
|
1076 |
|
1077 |
ใ์ ํ๋ ์์๋คใ
|
1078 |
+
- ํต์ฌ ์ฅ๋ฅด: {selected_genre_key.replace('_', ' ')}
|
1079 |
- ์บ๋ฆญํฐ: {character_desc}
|
1080 |
- ์บ๋ฆญํฐ ํน์ฑ: {', '.join(character_traits[:3])}
|
1081 |
+
- ๋ฐฐ๊ฒฝ: {selected_setting}
|
1082 |
+
- ํต์ฌ ๋ฉ์ปค๋์ฆ: {selected_mechanic.replace('_', ' ')}
|
1083 |
{"- ์์ดํ
: " + selected_item if selected_item else ""}
|
1084 |
{"- ๋ฐ์ ์์: " + selected_twist if selected_twist else ""}
|
1085 |
{"- ํจ์ ์ค์ : " + selected_fusion if selected_fusion else ""}
|
|
|
1088 |
{selected_hook}
|
1089 |
|
1090 |
ใ๋
์๋ฅผ ์ฌ๋ก์ก์ ์ง๋ฌธ๋คใ
|
1091 |
+
{chr(10).join(valid_questions[:2]) if valid_questions else "๋
์์ ํธ๊ธฐ์ฌ์ ์๊ทนํ๋ ์ง๋ฌธ๋ค"}
|
1092 |
|
1093 |
๋ค์ ํ์์ผ๋ก ์ ํํ ์์ฑํ์ธ์:
|
1094 |
|
|
|
1110 |
prompt = f"""Create an engaging web novel for {genre} genre using these elements:
|
1111 |
|
1112 |
ใSelected Elementsใ
|
1113 |
+
- Core genre: {selected_genre_key.replace('_', ' ')}
|
1114 |
- Character: {character_desc}
|
1115 |
- Character traits: {', '.join(character_traits[:3])}
|
1116 |
+
- Setting: {selected_setting}
|
1117 |
+
- Core mechanism: {selected_mechanic.replace('_', ' ')}
|
1118 |
{"- Item: " + selected_item if selected_item else ""}
|
1119 |
{"- Twist: " + selected_twist if selected_twist else ""}
|
1120 |
{"- Fusion: " + selected_fusion if selected_fusion else ""}
|
|
|
1122 |
ใReference Hookใ
|
1123 |
{selected_hook}
|
1124 |
|
1125 |
+
ใQuestions to captivate readersใ
|
1126 |
+
{chr(10).join(valid_questions[:2]) if valid_questions else "Questions that spark reader curiosity"}
|
1127 |
+
|
1128 |
Format exactly as follows:
|
1129 |
|
1130 |
๐ **Title:**
|
|
|
1145 |
messages = [{"role": "user", "content": prompt}]
|
1146 |
generated_theme = system.call_llm_sync(messages, "writer", language)
|
1147 |
|
1148 |
+
logger.info("Successfully generated theme using JSON elements")
|
1149 |
return generated_theme
|
1150 |
|
1151 |
except Exception as e:
|
1152 |
+
logger.error(f"Error generating theme from JSON: {e}", exc_info=True)
|
1153 |
return generate_fallback_theme(genre, language)
|
1154 |
|
1155 |
def generate_fallback_theme(genre: str, language: str) -> str:
|