import os import time import math import gradio as gr import html as py_html from gradio_client import Client # ========================================== # 0. 配置 # ========================================== PRIVATE_SPACE_ID = "Yoyo2004/Longstory-backend" HF_TOKEN = os.environ.get("HF_TOKEN") # ========================================== # 1. CSS # ========================================== custom_css = """ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&family=Noto+Serif+SC:wght@400;700&family=JetBrains+Mono:wght@400&family=Inter:wght@400;600&display=swap'); :root { --primary-color: #4f46e5; /* indigo 靛蓝色 */ --paper-bg: #fdf6e3; /* 米黄色 */ --glass-bg: rgba(255, 255, 255, 0.95); /* 半透明白 */ } body, .gradio-container { font-family: 'Inter', 'Noto Sans SC', system-ui, sans-serif !important; background: linear-gradient(135deg, #eef2f3 0%, #8e9eab 100%) !important; background-attachment: fixed !important; } /* --- 通用容器 --- */ .header-box { text-align: center; padding: 25px; background: var(--glass-bg); border-radius: 16px; margin-bottom: 20px; box-shadow: 0 4px 20px rgba(0,0,0,0.08); backdrop-filter: blur(10px); } .title-text { font-size: 2.2rem; font-weight: 800; font-family: 'Noto Serif SC', serif; color: #1a202c; letter-spacing: -0.5px; } .subtitle-text { color: #718096; letter-spacing: 1.5px; text-transform: uppercase; font-size: 0.85rem; font-weight: 600; margin-top: 5px; } /* --- 左侧控制面板 --- */ .control-panel { background: var(--glass-bg); padding: 24px !important; border-radius: 16px; border: 1px solid rgba(255,255,255,0.6); box-shadow: 0 8px 32px rgba(0,0,0,0.05); } /* 输入框与按钮 */ .input-label { font-weight: 700; color: #4a5568; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; } .input-box textarea { background: #f8fafc !important; border: 2px solid #e2e8f0 !important; border-radius: 12px !important; padding: 12px !important; } .input-box textarea:focus { border-color: var(--primary-color) !important; background: #fff !important; } .generate-btn { background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%) !important; color: white !important; font-weight: 600; border-radius: 12px !important; padding: 12px !important; font-size: 1.1rem !important; box-shadow: 0 4px 15px rgba(79, 70, 229, 0.4); transition: all 0.3s ease; } .generate-btn:hover { transform: translateY(-2px); filter: brightness(110%); } /* Examples */ .examples-container { margin-top: 15px; } .examples-label { font-size: 0.85rem; color: #a0aec0; font-weight: 600; margin-bottom: 10px; text-transform: uppercase; } .examples-table table { border-collapse: separate !important; border-spacing: 0 8px !important; background: transparent !important; } .examples-table thead { display: none; } .examples-table tbody tr { background: #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.05); border-radius: 8px; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; border: 1px solid #edf2f7; display: block; margin-bottom: 8px; } .examples-table tbody tr:hover { transform: translateY(-2px); box-shadow: 0 5px 12px rgba(0,0,0,0.1); border-color: var(--primary-color); } .examples-table td { padding: 12px 15px !important; font-size: 13px !important; color: #4a5568 !important; line-height: 1.5 !important; border: none !important; display: block; } /* 终端日志 */ .terminal-wrapper { margin-top: 25px; background: #1e1e1e; border-radius: 10px; overflow: hidden; } .terminal-header { background: #2d2d2d; padding: 8px 12px; display: flex; gap: 6px; border-bottom: 1px solid #333; } .dot { width: 10px; height: 10px; border-radius: 50%; } .dot-red { background: #ff5f56; } .dot-yellow { background: #ffbd2e; } .dot-green { background: #27c93f; } .terminal-log textarea { font-family: 'JetBrains Mono', monospace !important; background: #1e1e1e !important; color: #4ade80 !important; border: none !important; padding: 10px !important; } /* 1. 书页容器 */ .book-page-container { background-color: var(--paper-bg); height: 900px; width: 100%; border-radius: 8px 16px 16px 8px; box-shadow: inset 20px 0 50px rgba(0,0,0,0.05), 10px 10px 30px rgba(0,0,0,0.1); position: relative; display: flex; flex-direction: column; overflow: hidden; } .book-page-container::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 6px; background: linear-gradient(to right, rgba(0,0,0,0.1), transparent); z-index: 1; } /* 2. 内容包装器 */ .book-content-wrapper { flex: 1; padding: 50px 70px 20px 70px; overflow-y: hidden; position: relative; z-index: 2; } .book-page-container.empty-state .book-content-wrapper { display: flex; justify-content: center; align-items: center; } /* 3. 排版细节 */ .page-top-spacer { height: 20px; } .chapter-header-block { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 2px solid rgba(139, 69, 19, 0.2); } .chapter-subtitle { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 2px; color: #8b4513; opacity: 0.6; } .chapter-title { font-family: 'Noto Serif SC', serif; font-size: 2.2rem; font-weight: 700; color: #2c1810; line-height: 1.2; } .page-running-header { height: 40px; border-bottom: 1px solid rgba(0,0,0,0.05); margin-bottom: 30px; color: #a0aec0; font-size: 0.75rem; display: flex; justify-content: space-between; align-items: center; text-transform: uppercase; } .chapter-text-content { font-family: 'Noto Serif SC', serif; font-size: 1.15rem; color: #333; line-height: 1.9; text-align: justify; } .chapter-text-content p { margin-bottom: 1.2em; text-indent: 2em; } .page-footer { height: 50px; display: flex; align-items: center; justify-content: center; font-size: 0.8rem; color: #999; border-top: 1px dashed rgba(0,0,0,0.05); z-index: 2; } /* 1. 侧边箭头*/ .arrow-btn { background: transparent !important; border: none !important; box-shadow: none !important; color: #cbd5e0 !important; font-size: 3rem !important; font-weight: 200 !important; padding: 0 !important; height: 100% !important; width: 100% !important; display: flex !important; align-items: center !important; justify-content: center !important; transition: all 0.2s ease !important; } .arrow-btn:hover { color: var(--primary-color) !important; transform: scale(1.1); background: transparent !important; } /* 2. 底部章节导航按钮 */ .chapter-nav-btn { background: white !important; border: 1px solid #e2e8f0 !important; color: #718096 !important; border-radius: 8px !important; font-size: 0.9rem !important; padding: 8px 16px !important; box-shadow: 0 1px 2px rgba(0,0,0,0.05); } .chapter-nav-btn:hover { background: #f7fafc !important; color: var(--primary-color) !important; border-color: #cbd5e0 !important; } /* 全文导出区域 */ .fulltext-wrapper { padding: 10px 0; } .fulltext-copy-btn { background: #4f46e5; color: #fff; border: none; border-radius: 8px; padding: 6px 12px; font-size: 0.9rem; cursor: pointer; margin-bottom: 10px; } .fulltext-copy-btn:hover { filter: brightness(1.08); } .fulltext-area { width: 100%; min-height: 500px; height: 900px; border-radius: 8px; border: 1px solid #e2e8f0; padding: 10px; font-family: 'JetBrains Mono', monospace; font-size: 0.9rem; resize: vertical; } /* 移动端适配 */ @media (max-width: 768px) { .book-page-container { padding: 0; height: auto; min-height: 500px; } .book-content-wrapper { padding: 30px 20px; } .arrow-btn { font-size: 2rem !important; } } """ # ========================================== # 2. 逻辑处理 # ========================================== def paginate_story(story_data, chars_per_page=450, first_page_ratio=0.8): """ 分章 -> 分页 story_data: List[{"title": ..., "content": ...}, ...] """ if not story_data: return [], [] flat_pages = [] chapter_start_indices = [] current_global_page_index = 0 for chapter in story_data: chapter_start_indices.append(current_global_page_index) title = chapter.get("title", "") content = chapter.get("content", "") paragraphs = content.split('\n') current_page_content = [] current_char_count = 0 is_start = True # 首页为标题预留空间 limit = int(chars_per_page * first_page_ratio) for p in paragraphs: p = p.strip() if not p: continue # 换页 if current_char_count + len(p) > limit and current_page_content: flat_pages.append({ "title": title if is_start else "", "content": "\n".join(current_page_content), "is_chapter_start": is_start, "chapter_title": title }) current_global_page_index += 1 current_page_content = [p] current_char_count = len(p) is_start = False limit = chars_per_page else: current_page_content.append(p) current_char_count += len(p) if current_page_content: flat_pages.append({ "title": title if is_start else "", "content": "\n".join(current_page_content), "is_chapter_start": is_start, "chapter_title": title }) current_global_page_index += 1 return flat_pages, chapter_start_indices def render_book_page(flat_pages, page_index): # 空状态 if not flat_pages or len(flat_pages) == 0: return """
{p}
" for p in content.split('\n') if p.strip()] content_html = "".join(paragraphs) if is_start: header_html = f"""