"""
虚拟父母 语音服务
- TTS：豆包语音合成大模型（WebSocket ws_binary）
- ASR：豆包大模型流式语音识别（WebSocket v3 二进制协议代理）
"""

import asyncio, gzip, io, json, struct, uuid
import websockets
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import Response, JSONResponse
from pydantic import BaseModel

# ── TTS 凭证 ──
TTS_APPID   = "9975991600"
TTS_TOKEN   = "5YnN_op5zvSFP-7JrxxyY1pdLdY9UdtI"
TTS_CLUSTER = "volcano_tts"
TTS_URI     = "wss://openspeech.bytedance.com/api/v1/tts/ws_binary"

# ── ASR 凭证（豆包流式语音识别大模型2.0 Seed v3 二进制协议）──
ASR_APPID      = "6173189800"
ASR_TOKEN      = "W7Qv23m1cJUHZe1ystl7bgFGtXvjOX7Z"
ASR_URI        = "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel"
ASR_RESOURCE_ID = "volc.bigasr.sauc.duration"

# ── 角色音色映射 ──
VOICE_MAP = {
    "mother_gentle": "zh_female_wenroumama_uranus_bigtts",
    "mother_strict": "zh_female_vv_uranus_bigtts",
    "father_gentle": "zh_male_m191_uranus_bigtts",
    "father_strict": "zh_male_taocheng_uranus_bigtts",
    "default":       "zh_female_wenroumama_uranus_bigtts",
}

app = FastAPI(title="豆包语音服务")
app.add_middleware(
    CORSMiddleware, allow_origins=["*"], allow_credentials=True,
    allow_methods=["*"], allow_headers=["*"],
)

# ── TTS 二进制协议帧构建（按官方文档标准）──
# Header: 4 bytes
# byte0: version(4bit) | header_size(4bit) = 0x11
# byte1: message_type(4bit) | message_type_specific_flags(4bit)
#        full client request = 0x10
# byte2: serialization(4bit) | compression(4bit)
#        json=0x1, gzip=0x1 → 0x11
# byte3: reserved = 0x00
def _build_request(text: str, voice_type: str) -> bytes:
    payload = {
        "app":    {"appid": TTS_APPID, "token": TTS_TOKEN, "cluster": TTS_CLUSTER},
        "user":   {"uid": "parent_companion"},
        "audio":  {"voice_type": voice_type, "encoding": "mp3",
                   "speed_ratio": 1.0, "volume_ratio": 1.0, "pitch_ratio": 1.0},
        "request":{"reqid": str(uuid.uuid4()), "text": text,
                   "text_type": "plain", "operation": "query",
                   "silence_duration": "125"},
    }
    body = gzip.compress(json.dumps(payload).encode("utf-8"))
    header = bytes([0x11, 0x10, 0x11, 0x00])
    size   = struct.pack(">I", len(body))
    return header + size + body

async def _tts_synthesize(text: str, voice_type: str) -> bytes:
    auth = {"Authorization": f"Bearer;{TTS_TOKEN}"}
    buf  = bytearray()
    async with websockets.connect(
        TTS_URI, additional_headers=auth,
        ping_interval=None, open_timeout=10
    ) as ws:
        await ws.send(_build_request(text, voice_type))
        frame_count = 0
        while True:
            try:
                raw = await asyncio.wait_for(ws.recv(), timeout=5)
            except asyncio.TimeoutError:
                if buf:
                    print(f"[TTS] 超时但已有 {len(buf)} bytes 音频，正常返回")
                else:
                    print("[TTS] 等待响应超时，无数据")
                break
            if not raw or len(raw) < 4:
                break
            frame_count += 1
            header_size = (raw[0] & 0x0F) * 4  # header size in bytes
            msg_type = (raw[1] >> 4) & 0x0F
            flags    = raw[1] & 0x0F
            serial   = (raw[2] >> 4) & 0x0F
            compress = raw[2] & 0x0F
            
            if msg_type == 0xb or msg_type == 0xe:
                # audio frame: header + 4-byte size + payload
                if len(raw) < header_size + 4:
                    break
                size = struct.unpack(">I", raw[header_size:header_size+4])[0]
                payload = raw[header_size+4: header_size+4+size]
                buf.extend(payload)
                if msg_type == 0xe or flags == 2 or flags == 3:
                    break
            elif msg_type == 0xf:
                # error frame
                if len(raw) >= header_size + 8:
                    code = struct.unpack(">I", raw[header_size:header_size+4])[0]
                    msg_size = struct.unpack(">I", raw[header_size+4:header_size+8])[0]
                    msg = raw[header_size+8:header_size+8+msg_size].decode("utf-8", errors="replace")
                    raise RuntimeError(f"TTS error {code}: {msg}")
                break
            # other msg_type: skip
        print(f"[TTS] 完成：{frame_count} 帧, {len(buf)} bytes")
    return bytes(buf)
    return bytes(buf)

