push_book.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. """
  2. push_book.py — 书籍生成完成后,主动推送到小智音箱
  3. 使用小智服务端的新 /xiaozhi/push 端点。
  4. 当 book 生成完毕后,调用此脚本推送给音箱自动朗读。
  5. 用法:
  6. python push_book.py <book_id> [--device-id DEVICE_ID] [--server XIAOZHI_SERVER]
  7. 环境变量:
  8. XIAOZHI_SERVER: 小智服务端地址(默认 http://127.0.0.1:8003)
  9. DEVICE_ID: ESP32 设备ID
  10. AUDIOBOOK_API_BASE: 后端 API 地址
  11. """
  12. from __future__ import annotations
  13. import json
  14. import os
  15. import sys
  16. import time
  17. import urllib.request
  18. AUDIOBOOK_API = os.environ.get("AUDIOBOOK_API_BASE", "http://127.0.0.1:38381")
  19. XIAOZHI_SERVER = os.environ.get("XIAOZHI_SERVER", "http://127.0.0.1:8003")
  20. DEVICE_ID = os.environ.get("DEVICE_ID", "")
  21. def http_get(path: str) -> dict:
  22. with urllib.request.urlopen(f"{AUDIOBOOK_API}{path}", timeout=10) as r:
  23. return json.loads(r.read().decode("utf-8"))
  24. def push_to_device(device_id: str, text: str) -> bool:
  25. """调小智服务端 POST /xiaozhi/push"""
  26. url = f"{XIAOZHI_SERVER}/xiaozhi/push"
  27. payload = json.dumps({
  28. "device_id": device_id,
  29. "text": text,
  30. "type": "chat",
  31. }).encode("utf-8")
  32. req = urllib.request.Request(url, data=payload, method="POST",
  33. headers={"Content-Type": "application/json"})
  34. try:
  35. with urllib.request.urlopen(req, timeout=10) as r:
  36. resp = json.loads(r.read().decode("utf-8"))
  37. return resp.get("code") == 0
  38. except Exception as e:
  39. print(f"推送失败: {e}", file=sys.stderr)
  40. return False
  41. def collect_book_text(book_id: int) -> str | None:
  42. """收集书籍所有章节的文本"""
  43. data = http_get(f"/api/book-generator/books/{book_id}")
  44. info = data.get("data") or {}
  45. if not info:
  46. print(f"书籍 {book_id} 不存在", file=sys.stderr)
  47. return None
  48. chapters = info.get("chapters") or []
  49. if not chapters:
  50. print(f"书籍 {book_id} 尚无章节", file=sys.stderr)
  51. return None
  52. title = info.get("title") or "未命名"
  53. lines = [f"《{title}》已生成,共{len(chapters)}章。现在开始朗读:\n"]
  54. for i, ch in enumerate(chapters):
  55. ch_data = http_get(f"/api/book-generator/books/chapters/{ch['id']}")
  56. ch_info = ch_data.get("data") or {}
  57. ch_title = ch_info.get("title") or f"第{i+1}章"
  58. ch_content = ch_info.get("content") or ch_info.get("text") or ""
  59. # 每章取前 500 字推送给音箱
  60. snippet = ch_content[:500] if ch_content else "(暂无内容)"
  61. lines.append(f"{ch_title}\n\n{snippet}\n")
  62. if len(chapters) > 1 and i < len(chapters) - 1:
  63. lines.append("[请继续朗读下一章]\n")
  64. else:
  65. lines.append("[全书朗读完毕]")
  66. return "\n".join(lines)
  67. def wait_and_push(book_id: int, device_id: str, timeout: int = 600) -> bool:
  68. """轮询等生成完成,然后推送"""
  69. deadline = time.time() + timeout
  70. print(f"等待书籍 {book_id} 生成完成(最多 {timeout} 秒)...", file=sys.stderr)
  71. while time.time() < deadline:
  72. data = http_get(f"/api/book-generator/books/{book_id}")
  73. info = data.get("data") or {}
  74. progress = info.get("progress", 0)
  75. chapters = info.get("chapters") or []
  76. if chapters and progress >= 30:
  77. # 验证真有内容
  78. fd = http_get(f"/api/book-generator/books/chapters/{chapters[0]['id']}")
  79. fc = ((fd.get("data") or {}).get("content") or (fd.get("data") or {}).get("text") or "")
  80. if fc and len(fc) > 50:
  81. text = collect_book_text(book_id)
  82. if text:
  83. print(f"推送中 ({len(text)} 字)...", file=sys.stderr)
  84. return push_to_device(device_id, text)
  85. time.sleep(10)
  86. print("超时", file=sys.stderr)
  87. return False
  88. def main() -> int:
  89. if len(sys.argv) < 2:
  90. print("Usage: python push_book.py <book_id> [--device-id ID] [--server URL]")
  91. return 2
  92. book_id = int(sys.argv[1])
  93. device_id = DEVICE_ID
  94. server = XIAOZHI_SERVER
  95. # 解析命令行参数
  96. args = sys.argv[2:]
  97. i = 0
  98. while i < len(args):
  99. if args[i] == "--device-id" and i + 1 < len(args):
  100. device_id = args[i + 1]
  101. i += 2
  102. elif args[i] == "--server" and i + 1 < len(args):
  103. server = args[i + 1]
  104. os.environ["XIAOZHI_SERVER"] = server
  105. i += 2
  106. else:
  107. i += 1
  108. if not device_id:
  109. print("错误: 需要 device_id。用 --device-id 指定或在 .env 设置 DEVICE_ID", file=sys.stderr)
  110. print("提示: device_id 是小智音箱的唯一标识,在 app 或控制台可查看", file=sys.stderr)
  111. return 2
  112. print(f"XIAOZHI_SERVER={server}", file=sys.stderr)
  113. print(f"DEVICE_ID={device_id}", file=sys.stderr)
  114. ok = wait_and_push(book_id, device_id)
  115. if ok:
  116. print("推送成功!音箱应该开始朗读了")
  117. return 0
  118. else:
  119. print("推送失败")
  120. return 1
  121. if __name__ == "__main__":
  122. sys.exit(main())