整体架构
本页给出 RVC Next 的全貌:包结构与分层规则、服务怎样被组装、哪些工作在哪个进程里运行、一个请求从浏览器到引擎再回来的路径,以及贯穿各层的几样基础设施:错误、设置、事件、数据库和数据目录。
本页给出 RVC Next 的全貌:包结构与分层规则、服务怎样被组装、哪些工作在哪个进程里运行、一个请求从浏览器到引擎再回来的路径,以及贯穿各层的几样基础设施:错误、设置、事件、数据库和数据目录。各层的细节分别在引擎、核心服务、API 与事件、Web UI 和命令行实现中展开。
分层
flowchart TB
subgraph clients["两端(薄)"]
webui["rvc_next/webui<br/>Vue 3,构建到 dist/"]
api["rvc_next.api<br/>FastAPI 路由 · socket.io · 静态文件"]
cli["rvc_next.cli<br/>Typer 命令"]
end
core["rvc_next.core<br/>服务:settings · events · db · jobs · compute · assets · audio<br/>models · presets · conversion · separation · training · live"]
engine["rvc_next.engine<br/>机器学习与 DSP:模型、F0、HuBERT、索引、<br/>离线 / 流式转换、训练、分离、音频 I/O"]
workers["rvc_next.workers<br/>train · separate · live · devices"]
protocol["rvc_next.protocol<br/>worker 事件与实时变声控制消息(仅标准库)"]
webui -- "REST /api/v1/* · socket.io /ws" --> api
api --> core
cli --> core
core -- "进程内调用(延迟导入)" --> engine
core -- "启动子进程,读 JSON 行事件" --> workers
workers --> engine
workers --> protocol
core --> protocol
rvc_next/
engine/ 机器学习与 DSP,不含任何应用代码
protocol/ worker 事件(JSON 行)与 Live 控制消息;只用标准库
workers/ python -m rvc_next.workers.{train,separate,live,devices} <request.json>
core/ 服务;context.py 负责组装,errors.py 是全部错误类型
api/ FastAPI:每种资源一个路由模块,加安全、socket、静态文件。很薄
cli/ Typer:app.py 注册一切,commands/ 是普通函数。很薄
webui/ Vue 项目;dist/ 不入库,作为包数据随 wheel 发布
scripts/ dev.py、build_wheel.py、generate_openapi.py、golden.py
tests/ engine/ core/ api/ cli/ workers/ golden/,tiny.py,conftest.py分层规则及其测试
tests/test_architecture.py 用 ast 遍历每个模块的 import(只看绝对导入),对照下表;标准库总是允许的。
| 层 | 可导入的 rvc_next 子包 | 可导入的第三方模块 |
|---|---|---|
engine | engine | numpy、scipy、torch、transformers、faiss、parselmouth、librosa、av、soundfile、onnxruntime、torch_directml、tensorboard,以及 sounddevice(仅限 engine/audio_io/)、pymss 与 pymss_core(仅限 engine/separate/)、yaml |
protocol | protocol | 无 |
workers | engine、protocol、workers | 与 engine 相同,外加 sounddevice、pymss、yaml |
core | core、engine、protocol、version、logger | pydantic、httpx、tomli、tomli_w、send2trash、numpy、typing_extensions |
api | api、core、version、logger、webui | fastapi、starlette、socketio、uvicorn、pydantic、anyio |
cli | cli、core、version、logger、api | typer、click、rich、pydantic、uvicorn |
另有两个子进程测试:导入 rvc_next.core.context 不得加载 torch;导入 rvc_next.cli.app 既不得加载 torch 也不得加载 fastapi。
这些规则各有用意:
- 引擎不知道应用的存在。 它不导入核心层、任何 Web / CLI 框架或 pydantic;参数用冻结的 dataclass(
VoiceParams、StreamParams)表示。这样 worker 子进程和测试可以直接驱动引擎,而核心层的Record模型是唯一的 API 形状。原版的问题之一正是引擎返回 Gradio 的更新字典和中文状态文字,离开 UI 就无法使用。 - 核心层不导入框架,也不直接导入 torch。 它对引擎的 51 处导入全部写在函数内部。结果是
rvc-next version、config、--help这类命令不付出加载 torch 的代价,第二个测试守着这一点。 sounddevice和pymss被关在各自的目录里,设备访问和分离库的变动不会扩散。protocol只用标准库,因为它被两侧共享:服务端和 worker 子进程都要能导入它而不带上对方的依赖。api和cli从不导入engine。 唯一的跨端导入是cli的webui命令在函数内导入rvc_next.api.app.create_app来启动服务器,规则表为此允许cli → api和uvicorn。
一个操作,一个核心方法
每个操作在核心层恰好有一个方法,接收并返回 Record 模型(core/record.py)。路由和命令只做三件事:解析输入、调用这个方法、整理输出。以转换为例,api/routers/convert.py 的路由体只是检查服务器路径的访问权限、拒绝只属于命令行的字段,然后 return services.conversion.convert(body);CLI 的 convert 命令构造同一个 ConvertRequest,调用同一个方法(foreground=True)。
所以 API 返回的 JSON 和 CLI 的 --json 输出形状完全相同:tests/cli/test_cli.py 里的 test_model_json_matches_api_shape 直接比较 rvc-next model list --json 与 GET /api/v1/models 的结果。Record 设置了 json_schema_serialization_defaults_required,带默认值的字段在输出模式中标为必填,生成的 TypeScript 类型因此与服务器实际发送的内容一致。
组装:build_services()
core/context.py 的 build_services() 一次性按依赖顺序构建所有服务,返回一个 Services dataclass:
| 字段 | 类 | 职责 |
|---|---|---|
settings | SettingsService | settings.toml、环境变量覆盖、解析后的目录 |
events | LocalEventBus | 进程内发布 / 订阅 |
db | Database | rvc-next.db |
http | HttpClientProvider | 共享的 httpx 客户端(测试可注入 transport) |
compute | ComputeService | 设备、显存占用、GPU 租约、空闲卸载 |
jobs | JobService | 任务队列、资源、执行器、历史 |
assets | AssetService | 资源目录、下载、校验 |
audio | AudioFileService | 上传、服务器路径、探测、波形峰值、输出 |
models | VoiceModelService | 音色库 |
inbox | IndexInbox | 未分配的索引 |
imports | ImportService | 导入会话 |
base_models | BaseModelService | 训练底模 |
separation_models | SeparationLibrary | 导入的分离模型 |
presets | PresetService | 预设 |
separation / conversion / training / live | 对应的服务 | 各功能 |
几处相互引用(models.presets、assets.models、models.imports、imports.base、imports.separation、training.base、separation.library)在构造之后以属性赋值接上,避免构造函数之间的循环。TrainingService 和 LiveService 在函数内导入。最后调用 models.rescan(),让数据库里的音色缓存与磁盘一致。
start_background 决定后台线程是否启动:
服务端(rvc-next webui) | 命令行 | |
|---|---|---|
| 入口 | build_services(start_background=True),挂在 app.state.services 上,路由通过 ServicesDep 取得 | cli/output.py 的 open_services() 上下文管理器,每条命令构建一次、用完关闭 |
| 后台 | 清理过期上传和超过 convert.keep_outputs_days 的输出(audio.cleanup())、compute.start()(空闲卸载)、jobs.start()(调度线程)、live.start_background() | 不启动 |
| 任务 | jobs.submit() 入队,由调度器按资源执行 | jobs.run_now() 在调用线程上前台执行,Ctrl+C 取消 |
测试通过 environ=(代替 os.environ)、transport=(代替网络)和 data_dir= 构建完全隔离的服务(tests/conftest.py 的 make_services)。Services.close() 依次关闭 live、jobs、compute、http 和数据库。
进程模型
| 工作 | 在哪里运行 | 原因 |
|---|---|---|
| HTTP 与 socket.io | 服务进程的事件循环;涉及磁盘和数据库的路由是普通 def,在线程池中运行 | — |
| 离线转换、合并、提取、下载等 | 服务进程(或 CLI 进程)中的任务线程,带 CancelToken | HuBERT、F0 模型和最近用过的音色留在缓存里,第二个文件立刻开始 |
| 人声分离 | 每个任务一个子进程 workers.separate | PyMSS 没有内部取消机制,取消只能杀进程;进程退出后显存完全归还。沿用原版 tools/pymss_webui.py 的设计 |
| 训练阶段 | 每个阶段一个子进程 workers.train | 可以硬取消、得到干净的 CUDA 上下文,DDP 也需要自己的进程 |
| 实时变声 | 一个长驻子进程 workers.live | 它拥有音频设备和实时线程,要与服务端的事件循环、GIL 竞争和可能的崩溃隔离开 |
| 设备枚举 | 短命子进程 workers.devices | 在一个已有流打开的进程里重新枚举 PortAudio 会干扰它;原版为了刷新设备列表要调用私有的 sd._terminate() 并停掉转换 |
worker 协议
workers.train、workers.separate 和 workers.devices 遵循同一套约定,源自原版 tools/pymss_webui.py 的事件协议:
- 核心层把请求写成一个 JSON 文件,以
sys.executable -m <module> <request.json>启动 worker(core/process.py:自己的进程组,stdout 与 stderr 合并,PYTHONUNBUFFERED=1)。原版的--pycmd参数因此不再需要。 - worker 的入口经过
workers/base.py的run_worker():它把真正的 stdout 复制出来专门写事件,再把文件描述符 1 重定向到 stderr。库函数随手print的内容因此只会变成日志文本,不会被误当作事件。 - 事件是带前缀
@rvc的 JSON 行(protocol/jsonl.py),类型定义在protocol/messages.py:progress、step、log、metric、output、result、error。没有前缀的行一律当作info日志。 - 失败统一成
error事件:MissingAssetError→asset_missing(detail.assets),ModelFormatError→invalid_model,显存不足 →compute_unavailable且detail.reason = "out_of_memory",其余 →internal_error,完整堆栈作为error级日志另发。核心层用core/engine_errors.py的worker_error()把它重建为对应的领域错误。 - 退出码:0 成功;
EXIT_CANCELLED = 130表示已取消(SIGTERM 也映射到它);EXIT_RETRY = 75请求在新进程中重试(DirectML 下分离先试 fp16,失败再用 fp32)。 - 取消即杀掉整个进程树(
kill_process_tree,移植自原版tools/process_utils.py)。
workers.live 例外:它长驻,需要双向通信,所以走 multiprocessing.connection。核心层的 LiveSupervisor 开一个带随机 authkey 的 Listener,把地址写进请求文件;之后双方交换 protocol/live.py 中定义的 JSON 消息(Start、UpdateVoice、SetDevices……以及 State、Stats、DeviceLost、Log)。详见核心服务与引擎。
一个请求的路径
以网页上点"转换"为例:
sequenceDiagram
participant B as 浏览器
participant M as SecurityMiddleware
participant R as routers/convert.py
participant C as ConversionService
participant J as JobService
participant T as 任务线程
participant E as engine
participant Bus as LocalEventBus
participant S as SocketBridge
B->>M: POST /api/v1/convert
M->>R: Host / Origin / 令牌检查通过
R->>C: convert(ConvertRequest)
C->>C: validate():音色、说话人、资源是否齐全
C->>J: submit("convert", request)
J-->>R: Job(queued)
R-->>B: 200 Job
J->>Bus: job_updated
J->>T: 资源空闲后执行 JobSpec.run
T->>E: OfflineConverter(逐个分块,检查取消)
T->>Bus: job_updated(进度,每秒至多 4 次)· outputs_added
Bus->>S: 发布线程上同步回调
S->>S: call_soon_threadsafe 跨入事件循环
S-->>B: socket.io 事件
B->>B: 修补 / 失效 TanStack Query 缓存
要点:
- REST 是事实来源,socket 只做服务端到客户端的推送;客户端重连后会重新拉取活动任务、Live 状态和可见列表。
- 任务请求以 JSON 存进数据库,
JobSpec由按kind注册的工厂从请求重建,所以服务重启后重试依然可行。 - 路由在请求进入时就完成校验(例如缺少 RMVPE 时立即返回 409
asset_missing,界面就地提供下载),而不是让任务排队后才失败。
错误与退出码
core/errors.py 是全部错误词汇。每个类携带 code、http_status 和 exit_code,两端机械地翻译:API 把任何 RvcNextError 变成 {"code", "message", "detail"}(api/errors.py),CLI 把它变成退出码。
| 异常 | code | HTTP | 退出码 | 何时抛出 |
|---|---|---|---|---|
NotFoundError | not_found | 404 | 2 | 音色、任务、文件或实验不存在 |
ConflictError | conflict | 409 | 3 | 名称冲突,导入覆盖已有模型;有任务或 Live 在运行时请求释放显存(detail.reason = "busy") |
BusyError(继承 ConflictError) | busy | 409 | 3 | Live 已在运行,同一实验已有阶段在运行 |
InvalidPathError | invalid_path | 400 | 4 | 路径越出根目录,或服务器路径不在浏览根目录内 |
ValidationError | invalid_input | 400 | 4 | 参数越界、设置无效 |
ModelFormatError(继承 ValidationError) | invalid_model | 400 | 4 | .pth 既不是 RVC 小模型也不是 G 检查点 |
AssetMissingError | asset_missing | 409 | 7 | HuBERT、RMVPE、FCPE 或底模未安装;detail.assets 列出资源 id,界面据此提供下载 |
DeviceError | device_unavailable | 409 | 8 | 音频设备缺失、被占用或拒绝该格式;detail.reason 为 missing / busy / format / permission |
ComputeError | compute_unavailable | 503 | 9 | GPU 不可用或显存耗尽(detail.reason = "out_of_memory") |
RvcNextError(基类) | internal_error | 500 | 1 | 其他 |
此外,API 层自己定义了 NotLocalError(not_local,403),用于"在资源管理器中显示"这类只对本机浏览器有意义的操作。
错误从三个方向汇入这张表:
- 引擎异常(
engine/errors.py:MissingAssetError、ModelFormatError、AudioError、Cancelled)由core/engine_errors.py的map_engine_error()转换;引擎永远不导入核心层。显存不足由engine/runtime.is_oom()识别(torch.OutOfMemoryError、错误码为 2 的AcceleratorError、MPS / DirectML 的错误消息)。 - worker 的
error事件由worker_error()重建。 - Live worker 的
State.error也使用同样的code。
设置
core/settings/models.py 是一棵 Record 树,根为 Settings,分组为 server、paths、compute、downloads、convert、separation、training、live。SettingsService 拥有数据目录中的 settings.toml,按以下优先级合并(后者覆盖前者):
flowchart LR
d["模型默认值"] --> f["settings.toml"] --> e["环境变量<br/>RVC_NEXT_GROUP__FIELD"] --> o["overrides<br/>(宿主固定值)"]
- 环境变量用
__分隔层级,例如RVC_NEXT_SERVER__PORT=8000、RVC_NEXT_COMPUTE__DEVICE=cpu;值能按 JSON 解析就按 JSON,否则作为字符串,null/none表示空。环境变量和 overrides 从不写回文件。RVC_NEXT_DATA_DIR不是设置项,它决定数据目录本身。 update()深度合并补丁、校验、原子写入(先写.toml.tmp再替换),然后通知监听者,长驻服务因此不用重启就能跟上设置变化。TOML 没有 null,保存时剔除值为None的键。config set和 API 的PATCH /settings共用同一套点号路径逻辑。- 对外的
SettingsView不含访问令牌,只给出access_token_configured: bool;env_overrides列出被环境变量固定的名字,界面据此提示"此项由环境变量设定"。 - 目录类设置为空表示"在数据目录内":
SettingsService.models_dir、experiments_dir、outputs_dir、assets_dir负责解析。paths.assets_dir可以直接指向一个原版 RVC 的assets/目录,避免重复下载。
live 组(设备、流参数、上次的音色与参数)放在设置而不是客户端状态里,因为设备属于服务器所在的机器,CLI 的 live run 也要读同样的值。纯界面偏好(主题、语言、上次的页面)则是客户端状态。各分组字段的含义见配置与环境变量。
事件
core/events/models.py 定义 EventBase(Record),每个子类用类属性 __event_name__ 命名;EventBase.get_events() 收集所有具体子类,API 把它们的 JSON 模式写进 OpenAPI,前端的事件类型因此也是生成的。现有事件:
job_updated、job_log、jobs_cleared、outputs_added、outputs_removed、models_changed、presets_changed、assets_changed、experiment_changed、train_metrics、live_state、live_stats、devices_changed、compute_changed、settings_changed。
约定:
- 事件携带快照,而不是正在被工作线程修改的活对象:
job_updated带的是整个Job的副本。 - 在源头节流:任务进度每个任务每秒至多 4 次,日志成批发送(每批至多 50 行,界面再按字节偏移经 REST 补齐)。
LocalEventBus在发布者的线程上同步调用处理器,处理器异常只记日志不传播。需要在别处运行的订阅者自己转交:api/sockets.py的SocketBridge用loop.call_soon_threadsafe把事件放进 asyncio 队列,再由一个任务sio.emit(event.__event_name__, event.model_dump(mode="json"))。CLI 的进度条(cli/progress.py)订阅的是同一批事件。
载荷细节见 API 与事件。
数据库
core/db/database.py:一个共享的 sqlite3 连接,外加 RLock;WAL 模式、外键开启、synchronous=NORMAL、busy_timeout=5000(服务器写入时 CLI 也会读同一个文件,例如 rvc-next jobs list 能看到正在运行的服务器的任务)。迁移是 MIGRATIONS 列表中的 SQL 脚本,只追加、不修改已发布的条目,PRAGMA user_version 记录执行到第几条。目前有两条:第 1 条建表 voice_models、presets、jobs、outputs、audio_files、client_state;第 2 条给 voice_models 加 catalog_id(标记从演示音色目录下载的音色)。
数据库里的内容分两类:
- 缓存:
voice_models可以由models/目录和旧版 RVC 安装目录完整重建(models.rescan())。 - 记录:任务历史和输出的来源信息(输入、音色、参数)。
删除数据库损失的是任务历史、输出来源、自定义预设(默认预设会重建)和界面偏好的服务器端副本;音色库会从磁盘重建。实验、底模、分离模型和未分配的索引根本不在数据库里:它们是带 JSON 附属文件的目录,每次列出时扫描,所以不会与磁盘上的文件不一致。
数据目录
core/paths.py 的 default_data_dir() 依次取 RVC_NEXT_DATA_DIR、Windows 的 %APPDATA%\rvc-next、macOS 的 ~/Library/Application Support/rvc-next、其他平台的 $XDG_DATA_HOME/rvc-next(默认 ~/.local/share/rvc-next)。
<data>/
settings.toml 设置文件
rvc-next.db SQLite 数据库
server.json 运行中服务器的主机、端口、pid(退出时删除)
legacy-voices.json 旧版安装中音色与索引的配对(含用户的修改)
assets-verified.json 已校验资源的缓存
assets/ HuBERT、RMVPE、FCPE、底模、分离权重(原版的 assets/ 布局)
models/ voices/ indexes/ base/ separation/ attached-indexes/
experiments/<name>/ experiment.json 与各阶段的目录
outputs/<日期>/<任务 id>/ 转换与分离结果
uploads/ 原始上传(24 小时后清理,被输出引用的除外);imports/<会话>/
cache/peaks/ 波形峰值缓存
jobs/<任务 id>.log 任务日志
trash/ 操作系统回收站不可用时的回退server.json 还有一个用途:启动时 JobService 会把仍处于活动状态的任务标为 interrupted,但如果 server.json 表明另一个服务器正在使用这个数据目录,就跳过这一步,以免把别人的任务误判为中断。