class TTSRequest(BaseModel):
    text: str; voice_key: str = "default"
    rate: str = "+0%"; volume: str = "+0%"
    role: str = "mother"; personality: str = "gentle"

import re as _re

def _split_tts_text(text: str, max_len: int = 200) -> list:
    """将长文本按标点分割成 ≤max_len 的段落"""
    if len(text) <= max_len:
        return [text]
    parts = _re.split(r'([。！？]+)', text)
    chunks, buf = [], ''
    for i in range(0, len(parts), 2):
        segment = parts[i] + (parts[i+1] if i+1 < len(parts) else '')
        if len(buf) + len(segment) > max_len and buf:
            chunks.append(buf)
            buf = segment
        else:
            buf += segment
    if buf:
        chunks.append(buf)
    return chunks if chunks else [text[:max_len]]

@app.post("/v1/audio/speech")
async def tts_endpoint(req: TTSRequest):
    if not req.text.strip():
        raise HTTPException(400, "text 不能为空")
    key   = req.voice_key if req.voice_key in VOICE_MAP else f"{req.role}_{req.personality}"
    voice = VOICE_MAP.get(key, VOICE_MAP["default"])
    print(f"[TTS] {voice}  text={req.text[:40]}...")
    try:
        # ★ 整段文本一次性合成，最多重试2次
        audio = b""
        for attempt in range(2):
            audio = await _tts_synthesize(req.text, voice)
            if audio:
                break
            print(f"[TTS] 第{attempt+1}次合成为空，重试...")
        if not audio:
            raise HTTPException(500, "TTS 返回音频为空")
        return Response(content=audio, media_type="audio/mpeg")
    except HTTPException:
        raise
    except Exception as e:
        import traceback; traceback.print_exc()
        raise HTTPException(500, f"TTS 失败: {e}")

# ── ASR WebSocket 代理（v3 大模型二进制协议）──
# 按官方文档：使用二进制帧协议，header(4B) + payload_size(4B) + payload
# 鉴权通过 HTTP Header：X-Api-App-Key / X-Api-Access-Key / X-Api-Resource-Id
#
# 流程：
#   1. 建连（带鉴权 header）
#   2. 发送 full_client_request（二进制帧，payload = gzip(json)）
#   3. 持续发送 audio_only_request（二进制帧，payload = gzip(pcm)）
#   4. 最后一包设置 flags=0b0010
#   5. 服务端每包返回 full_server_response（二进制帧，payload = gzip(json)）

def _asr_build_full_client_request() -> bytes:
    """构建 full_client_request 二进制帧"""
    payload_json = {
        "user": {"uid": "parent_companion"},
        "audio": {
            "format": "pcm",
            "rate": 16000,
            "bits": 16,
            "channel": 1,
            "language": "zh-CN",
        },
        "request": {
            "model_name": "bigmodel",
            "enable_itn": True,
            "enable_punc": True,
            "result_type": "full",
            "show_utterances": True,
        },
    }
    body = gzip.compress(json.dumps(payload_json).encode("utf-8"))
    # header: version=1, header_size=1, msg_type=0001(full_client_request),
    #         flags=0000, serialization=0001(JSON), compression=0001(Gzip), reserved=0x00
    header = bytes([0x11, 0x10, 0x11, 0x00])
    size = struct.pack(">I", len(body))
    return header + size + body

def _asr_build_audio_frame(audio_data: bytes, is_last: bool = False) -> bytes:
    """构建 audio_only_request 二进制帧"""
    body = gzip.compress(audio_data)
    # header: version=1, header_size=1, msg_type=0010(audio_only),
    #         flags=0010 if last else 0000, serialization=0000(none), compression=0001(Gzip)
    flags = 0x22 if is_last else 0x20  # 0b0010_0010 vs 0b0010_0000
    header = bytes([0x11, flags, 0x01, 0x00])
    size = struct.pack(">I", len(body))
    return header + size + body

