feat: 佛经诵读站点初版(心经/金刚经 + 千问TTS)

- 仿故宫词条页视觉:宣纸底、朱砂印章、横竖排、分页阅读
- 千问 qwen3-tts-flash 服务端代理 + 磁盘/会话双缓存
- 整文合并为单 WAV 播放,段间静音内嵌,锁屏后台不断
- 循环开关、点击段落起读、跟读高亮自动滚动翻页
- 移动端折叠控制栏,部署于 fj.all8ai.top

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-09-28 13:25:43 +08:00
co-authored by Devin
commit 3c627d9dd6
10 changed files with 1331 additions and 0 deletions
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""心经诵读站点后端:静态文件托管 + 千问 TTS 代理。"""
import hashlib
import json
import os
import urllib.error
import urllib.request
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
CACHE_DIR = BASE_DIR / "cache"
ENV_PATH = BASE_DIR / ".env"
def load_env(path: Path) -> None:
"""读取 .env 文件并注入环境变量(不覆盖已有变量)。"""
if not path.exists():
return
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
os.environ.setdefault(key.strip(), value.strip())
load_env(ENV_PATH)
API_KEY = os.environ.get("DASHSCOPE_API_KEY", "")
DASHSCOPE_BASE = os.environ.get(
"DASHSCOPE_BASE",
"https://llm-znfsxsdp6uwik7ad.cn-beijing.maas.aliyuncs.com/api/v1",
).rstrip("/")
TTS_MODEL = os.environ.get("TTS_MODEL", "qwen3-tts-flash")
PORT = int(os.environ.get("PORT", "8000"))
GENERATION_URL = f"{DASHSCOPE_BASE}/services/aigc/multimodal-generation/generation"
def http_post_json(url: str, payload: dict, headers: dict, timeout: int = 60) -> dict:
"""发送 JSON POST 请求并返回解析后的响应。"""
req = urllib.request.Request(
url,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json", **headers},
method="POST",
)
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
def synthesize(text: str, voice: str) -> bytes:
"""调用千问 TTS 合成语音,返回 wav 音频字节流。"""
payload = {
"model": TTS_MODEL,
"input": {"text": text, "voice": voice},
}
data = http_post_json(GENERATION_URL, payload, {"Authorization": f"Bearer {API_KEY}"})
audio_url = (data.get("output") or {}).get("audio", {}).get("url")
if not audio_url:
raise RuntimeError(f"TTS 响应缺少音频地址:{json.dumps(data, ensure_ascii=False)[:300]}")
with urllib.request.urlopen(audio_url, timeout=60) as resp:
return resp.read()
class Handler(SimpleHTTPRequestHandler):
"""静态资源 + /api/tts 合成接口。"""
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=str(BASE_DIR), **kwargs)
def do_POST(self) -> None:
"""处理 POST 请求,目前仅支持 /api/tts。"""
if self.path != "/api/tts":
self._send_json({"error": "not found"}, status=404)
return
try:
length = int(self.headers.get("Content-Length", "0"))
body = json.loads(self.rfile.read(length).decode("utf-8"))
except (ValueError, json.JSONDecodeError):
self._send_json({"error": "请求体不是合法 JSON"}, status=400)
return
text = (body.get("text") or "").strip()
voice = (body.get("voice") or "").strip()
if not text or not voice:
self._send_json({"error": "缺少 text 或 voice 参数"}, status=400)
return
if len(text) > 600:
self._send_json({"error": "文本超过 600 字上限"}, status=400)
return
if not API_KEY:
self._send_json({"error": "服务端未配置 DASHSCOPE_API_KEY"}, status=500)
return
cache_key = hashlib.sha256(f"{TTS_MODEL}|{voice}|{text}".encode("utf-8")).hexdigest()[:24]
cache_file = CACHE_DIR / f"{cache_key}.wav"
try:
if cache_file.exists():
audio = cache_file.read_bytes()
else:
audio = synthesize(text, voice)
CACHE_DIR.mkdir(exist_ok=True)
cache_file.write_bytes(audio)
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")[:300]
self._send_json({"error": f"TTS 服务返回 {exc.code}:{detail}"}, status=502)
return
except Exception as exc: # noqa: BLE001 - 统一兜底,向前端返回可读错误
self._send_json({"error": f"语音合成失败:{exc}"}, status=502)
return
self.send_response(200)
self.send_header("Content-Type", "audio/wav")
self.send_header("Content-Length", str(len(audio)))
self.end_headers()
self.wfile.write(audio)
def _send_json(self, obj: dict, status: int = 200) -> None:
"""以 JSON 形式返回响应。"""
data = json.dumps(obj, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def end_headers(self) -> None:
"""本地开发环境禁用静态资源缓存,避免页面拿到旧版 JS/CSS。"""
if not self.path.startswith("/api/"):
self.send_header("Cache-Control", "no-cache")
super().end_headers()
def log_message(self, fmt, *args) -> None:
"""精简请求日志输出。"""
if "/api/" in self.path:
super().log_message(fmt, *args)
if __name__ == "__main__":
server = ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
print(f"心经诵读站点已启动:http://127.0.0.1:{PORT}")
print(f"TTS 模型:{TTS_MODEL} | 上游:{DASHSCOPE_BASE}")
if not API_KEY:
print("警告:未配置 DASHSCOPE_API_KEY,请复制 .env.example 为 .env 并填入密钥")
server.serve_forever()