[测试、性能、安全、验收与归档]:完成全量测试验收与 OpenSpec 归档,包含 CLI 验收、NewAPI smoke、安全扫描和主 spec 更新
This commit is contained in:
@@ -2,13 +2,33 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from .assets import validate_pet_assets
|
||||
from .config import AppConfig
|
||||
from .conversation import ConversationContext
|
||||
from .llm import MockLlmProvider, OpenAICompatibleLlmProvider
|
||||
from .models import AudioFrame, ProviderError
|
||||
from .pipeline import VoicePipeline
|
||||
from .stt import MetadataSttProvider
|
||||
from .transport import MemoryAudioTransport
|
||||
from .tts import SineTtsProvider
|
||||
from .vad import EnergyVadProvider, VadRecorder
|
||||
from .wakeword import KeywordWakeWordProvider
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(prog="owner-voice-pet")
|
||||
parser.add_argument("--show-config", action="store_true", help="Print non-secret config summary")
|
||||
subparsers = parser.add_subparsers(dest="command")
|
||||
subparsers.add_parser("acceptance", help="Run deterministic end-to-end pipeline acceptance")
|
||||
subparsers.add_parser("validate-assets", help="Validate project pet assets")
|
||||
subparsers.add_parser("security-check", help="Scan tracked files for leaked API keys")
|
||||
smoke = subparsers.add_parser("llm-smoke", help="Call configured OpenAI/NewAPI endpoint")
|
||||
smoke.add_argument("--message", default="用一句中文回复:小杰在线。")
|
||||
smoke.add_argument("--no-stream", action="store_true")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.show_config:
|
||||
@@ -22,6 +42,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"llm_base_url": config.llm_base_url,
|
||||
"llm_model": config.llm_model,
|
||||
"llm_api_style": config.llm_api_style,
|
||||
"llm_stream": config.llm_stream,
|
||||
"llm_api_key_present": bool(config.llm_api_key),
|
||||
"asset_dir": str(config.asset_dir),
|
||||
},
|
||||
@@ -31,5 +52,99 @@ def main(argv: list[str] | None = None) -> int:
|
||||
)
|
||||
return 0
|
||||
|
||||
if args.command == "validate-assets":
|
||||
infos = validate_pet_assets(AppConfig.from_env().asset_dir)
|
||||
print(json.dumps({"assets": len(infos), "valid": True}, ensure_ascii=False, sort_keys=True))
|
||||
return 0
|
||||
|
||||
if args.command == "security-check":
|
||||
leaks = find_secret_leaks(Path.cwd())
|
||||
print(json.dumps({"secret_leaks": leaks, "valid": not leaks}, ensure_ascii=False, sort_keys=True))
|
||||
return 1 if leaks else 0
|
||||
|
||||
if args.command == "acceptance":
|
||||
result = run_acceptance()
|
||||
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
|
||||
return 0 if result["success"] else 1
|
||||
|
||||
if args.command == "llm-smoke":
|
||||
config = AppConfig.from_env()
|
||||
if args.no_stream:
|
||||
config = AppConfig(
|
||||
wake_word=config.wake_word,
|
||||
sample_rate=config.sample_rate,
|
||||
channels=config.channels,
|
||||
llm_base_url=config.llm_base_url,
|
||||
llm_api_key=config.llm_api_key,
|
||||
llm_model=config.llm_model,
|
||||
llm_api_style=config.llm_api_style,
|
||||
llm_stream=False,
|
||||
audio_input_device=config.audio_input_device,
|
||||
audio_output_device=config.audio_output_device,
|
||||
asset_dir=config.asset_dir,
|
||||
log_dir=config.log_dir,
|
||||
context_max_messages=config.context_max_messages,
|
||||
context_max_chars=config.context_max_chars,
|
||||
)
|
||||
try:
|
||||
provider = OpenAICompatibleLlmProvider(config, timeout_s=30)
|
||||
messages = [ConversationContext().build_llm_messages()[0]]
|
||||
messages.append(__import__("owner_voice_pet.models", fromlist=["Message"]).Message("user", args.message, 1.0))
|
||||
text = "".join(delta.text_delta for delta in provider.stream_reply(messages)).strip()
|
||||
print(json.dumps({"ok": bool(text), "reply_preview": text[:80]}, ensure_ascii=False, sort_keys=True))
|
||||
return 0 if text else 1
|
||||
except ProviderError as exc:
|
||||
print(json.dumps({"ok": False, "code": exc.code.value, "message": exc.message}, ensure_ascii=False, sort_keys=True))
|
||||
return 1
|
||||
|
||||
parser.print_help()
|
||||
return 0
|
||||
|
||||
|
||||
def run_acceptance() -> dict[str, object]:
|
||||
validate_pet_assets(AppConfig.from_env().asset_dir)
|
||||
frames = [
|
||||
AudioFrame(b"\x80\x80", 16000, 1, 0, 0, {"duration_ms": 20, "wake_word": "小杰小杰", "wake_confidence": 0.99}),
|
||||
AudioFrame(b"\xff\xff", 16000, 1, 20, 1, {"duration_ms": 20, "speech": True, "transcript": "你是谁"}),
|
||||
AudioFrame(b"\xff\xff", 16000, 1, 40, 2, {"duration_ms": 20, "speech": True}),
|
||||
AudioFrame(b"\x80\x80", 16000, 1, 60, 3, {"duration_ms": 20, "speech": False}),
|
||||
AudioFrame(b"\x80\x80", 16000, 1, 80, 4, {"duration_ms": 20, "speech": False}),
|
||||
]
|
||||
transport = MemoryAudioTransport(frames)
|
||||
pipeline = VoicePipeline(
|
||||
transport=transport,
|
||||
wakeword=KeywordWakeWordProvider(),
|
||||
vad_recorder=VadRecorder(EnergyVadProvider(), min_duration_ms=40, end_silence_ms=40),
|
||||
stt=MetadataSttProvider(),
|
||||
context=ConversationContext(),
|
||||
llm=MockLlmProvider(["我是小杰桌宠,已经在线。"]),
|
||||
tts=SineTtsProvider(),
|
||||
)
|
||||
pipeline.load()
|
||||
result = pipeline.run_once()
|
||||
return {
|
||||
"success": result.success,
|
||||
"transcript": result.transcript,
|
||||
"assistant_text": result.assistant_text,
|
||||
"played_segments": result.played_segments,
|
||||
"states": [state.value for state in result.states],
|
||||
}
|
||||
|
||||
|
||||
def find_secret_leaks(root: Path) -> list[str]:
|
||||
completed = subprocess.run(["git", "ls-files"], cwd=root, check=True, stdout=subprocess.PIPE, text=True)
|
||||
pattern = re.compile(r"sk-[A-Za-z0-9_\\-]{16,}")
|
||||
leaks: list[str] = []
|
||||
for rel in completed.stdout.splitlines():
|
||||
path = root / rel
|
||||
if not path.exists():
|
||||
continue
|
||||
if path.suffix.lower() in {".png", ".jpg", ".jpeg", ".webp"}:
|
||||
continue
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
if pattern.search(text):
|
||||
leaks.append(rel)
|
||||
return leaks
|
||||
|
||||
Reference in New Issue
Block a user