def _asr_parse_response(raw: bytes):
    """解析 full_server_response 二进制帧，返回 dict 或 None"""
    if not raw or len(raw) < 4:
        return None
    msg_type = (raw[1] >> 4) & 0x0F
    if msg_type == 0x0F:
        # error frame
        if len(raw) >= 12:
            code = struct.unpack(">I", raw[4:8])[0]
            msg_size = struct.unpack(">I", raw[8:12])[0]
            msg = raw[12:12+msg_size].decode("utf-8", errors="replace")
            print(f"[ASR] 服务端错误: code={code}, msg={msg}")
        return None
    if msg_type != 0x09:  # 0b1001 = full_server_response
        return None
    compression = raw[2] & 0x0F
    # 跳过 header(4B) + sequence(4B) = 从 offset 8 开始
    if len(raw) < 12:
        return None
    payload_size = struct.unpack(">I", raw[8:12])[0]
    payload_data = raw[12:12+payload_size]
    if compression == 0x01:  # gzip
        payload_data = gzip.decompress(payload_data)
    try:
        return json.loads(payload_data.decode("utf-8"))
    except Exception:
        return None


@app.websocket("/asr")
async def asr_proxy(client_ws: WebSocket):
    await client_ws.accept()
    connect_id = str(uuid.uuid4())
    request_id = str(uuid.uuid4())
    # ★ 旧版鉴权（已测试通过）
    auth_headers = {
        "X-Api-App-Key": ASR_APPID,
        "X-Api-Access-Key": ASR_TOKEN,
        "X-Api-Resource-Id": ASR_RESOURCE_ID,
        "X-Api-Connect-Id": connect_id,
        "X-Api-Request-Id": request_id,
        "X-Api-Sequence": "-1",
    }
    try:
        async with websockets.connect(
            ASR_URI, additional_headers=auth_headers,
            ping_interval=None, open_timeout=10
        ) as asr_ws:
            print(f"[ASR] 已连接大模型 ASR (connect_id={connect_id[:8]}...)")

            # 1. 发送 full_client_request
            await asr_ws.send(_asr_build_full_client_request())
            print("[ASR] 已发送 full_client_request")

            # 2. ★ 等待服务端对 full_client_request 的响应，确认握手成功
            try:
                init_resp = await asyncio.wait_for(asr_ws.recv(), timeout=5)
                if isinstance(init_resp, bytes):
                    init_data = _asr_parse_response(init_resp)
                    if init_data is not None:
                        print(f"[ASR] 握手响应: {str(init_data)[:100]}")
                    else:
                        # 可能是错误帧，_asr_parse_response 已经打印了
                        msg_type = (init_resp[1] >> 4) & 0x0F if len(init_resp) >= 2 else -1
                        if msg_type == 0x0F:
                            print("[ASR] 握手失败（服务端返回错误），关闭连接")
                            await client_ws.send_json({"error": "ASR handshake failed"})
                            return
                        print(f"[ASR] 握手响应 msg_type=0x{msg_type:x}")
            except asyncio.TimeoutError:
                print("[ASR] 握手响应超时，继续尝试...")
            except Exception as e:
                print(f"[ASR] 等待握手响应异常: {e}")

            async def c2a():
                """前端 PCM → 豆包 ASR（包装为二进制帧）"""
                try:
                    async for pcm_data in client_ws.iter_bytes():
                        frame = _asr_build_audio_frame(pcm_data, is_last=False)
                        await asr_ws.send(frame)
                except WebSocketDisconnect:
                    # 发送最后一包（负包/空包）
                    try:
                        last_frame = _asr_build_audio_frame(b"", is_last=True)
                        await asr_ws.send(last_frame)
                    except Exception:
                        pass

            async def a2c():
                """豆包 ASR 二进制响应 → 解析 → 只转发新增的 definite 分句给前端"""
                forwarded_count = 0  # 已转发的 utterance 数量
                try:
                    async for raw in asr_ws:
                        if isinstance(raw, bytes):
                            data = _asr_parse_response(raw)
                            if data and "result" in data:
                                result = data["result"]
                                utterances = result.get("utterances", [])
                                # ★ 只转发新增的 definite=true 的分句
                                new_definite = []
                                for i, u in enumerate(utterances):
                                    if i >= forwarded_count and u.get("definite", False):
                                        new_definite.append(u)
                                if new_definite:
                                    forwarded_count = len(utterances)
                                    for u in new_definite:
                                        text = u.get("text", "").strip()
                                        if text:
                                            print(f"[ASR] 新分句: {text[:60]}")
                                            resp = {"result": {"utterances": [{"text": text, "definite": True, "is_final": True}]}}
                                            await client_ws.send_json(resp)
                        else:
                            print(f"[ASR] 收到文本: {str(raw)[:100]}")
                except Exception as e:
                    print(f"[ASR] a2c 错误: {e}")

            await asyncio.gather(c2a(), a2c())
    except WebSocketDisconnect:
        print("[ASR] 客户端断开")
    except Exception as e:
        print(f"[ASR] 连接大模型 ASR 失败: {e}")
        import traceback; traceback.print_exc()
        try: await client_ws.send_json({"error": str(e)})
        except Exception: pass

