diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -255,6 +255,25 @@ class NovelDatabase: ) ''') + # NEW: Random themes library table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS random_themes_library ( + theme_id TEXT PRIMARY KEY, + theme_text TEXT NOT NULL, + language TEXT NOT NULL, + title TEXT, + opening_sentence TEXT, + protagonist TEXT, + conflict TEXT, + philosophical_question TEXT, + generated_at TEXT DEFAULT (datetime('now')), + view_count INTEGER DEFAULT 0, + used_count INTEGER DEFAULT 0, + tags TEXT, + metadata TEXT + ) + ''') + conn.commit() @staticmethod @@ -401,6 +420,81 @@ class NovelDatabase: return tracker return None + # NEW: Random theme library methods + @staticmethod + def save_random_theme(theme_text: str, language: str, metadata: Dict[str, Any]) -> str: + """Save randomly generated theme to library""" + theme_id = hashlib.md5(f"{theme_text}{datetime.now()}".encode()).hexdigest()[:12] + + # Extract components from theme text + title = metadata.get('title', '') + opening_sentence = metadata.get('opening_sentence', '') + protagonist = metadata.get('protagonist', '') + conflict = metadata.get('conflict', '') + philosophical_question = metadata.get('philosophical_question', '') + tags = json.dumps(metadata.get('tags', [])) + + with NovelDatabase.get_db() as conn: + conn.cursor().execute(''' + INSERT INTO random_themes_library + (theme_id, theme_text, language, title, opening_sentence, + protagonist, conflict, philosophical_question, tags, metadata) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', (theme_id, theme_text, language, title, opening_sentence, + protagonist, conflict, philosophical_question, tags, + json.dumps(metadata))) + conn.commit() + + return theme_id + + @staticmethod + def get_random_themes_library(language: str = None, limit: int = 50) -> List[Dict]: + """Get random themes from library""" + with NovelDatabase.get_db() as conn: + query = ''' + SELECT * FROM random_themes_library + {} + ORDER BY generated_at DESC + LIMIT ? + '''.format('WHERE language = ?' if language else '') + + if language: + rows = conn.cursor().execute(query, (language, limit)).fetchall() + else: + rows = conn.cursor().execute(query, (limit,)).fetchall() + + return [dict(row) for row in rows] + + @staticmethod + def update_theme_view_count(theme_id: str): + """Update view count for a theme""" + with NovelDatabase.get_db() as conn: + conn.cursor().execute( + 'UPDATE random_themes_library SET view_count = view_count + 1 WHERE theme_id = ?', + (theme_id,) + ) + conn.commit() + + @staticmethod + def update_theme_used_count(theme_id: str): + """Update used count when theme is used for novel""" + with NovelDatabase.get_db() as conn: + conn.cursor().execute( + 'UPDATE random_themes_library SET used_count = used_count + 1 WHERE theme_id = ?', + (theme_id,) + ) + conn.commit() + + @staticmethod + def get_theme_by_id(theme_id: str) -> Optional[Dict]: + """Get specific theme by ID""" + with NovelDatabase.get_db() as conn: + row = conn.cursor().execute( + 'SELECT * FROM random_themes_library WHERE theme_id = ?', + (theme_id,) + ).fetchone() + return dict(row) if row else None + # Maintain existing methods @staticmethod def get_session(session_id: str) -> Optional[Dict]: @@ -1408,7 +1502,7 @@ Provide specific and actionable revision instructions.""" logger.error(f"Novel generation process error: {e}", exc_info=True) yield f"❌ Error occurred: {e}", stages if 'stages' in locals() else [], self.current_session_id - def get_stage_prompt(self, stage_idx: int, role: str, query: str, +def get_stage_prompt(self, stage_idx: int, role: str, query: str, language: str, stages: List[Dict]) -> str: """Generate stage-specific prompt""" if stage_idx == 0: # Director initial planning @@ -1739,8 +1833,8 @@ def export_to_docx(content: str, filename: str, language: str, session_id: str) patterns_to_remove = [ r'^#{1,6}\s+.*', # Markdown headers r'^\*\*.*\*\*', # 굵은 글씨 **text** - r'^Part\s*\d+.*', # “Part 1 …” 형식 - r'^\d+\.\s+.*:.*', # “1. 제목: …” 형식 + r'^Part\s*\d+.*', # "Part 1 …" 형식 + r'^\d+\.\s+.*:.*', # "1. 제목: …" 형식 r'^---+', # 구분선 r'^\s*\[.*\]\s*', # 대괄호 라벨 ] @@ -1829,131 +1923,561 @@ def export_to_txt(content: str, filename: str) -> str: return filepath -# CSS styles -# CSS styles - Writer's Study Theme (Light & Warm) -custom_css = """ -/* Global container - Light paper background */ -.gradio-container { - background: linear-gradient(135deg, #faf8f3 0%, #f5f2e8 50%, #f0ebe0 100%); - min-height: 100vh; - font-family: 'Georgia', 'Times New Roman', serif; -} +def generate_random_theme(language="English"): + """Generate a coherent and natural novel theme using LLM""" + try: + # JSON 파일 로드 + json_path = Path("novel_themes.json") + if not json_path.exists(): + print("[WARNING] novel_themes.json not found, using built-in data") + # 기본 데이터 정의 - 더 현실적인 테마로 수정 + themes_data = { + "themes": ["family secrets", "career transition", "lost love", "friendship test", "generational conflict"], + "characters": ["middle-aged teacher", "retiring doctor", "single parent", "immigrant artist", "war veteran"], + "hooks": ["unexpected inheritance", "old diary discovery", "chance reunion", "life-changing diagnosis", "sudden job loss"], + "questions": ["What defines family?", "Can people truly change?", "What is worth sacrificing?", "How do we forgive?"] + } + else: + with open(json_path, 'r', encoding='utf-8') as f: + data = json.load(f) + # 가중치 기반 필터링 - 현실적인 테마 우선 + realistic_themes = [] + for theme_key, theme_data in data.get('core_themes', {}).items(): + weight = theme_data.get('weight', 0.1) + # 현실적인 테마에 더 높은 가중치 + if any(word in theme_key for word in ['family', 'love', 'work', 'memory', 'identity', 'aging']): + weight *= 1.5 + elif any(word in theme_key for word in ['digital', 'extinction', 'apocalypse', 'quantum']): + weight *= 0.5 + realistic_themes.append((theme_key, weight)) + + # 가중치 기반 선택 + themes = [t[0] for t in sorted(realistic_themes, key=lambda x: x[1], reverse=True)[:10]] + + themes_data = { + "themes": themes if themes else ["family secrets", "career crisis", "lost love"], + "characters": [], + "hooks": [], + "questions": [] + } + + # Extract realistic data + for char_data in data.get('characters', {}).values(): + for variation in char_data.get('variations', []): + # 현실적인 캐릭터 필터링 + if not any(word in variation.lower() for word in ['cyborg', 'quantum', 'binary', 'extinct']): + themes_data["characters"].append(variation) + + for hook_list in data.get('narrative_hooks', {}).values(): + for hook in hook_list: + # 현실적인 사건 필터링 + if not any(word in hook.lower() for word in ['download', 'digital', 'algorithm', 'corporate subscription']): + themes_data["hooks"].append(hook) + + for phil_data in data.get('philosophies', {}).values(): + themes_data["questions"].extend(phil_data.get('core_questions', [])) + + # 기본값 설정 + if not themes_data["characters"]: + themes_data["characters"] = ["struggling artist", "retired teacher", "young mother", "elderly caregiver", "small business owner"] + if not themes_data["hooks"]: + themes_data["hooks"] = ["discovering family secret", "unexpected reunion", "facing illness", "losing home", "finding old letters"] + if not themes_data["questions"]: + themes_data["questions"] = ["What makes a family?", "How do we find meaning?", "Can we escape our past?", "What legacy do we leave?"] + + # Random selection + import secrets + theme = secrets.choice(themes_data["themes"]) + character = secrets.choice(themes_data["characters"]) + hook = secrets.choice(themes_data["hooks"]) + question = secrets.choice(themes_data["questions"]) + + # 언어별 프롬프트 - 톤과 스타일 섹션 제거 + if language == "Korean": + # 한국어 번역 및 자연스러�� 표현 + theme_kr = translate_theme_naturally(theme, "theme") + character_kr = translate_theme_naturally(character, "character") + hook_kr = translate_theme_naturally(hook, "hook") + question_kr = translate_theme_naturally(question, "question") + + prompt = f"""다음 요소들을 사용하여 현실적이고 공감가능한 소설 주제를 생성하세요: -/* Main header - Classic book cover feel */ -.main-header { - background: linear-gradient(145deg, #ffffff 0%, #fdfcf8 100%); - backdrop-filter: blur(10px); - padding: 45px; - border-radius: 20px; - margin-bottom: 35px; - text-align: center; - color: #3d2914; - border: 1px solid #e8dcc6; - box-shadow: 0 10px 30px rgba(139, 69, 19, 0.08), - 0 5px 15px rgba(139, 69, 19, 0.05), - inset 0 1px 2px rgba(255, 255, 255, 0.9); - position: relative; - overflow: hidden; -} +주제: {theme_kr} +인물: {character_kr} +사건: {hook_kr} +핵심 질문: {question_kr} -/* Book spine decoration */ -.main-header::before { - content: ''; - position: absolute; - left: 50px; - top: 0; - bottom: 0; - width: 3px; - background: linear-gradient(180deg, #d4a574 0%, #c19656 50%, #d4a574 100%); - box-shadow: 1px 0 2px rgba(0, 0, 0, 0.1); -} +요구사항: +1. 현대 한국 사회에서 일어날 수 있는 현실적인 이야기 +2. 보편적으로 공감할 수 있는 인물과 상황 +3. 구체적이고 생생한 배경 설정 +4. 깊이 있는 심리 묘사가 가능한 갈등 -.header-title { - font-size: 3.2em; - margin-bottom: 20px; - font-weight: 700; - color: #2c1810; - text-shadow: 2px 2px 4px rgba(139, 69, 19, 0.1); - font-family: 'Playfair Display', 'Georgia', serif; - letter-spacing: -0.5px; -} +다음 형식으로 간결하게 작성하세요: -.header-description { - font-size: 0.95em; - color: #5a453a; - line-height: 1.7; - margin-top: 25px; - text-align: justify; - max-width: 920px; - margin-left: auto; - margin-right: auto; - font-family: 'Georgia', serif; -} +[한 문장으로 된 매력적인 첫 문장] -.badges-container { - display: flex; - justify-content: center; - gap: 12px; - margin-top: 25px; - margin-bottom: 25px; -} +주인공은 [구체적인 상황의 인물]입니다. +[핵심 사건]을 계기로 [내적 갈등]에 직면하게 되고, +결국 [철학적 질문]에 대한 답을 찾아가는 여정을 그립니다.""" + + else: + prompt = f"""Generate a realistic and relatable novel theme using these elements: -/* Progress notes - Manuscript notes style */ -.progress-note { - background: linear-gradient(135deg, #fff9e6 0%, #fff5d6 100%); - border-left: 4px solid #d4a574; - padding: 22px 30px; - margin: 25px auto; - border-radius: 12px; - color: #5a453a; - max-width: 820px; - font-weight: 500; - box-shadow: 0 4px 12px rgba(212, 165, 116, 0.15); - position: relative; -} +Theme: {theme} +Character: {character} +Event: {hook} +Core Question: {question} -/* Handwritten note effect */ -.progress-note::after { - content: '📌'; - position: absolute; - top: -10px; - right: 20px; - font-size: 24px; - transform: rotate(15deg); -} +Requirements: +1. A story that could happen in contemporary society +2. Universally relatable characters and situations +3. Specific and vivid settings +4. Conflicts allowing deep psychological exploration -.warning-note { - background: #fef3e2; - border-left: 4px solid #f6b73c; - padding: 18px 25px; - margin: 20px auto; - border-radius: 10px; - color: #7a5c00; - max-width: 820px; - font-size: 0.92em; - box-shadow: 0 3px 10px rgba(246, 183, 60, 0.15); -} +Write concisely in this format: -/* Input section - Writing desk feel */ -.input-section { - background: linear-gradient(145deg, #ffffff 0%, #fcfaf7 100%); - backdrop-filter: blur(10px); - padding: 30px; - border-radius: 16px; - margin-bottom: 28px; - border: 1px solid #e8dcc6; - box-shadow: 0 6px 20px rgba(139, 69, 19, 0.06), - inset 0 1px 3px rgba(255, 255, 255, 0.8); -} +[One compelling opening sentence] -/* Session section - File cabinet style */ -.session-section { - background: linear-gradient(145deg, #f8f4ed 0%, #f3ede2 100%); - backdrop-filter: blur(8px); - padding: 22px; - border-radius: 14px; - margin-top: 28px; - color: #3d2914; +The protagonist is [character in specific situation]. +Through [key event], they face [internal conflict], +ultimately embarking on a journey to answer [philosophical question].""" + + # Use the UnifiedLiterarySystem's LLM to generate coherent theme + system = UnifiedLiterarySystem() + + # Call LLM synchronously for theme generation + messages = [{"role": "user", "content": prompt}] + generated_theme = system.call_llm_sync(messages, "director", language) + + # Extract metadata for database storage + metadata = extract_theme_metadata(generated_theme, language) + metadata.update({ + 'original_theme': theme, + 'original_character': character, + 'original_hook': hook, + 'original_question': question + }) + + # Save to database + theme_id = NovelDatabase.save_random_theme(generated_theme, language, metadata) + logger.info(f"Saved random theme with ID: {theme_id}") + + # 톤과 스타일 섹션 제거 - 불필요한 반복 내용 삭제 + if "**톤과 스타일:**" in generated_theme or "**Tone and Style:**" in generated_theme: + lines = generated_theme.split('\n') + filtered_lines = [] + skip = False + for line in lines: + if "톤과 스타일" in line or "Tone and Style" in line: + skip = True + elif skip and (line.strip() == "" or line.startswith("**")): + skip = False + if not skip: + filtered_lines.append(line) + generated_theme = '\n'.join(filtered_lines).strip() + + return generated_theme + + except Exception as e: + logger.error(f"Theme generation error: {str(e)}") + # Fallback to simple realistic themes + fallback_themes = { + "Korean": [ + """"아버지가 돌아가신 날, 나는 그가 평생 숨겨온 또 다른 가족의 존재를 알게 되었다." + +주인공은 평범한 회사원으로 살아온 40대 여성입니다. +아버지의 장례식에서 낯선 여인과 그녀의 딸을 만나게 되면서 가족의 의미에 대해 다시 생각하게 되고, +결국 진정한 가족이란 무엇인지에 대한 답을 찾아가는 여정을 그립니다.""", + + """"서른 년간 가르친 학교에서 나온 날, 처음으로 내가 누구인지 몰랐다." + +주인공은 정년퇴직을 맞은 고등학교 국어 교사입니다. +갑작스러운 일상의 공백 속에서 잊고 지냈던 젊은 날의 꿈을 마주하게 되고, +결국 남은 인생에서 무엇을 할 것인가에 대한 답을 찾아가는 여정을 그립니다.""" + ], + "English": [ + """"The day my father died, I discovered he had another family he'd hidden all his life." + +The protagonist is a woman in her 40s who has lived as an ordinary office worker. +Through meeting a strange woman and her daughter at her father's funeral, she confronts what family truly means, +ultimately embarking on a journey to answer what constitutes a real family.""", + + """"The day I left the school where I'd taught for thirty years, I didn't know who I was anymore." + +The protagonist is a high school literature teacher facing retirement. +Through the sudden emptiness of daily life, they confront long-forgotten dreams of youth, +ultimately embarking on a journey to answer what to do with the remaining years.""" + ] + } + + import secrets + return secrets.choice(fallback_themes.get(language, fallback_themes["English"])) + +def translate_theme_naturally(text, category): + """자연스러운 한국어 번역""" + translations = { + # 테마 + "family secrets": "가족의 비밀", + "career transition": "인생의 전환점", + "lost love": "잃어버린 사랑", + "friendship test": "우정의 시험", + "generational conflict": "세대 간 갈등", + "digital extinction": "디지털 시대의 소외", + "sensory revolution": "감각의 혁명", + "temporal paradox": "시간의 역설", + + # 캐릭터 + "struggling artist": "생활고에 시달리는 예술가", + "retired teacher": "은퇴한 교사", + "young mother": "젊은 엄마", + "elderly caregiver": "노인을 돌보는 간병인", + "small business owner": "작은 가게 주인", + "middle-aged teacher": "중년의 교사", + "retiring doctor": "은퇴를 앞둔 의사", + "single parent": "혼자 아이를 키우는 부모", + "immigrant artist": "이민자 예술가", + "war veteran": "전쟁 참전용사", + "last person who dreams without ads": "광고 없이 꿈꾸는 마지막 사람", + "memory trader": "기억 거래상", + + # 사건 + "discovering family secret": "가족의 비밀을 발견하다", + "unexpected reunion": "예상치 못한 재회", + "facing illness": "질병과 마주하다", + "losing home": "집을 잃다", + "finding old letters": "오래된 편지를 발견하다", + "unexpected inheritance": "뜻밖의 유산", + "old diary discovery": "오래된 일기장 발견", + "chance reunion": "우연한 재회", + "life-changing diagnosis": "인생을 바꾸는 진단", + "sudden job loss": "갑작스러운 실직", + "discovers their memories belong to a corporate subscription": "기억이 기업 서비스의 일부임을 발견하다", + + # 질문 + "What makes a family?": "가족이란 무엇인가?", + "How do we find meaning?": "우리는 어떻게 의미를 찾는가?", + "Can we escape our past?": "과거로부터 벗어날 수 있는가?", + "What legacy do we leave?": "우리는 어떤 유산을 남기는가?", + "What defines family?": "무엇이 가족을 정의하는가?", + "Can people truly change?": "사람은 정말 변할 수 있는가?", + "What is worth sacrificing?": "무엇을 위해 희생할 가치가 있는가?", + "How do we forgive?": "우리는 어떻게 용서하는가?", + "What remains human when humanity is optional?": "인간성이 선택사항일 때 무엇이 인간으로 남는가?" + } + + # 먼저 정확한 매칭 시도 + if text in translations: + return translations[text] + + # 부분 매칭 시도 + text_lower = text.lower() + for key, value in translations.items(): + if key.lower() in text_lower or text_lower in key.lower(): + return value + + # 번역이 없으면 원문 반환 + return text + +def extract_theme_metadata(theme_text: str, language: str) -> Dict[str, Any]: + """Extract metadata from generated theme text""" + metadata = { + 'title': '', + 'opening_sentence': '', + 'protagonist': '', + 'conflict': '', + 'philosophical_question': '', + 'tags': [] + } + + lines = theme_text.split('\n') + + # Extract opening sentence (usually in quotes) + for line in lines: + if '"' in line or '"' in line or '「' in line: + # Extract text between quotes + import re + quotes = re.findall(r'["""「](.*?)["""」]', line) + if quotes: + metadata['opening_sentence'] = quotes[0] + break + + # Extract other elements based on patterns + for i, line in enumerate(lines): + line = line.strip() + + # Title extraction (if exists) + if i == 0 and not any(quote in line for quote in ['"', '"', '「']): + metadata['title'] = line.replace('**', '').strip() + + # Protagonist + if any(marker in line for marker in ['protagonist is', '주인공은', 'The protagonist']): + metadata['protagonist'] = line.split('is' if 'is' in line else '은')[-1].strip().rstrip('.') + + # Conflict/Event + if any(marker in line for marker in ['Through', '통해', '계기로', 'face']): + metadata['conflict'] = line + + # Question + if any(marker in line for marker in ['answer', '답을', 'question', '질문']): + metadata['philosophical_question'] = line + + # Generate tags based on content + tag_keywords = { + 'family': ['family', '가족', 'father', '아버지', 'mother', '어머니'], + 'love': ['love', '사랑', 'relationship', '관계'], + 'death': ['death', '죽음', 'died', '돌아가신'], + 'memory': ['memory', '기억', 'remember', '추억'], + 'identity': ['identity', '정체성', 'who am I', '누구인지'], + 'work': ['work', '일', 'career', '직업', 'retirement', '은퇴'], + 'aging': ['aging', '노화', 'old', '늙은', 'elderly', '노인'] + } + + theme_lower = theme_text.lower() + for tag, keywords in tag_keywords.items(): + if any(keyword in theme_lower for keyword in keywords): + metadata['tags'].append(tag) + + return metadata + +def format_theme_card(theme_data: Dict, language: str) -> str: + """Format theme data as a card for display""" + theme_id = theme_data.get('theme_id', '') + theme_text = theme_data.get('theme_text', '') + generated_at = theme_data.get('generated_at', '') + view_count = theme_data.get('view_count', 0) + used_count = theme_data.get('used_count', 0) + tags = json.loads(theme_data.get('tags', '[]')) + + # Format timestamp + if generated_at: + try: + dt = datetime.fromisoformat(generated_at.replace(' ', 'T')) + time_str = dt.strftime('%Y-%m-%d %H:%M') + except: + time_str = generated_at + else: + time_str = "" + + # Create tag badges + tag_badges = ' '.join([f'{tag}' for tag in tags]) + + # Truncate theme text for card display + if len(theme_text) > 300: + preview_text = theme_text[:300] + "..." + else: + preview_text = theme_text + + # Create card HTML + card_html = f""" +
+
+ #{theme_id[:8]} + {time_str} +
+
+
{preview_text}
+
{tag_badges}
+
+ +
""" + + return card_html + +def get_theme_library_display(language: str = None, search_query: str = "") -> str: + """Get formatted display of theme library""" + themes = NovelDatabase.get_random_themes_library(language, limit=50) + + if not themes: + empty_msg = { + "Korean": "아직 생성된 테마가 없습니다. 랜덤 버튼을 눌러 첫 테마를 만들어보세요!", + "English": "No themes generated yet. Click the Random button to create your first theme!" + } + return f'
{empty_msg.get(language, empty_msg["English"])}
' + + # Filter by search query if provided + if search_query: + search_lower = search_query.lower() + themes = [t for t in themes if search_lower in t.get('theme_text', '').lower()] + + # Statistics + total_themes = len(themes) + total_views = sum(t.get('view_count', 0) for t in themes) + total_uses = sum(t.get('used_count', 0) for t in themes) + + stats_html = f""" +
+
+ {'총 테마' if language == 'Korean' else 'Total Themes'} + {total_themes} +
+
+ {'총 조회수' if language == 'Korean' else 'Total Views'} + {total_views} +
+
+ {'총 사용수' if language == 'Korean' else 'Total Uses'} + {total_uses} +
+
""" + + # Theme cards + cards_html = '
' + for theme in themes: + cards_html += format_theme_card(theme, language) + cards_html += '
' + + # JavaScript for interactions + js_script = """ +""" + + return stats_html + cards_html + js_script + +# CSS styles +custom_css = """ +/* Global container - Light paper background */ +.gradio-container { + background: linear-gradient(135deg, #faf8f3 0%, #f5f2e8 50%, #f0ebe0 100%); + min-height: 100vh; + font-family: 'Georgia', 'Times New Roman', serif; +} + +/* Main header - Classic book cover feel */ +.main-header { + background: linear-gradient(145deg, #ffffff 0%, #fdfcf8 100%); + backdrop-filter: blur(10px); + padding: 45px; + border-radius: 20px; + margin-bottom: 35px; + text-align: center; + color: #3d2914; + border: 1px solid #e8dcc6; + box-shadow: 0 10px 30px rgba(139, 69, 19, 0.08), + 0 5px 15px rgba(139, 69, 19, 0.05), + inset 0 1px 2px rgba(255, 255, 255, 0.9); + position: relative; + overflow: hidden; +} + +/* Book spine decoration */ +.main-header::before { + content: ''; + position: absolute; + left: 50px; + top: 0; + bottom: 0; + width: 3px; + background: linear-gradient(180deg, #d4a574 0%, #c19656 50%, #d4a574 100%); + box-shadow: 1px 0 2px rgba(0, 0, 0, 0.1); +} + +.header-title { + font-size: 3.2em; + margin-bottom: 20px; + font-weight: 700; + color: #2c1810; + text-shadow: 2px 2px 4px rgba(139, 69, 19, 0.1); + font-family: 'Playfair Display', 'Georgia', serif; + letter-spacing: -0.5px; +} + +.header-description { + font-size: 0.95em; + color: #5a453a; + line-height: 1.7; + margin-top: 25px; + text-align: justify; + max-width: 920px; + margin-left: auto; + margin-right: auto; + font-family: 'Georgia', serif; +} + +.badges-container { + display: flex; + justify-content: center; + gap: 12px; + margin-top: 25px; + margin-bottom: 25px; +} + +/* Progress notes - Manuscript notes style */ +.progress-note { + background: linear-gradient(135deg, #fff9e6 0%, #fff5d6 100%); + border-left: 4px solid #d4a574; + padding: 22px 30px; + margin: 25px auto; + border-radius: 12px; + color: #5a453a; + max-width: 820px; + font-weight: 500; + box-shadow: 0 4px 12px rgba(212, 165, 116, 0.15); + position: relative; +} + +/* Handwritten note effect */ +.progress-note::after { + content: '📌'; + position: absolute; + top: -10px; + right: 20px; + font-size: 24px; + transform: rotate(15deg); +} + +.warning-note { + background: #fef3e2; + border-left: 4px solid #f6b73c; + padding: 18px 25px; + margin: 20px auto; + border-radius: 10px; + color: #7a5c00; + max-width: 820px; + font-size: 0.92em; + box-shadow: 0 3px 10px rgba(246, 183, 60, 0.15); +} + +/* Input section - Writing desk feel */ +.input-section { + background: linear-gradient(145deg, #ffffff 0%, #fcfaf7 100%); + backdrop-filter: blur(10px); + padding: 30px; + border-radius: 16px; + margin-bottom: 28px; + border: 1px solid #e8dcc6; + box-shadow: 0 6px 20px rgba(139, 69, 19, 0.06), + inset 0 1px 3px rgba(255, 255, 255, 0.8); +} + +/* Session section - File cabinet style */ +.session-section { + background: linear-gradient(145deg, #f8f4ed 0%, #f3ede2 100%); + backdrop-filter: blur(8px); + padding: 22px; + border-radius: 14px; + margin-top: 28px; + color: #3d2914; border: 1px solid #ddd0b8; box-shadow: inset 0 2px 4px rgba(139, 69, 19, 0.08); } @@ -2205,405 +2729,207 @@ select, .gr-dropdown { } """ -def load_theme_data(): - """Load theme data from JSON file""" - json_path = Path("novel_themes.json") - if json_path.exists(): - with open(json_path, 'r', encoding='utf-8') as f: - return json.load(f) - else: - # Fallback data if JSON file not found - return { - "core_themes": { - "digital_extinction": { - "weight": 0.5, - "compatible_elements": { - "characters": ["last_human"], - "philosophies": ["posthuman"] - } - } - }, - "characters": { - "last_human": { - "variations": ["last person who dreams without ads"], - "traits": ["stubborn", "melancholic"], - "arc_potential": "preservation_vs_evolution" - } - }, - "philosophies": { - "posthuman": { - "core_questions": ["What remains human when humanity is optional?"], - "manifestations": ["voluntary human extinction movements"] - } - }, - "narrative_hooks": { - "identity_crisis": ["discovers their memories belong to a corporate subscription"] - }, - "opening_sentences": { - "shocking": ["The notification read: 'Your humanity subscription expires in 24 hours.'"] - } - } +# Additional CSS for theme library +theme_library_css = """ +/* Theme Library Styles */ +.library-stats { + display: flex; + justify-content: space-around; + margin-bottom: 30px; + padding: 20px; + background: linear-gradient(145deg, #f8f4ed 0%, #f3ede2 100%); + border-radius: 12px; + box-shadow: 0 4px 12px rgba(139, 69, 19, 0.08); +} -def weighted_random_choice(items_dict): - """Select item based on weights""" - items = list(items_dict.keys()) - weights = [items_dict[item].get('weight', 0.1) for item in items] - # random.choices 대신 numpy 사용하거나 수동으로 구현 - total_weight = sum(weights) - r = random.uniform(0, total_weight) - upto = 0 - for i, item in enumerate(items): - if upto + weights[i] >= r: - return item - upto += weights[i] - return items[-1] +.stat-item { + text-align: center; +} - -def generate_random_theme(language="English"): - """Generate a coherent and natural novel theme using LLM""" - try: - # JSON 파일 로드 - json_path = Path("novel_themes.json") - if not json_path.exists(): - print("[WARNING] novel_themes.json not found, using built-in data") - # 기본 데이터 정의 - 더 현실적인 테마로 수정 - themes_data = { - "themes": ["family secrets", "career transition", "lost love", "friendship test", "generational conflict"], - "characters": ["middle-aged teacher", "retiring doctor", "single parent", "immigrant artist", "war veteran"], - "hooks": ["unexpected inheritance", "old diary discovery", "chance reunion", "life-changing diagnosis", "sudden job loss"], - "questions": ["What defines family?", "Can people truly change?", "What is worth sacrificing?", "How do we forgive?"] - } - else: - with open(json_path, 'r', encoding='utf-8') as f: - data = json.load(f) - # 가중치 기반 필터링 - 현실적인 테마 우선 - realistic_themes = [] - for theme_key, theme_data in data.get('core_themes', {}).items(): - weight = theme_data.get('weight', 0.1) - # 현실적인 테마에 더 높은 가중치 - if any(word in theme_key for word in ['family', 'love', 'work', 'memory', 'identity', 'aging']): - weight *= 1.5 - elif any(word in theme_key for word in ['digital', 'extinction', 'apocalypse', 'quantum']): - weight *= 0.5 - realistic_themes.append((theme_key, weight)) - - # 가중치 기반 선택 - themes = [t[0] for t in sorted(realistic_themes, key=lambda x: x[1], reverse=True)[:10]] - - themes_data = { - "themes": themes if themes else ["family secrets", "career crisis", "lost love"], - "characters": [], - "hooks": [], - "questions": [] - } - - # Extract realistic data - for char_data in data.get('characters', {}).values(): - for variation in char_data.get('variations', []): - # 현실적인 캐릭터 필터링 - if not any(word in variation.lower() for word in ['cyborg', 'quantum', 'binary', 'extinct']): - themes_data["characters"].append(variation) - - for hook_list in data.get('narrative_hooks', {}).values(): - for hook in hook_list: - # 현실적인 사건 필터링 - if not any(word in hook.lower() for word in ['download', 'digital', 'algorithm', 'corporate subscription']): - themes_data["hooks"].append(hook) - - for phil_data in data.get('philosophies', {}).values(): - themes_data["questions"].extend(phil_data.get('core_questions', [])) - - # 기본값 설정 - if not themes_data["characters"]: - themes_data["characters"] = ["struggling artist", "retired teacher", "young mother", "elderly caregiver", "small business owner"] - if not themes_data["hooks"]: - themes_data["hooks"] = ["discovering family secret", "unexpected reunion", "facing illness", "losing home", "finding old letters"] - if not themes_data["questions"]: - themes_data["questions"] = ["What makes a family?", "How do we find meaning?", "Can we escape our past?", "What legacy do we leave?"] - - # Random selection - import secrets - theme = secrets.choice(themes_data["themes"]) - character = secrets.choice(themes_data["characters"]) - hook = secrets.choice(themes_data["hooks"]) - question = secrets.choice(themes_data["questions"]) - - # 언어별 프롬프트 - 톤과 스타일 섹션 제거 - if language == "Korean": - # 한국어 번역 및 자연스러운 표현 - theme_kr = translate_theme_naturally(theme, "theme") - character_kr = translate_theme_naturally(character, "character") - hook_kr = translate_theme_naturally(hook, "hook") - question_kr = translate_theme_naturally(question, "question") - - prompt = f"""다음 요소들을 사용하여 현실적이고 공감가능한 소설 주제를 생성하세요: +.stat-label { + display: block; + font-size: 0.9em; + color: #5a453a; + margin-bottom: 5px; +} -주제: {theme_kr} -인물: {character_kr} -사건: {hook_kr} -핵심 질문: {question_kr} +.stat-value { + display: block; + font-size: 2em; + font-weight: bold; + color: #2c1810; + font-family: 'Playfair Display', 'Georgia', serif; +} -요구사항: -1. 현대 한국 사회에서 일어날 수 있는 현실적인 이야기 -2. 보편적으로 공감할 수 있는 인물과 상황 -3. 구체적이고 생생한 배경 설정 -4. 깊이 있는 심리 묘사가 가능한 갈등 +.theme-cards-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + gap: 20px; + padding: 20px; +} -다음 형식으로 간결하게 작성하세요: +.theme-card { + background: linear-gradient(145deg, #ffffff 0%, #fdfcf8 100%); + border: 1px solid #e8dcc6; + border-radius: 12px; + padding: 20px; + box-shadow: 0 4px 12px rgba(139, 69, 19, 0.06); + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} -[한 문장으로 된 매력적인 첫 문장] +.theme-card:hover { + transform: translateY(-3px); + box-shadow: 0 6px 20px rgba(139, 69, 19, 0.12); +} -주인공은 [구체적인 상황의 인물]입니다. -[핵심 사건]을 계기로 [내적 갈등]에 직면하게 되고, -결국 [철학적 질문]에 대한 답을 찾아가는 여정을 그립니다.""" - - else: - prompt = f"""Generate a realistic and relatable novel theme using these elements: +.theme-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 1px solid #e8dcc6; +} -Theme: {theme} -Character: {character} -Event: {hook} -Core Question: {question} +.theme-id { + font-family: 'Courier New', monospace; + color: #8b6239; + font-size: 0.85em; + font-weight: bold; +} -Requirements: -1. A story that could happen in contemporary society -2. Universally relatable characters and situations -3. Specific and vivid settings -4. Conflicts allowing deep psychological exploration +.theme-timestamp { + font-size: 0.8em; + color: #8a7968; +} -Write concisely in this format: +.theme-card-content { + margin-bottom: 15px; +} -[One compelling opening sentence] +.theme-preview { + font-family: 'Georgia', serif; + line-height: 1.6; + color: #3d2914; + margin-bottom: 12px; + min-height: 80px; +} -The protagonist is [character in specific situation]. -Through [key event], they face [internal conflict], -ultimately embarking on a journey to answer [philosophical question].""" +.theme-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; +} - # Use the UnifiedLiterarySystem's LLM to generate coherent theme - system = UnifiedLiterarySystem() - - # Call LLM synchronously for theme generation - messages = [{"role": "user", "content": prompt}] - generated_theme = system.call_llm_sync(messages, "director", language) - - # 톤과 스타일 섹션 제거 - 불필요한 반복 내용 삭제 - if "**톤과 스타일:**" in generated_theme or "**Tone and Style:**" in generated_theme: - lines = generated_theme.split('\n') - filtered_lines = [] - skip = False - for line in lines: - if "톤과 스타일" in line or "Tone and Style" in line: - skip = True - elif skip and (line.strip() == "" or line.startswith("**")): - skip = False - if not skip: - filtered_lines.append(line) - generated_theme = '\n'.join(filtered_lines).strip() - - return generated_theme - - except Exception as e: - logger.error(f"Theme generation error: {str(e)}") - # Fallback to simple realistic themes - fallback_themes = { - "Korean": [ - """"아버지가 돌아가신 날, 나는 그가 평생 숨겨온 또 다른 가족의 존재를 알게 되었다." +.theme-tag { + display: inline-block; + padding: 3px 10px; + background: #f0e6d6; + border-radius: 15px; + font-size: 0.75em; + color: #6d4e31; + border: 1px solid #d4c4b0; +} -주인공은 평범한 회사원으로 살아온 40대 여성입니다. -아버지의 장례식에서 낯선 여인과 그녀의 딸을 만나게 되면서 가족의 의미에 대해 다시 생각하게 되고, -결국 진정한 가족이란 무엇인지에 대한 답을 찾아가는 여정을 그립니다.""", - - """"서른 년간 가르친 학교에서 나온 날, 처음으로 내가 누구인지 몰랐다." +.theme-card-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 15px; + border-top: 1px solid #e8dcc6; +} -주인공은 정년퇴직을 맞은 고등학교 국어 교사입니다. -갑작스러운 일상의 공백 속에서 잊고 지냈던 젊은 날의 꿈을 마주하게 되고, -결국 남은 인생에서 무엇을 할 것인가에 대한 답을 찾아가는 여정을 그립니다.""" - ], - "English": [ - """"The day my father died, I discovered he had another family he'd hidden all his life." +.theme-stat { + font-size: 0.85em; + color: #8a7968; + margin-right: 10px; +} + +.theme-action-btn { + padding: 6px 15px; + font-size: 0.85em; + border-radius: 6px; + border: 1px solid #d4a574; + background: linear-gradient(145deg, #faf8f5 0%, #f0e8dc 100%); + color: #3d2914; + cursor: pointer; + transition: all 0.2s ease; + font-family: 'Georgia', serif; +} + +.theme-action-btn:hover { + background: linear-gradient(145deg, #e4c896 0%, #d4a574 100%); + transform: translateY(-1px); +} + +.use-btn { + background: linear-gradient(145deg, #e4c896 0%, #d4a574 100%); + font-weight: bold; +} + +.empty-library { + text-align: center; + padding: 60px 20px; + color: #5a453a; + font-size: 1.1em; + font-style: italic; +} + +/* Search box styling */ +.library-search { + margin-bottom: 20px; +} + +.library-search input { + width: 100%; + padding: 12px 20px; + font-size: 1em; + border: 1px solid #d4c4b0; + border-radius: 8px; + background: #fffefa; + font-family: 'Georgia', serif; +} -The protagonist is a woman in her 40s who has lived as an ordinary office worker. -Through meeting a strange woman and her daughter at her father's funeral, she confronts what family truly means, -ultimately embarking on a journey to answer what constitutes a real family.""", - - """"The day I left the school where I'd taught for thirty years, I didn't know who I was anymore." +/* Theme viewer modal */ +.theme-viewer { + background: linear-gradient(to bottom, #ffffff 0%, #fdfcfa 100%); + padding: 30px; + border-radius: 12px; + box-shadow: 0 10px 30px rgba(139, 69, 19, 0.15); + max-height: 600px; + overflow-y: auto; +} -The protagonist is a high school literature teacher facing retirement. -Through the sudden emptiness of daily life, they confront long-forgotten dreams of youth, -ultimately embarking on a journey to answer what to do with the remaining years.""" - ] - } - - import secrets - return secrets.choice(fallback_themes.get(language, fallback_themes["English"])) +.theme-viewer-header { + border-bottom: 2px solid #d4a574; + padding-bottom: 15px; + margin-bottom: 20px; +} -def translate_theme_naturally(text, category): - """자연스러운 한국어 번역""" - translations = { - # 테마 - "family secrets": "가족의 비밀", - "career transition": "인생의 전환점", - "lost love": "잃어버린 사랑", - "friendship test": "우정의 시험", - "generational conflict": "세대 간 갈등", - "digital extinction": "디지털 시대의 소외", - "sensory revolution": "감각의 혁명", - "temporal paradox": "시간의 역설", - - # 캐릭터 - "struggling artist": "생활고에 시달리는 예술가", - "retired teacher": "은퇴한 교사", - "young mother": "젊은 엄마", - "elderly caregiver": "노인을 돌보는 간병인", - "small business owner": "작은 가게 주인", - "middle-aged teacher": "중년의 교사", - "retiring doctor": "은퇴를 앞둔 의사", - "single parent": "혼자 아이를 키우는 부모", - "immigrant artist": "이민자 예술가", - "war veteran": "전쟁 참전용사", - "last person who dreams without ads": "광고 없이 꿈꾸는 마지막 사람", - "memory trader": "기억 거래상", - - # 사건 - "discovering family secret": "가족의 비밀을 발견하다", - "unexpected reunion": "예상치 못한 재회", - "facing illness": "질병과 마주하다", - "losing home": "집을 잃다", - "finding old letters": "오래된 편지를 발견하다", - "unexpected inheritance": "뜻밖의 유산", - "old diary discovery": "오래된 일기장 발견", - "chance reunion": "우연한 재회", - "life-changing diagnosis": "인생을 바꾸는 진단", - "sudden job loss": "갑작스러운 실직", - "discovers their memories belong to a corporate subscription": "기억이 기업 서비스의 일부임을 발견하다", - - # 질문 - "What makes a family?": "가족이란 무엇인가?", - "How do we find meaning?": "우리는 어떻게 의미를 찾는가?", - "Can we escape our past?": "과거로부터 벗어날 수 있는가?", - "What legacy do we leave?": "우리는 어떤 유산을 남기는가?", - "What defines family?": "무엇이 가족을 정의하는가?", - "Can people truly change?": "사람은 정말 변할 수 있는가?", - "What is worth sacrificing?": "무엇을 위해 희생할 가치가 있는가?", - "How do we forgive?": "우리는 어떻게 용서하는가?", - "What remains human when humanity is optional?": "인간성이 선택사항일 때 무엇이 인간으로 남는가?" - } - - # 먼저 정확한 매칭 시도 - if text in translations: - return translations[text] - - # 부분 매칭 시도 - text_lower = text.lower() - for key, value in translations.items(): - if key.lower() in text_lower or text_lower in key.lower(): - return value - - # 번역이 없으면 원문 반환 - return text +.theme-viewer-content { + font-family: 'Georgia', serif; + line-height: 1.8; + color: #2c1810; +} -# Update the handle_random_theme function in create_interface -def handle_random_theme(language): - """Handle random theme generation with improved feedback""" - try: - # Generate theme using LLM for natural output - theme = generate_random_theme(language) - logger.info(f"Generated theme successfully") - return theme - except Exception as e: - logger.error(f"Random theme generation failed: {str(e)}") - # Return a simple fallback theme - if language == "Korean": - return "기억을 잃어가는 노인과 AI 간병인의 특별한 우정" - else: - return "An unlikely friendship between an elderly person losing memories and their AI caregiver" - -# Update the augment_query method to better handle generated themes -def augment_query(self, user_query: str, language: str) -> str: - """Augment and clean user query""" - # Remove any formatting artifacts from random generation - if "**" in user_query or "##" in user_query: - # This is likely a generated theme with formatting - # Extract the essence without formatting - lines = user_query.split('\n') - cleaned_parts = [] - - for line in lines: - # Remove markdown formatting - line = line.replace('**', '').replace('##', '').strip() - if line and not line.startswith(('-', '•', '*')) and ':' not in line[:20]: - cleaned_parts.append(line) - - if cleaned_parts: - user_query = ' '.join(cleaned_parts[:3]) # Use first few meaningful lines - - # If query is too short, enhance it - if len(user_query.split()) < 15: - if language == "Korean": - return f"{user_query}\n\n이 주제를 현대적 관점에서 재해석하여 인간 존재의 본질과 기술 시대의 딜레마를 탐구하는 8,000단어 분량의 철학적 중편소설을 작성하세요." - else: - return f"{user_query}\n\nReinterpret this theme from a contemporary perspective to explore the essence of human existence and dilemmas of the technological age in an 8,000-word philosophical novella." - - return user_query - -# Add method to UnifiedLiterarySystem class for better theme processing -def process_generated_theme(self, theme_text: str, language: str) -> str: - """Process generated theme for novel writing""" - # Extract key elements from generated theme - theme_elements = { - "title": "", - "opening": "", - "protagonist": "", - "conflict": "", - "exploration": "" - } - - lines = theme_text.split('\n') - current_key = None - - for line in lines: - line = line.strip() - if not line: - continue - - # Detect sections - if any(marker in line.lower() for marker in ['title:', 'opening:', 'protagonist:', 'conflict:', 'exploration:', '제목:', '첫 문장:', '주인공:', '갈등:', '탐구']): - for key in theme_elements: - if key in line.lower() or (language == "Korean" and key in translate_to_korean(line.lower())): - current_key = key - # Extract content after colon - if ':' in line: - content = line.split(':', 1)[1].strip() - if content: - theme_elements[current_key] = content - break - elif current_key and line: - # Continue adding to current element - theme_elements[current_key] = (theme_elements[current_key] + " " + line).strip() - - # Construct a coherent theme summary - if language == "Korean": - summary = f"{theme_elements.get('title', '무제')}. " - if theme_elements.get('opening'): - summary += f"'{theme_elements['opening']}' " - summary += f"{theme_elements.get('protagonist', '주인공')}의 이야기. " - summary += f"{theme_elements.get('conflict', '')} " - summary += f"{theme_elements.get('exploration', '')}" - else: - summary = f"{theme_elements.get('title', 'Untitled')}. " - if theme_elements.get('opening'): - summary += f"'{theme_elements['opening']}' " - summary += f"The story of {theme_elements.get('protagonist', 'a protagonist')}. " - summary += f"{theme_elements.get('conflict', '')} " - summary += f"{theme_elements.get('exploration', '')}" - - return summary.strip() - -# Create Gradio interface -# Create Gradio interface - Writer's Study Theme +.theme-viewer-metadata { + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid #e8dcc6; + font-size: 0.9em; + color: #5a453a; +} +""" + +# Create Gradio interface - Writer's Study Theme with Random Library def create_interface(): + # Combine CSS + combined_css = custom_css + theme_library_css + # Using Soft theme with safe color options - with gr.Blocks(theme=gr.themes.Soft(), css=custom_css, title="AGI NOVEL Generator") as interface: + with gr.Blocks(theme=gr.themes.Soft(), css=combined_css, title="AGI NOVEL Generator") as interface: gr.HTML("""

📖 AGI NOVEL Generator

@@ -2613,263 +2939,812 @@ def create_interface(): badge - badge - - - badge - -
- -

- Artificial General Intelligence (AGI) denotes an artificial system possessing human-level, general-purpose intelligence and is now commonly framed as AI that can outperform humans in most economically and intellectually valuable tasks. Demonstrating such breadth requires evaluating not only calculation, logical reasoning, and perception but also the distinctly human faculties of creativity and language. Among the creative tests, the most demanding is the production of a full-length novel running 100k–200k words. An extended narrative forces an AGI candidate to exhibit (1) sustained long-term memory and context tracking (2) intricate causal and plot planning (3) nuanced cultural and emotional expression (4) autonomous self-censorship and ethical filtering to avoid harmful or biased content and (5) verifiable originality beyond simple recombination of training data. -

- -
- 🎲 Novel Theme Random Generator: This system can generate up to approximately 170 quadrillion (1.7 × 10¹⁷) unique novel themes. - Even writing 100 novels per day, it would take 4.6 million years to exhaust all combinations. - Click the "Random" button to explore infinite creative possibilities! -
- -
- ⏱️ Note: Creating a complete novel takes approximately 20 minutes. If your web session disconnects, you can restore your work using the "Session Recovery" feature. -
- -
- 🎯 Core Innovation: Not fragmented texts from multiple writers, - but a genuine full-length novel written consistently by a single author from beginning to end. -
- - """) - - # State management - current_session_id = gr.State(None) - - with gr.Row(): - with gr.Column(scale=1): - with gr.Group(elem_classes=["input-section"]): - gr.Markdown("### ✍️ Writing Desk") - query_input = gr.Textbox( - label="Novel Theme", - placeholder="""Enter your novella theme. Like a seed that grows into a tree, your theme will blossom into a full narrative...""", - lines=5, - elem_id="theme_input" - ) - - language_select = gr.Radio( - choices=["English", "Korean"], - value="English", - label="Language", - elem_id="language_select" - ) - - with gr.Row(): - random_btn = gr.Button("🎲 Random Theme", variant="primary", scale=1) - submit_btn = gr.Button("🖋️ Begin Writing", variant="secondary", scale=2) - clear_btn = gr.Button("🗑️ Clear", scale=1) - - status_text = gr.Textbox( - label="Writing Progress", - interactive=False, - value="✨ Ready to begin your literary journey", - elem_id="status_text" - ) - - # Session management - with gr.Group(elem_classes=["session-section"]): - gr.Markdown("### 📚 Your Library") - session_dropdown = gr.Dropdown( - label="Saved Manuscripts", - choices=[], - interactive=True, - elem_id="session_dropdown" - ) - with gr.Row(): - refresh_btn = gr.Button("🔄 Refresh Library", scale=1) - resume_btn = gr.Button("📖 Continue Writing", variant="secondary", scale=1) - auto_recover_btn = gr.Button("🔮 Recover Last Work", scale=1) - - with gr.Column(scale=2): - with gr.Tab("🖋️ Writing Process", elem_id="writing_tab"): - stages_display = gr.Markdown( - value="*Your writing journey will unfold here, like pages turning in a book...*", - elem_id="stages-display" - ) - - with gr.Tab("📖 Completed Manuscript", elem_id="manuscript_tab"): - novel_output = gr.Markdown( - value="*Your completed novel will appear here, ready to be read and cherished...*", - elem_id="novel-output" - ) - - with gr.Group(elem_classes=["download-section"]): - gr.Markdown("### 📦 Bind Your Book") - with gr.Row(): - format_select = gr.Radio( - choices=["DOCX", "TXT"], - value="DOCX" if DOCX_AVAILABLE else "TXT", - label="Format", - elem_id="format_select" - ) - download_btn = gr.Button("📥 Download Manuscript", variant="secondary") - - download_file = gr.File( - label="Your Manuscript", - visible=False, - elem_id="download_file" - ) - - # Hidden state - novel_text_state = gr.State("") - - # Examples with literary flair - with gr.Row(): - gr.Examples( - examples=[ - ["A daughter discovering her mother's hidden past through old letters found in an attic trunk"], - ["An architect losing sight who learns to design through touch, sound, and the memories of light"], - ["A translator replaced by AI rediscovering the essence of language through handwritten poetry"], - ["A middle-aged man who lost his job finding new meaning in the rhythms of rural life"], - ["A doctor with war trauma finding healing through Doctors Without Borders missions"], - ["A neighborhood coming together to save their beloved bookstore from corporate development"], - ["A year in the life of a professor losing memory and his devoted last student"] - ], - inputs=query_input, - label="💡 Inspiring Themes", - examples_per_page=7, - elem_id="example_themes" - ) - - # Event handlers - def refresh_sessions(): - try: - sessions = get_active_sessions("English") - return gr.update(choices=sessions) - except Exception as e: - logger.error(f"Session refresh error: {str(e)}") - return gr.update(choices=[]) - - def handle_auto_recover(language): - session_id, message = auto_recover_session(language) - return session_id, message - - def handle_random_theme(language): - """Handle random theme generation with language support""" - import time - import datetime - time.sleep(0.05) - logger.info(f"Random theme requested at {datetime.datetime.now()}") - theme = generate_random_theme(language) - logger.info(f"Generated theme: {theme[:100]}...") - return theme - - # Event connections - submit_btn.click( - fn=process_query, - inputs=[query_input, language_select, current_session_id], - outputs=[stages_display, novel_output, status_text, current_session_id] - ) - - novel_output.change( - fn=lambda x: x, - inputs=[novel_output], - outputs=[novel_text_state] - ) - - resume_btn.click( - fn=lambda x: x.split("...")[0] if x and "..." in x else x, - inputs=[session_dropdown], - outputs=[current_session_id] - ).then( - fn=resume_session, - inputs=[current_session_id, language_select], - outputs=[stages_display, novel_output, status_text, current_session_id] - ) - - auto_recover_btn.click( - fn=handle_auto_recover, - inputs=[language_select], - outputs=[current_session_id, status_text] - ).then( - fn=resume_session, - inputs=[current_session_id, language_select], - outputs=[stages_display, novel_output, status_text, current_session_id] - ) - - refresh_btn.click( - fn=refresh_sessions, - outputs=[session_dropdown] - ) - - clear_btn.click( - fn=lambda: ("", "", "✨ Ready to begin your literary journey", "", None), - outputs=[stages_display, novel_output, status_text, novel_text_state, current_session_id] - ) - - random_btn.click( - fn=lambda lang: generate_random_theme(lang), - inputs=[language_select], - outputs=[query_input], - queue=False - ) - - def handle_download(format_type, language, session_id, novel_text): - if not session_id or not novel_text: - return gr.update(visible=False) - - file_path = download_novel(novel_text, format_type, language, session_id) - if file_path: - return gr.update(value=file_path, visible=True) - else: - return gr.update(visible=False) - - download_btn.click( - fn=handle_download, - inputs=[format_select, language_select, current_session_id, novel_text_state], - outputs=[download_file] - ) - - # Load sessions on start - interface.load( - fn=refresh_sessions, - outputs=[session_dropdown] - ) - - return interface + + +badge + + + badge + + + +

+ Artificial General Intelligence (AGI) denotes an artificial system possessing human-level, general-purpose intelligence and is now commonly framed as AI that can outperform humans in most economically and intellectually valuable tasks. Demonstrating such breadth requires evaluating not only calculation, logical reasoning, and perception but also the distinctly human faculties of creativity and language. Among the creative tests, the most demanding is the production of a full-length novel running 100k–200k words. An extended narrative forces an AGI candidate to exhibit (1) sustained long-term memory and context tracking (2) intricate causal and plot planning (3) nuanced cultural and emotional expression (4) autonomous self-censorship and ethical filtering to avoid harmful or biased content and (5) verifiable originality beyond simple recombination of training data. +

+ +
+ 🎲 Novel Theme Random Generator: This system can generate up to approximately 170 quadrillion (1.7 × 10¹⁷) unique novel themes. + Even writing 100 novels per day, it would take 4.6 million years to exhaust all combinations. + Click the "Random" button to explore infinite creative possibilities! +
+ +
+ ⏱️ Note: Creating a complete novel takes approximately 20 minutes. If your web session disconnects, you can restore your work using the "Session Recovery" feature. +
+ +
+ 🎯 Core Innovation: Not fragmented texts from multiple writers, + but a genuine full-length novel written consistently by a single author from beginning to end. +
+ + """) + + # State management + current_session_id = gr.State(None) + selected_theme_id = gr.State(None) + + # Create tabs + with gr.Tabs(): + # Main Novel Writing Tab + with gr.Tab("📝 Novel Writing", elem_id="writing_main_tab"): + with gr.Row(): + with gr.Column(scale=1): + with gr.Group(elem_classes=["input-section"]): + gr.Markdown("### ✍️ Writing Desk") + query_input = gr.Textbox( + label="Novel Theme", + placeholder="""Enter your novella theme. Like a seed that grows into a tree, your theme will blossom into a full narrative...""", + lines=5, + elem_id="theme_input" + ) + + language_select = gr.Radio( + choices=["English", "Korean"], + value="English", + label="Language", + elem_id="language_select" + ) + + with gr.Row(): + random_btn = gr.Button("🎲 Random Theme", variant="primary", scale=1) + submit_btn = gr.Button("🖋️ Begin Writing", variant="secondary", scale=2) + clear_btn = gr.Button("🗑️ Clear", scale=1) + + status_text = gr.Textbox( + label="Writing Progress", + interactive=False, + value="✨ Ready to begin your literary journey", + elem_id="status_text" + ) + + # Session management + with gr.Group(elem_classes=["session-section"]): + gr.Markdown("### 📚 Your Library") + session_dropdown = gr.Dropdown( + label="Saved Manuscripts", + choices=[], + interactive=True, + elem_id="session_dropdown" + ) + with gr.Row(): + refresh_btn = gr.Button("🔄 Refresh Library", scale=1) + resume_btn = gr.Button("📖 Continue Writing", variant="secondary", scale=1) + auto_recover_btn = gr.Button("🔮 Recover Last Work", scale=1) + + with gr.Column(scale=2): + with gr.Tab("🖋️ Writing Process", elem_id="writing_tab"): + stages_display = gr.Markdown( + value="*Your writing journey will unfold here, like pages turning in a book...*", + elem_id="stages-display" + ) + + with gr.Tab("📖 Completed Manuscript", elem_id="manuscript_tab"): + novel_output = gr.Markdown( + value="*Your completed novel will appear here, ready to be read and cherished...*", + elem_id="novel-output" + ) + + with gr.Group(elem_classes=["download-section"]): + gr.Markdown("### 📦 Bind Your Book") + with gr.Row(): + format_select = gr.Radio( + choices=["DOCX", "TXT"], + value="DOCX" if DOCX_AVAILABLE else "TXT", + label="Format", + elem_id="format_select" + ) + download_btn = gr.Button("📥 Download Manuscript", variant="secondary") + + download_file = gr.File( + label="Your Manuscript", + visible=False, + elem_id="download_file" + ) + + # Hidden state + novel_text_state = gr.State("") + + # Examples with literary flair + with gr.Row(): + gr.Examples( + examples=[ + ["A daughter discovering her mother's hidden past through old letters found in an attic trunk"], + ["An architect losing sight who learns to design through touch, sound, and the memories of light"], + ["A translator replaced by AI rediscovering the essence of language through handwritten poetry"], + ["A middle-aged man who lost his job finding new meaning in the rhythms of rural life"], + ["A doctor with war trauma finding healing through Doctors Without Borders missions"], + ["A neighborhood coming together to save their beloved bookstore from corporate development"], + ["A year in the life of a professor losing memory and his devoted last student"] + ], + inputs=query_input, + label="💡 Inspiring Themes", + examples_per_page=7, + elem_id="example_themes" + ) + + # Random Theme Library Tab + with gr.Tab("🎲 Random Theme Library", elem_id="library_tab"): + with gr.Column(): + gr.Markdown(""" + ### 📚 Random Theme Library + + Browse through all randomly generated themes. Each theme is unique and can be used to create a novel. + Click "View Full" to see the complete theme or "Use This" to start writing with it. + """) + + with gr.Row(): + library_search = gr.Textbox( + label="Search Themes", + placeholder="Search by keywords...", + elem_classes=["library-search"] + ) + library_refresh_btn = gr.Button("🔄 Refresh", scale=1) + library_language_filter = gr.Radio( + choices=["All", "English", "Korean"], + value="All", + label="Filter by Language", + scale=2 + ) + + library_display = gr.HTML( + value=get_theme_library_display(), + elem_id="library-display" + ) + + # Hidden components for JavaScript interaction + with gr.Row(visible=False): + view_theme_btn = gr.Button("View Theme", elem_id="view_theme_btn") + use_theme_btn = gr.Button("Use Theme", elem_id="use_theme_btn") + + # Theme viewer + with gr.Group(visible=False) as theme_viewer_group: + theme_viewer = gr.Markdown( + elem_classes=["theme-viewer"] + ) + with gr.Row(): + close_viewer_btn = gr.Button("Close", scale=1) + use_viewed_theme_btn = gr.Button("Use This Theme", variant="primary", scale=2) + + # Event handlers + def refresh_sessions(): + try: + sessions = get_active_sessions("English") + return gr.update(choices=sessions) + except Exception as e: + logger.error(f"Session refresh error: {str(e)}") + return gr.update(choices=[]) + + def handle_auto_recover(language): + session_id, message = auto_recover_session(language) + return session_id, message + + def handle_random_theme(language): + """Handle random theme generation with library storage""" + import time + import datetime + time.sleep(0.05) + logger.info(f"Random theme requested at {datetime.datetime.now()}") + theme = generate_random_theme(language) + logger.info(f"Generated theme: {theme[:100]}...") + return theme + + def refresh_library(language_filter, search_query): + """Refresh theme library display""" + lang = None if language_filter == "All" else language_filter + return get_theme_library_display(lang, search_query) + + def handle_view_theme(evt: gr.EventData): + """Handle theme viewing""" + # Extract theme ID from JavaScript + theme_id = getattr(evt, '_js', {}).get('viewThemeId', '') + if not theme_id: + return gr.update(), gr.update(), None + + theme_data = NovelDatabase.get_theme_by_id(theme_id) + if theme_data: + NovelDatabase.update_theme_view_count(theme_id) + + viewer_content = f""" +### Theme #{theme_id[:8]} + +**Generated:** {theme_data.get('generated_at', 'Unknown')} +**Language:** {theme_data.get('language', 'Unknown')} +**Views:** {theme_data.get('view_count', 0)} | **Uses:** {theme_data.get('used_count', 0)} + +--- + +{theme_data.get('theme_text', '')} + +--- + +**Tags:** {', '.join(json.loads(theme_data.get('tags', '[]')))} +""" + return gr.update(value=viewer_content), gr.update(visible=True), theme_id + + return gr.update(), gr.update(), None + + def handle_use_theme(theme_id): + """Handle using a theme from library""" + if not theme_id: + return gr.update(), "No theme selected" + + theme_data = NovelDatabase.get_theme_by_id(theme_id) + if theme_data: + NovelDatabase.update_theme_used_count(theme_id) + return gr.update(value=theme_data.get('theme_text', '')), f"Theme #{theme_id[:8]} loaded" + + return gr.update(), "Theme not found" + + # Event connections + submit_btn.click( + fn=process_query, + inputs=[query_input, language_select, current_session_id], + outputs=[stages_display, novel_output, status_text, current_session_id] + ) + + novel_output.change( + fn=lambda x: x, + inputs=[novel_output], + outputs=[novel_text_state] + ) + + resume_btn.click( + fn=lambda x: x.split("...")[0] if x and "..." in x else x, + inputs=[session_dropdown], + outputs=[current_session_id] + ).then( + fn=resume_session, + inputs=[current_session_id, language_select], + outputs=[stages_display, novel_output, status_text, current_session_id] + ) + + auto_recover_btn.click( + fn=handle_auto_recover, + inputs=[language_select], + outputs=[current_session_id, status_text] + ).then( + fn=resume_session, + inputs=[current_session_id, language_select], + outputs=[stages_display, novel_output, status_text, current_session_id] + ) + + refresh_btn.click( + fn=refresh_sessions, + outputs=[session_dropdown] + ) + + clear_btn.click( + fn=lambda: ("", "", "✨ Ready to begin your literary journey", "", None), + outputs=[stages_display, novel_output, status_text, novel_text_state, current_session_id] + ) + + random_btn.click( + fn=handle_random_theme, + inputs=[language_select], + outputs=[query_input], + queue=False + ) + + # Library event handlers + library_refresh_btn.click( + fn=refresh_library, + inputs=[library_language_filter, library_search], + outputs=[library_display] + ) + + library_search.change( + fn=refresh_library, + inputs=[library_language_filter, library_search], + outputs=[library_display] + ) + + library_language_filter.change( + fn=refresh_library, + inputs=[library_language_filter, library_search], + outputs=[library_display] + ) + + view_theme_btn.click( + fn=handle_view_theme, + outputs=[theme_viewer, theme_viewer_group, selected_theme_id], + _js="() => ({ viewThemeId: window.viewThemeId })" + ) + + use_theme_btn.click( + fn=lambda: gr.update(selected=0), # Switch to main tab + outputs=[gr.Tabs] + ).then( + fn=handle_use_theme, + inputs=[selected_theme_id], + outputs=[query_input, status_text], + _js="() => window.useThemeId" + ) + + use_viewed_theme_btn.click( + fn=lambda: gr.update(selected=0), # Switch to main tab + outputs=[gr.Tabs] + ).then( + fn=handle_use_theme, + inputs=[selected_theme_id], + outputs=[query_input, status_text] + ) + + close_viewer_btn.click( + fn=lambda: gr.update(visible=False), + outputs=[theme_viewer_group] + ) + + def handle_download(format_type, language, session_id, novel_text): + if not session_id or not novel_text: + return gr.update(visible=False) + + file_path = download_novel(novel_text, format_type, language, session_id) + if file_path: + return gr.update(value=file_path, visible=True) + else: + return gr.update(visible=False) + + download_btn.click( + fn=handle_download, + inputs=[format_select, language_select, current_session_id, novel_text_state], + outputs=[download_file] + ) + + # Load sessions and library on start + interface.load( + fn=lambda: (refresh_sessions(), refresh_library("All", "")), + outputs=[session_dropdown, library_display] + ) + + return interface + +# Main execution +if __name__ == "__main__": + logger.info("AGI NOVEL Generator v2.0 Starting...") + logger.info("=" * 60) + + # Environment check + logger.info(f"API Endpoint: {API_URL}") + logger.info(f"Target Length: {TARGET_WORDS:,} words") + logger.info(f"Minimum Words per Part: {MIN_WORDS_PER_PART:,} words") + logger.info("System Features: Single writer + Immediate part-by-part critique") + + if BRAVE_SEARCH_API_KEY: + logger.info("Web search enabled.") + else: + logger.warning("Web search disabled.") + + if DOCX_AVAILABLE: + logger.info("DOCX export enabled.") + else: + logger.warning("DOCX export disabled.") + + logger.info("=" * 60) + + # Initialize database + logger.info("Initializing database...") + NovelDatabase.init_db() + logger.info("Database initialization complete.") + + # Create and launch interface + interface = create_interface() + + interface.launch( + server_name="0.0.0.0", + server_port=7860, + share=False, + debug=True + )badge + + + badge + + + +

+ Artificial General Intelligence (AGI) denotes an artificial system possessing human-level, general-purpose intelligence and is now commonly framed as AI that can outperform humans in most economically and intellectually valuable tasks. Demonstrating such breadth requires evaluating not only calculation, logical reasoning, and perception but also the distinctly human faculties of creativity and language. Among the creative tests, the most demanding is the production of a full-length novel running 100k–200k words. An extended narrative forces an AGI candidate to exhibit (1) sustained long-term memory and context tracking (2) intricate causal and plot planning (3) nuanced cultural and emotional expression (4) autonomous self-censorship and ethical filtering to avoid harmful or biased content and (5) verifiable originality beyond simple recombination of training data. +

+ +
+ 🎲 Novel Theme Random Generator: This system can generate up to approximately 170 quadrillion (1.7 × 10¹⁷) unique novel themes. + Even writing 100 novels per day, it would take 4.6 million years to exhaust all combinations. + Click the "Random" button to explore infinite creative possibilities! +
+ +
+ ⏱️ Note: Creating a complete novel takes approximately 20 minutes. If your web session disconnects, you can restore your work using the "Session Recovery" feature. +
+ +
+ 🎯 Core Innovation: Not fragmented texts from multiple writers, + but a genuine full-length novel written consistently by a single author from beginning to end. +
+ + """) + + # State management + current_session_id = gr.State(None) + selected_theme_id = gr.State(None) + + # Create tabs + with gr.Tabs(): + # Main Novel Writing Tab + with gr.Tab("📝 Novel Writing", elem_id="writing_main_tab"): + with gr.Row(): + with gr.Column(scale=1): + with gr.Group(elem_classes=["input-section"]): + gr.Markdown("### ✍️ Writing Desk") + query_input = gr.Textbox( + label="Novel Theme", + placeholder="""Enter your novella theme. Like a seed that grows into a tree, your theme will blossom into a full narrative...""", + lines=5, + elem_id="theme_input" + ) + + language_select = gr.Radio( + choices=["English", "Korean"], + value="English", + label="Language", + elem_id="language_select" + ) + + with gr.Row(): + random_btn = gr.Button("🎲 Random Theme", variant="primary", scale=1) + submit_btn = gr.Button("🖋️ Begin Writing", variant="secondary", scale=2) + clear_btn = gr.Button("🗑️ Clear", scale=1) + + status_text = gr.Textbox( + label="Writing Progress", + interactive=False, + value="✨ Ready to begin your literary journey", + elem_id="status_text" + ) + + # Session management + with gr.Group(elem_classes=["session-section"]): + gr.Markdown("### 📚 Your Library") + session_dropdown = gr.Dropdown( + label="Saved Manuscripts", + choices=[], + interactive=True, + elem_id="session_dropdown" + ) + with gr.Row(): + refresh_btn = gr.Button("🔄 Refresh Library", scale=1) + resume_btn = gr.Button("📖 Continue Writing", variant="secondary", scale=1) + auto_recover_btn = gr.Button("🔮 Recover Last Work", scale=1) + + with gr.Column(scale=2): + with gr.Tab("🖋️ Writing Process", elem_id="writing_tab"): + stages_display = gr.Markdown( + value="*Your writing journey will unfold here, like pages turning in a book...*", + elem_id="stages-display" + ) + + with gr.Tab("📖 Completed Manuscript", elem_id="manuscript_tab"): + novel_output = gr.Markdown( + value="*Your completed novel will appear here, ready to be read and cherished...*", + elem_id="novel-output" + ) + + with gr.Group(elem_classes=["download-section"]): + gr.Markdown("### 📦 Bind Your Book") + with gr.Row(): + format_select = gr.Radio( + choices=["DOCX", "TXT"], + value="DOCX" if DOCX_AVAILABLE else "TXT", + label="Format", + elem_id="format_select" + ) + download_btn = gr.Button("📥 Download Manuscript", variant="secondary") + + download_file = gr.File( + label="Your Manuscript", + visible=False, + elem_id="download_file" + ) + + # Hidden state + novel_text_state = gr.State("") + + # Examples with literary flair + with gr.Row(): + gr.Examples( + examples=[ + ["A daughter discovering her mother's hidden past through old letters found in an attic trunk"], + ["An architect losing sight who learns to design through touch, sound, and the memories of light"], + ["A translator replaced by AI rediscovering the essence of language through handwritten poetry"], + ["A middle-aged man who lost his job finding new meaning in the rhythms of rural life"], + ["A doctor with war trauma finding healing through Doctors Without Borders missions"], + ["A neighborhood coming together to save their beloved bookstore from corporate development"], + ["A year in the life of a professor losing memory and his devoted last student"] + ], + inputs=query_input, + label="💡 Inspiring Themes", + examples_per_page=7, + elem_id="example_themes" + ) + + # Random Theme Library Tab + with gr.Tab("🎲 Random Theme Library", elem_id="library_tab"): + with gr.Column(): + gr.Markdown(""" + ### 📚 Random Theme Library + + Browse through all randomly generated themes. Each theme is unique and can be used to create a novel. + Click "View Full" to see the complete theme or "Use This" to start writing with it. + """) + + with gr.Row(): + library_search = gr.Textbox( + label="Search Themes", + placeholder="Search by keywords...", + elem_classes=["library-search"] + ) + library_refresh_btn = gr.Button("🔄 Refresh", scale=1) + library_language_filter = gr.Radio( + choices=["All", "English", "Korean"], + value="All", + label="Filter by Language", + scale=2 + ) + + library_display = gr.HTML( + value=get_theme_library_display(), + elem_id="library-display" + ) + + # Hidden components for JavaScript interaction + with gr.Row(visible=False): + view_theme_btn = gr.Button("View Theme", elem_id="view_theme_btn") + use_theme_btn = gr.Button("Use Theme", elem_id="use_theme_btn") + + # Theme viewer + with gr.Group(visible=False) as theme_viewer_group: + theme_viewer = gr.Markdown( + elem_classes=["theme-viewer"] + ) + with gr.Row(): + close_viewer_btn = gr.Button("Close", scale=1) + use_viewed_theme_btn = gr.Button("Use This Theme", variant="primary", scale=2) + + # Event handlers + def refresh_sessions(): + try: + sessions = get_active_sessions("English") + return gr.update(choices=sessions) + except Exception as e: + logger.error(f"Session refresh error: {str(e)}") + return gr.update(choices=[]) + + def handle_auto_recover(language): + session_id, message = auto_recover_session(language) + return session_id, message + + def handle_random_theme(language): + """Handle random theme generation with library storage""" + import time + import datetime + time.sleep(0.05) + logger.info(f"Random theme requested at {datetime.datetime.now()}") + theme = generate_random_theme(language) + logger.info(f"Generated theme: {theme[:100]}...") + return theme + + def refresh_library(language_filter, search_query): + """Refresh theme library display""" + lang = None if language_filter == "All" else language_filter + return get_theme_library_display(lang, search_query) + + def handle_view_theme(evt: gr.EventData): + """Handle theme viewing""" + # Extract theme ID from JavaScript + theme_id = getattr(evt, '_js', {}).get('viewThemeId', '') + if not theme_id: + return gr.update(), gr.update(), None + + theme_data = NovelDatabase.get_theme_by_id(theme_id) + if theme_data: + NovelDatabase.update_theme_view_count(theme_id) + + viewer_content = f""" +### Theme #{theme_id[:8]} + +**Generated:** {theme_data.get('generated_at', 'Unknown')} +**Language:** {theme_data.get('language', 'Unknown')} +**Views:** {theme_data.get('view_count', 0)} | **Uses:** {theme_data.get('used_count', 0)} + +--- + +{theme_data.get('theme_text', '')} + +--- + +**Tags:** {', '.join(json.loads(theme_data.get('tags', '[]')))} +""" + return gr.update(value=viewer_content), gr.update(visible=True), theme_id + + return gr.update(), gr.update(), None + + def handle_use_theme(theme_id): + """Handle using a theme from library""" + if not theme_id: + return gr.update(), "No theme selected" + + theme_data = NovelDatabase.get_theme_by_id(theme_id) + if theme_data: + NovelDatabase.update_theme_used_count(theme_id) + return gr.update(value=theme_data.get('theme_text', '')), f"Theme #{theme_id[:8]} loaded" + + return gr.update(), "Theme not found" + + # Event connections + submit_btn.click( + fn=process_query, + inputs=[query_input, language_select, current_session_id], + outputs=[stages_display, novel_output, status_text, current_session_id] + ) + + novel_output.change( + fn=lambda x: x, + inputs=[novel_output], + outputs=[novel_text_state] + ) + + resume_btn.click( + fn=lambda x: x.split("...")[0] if x and "..." in x else x, + inputs=[session_dropdown], + outputs=[current_session_id] + ).then( + fn=resume_session, + inputs=[current_session_id, language_select], + outputs=[stages_display, novel_output, status_text, current_session_id] + ) + + auto_recover_btn.click( + fn=handle_auto_recover, + inputs=[language_select], + outputs=[current_session_id, status_text] + ).then( + fn=resume_session, + inputs=[current_session_id, language_select], + outputs=[stages_display, novel_output, status_text, current_session_id] + ) + + refresh_btn.click( + fn=refresh_sessions, + outputs=[session_dropdown] + ) + + clear_btn.click( + fn=lambda: ("", "", "✨ Ready to begin your literary journey", "", None), + outputs=[stages_display, novel_output, status_text, novel_text_state, current_session_id] + ) + + random_btn.click( + fn=handle_random_theme, + inputs=[language_select], + outputs=[query_input], + queue=False + ) + + # Library event handlers + library_refresh_btn.click( + fn=refresh_library, + inputs=[library_language_filter, library_search], + outputs=[library_display] + ) + + library_search.change( + fn=refresh_library, + inputs=[library_language_filter, library_search], + outputs=[library_display] + ) + + library_language_filter.change( + fn=refresh_library, + inputs=[library_language_filter, library_search], + outputs=[library_display] + ) + + view_theme_btn.click( + fn=handle_view_theme, + outputs=[theme_viewer, theme_viewer_group, selected_theme_id], + _js="() => ({ viewThemeId: window.viewThemeId })" + ) + + use_theme_btn.click( + fn=lambda: gr.update(selected=0), # Switch to main tab + outputs=[gr.Tabs] + ).then( + fn=handle_use_theme, + inputs=[selected_theme_id], + outputs=[query_input, status_text], + _js="() => window.useThemeId" + ) + + use_viewed_theme_btn.click( + fn=lambda: gr.update(selected=0), # Switch to main tab + outputs=[gr.Tabs] + ).then( + fn=handle_use_theme, + inputs=[selected_theme_id], + outputs=[query_input, status_text] + ) + + close_viewer_btn.click( + fn=lambda: gr.update(visible=False), + outputs=[theme_viewer_group] + ) + + def handle_download(format_type, language, session_id, novel_text): + if not session_id or not novel_text: + return gr.update(visible=False) + + file_path = download_novel(novel_text, format_type, language, session_id) + if file_path: + return gr.update(value=file_path, visible=True) + else: + return gr.update(visible=False) + + download_btn.click( + fn=handle_download, + inputs=[format_select, language_select, current_session_id, novel_text_state], + outputs=[download_file] + ) + + # Load sessions and library on start + interface.load( + fn=lambda: (refresh_sessions(), refresh_library("All", "")), + outputs=[session_dropdown, library_display] + ) + + return interface # Main execution if __name__ == "__main__": - logger.info("AGI NOVEL Generator v2.0 Starting...") - logger.info("=" * 60) - - # Environment check - logger.info(f"API Endpoint: {API_URL}") - logger.info(f"Target Length: {TARGET_WORDS:,} words") - logger.info(f"Minimum Words per Part: {MIN_WORDS_PER_PART:,} words") - logger.info("System Features: Single writer + Immediate part-by-part critique") - - if BRAVE_SEARCH_API_KEY: - logger.info("Web search enabled.") - else: - logger.warning("Web search disabled.") - - if DOCX_AVAILABLE: - logger.info("DOCX export enabled.") - else: - logger.warning("DOCX export disabled.") - - logger.info("=" * 60) - - # Initialize database - logger.info("Initializing database...") - NovelDatabase.init_db() - logger.info("Database initialization complete.") - - # Create and launch interface - interface = create_interface() - - interface.launch( - server_name="0.0.0.0", - server_port=7860, - share=False, - debug=True - ) \ No newline at end of file + logger.info("AGI NOVEL Generator v2.0 Starting...") + logger.info("=" * 60) + + # Environment check + logger.info(f"API Endpoint: {API_URL}") + logger.info(f"Target Length: {TARGET_WORDS:,} words") + logger.info(f"Minimum Words per Part: {MIN_WORDS_PER_PART:,} words") + logger.info("System Features: Single writer + Immediate part-by-part critique") + + if BRAVE_SEARCH_API_KEY: + logger.info("Web search enabled.") + else: + logger.warning("Web search disabled.") + + if DOCX_AVAILABLE: + logger.info("DOCX export enabled.") + else: + logger.warning("DOCX export disabled.") + + logger.info("=" * 60) + + # Initialize database + logger.info("Initializing database...") + NovelDatabase.init_db() + logger.info("Database initialization complete.") + + # Create and launch interface + interface = create_interface() + + interface.launch( + server_name="0.0.0.0", + server_port=7860, + share=False, + debug=True + ) \ No newline at end of file