Yoyo2004 commited on
Commit
1ccf718
·
verified ·
1 Parent(s): 0a32a7b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +197 -61
app.py CHANGED
@@ -1,6 +1,7 @@
1
  import os
2
  import time
3
  import math
 
4
  import gradio as gr
5
  from gradio_client import Client
6
 
@@ -139,7 +140,6 @@ body, .gradio-container {
139
  justify-content: center !important;
140
  transition: all 0.2s ease !important;
141
  }
142
- /* Hover */
143
  .arrow-btn:hover {
144
  color: var(--primary-color) !important;
145
  transform: scale(1.1);
@@ -162,6 +162,32 @@ body, .gradio-container {
162
  border-color: #cbd5e0 !important;
163
  }
164
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  /* 移动端适配 */
166
  @media (max-width: 768px) {
167
  .book-page-container { padding: 0; height: auto; min-height: 500px; }
@@ -174,17 +200,18 @@ body, .gradio-container {
174
  # 2. 逻辑处理
175
  # ==========================================
176
 
177
- def paginate_story(story_data, chars_per_page=600):
178
  """
179
- 分章 -> 分页。同时记录每一章的起始页码,方便章节跳转。
 
 
180
  返回: (flat_pages, chapter_start_indices)
181
  """
182
  if not story_data:
183
  return [], []
184
 
185
  flat_pages = []
186
- chapter_start_indices = [] # 记录 Chapter i 的起始页 index
187
-
188
  current_global_page_index = 0
189
 
190
  for chapter in story_data:
@@ -193,17 +220,20 @@ def paginate_story(story_data, chars_per_page=600):
193
  title = chapter.get("title", "")
194
  content = chapter.get("content", "")
195
  paragraphs = content.split('\n')
196
-
197
  current_page_content = []
198
  current_char_count = 0
199
- limit = chars_per_page // 1.5
200
  is_start = True
201
-
202
- # --- 分算法 ---
 
 
203
  for p in paragraphs:
204
  p = p.strip()
205
- if not p: continue
206
-
 
 
207
  if current_char_count + len(p) > limit and current_page_content:
208
  flat_pages.append({
209
  "title": title if is_start else "",
@@ -211,16 +241,18 @@ def paginate_story(story_data, chars_per_page=600):
211
  "is_chapter_start": is_start,
212
  "chapter_title": title
213
  })
214
- current_global_page_index += 1 # 全局页码 +1
215
 
 
216
  current_page_content = [p]
217
  current_char_count = len(p)
218
  is_start = False
219
- limit = chars_per_page
220
  else:
221
  current_page_content.append(p)
222
  current_char_count += len(p)
223
-
 
224
  if current_page_content:
225
  flat_pages.append({
226
  "title": title if is_start else "",
@@ -232,6 +264,7 @@ def paginate_story(story_data, chars_per_page=600):
232
 
233
  return flat_pages, chapter_start_indices
234
 
 
235
  def render_book_page(flat_pages, page_index):
236
  # 1. 空状态
237
  if not flat_pages or len(flat_pages) == 0:
@@ -245,20 +278,20 @@ def render_book_page(flat_pages, page_index):
245
  </div>
246
  </div>
247
  """
248
-
249
- # 2. 渲染
250
  total_pages = len(flat_pages)
251
  page_index = max(0, min(page_index, total_pages - 1))
252
  page_data = flat_pages[page_index]
253
-
254
  is_start = page_data.get("is_chapter_start", False)
255
  display_title = page_data.get("title", "")
256
  chapter_ref = page_data.get("chapter_title", "")
257
  content = page_data.get("content", "")
258
-
259
  paragraphs = [f"<p>{p}</p>" for p in content.split('\n') if p.strip()]
260
  content_html = "".join(paragraphs)
261
-
262
  if is_start:
263
  header_html = f"""
264
  <div class="page-top-spacer"></div>
@@ -290,47 +323,105 @@ def render_book_page(flat_pages, page_index):
290
  """
291
  return html
292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  # ==========================================
294
  # 3. 后端连接
295
  # ==========================================
296
  def bridge_to_backend(premise):
297
  if not premise.strip():
298
- yield "⚠️ 请输入故事梗概...", None, None, None, [], [], render_book_page([], 0)
 
299
  return
300
 
301
  log_buffer = "🚀 初始化前端连接...\n"
302
  initial_html = render_book_page([], 0)
303
-
304
- # 初始状态 [log, outline, plan, personas, pages, chap_indices, html]
305
- yield log_buffer, None, None, None, [], [], initial_html
 
306
 
307
  try:
308
  log_buffer += f"🔗 连接后端 Space: {PRIVATE_SPACE_ID}...\n"
309
  client = Client(PRIVATE_SPACE_ID, hf_token=HF_TOKEN)
310
  job = client.submit(premise, api_name="/generate_novel")
311
-
312
  for result in job:
313
- # result: [0]Log, [1]Outline, [2]Plan, [3]Personas, [4]StoryList
314
  backend_log = result[0]
315
  outline = result[1]
316
  plan = result[2]
317
  personas_html = result[3]
318
- raw_story_list = result[4]
319
-
320
- # 分页 + 计算章节索引
321
- flat_pages, chap_indices = paginate_story(raw_story_list, chars_per_page=600)
 
 
 
 
 
 
 
 
 
322
  book_html = render_book_page(flat_pages, 0)
323
-
324
- yield backend_log, outline, plan, personas_html, flat_pages, chap_indices, book_html
325
 
326
  except Exception as e:
327
  error_msg = f"❌ 前端连接错误: {str(e)}"
328
- yield error_msg, None, None, None, [], [], render_book_page([], 0)
 
329
 
330
  # ==========================================
331
  # 4. 前端 UI 布局
332
  # ==========================================
333
- with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate"), css=custom_css, title="LongStory Agent") as demo:
 
 
 
 
334
 
335
  # --- 状态管理 ---
336
  story_pages_state = gr.State([]) # 所有的页面 List[Dict]
@@ -359,7 +450,13 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate"),
359
  Story Premise
360
  </div>
361
  """)
362
- premise_input = gr.Textbox(label="Premise", show_label=False, lines=5, elem_classes=["input-box"], placeholder="输入故事创意...")
 
 
 
 
 
 
363
 
364
  # Examples
365
  gr.HTML('<div class="examples-container"><div class="examples-label">⚡ 快速开始 (Quick Inspirations)</div></div>')
@@ -379,8 +476,20 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate"),
379
 
380
  submit_btn = gr.Button("✨ 开始生成 (GENERATE)", elem_classes=["generate-btn"])
381
 
382
- gr.HTML('<div class="terminal-wrapper"><div class="terminal-header"><div class="dot dot-red"></div><div class="dot dot-yellow"></div><div class="dot dot-green"></div><div class="terminal-title">system.log</div></div>')
383
- log_output = gr.Textbox(label="Log", lines=10, interactive=False, elem_classes=["terminal-log"], show_label=False, value="> System initialized...")
 
 
 
 
 
 
 
 
 
 
 
 
384
  gr.HTML("</div>")
385
 
386
  # === 右侧:内容展示 ===
@@ -397,17 +506,18 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate"),
397
 
398
  # 核心内容
399
  with gr.Column(scale=15):
400
- story_display = gr.HTML(label="Book View", value=render_book_page([], 0))
 
 
 
401
 
402
  # 右侧箭头
403
  with gr.Column(scale=1, min_width=40, elem_classes=["arrow-col"]):
404
  btn_next_page = gr.Button("›", elem_classes=["arrow-btn"])
405
 
406
- # 2. 章节导航
407
  with gr.Row(elem_classes=["chapter-nav-row"]):
408
  btn_prev_chap = gr.Button("⏮️ 上一章", elem_classes=["chapter-nav-btn"])
409
- # 占位符,把按钮挤到两边或中间
410
- # gr.Spacer()
411
  btn_next_chap = gr.Button("⏭️ 下一章", elem_classes=["chapter-nav-btn"])
412
 
413
  # Tab 2: 大纲
@@ -422,6 +532,10 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate"),
422
  with gr.TabItem("👥 人物档案", id="tab-persona"):
423
  persona_output = gr.HTML(label="Character Cards")
424
 
 
 
 
 
425
  # ==========================================
426
  # 5. 事件交互
427
  # ==========================================
@@ -430,56 +544,78 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate"),
430
  submit_btn.click(
431
  fn=bridge_to_backend,
432
  inputs=[premise_input],
433
- outputs=[log_output, outline_output, plan_output, persona_output, story_pages_state, chapter_indices_state, story_display],
 
 
 
 
 
 
 
 
 
434
  concurrency_limit=1
435
  )
436
 
437
  # B. 翻页 (上一页/下一页)
438
  def on_prev_page(pages, current_idx):
439
- if not pages: return 0, render_book_page([], 0)
 
440
  new_idx = max(0, current_idx - 1)
441
  return new_idx, render_book_page(pages, new_idx)
442
 
443
  def on_next_page(pages, current_idx):
444
- if not pages: return 0, render_book_page([], 0)
 
445
  new_idx = min(len(pages) - 1, current_idx + 1)
446
  return new_idx, render_book_page(pages, new_idx)
447
 
448
- btn_prev_page.click(fn=on_prev_page, inputs=[story_pages_state, current_page_state], outputs=[current_page_state, story_display])
449
- btn_next_page.click(fn=on_next_page, inputs=[story_pages_state, current_page_state], outputs=[current_page_state, story_display])
 
 
 
 
 
 
 
 
450
 
451
  # C. 章节跳转 (上��章/下一章)
452
  def on_prev_chap(pages, current_idx, chap_indices):
453
- if not pages or not chap_indices: return current_idx, render_book_page(pages, current_idx)
454
-
455
- # 找到当前页属于第几章 (找到最后一个 <= current_idx 的 start_index)
456
- # 例如 chap_indices = [0, 5, 12], current = 6. 属于索引为1的章节(index 5)
457
- # 我们要跳到索引为0的章节(index 0)
458
-
459
  target_idx = 0
460
- # 简单遍历寻找目标
461
  for start_idx in reversed(chap_indices):
462
  if start_idx < current_idx:
463
  target_idx = start_idx
464
  break
465
-
466
  return target_idx, render_book_page(pages, target_idx)
467
 
468
  def on_next_chap(pages, current_idx, chap_indices):
469
- if not pages or not chap_indices: return current_idx, render_book_page(pages, current_idx)
470
-
 
471
  target_idx = current_idx
472
- # 找到第一个 > current_idx 的 start_index
473
  for start_idx in chap_indices:
474
  if start_idx > current_idx:
475
  target_idx = start_idx
476
  break
477
-
478
- # 如果没找到(已经是最后一章),保持不变或跳到最后一页,这里保持不变
479
  return target_idx, render_book_page(pages, target_idx)
480
 
481
- btn_prev_chap.click(fn=on_prev_chap, inputs=[story_pages_state, current_page_state, chapter_indices_state], outputs=[current_page_state, story_display])
482
- btn_next_chap.click(fn=on_next_chap, inputs=[story_pages_state, current_page_state, chapter_indices_state], outputs=[current_page_state, story_display])
 
 
 
 
 
 
 
 
483
 
484
  if __name__ == "__main__":
485
- demo.queue().launch()
 
1
  import os
2
  import time
3
  import math
4
+ import html as py_html # ✅ 为了转义全文文本
5
  import gradio as gr
6
  from gradio_client import Client
7
 
 
140
  justify-content: center !important;
141
  transition: all 0.2s ease !important;
142
  }
 
143
  .arrow-btn:hover {
144
  color: var(--primary-color) !important;
145
  transform: scale(1.1);
 
162
  border-color: #cbd5e0 !important;
163
  }
164
 
165
+ /* 全文导出区域 */
166
+ .fulltext-wrapper { padding: 10px 0; }
167
+ .fulltext-copy-btn {
168
+ background: #4f46e5;
169
+ color: #fff;
170
+ border: none;
171
+ border-radius: 8px;
172
+ padding: 6px 12px;
173
+ font-size: 0.9rem;
174
+ cursor: pointer;
175
+ margin-bottom: 10px;
176
+ }
177
+ .fulltext-copy-btn:hover {
178
+ filter: brightness(1.08);
179
+ }
180
+ .fulltext-area {
181
+ width: 100%;
182
+ min-height: 500px;
183
+ border-radius: 8px;
184
+ border: 1px solid #e2e8f0;
185
+ padding: 10px;
186
+ font-family: 'JetBrains Mono', monospace;
187
+ font-size: 0.9rem;
188
+ resize: vertical;
189
+ }
190
+
191
  /* 移动端适配 */
192
  @media (max-width: 768px) {
193
  .book-page-container { padding: 0; height: auto; min-height: 500px; }
 
200
  # 2. 逻辑处理
201
  # ==========================================
202
 
203
+ def paginate_story(story_data, chars_per_page=1200, first_page_ratio=0.8):
204
  """
205
+ 分章 -> 分页。
206
+ - 首章首页可用字符数 = chars_per_page * first_page_ratio(因为有大标题和空白)
207
+ - 之后的每一页都用 chars_per_page,尽量“填满”固定高度。
208
  返回: (flat_pages, chapter_start_indices)
209
  """
210
  if not story_data:
211
  return [], []
212
 
213
  flat_pages = []
214
+ chapter_start_indices = []
 
215
  current_global_page_index = 0
216
 
217
  for chapter in story_data:
 
220
  title = chapter.get("title", "")
221
  content = chapter.get("content", "")
222
  paragraphs = content.split('\n')
223
+
224
  current_page_content = []
225
  current_char_count = 0
 
226
  is_start = True
227
+
228
+ # 稍微少一点,为标题留空间
229
+ limit = int(chars_per_page * first_page_ratio)
230
+
231
  for p in paragraphs:
232
  p = p.strip()
233
+ if not p:
234
+ continue
235
+
236
+ # 如果当前段落加进去会超过限制,并且当前页已有内容 -> 换页
237
  if current_char_count + len(p) > limit and current_page_content:
238
  flat_pages.append({
239
  "title": title if is_start else "",
 
241
  "is_chapter_start": is_start,
242
  "chapter_title": title
243
  })
244
+ current_global_page_index += 1
245
 
246
+ # 新的一页
247
  current_page_content = [p]
248
  current_char_count = len(p)
249
  is_start = False
250
+ limit = chars_per_page # 之后的页按照标准容量
251
  else:
252
  current_page_content.append(p)
253
  current_char_count += len(p)
254
+
255
+ # 收尾
256
  if current_page_content:
257
  flat_pages.append({
258
  "title": title if is_start else "",
 
264
 
265
  return flat_pages, chapter_start_indices
266
 
267
+
268
  def render_book_page(flat_pages, page_index):
269
  # 1. 空状态
270
  if not flat_pages or len(flat_pages) == 0:
 
278
  </div>
279
  </div>
280
  """
281
+
282
+ # 2. 渲染正常页面
283
  total_pages = len(flat_pages)
284
  page_index = max(0, min(page_index, total_pages - 1))
285
  page_data = flat_pages[page_index]
286
+
287
  is_start = page_data.get("is_chapter_start", False)
288
  display_title = page_data.get("title", "")
289
  chapter_ref = page_data.get("chapter_title", "")
290
  content = page_data.get("content", "")
291
+
292
  paragraphs = [f"<p>{p}</p>" for p in content.split('\n') if p.strip()]
293
  content_html = "".join(paragraphs)
294
+
295
  if is_start:
296
  header_html = f"""
297
  <div class="page-top-spacer"></div>
 
323
  """
324
  return html
325
 
326
+
327
+ def build_full_text(story_data):
328
+ """
329
+ 把章节列表拼成一整本小说文本,供导出/复制。
330
+ story_data: List[{"title": ..., "content": ...}, ...]
331
+ """
332
+ if not story_data:
333
+ return ""
334
+
335
+ blocks = []
336
+ for ch in story_data:
337
+ title = ch.get("title", "").strip()
338
+ content = ch.get("content", "").strip()
339
+ if title:
340
+ blocks.append(title)
341
+ if content:
342
+ blocks.append(content)
343
+ return "\n\n".join(blocks).strip()
344
+
345
+
346
+ def build_full_text_html(full_text: str) -> str:
347
+ """
348
+ 生成带「一键复制」按钮的 HTML。
349
+ 使用 textarea + JS 调用 navigator.clipboard.writeText。
350
+ """
351
+ escaped = py_html.escape(full_text or "")
352
+ return f"""
353
+ <div class="fulltext-wrapper">
354
+ <button class="fulltext-copy-btn" onclick="
355
+ const ta = document.getElementById('full-story-area');
356
+ if (ta) {{
357
+ ta.select();
358
+ ta.setSelectionRange(0, 999999);
359
+ navigator.clipboard && navigator.clipboard.writeText(ta.value);
360
+ }}
361
+ ">
362
+ 📋 一键复制全文
363
+ </button>
364
+ <textarea id="full-story-area" class="fulltext-area">{escaped}</textarea>
365
+ </div>
366
+ """
367
+
368
  # ==========================================
369
  # 3. 后端连接
370
  # ==========================================
371
  def bridge_to_backend(premise):
372
  if not premise.strip():
373
+ empty_full_html = build_full_text_html("")
374
+ yield "⚠️ 请输入故事梗概...", None, None, None, [], [], empty_full_html, render_book_page([], 0)
375
  return
376
 
377
  log_buffer = "🚀 初始化前端连接...\n"
378
  initial_html = render_book_page([], 0)
379
+ empty_full_html = build_full_text_html("")
380
+
381
+ # 初始状态 [log, outline, plan, personas, pages, chap_indices, full_text_html, book_html]
382
+ yield log_buffer, None, None, None, [], [], empty_full_html, initial_html
383
 
384
  try:
385
  log_buffer += f"🔗 连接后端 Space: {PRIVATE_SPACE_ID}...\n"
386
  client = Client(PRIVATE_SPACE_ID, hf_token=HF_TOKEN)
387
  job = client.submit(premise, api_name="/generate_novel")
388
+
389
  for result in job:
390
+ # result: [0]Log, [1]Outline, [2]Plan, [3]Personas, [4]StoryList(章节列表)
391
  backend_log = result[0]
392
  outline = result[1]
393
  plan = result[2]
394
  personas_html = result[3]
395
+ raw_story_list = result[4]
396
+
397
+ # 分页 + 计算章节索引(让页面尽量填满)
398
+ flat_pages, chap_indices = paginate_story(
399
+ raw_story_list,
400
+ chars_per_page=1200,
401
+ first_page_ratio=0.8,
402
+ )
403
+
404
+ # 一键复制全文的文本 + HTML
405
+ full_text = build_full_text(raw_story_list)
406
+ full_text_html = build_full_text_html(full_text)
407
+
408
  book_html = render_book_page(flat_pages, 0)
409
+
410
+ yield backend_log, outline, plan, personas_html, flat_pages, chap_indices, full_text_html, book_html
411
 
412
  except Exception as e:
413
  error_msg = f"❌ 前端连接错误: {str(e)}"
414
+ error_full_html = build_full_text_html(str(e))
415
+ yield error_msg, None, None, None, [], [], error_full_html, render_book_page([], 0)
416
 
417
  # ==========================================
418
  # 4. 前端 UI 布局
419
  # ==========================================
420
+ with gr.Blocks(
421
+ theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate"),
422
+ css=custom_css,
423
+ title="LongStory Agent"
424
+ ) as demo:
425
 
426
  # --- 状态管理 ---
427
  story_pages_state = gr.State([]) # 所有的页面 List[Dict]
 
450
  Story Premise
451
  </div>
452
  """)
453
+ premise_input = gr.Textbox(
454
+ label="Premise",
455
+ show_label=False,
456
+ lines=5,
457
+ elem_classes=["input-box"],
458
+ placeholder="输入故事创意..."
459
+ )
460
 
461
  # Examples
462
  gr.HTML('<div class="examples-container"><div class="examples-label">⚡ 快速开始 (Quick Inspirations)</div></div>')
 
476
 
477
  submit_btn = gr.Button("✨ 开始生成 (GENERATE)", elem_classes=["generate-btn"])
478
 
479
+ gr.HTML(
480
+ '<div class="terminal-wrapper"><div class="terminal-header">'
481
+ '<div class="dot dot-red"></div><div class="dot dot-yellow"></div>'
482
+ '<div class="dot dot-green"></div><div class="terminal-title">system.log</div>'
483
+ '</div>'
484
+ )
485
+ log_output = gr.Textbox(
486
+ label="Log",
487
+ lines=10,
488
+ interactive=False,
489
+ elem_classes=["terminal-log"],
490
+ show_label=False,
491
+ value="> System initialized..."
492
+ )
493
  gr.HTML("</div>")
494
 
495
  # === 右侧:内容展示 ===
 
506
 
507
  # 核心内容
508
  with gr.Column(scale=15):
509
+ story_display = gr.HTML(
510
+ label="Book View",
511
+ value=render_book_page([], 0)
512
+ )
513
 
514
  # 右侧箭头
515
  with gr.Column(scale=1, min_width=40, elem_classes=["arrow-col"]):
516
  btn_next_page = gr.Button("›", elem_classes=["arrow-btn"])
517
 
518
+ # 章节导航
519
  with gr.Row(elem_classes=["chapter-nav-row"]):
520
  btn_prev_chap = gr.Button("⏮️ 上一章", elem_classes=["chapter-nav-btn"])
 
 
521
  btn_next_chap = gr.Button("⏭️ 下一章", elem_classes=["chapter-nav-btn"])
522
 
523
  # Tab 2: 大纲
 
532
  with gr.TabItem("👥 人物档案", id="tab-persona"):
533
  persona_output = gr.HTML(label="Character Cards")
534
 
535
+ # Tab 5: 全文导出
536
+ with gr.TabItem("📄 全文导出", id="tab-fulltext"):
537
+ full_text_html = gr.HTML(label="Full Story Export")
538
+
539
  # ==========================================
540
  # 5. 事件交互
541
  # ==========================================
 
544
  submit_btn.click(
545
  fn=bridge_to_backend,
546
  inputs=[premise_input],
547
+ outputs=[
548
+ log_output,
549
+ outline_output,
550
+ plan_output,
551
+ persona_output,
552
+ story_pages_state,
553
+ chapter_indices_state,
554
+ full_text_html, # ✅ 新增:全文导出 HTML
555
+ story_display
556
+ ],
557
  concurrency_limit=1
558
  )
559
 
560
  # B. 翻页 (上一页/下一页)
561
  def on_prev_page(pages, current_idx):
562
+ if not pages:
563
+ return 0, render_book_page([], 0)
564
  new_idx = max(0, current_idx - 1)
565
  return new_idx, render_book_page(pages, new_idx)
566
 
567
  def on_next_page(pages, current_idx):
568
+ if not pages:
569
+ return 0, render_book_page([], 0)
570
  new_idx = min(len(pages) - 1, current_idx + 1)
571
  return new_idx, render_book_page(pages, new_idx)
572
 
573
+ btn_prev_page.click(
574
+ fn=on_prev_page,
575
+ inputs=[story_pages_state, current_page_state],
576
+ outputs=[current_page_state, story_display]
577
+ )
578
+ btn_next_page.click(
579
+ fn=on_next_page,
580
+ inputs=[story_pages_state, current_page_state],
581
+ outputs=[current_page_state, story_display]
582
+ )
583
 
584
  # C. 章节跳转 (上��章/下一章)
585
  def on_prev_chap(pages, current_idx, chap_indices):
586
+ if not pages or not chap_indices:
587
+ return current_idx, render_book_page(pages, current_idx)
588
+
 
 
 
589
  target_idx = 0
 
590
  for start_idx in reversed(chap_indices):
591
  if start_idx < current_idx:
592
  target_idx = start_idx
593
  break
594
+
595
  return target_idx, render_book_page(pages, target_idx)
596
 
597
  def on_next_chap(pages, current_idx, chap_indices):
598
+ if not pages or not chap_indices:
599
+ return current_idx, render_book_page(pages, current_idx)
600
+
601
  target_idx = current_idx
 
602
  for start_idx in chap_indices:
603
  if start_idx > current_idx:
604
  target_idx = start_idx
605
  break
606
+
 
607
  return target_idx, render_book_page(pages, target_idx)
608
 
609
+ btn_prev_chap.click(
610
+ fn=on_prev_chap,
611
+ inputs=[story_pages_state, current_page_state, chapter_indices_state],
612
+ outputs=[current_page_state, story_display]
613
+ )
614
+ btn_next_chap.click(
615
+ fn=on_next_chap,
616
+ inputs=[story_pages_state, current_page_state, chapter_indices_state],
617
+ outputs=[current_page_state, story_display]
618
+ )
619
 
620
  if __name__ == "__main__":
621
+ demo.queue().launch()