Files
fanlusky a7779d2b05 refactor(engine): 将 engine 从 submodule 内嵌为仓内一等组件
`vendor/MetasequoiaImeEngine` submodule 改为 `engine/` 目录,导入的是
product-lock.json 原本钉住的提交 c810d201,逐文件核对过 blob,因此这是一次
搬迁而不是引擎升级。引擎自己的嵌套 submodule(googlepinyinime-rev、utfcpp、
miniaudio)按各自 gitlink 一并展开,来源与提交见 engine/UPSTREAM.md。

Windows 专门化裁掉了不属于本产品的部分:whisper.cpp 与本地推理 provider、
macOS 示例宿主、整个 dictionary/ 建库流水线、上游 CI 与 release-please 配置。
词库仍按 product-lock.json 在构建时取发布产物。

引擎不再进 product-lock.json:没有 gitlink 就没有可比对的东西,锁里写一个上游
commit 等于宣称这棵树仍等于上游,而第一个 Windows 专门化改动落地时这句话就假了。
verify-contracts / verify-checkout 随之移除,verify-published 保留。

CI 与工具链:引擎源码并入根 scripts/format.sh(删掉引擎自己的那份,避免两套排除
列表分叉);引擎 voice.yml 里的 silero_vad.onnx 摘要校验移到本仓 lock job;
engine-bump-triage 工作流与其测试删除;gitsubmodule 的 dependabot 频率由 daily
改为 monthly。

README、README.en、AGENTS.md、CONTRIBUTING、docs/product-release.md 及各子目录
文档同步说明新的位置与边界。
2026-09-12 03:40:48 +08:00

77 lines
3.5 KiB
Python

"""Real libcurl transport against loopback only; never uses microphone or credentials."""
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from email.parser import BytesParser
from email.policy import default
import json
import subprocess
import sys
from socketserver import TCPServer
import threading
import time
errors = []
class Handler(BaseHTTPRequestHandler):
def log_message(self, *_):
pass
def do_POST(self):
try:
body = self.rfile.read(int(self.headers['Content-Length']))
assert self.headers.get('Authorization') == 'Bearer fixture-token'
if self.path == '/asr':
assert self.headers['Content-Type'].startswith('multipart/form-data; boundary=')
assert b'name="file"' in body and b'RIFF' in body and b'WAVE' in body
assert b'name="model"' in body and b'fixture-model' in body
multipart = BytesParser(policy=default).parsebytes(
('Content-Type: ' + self.headers['Content-Type'] + '\r\nMIME-Version: 1.0\r\n\r\n').encode() + body)
parts = {part.get_param('name', header='content-disposition'): part for part in multipart.iter_parts()}
assert set(parts) == {'model', 'file'}
assert parts['model'].get_payload(decode=True) == b'fixture-model'
audio = parts['file'].get_payload(decode=True)
assert len(audio) == 364 and audio[:4] == b'RIFF' and audio[8:12] == b'WAVE'
assert parts['file'].get_filename() == 'audio.wav'
if self.path == '/polish':
request = json.loads(body)
assert request['model'] == 'fixture-model'
assert request['messages'][1]['content'].startswith('<asr_text>\n')
if self.path == '/slow':
time.sleep(2)
status = 401 if self.path == '/denied' else 302 if self.path == '/redirect' else 200
self.send_response(status)
if status == 302: self.send_header('Location', '/asr')
self.end_headers()
response = {'text': '水杉 voice'}
if self.path == '/missing': response = {'other': 'not a transcript'}
if self.path == '/polish': response = {'choices': [{'message': {'content': '水杉'}}]}
data = json.dumps(response, ensure_ascii=False).encode()
if self.path == '/invalid': data = b'invalid json'
if self.path == '/oversize': data = b'x' * (1024 * 1024 + 1)
self.wfile.write(data)
except (BrokenPipeError, ConnectionResetError):
pass
except Exception as error:
errors.append(repr(error))
self.send_error(500)
class LoopbackServer(ThreadingHTTPServer):
def server_bind(self):
# HTTPServer.server_bind calls getfqdn(), which may wait on external DNS.
# This fixture is loopback-only and has no need for reverse DNS.
TCPServer.server_bind(self)
self.server_name = 'localhost'
self.server_port = self.server_address[1]
print('Starting loopback HTTP fixture', flush=True)
server = LoopbackServer(('127.0.0.1', 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
print('Running HTTP consumer', flush=True)
result = subprocess.run([sys.argv[1], f'http://127.0.0.1:{server.server_port}'], timeout=20)
assert not errors, errors
raise SystemExit(result.returncode)
finally:
print('Stopping loopback HTTP fixture', flush=True)
server.shutdown()
server.server_close()