@app.get("/voices")
async def list_voices(): return JSONResponse(VOICE_MAP)

@app.get("/health")
async def health(): return {"status": "ok"}

# ── 图片生成代理（Grsai GPT-Image-2）──
GRSAI_KEY = "sk-70f90b7377ec48f7b564da4f085b44d3"
GRSAI_URLS = [
    "https://api.grsai.com/v1/images/generations",
    "https://api.grsai.cn/v1/images/generations",
]

class ImageRequest(BaseModel):
    prompt: str
    size: str = "1024x1024"
    quality: str = "medium"

@app.post("/v1/images/generate")
async def image_generate(req: ImageRequest):
    import aiohttp
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {GRSAI_KEY}",
    }
    payload = {
        "model": "gpt-image-2",
        "prompt": req.prompt,
        "size": req.size,
        "quality": req.quality,
        "n": 1,
    }
    for url in GRSAI_URLS:
        try:
            print(f"[生图] 请求: {req.prompt[:40]}... → {url.split('//')[1].split('/')[0]}")
            async with aiohttp.ClientSession() as session:
                async with session.post(url, headers=headers, json=payload,
                    proxy="http://127.0.0.1:7890",
                    timeout=aiohttp.ClientTimeout(total=120)) as resp:
                    if resp.status != 200:
                        err = await resp.text()
                        print(f"[生图] HTTP {resp.status}: {err[:100]}")
                        continue
                    data = await resp.json()
                    if data.get("data") and data["data"][0]:
                        img_url = data["data"][0].get("url", "")
                        b64 = data["data"][0].get("b64_json", "")
                        if img_url:
                            print(f"[生图] 成功 (url)")
                            return JSONResponse({"url": img_url})
                        elif b64:
                            print(f"[生图] 成功 (b64)")
                            return JSONResponse({"url": f"data:image/png;base64,{b64}"})
                    print(f"[生图] 返回数据无图片")
        except Exception as e:
            print(f"[生图] {url.split('//')[1].split('/')[0]} 失败: {e}")
    raise HTTPException(500, "图片生成失败")


# ── 家人动态系统 ──
import os, time, random
from datetime import datetime, timedelta
from pathlib import Path

MOMENTS_FILE = Path(__file__).parent.parent / "moments_data.json"
ARK_API_KEY = "9ec9fd5a-4e49-46b3-9c41-e03a48a24f67"  # 豆包 API Key（和前端共用）
ARK_MODEL = "ep-m-20260401231121-qf7cp"

# 动态类型模板
MOMENT_TYPES = [
    {"type": "image_text",  "weight": 3, "images": 1, "need_image": True},
    {"type": "image_mood",  "weight": 2, "images": 1, "need_image": True},
    {"type": "text_only",   "weight": 3, "images": 0, "need_image": False},
    {"type": "music_text",  "weight": 1, "images": 0, "need_image": False},
    {"type": "music_mood",  "weight": 1, "images": 0, "need_image": False},
]

MUSIC_POOL = [
    {"title": "时光慢慢", "artist": "妈妈的电台", "bg": "linear-gradient(135deg,#D4956A,#C17A50)", "emoji": "🎵"},
    {"title": "月光摇篮曲", "artist": "晚安频道", "bg": "linear-gradient(135deg,#2C3E6B,#4A6FA5)", "emoji": "🌙"},
    {"title": "春风十里", "artist": "午后时光", "bg": "linear-gradient(135deg,#7CB342,#558B2F)", "emoji": "🌿"},
    {"title": "温暖的风", "artist": "回家的路", "bg": "linear-gradient(135deg,#FF8A65,#D84315)", "emoji": "🌅"},
    {"title": "星空下的约定", "artist": "深夜电台", "bg": "linear-gradient(135deg,#5C6BC0,#283593)", "emoji": "⭐"},
]

