wangzhibo
2026-09-11 659c470eba542383987aad227b7f918100f131fa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
"""Persist user/agent audio by device, scene mode, and time for later replay."""
 
from __future__ import annotations
 
import os
import time
from typing import Any, Optional
 
from config.config_loader import get_project_dir
from config.logger import setup_logging
 
TAG = __name__
logger = setup_logging()
 
MODE_DIR = {
    "CHAT": "chat",
    "SHADOWING": "shadowing",
    "PLAY_AUDIO": "repeater",
    "TRANSLATION": "translation",
    "BILINGUAL": "bilingual",
}
 
 
def _safe_segment(value: Any, fallback: str = "unknown") -> str:
    raw = str(value or "").strip() or fallback
    out = []
    for ch in raw:
        if ch.isalnum() or ch in "-_":
            out.append(ch)
        elif ch in (":", " ", "."):
            out.append("-")
        else:
            out.append("_")
    text = "".join(out).strip("-_") or fallback
    return text[:80]
 
 
def _mode_dir(conn: Any) -> str:
    mode = str(getattr(conn, "interaction_mode", "") or "").strip().upper()
    return MODE_DIR.get(mode, _safe_segment(mode.lower() or "unknown"))
 
 
def _archive_cfg(conn: Any) -> dict:
    return dict(((getattr(conn, "config", None) or {}).get("audio_archive") or {}))
 
 
def archive_enabled(conn: Any = None) -> bool:
    cfg = _archive_cfg(conn)
    if cfg.get("enabled") is False:
        return False
    return True
 
 
def archive_root(conn: Any = None) -> str:
    cfg = _archive_cfg(conn)
    rel = str(cfg.get("dir") or "data/recordings")
    if os.path.isabs(rel):
        return rel
    return os.path.join(get_project_dir(), rel)
 
 
def archive_wav(
    conn: Any,
    wav_bytes: bytes,
    *,
    kind: str = "asr",
    extra: str = "",
) -> Optional[str]:
    """Write WAV under recordings/{device}/{mode}/{date}/{time}_{kind}_{extra}.wav."""
    if not wav_bytes or not archive_enabled(conn):
        return None
    try:
        now = time.time()
        local = time.localtime(now)
        day = time.strftime("%Y-%m-%d", local)
        stamp = time.strftime("%H%M%S", local) + f"{int((now % 1) * 1000):03d}"
        device = _safe_segment(getattr(conn, "device_id", "") or "", "unknown")
        mode = _mode_dir(conn)
        parts = [stamp, _safe_segment(kind, "asr")]
        extra_part = _safe_segment(extra, "") if extra else ""
        if extra_part:
            parts.append(extra_part)
        filename = "_".join(parts) + ".wav"
        directory = os.path.join(archive_root(conn), device, mode, day)
        os.makedirs(directory, exist_ok=True)
        path = os.path.join(directory, filename)
        with open(path, "wb") as handle:
            handle.write(wav_bytes)
        relative = os.path.join(device, mode, day, filename).replace("\\", "/")
        logger.bind(tag=TAG).info(f"音频归档 {relative} bytes={len(wav_bytes)}")
        return relative
    except Exception as exc:
        logger.bind(tag=TAG).warning(f"音频归档失败: {exc}")
        return None
 
 
def archive_pcm_snapshot(conn: Any, snapshot: Any, *, extra: str = "") -> Optional[str]:
    from core.app_runtime.assessment import pcm_snapshot_to_wav
 
    wav_bytes = pcm_snapshot_to_wav(snapshot)
    if not wav_bytes:
        return None
    return archive_wav(conn, wav_bytes, kind="asr", extra=extra)