def _load_moments():
    if MOMENTS_FILE.exists():
        with open(MOMENTS_FILE, "r", encoding="utf-8") as f:
            return json.loads(f.read())
    return []

def _save_moments(moments):
    with open(MOMENTS_FILE, "w", encoding="utf-8") as f:
        f.write(json.dumps(moments, ensure_ascii=False, indent=2))

async def _call_ark_api(prompt, max_tokens=200):
    """调用豆包大模型生成文案"""
    import aiohttp
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {ARK_API_KEY}",
    }
    payload = {
        "model": ARK_MODEL,
        "messages": [{"role": "user", "content": prompt}],
        "temperature": 0.85,
        "max_tokens": max_tokens,
        "stream": False,
    }
    async with aiohttp.ClientSession() as session:
        async with session.post(
            "https://ark.cn-beijing.volces.com/api/v3/chat/completions",
            headers=headers, json=payload, timeout=aiohttp.ClientTimeout(total=30)
        ) as resp:
            if resp.status != 200:
                raise Exception(f"Ark API {resp.status}")
            data = await resp.json()
            return data["choices"][0]["message"]["content"].strip()

async def _generate_image_url(prompt):
    """调用 Grsai 生成图片，返回 URL"""
    import aiohttp
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {GRSAI_KEY}",
    }
    payload = {
        "model": "gpt-image-2",
        "prompt": prompt,
        "size": "1024x1024",
        "quality": "medium",
        "n": 1,
    }
    for url in GRSAI_URLS:
        try:
            async with aiohttp.ClientSession() as session:
                async with session.post(url, headers=headers, json=payload,
                    proxy="http://127.0.0.1:7890",
                    timeout=aiohttp.ClientTimeout(total=120)) as resp:
                    if resp.status != 200:
                        continue
                    data = await resp.json()
                    if data.get("data") and data["data"][0]:
                        return data["data"][0].get("url", "")
        except Exception:
            continue
    return ""

async def generate_one_moment(memory_summary=""):
    """生成一条家人动态"""
    # 1. 随机选择动态类型
    types = MOMENT_TYPES
    weights = [t["weight"] for t in types]
    chosen = random.choices(types, weights=weights, k=1)[0]
    moment_type = chosen["type"]

    now = datetime.now()
    hour = now.hour
    time_context = "早上" if hour < 11 else "中午" if hour < 14 else "下午" if hour < 18 else "晚上"

    # 2. 用 AI 生成文案
    base_prompt = f"""你是一位温柔的妈妈，正在给远在外地工作的孩子发朋友圈。
现在是{time_context}，请按以下要求生成一条朋友圈动态：
- 类型：{moment_type}
- 语气自然真实，像普通中年妈妈发微信朋友圈
- 不要太文艺，要接地气
- 可以涉及：买菜做饭、天气变化、想念孩子、邻居趣事、散步遛弯、看到什么有感而发
"""
    if memory_summary:
        base_prompt += f"\n你对孩子的了解：{memory_summary}\n可以自然地关联这些信息。"

    if moment_type == "image_text":
        base_prompt += "\n请生成：1. 朋友圈文字（30-80字）2. 配图描述（用于AI生图，英文，描述你拍的照片场景，要求真实手机拍摄风格）\n格式：\n文字：...\n图片：..."
    elif moment_type == "image_mood":
        base_prompt += "\n请生成：1. 心情标签（emoji+4-8个字）2. 配图描述（英文，AI生图用，真实手机拍摄风格）\n格式：\n心情：...\n图片：..."
    elif moment_type == "text_only":
        base_prompt += "\n请生成一段朋友圈纯文字（40-120字），要有情感，像真的在想孩子或分享生活。\n格式：\n文字：..."
    elif moment_type == "music_text":
        base_prompt += "\n请生成一段配音乐的朋友圈文字（30-60字），内容是分享一首歌或听歌有感。\n格式：\n文字：..."
    elif moment_type == "music_mood":
        base_prompt += "\n请生成一个听歌时的心情标签（emoji+4-8个字）。\n格式：\n心情：..."

    try:
        ai_text = await _call_ark_api(base_prompt)
    except Exception as e:
        print(f"[动态] AI 文案生成失败: {e}")
        return None

    # 3. 解析 AI 输出
    moment = {
        "id": str(uuid.uuid4())[:8],
        "type": moment_type,
        "text": "",
        "images": [],
        "mood": "",
        "music": None,
        "time": now.strftime("%m月%d日 %H:%M"),
        "timestamp": int(now.timestamp()),
        "likes": random.randint(0, 5),
        "liked": False,
    }

    lines = ai_text.split("\n")
    for line in lines:
        line = line.strip()
        if line.startswith("文字：") or line.startswith("文字:"):
            moment["text"] = line.split("：", 1)[-1].split(":", 1)[-1].strip()
        elif line.startswith("心情：") or line.startswith("心情:"):
            moment["mood"] = line.split("：", 1)[-1].split(":", 1)[-1].strip()
        elif line.startswith("图片：") or line.startswith("图片:"):
            img_prompt = line.split("：", 1)[-1].split(":", 1)[-1].strip()
            if img_prompt:
                moment["_img_prompt"] = img_prompt

    # 纯文字类型，如果没有解析到文字，用全文
    if not moment["text"] and moment_type in ["text_only", "image_text", "music_text"]:
        # 去掉格式标记，用全文
        clean = ai_text.replace("文字：", "").replace("图片：", "").replace("心情：", "").strip()
        moment["text"] = clean[:120]

    if not moment["mood"] and moment_type in ["image_mood", "music_mood"]:
        moment["mood"] = "💭 有所思"

    # 4. 音乐类型随机选一首
    if moment_type in ["music_text", "music_mood"]:
        moment["music"] = random.choice(MUSIC_POOL)

    # 5. 生成图片（如果需要）
    if chosen["need_image"] and moment.get("_img_prompt"):
        print(f"[动态] 生成配图: {moment['_img_prompt'][:40]}...")
        img_url = await _generate_image_url(moment["_img_prompt"] + ", no text, no watermark, realistic phone photography")
        if img_url:
            moment["images"] = [img_url]
            print(f"[动态] 配图生成成功")
        else:
            print(f"[动态] 配图生成失败")

    # 清理临时字段
    moment.pop("_img_prompt", None)

    print(f"[动态] 生成完成: type={moment_type}, text={moment.get('text','')[:30]}...")
    return moment

# ── API 端点 ──

@app.get("/v1/moments")
async def get_moments():
    """获取所有动态"""
    moments = _load_moments()
    # 按时间倒序
    moments.sort(key=lambda x: x.get("timestamp", 0), reverse=True)
    return JSONResponse(moments)

@app.post("/v1/moments/generate")
async def generate_moment_endpoint():
    """手动触发生成一条新动态"""
    # 读取记忆摘要（如果有）
    memory = ""
    moment = await generate_one_moment(memory)
    if not moment:
        raise HTTPException(500, "动态生成失败")
    # 追加到文件
    moments = _load_moments()
    moments.append(moment)
    _save_moments(moments)
    return JSONResponse(moment)

@app.post("/v1/moments/batch-generate")
async def batch_generate_moments():
    """批量生成多条动态（首次初始化用）"""
    moments = _load_moments()
    count = 0
    for i in range(5):
        print(f"\n[动态] 正在生成第 {i+1}/5 条...")
        moment = await generate_one_moment()
        if moment:
            moments.append(moment)
            count += 1
    _save_moments(moments)
    print(f"[动态] 批量生成完成，共 {count} 条")
    return JSONResponse({"generated": count, "total": len(moments)})


if __name__ == "__main__":
    import uvicorn

    async def verify():
        print("\n[启动检测] 验证豆包 TTS...")
        for key, voice in VOICE_MAP.items():
            if key == "default": continue
            try:
                audio = await _tts_synthesize("你好", voice)
                print(f"  ✅ {key}: {voice}  ({len(audio)} bytes)")
            except Exception as e:
                print(f"  ❌ {key}: {voice} → {e}")
        print("[启动检测] 完成\n")

    asyncio.run(verify())
    print("豆包语音服务启动在 http://localhost:8000")
    uvicorn.run(app, host="0.0.0.0", port=8000)
