RVC Next

整体架构

本页给出 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 子包可导入的第三方模块
engineenginenumpy、scipy、torch、transformers、faiss、parselmouth、librosa、av、soundfile、onnxruntime、torch_directml、tensorboard,以及 sounddevice(仅限 engine/audio_io/)、pymss 与 pymss_core(仅限 engine/separate/)、yaml
protocolprotocol无
workersengine、protocol、workers与 engine 相同,外加 sounddevice、pymss、yaml
corecore、engine、protocol、version、loggerpydantic、httpx、tomli、tomli_w、send2trash、numpy、typing_extensions
apiapi、core、version、logger、webuifastapi、starlette、socketio、uvicorn、pydantic、anyio
clicli、core、version、logger、apityper、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:

字段类职责
settingsSettingsServicesettings.toml、环境变量覆盖、解析后的目录
eventsLocalEventBus进程内发布 / 订阅
dbDatabaservc-next.db
httpHttpClientProvider共享的 httpx 客户端(测试可注入 transport)
computeComputeService设备、显存占用、GPU 租约、空闲卸载
jobsJobService任务队列、资源、执行器、历史
assetsAssetService资源目录、下载、校验
audioAudioFileService上传、服务器路径、探测、波形峰值、输出
modelsVoiceModelService音色库
inboxIndexInbox未分配的索引
importsImportService导入会话
base_modelsBaseModelService训练底模
separation_modelsSeparationLibrary导入的分离模型
presetsPresetService预设
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 进程)中的任务线程,带 CancelTokenHuBERT、F0 模型和最近用过的音色留在缓存里,第二个文件立刻开始
人声分离每个任务一个子进程 workers.separatePyMSS 没有内部取消机制,取消只能杀进程;进程退出后显存完全归还。沿用原版 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 的事件协议:

  1. 核心层把请求写成一个 JSON 文件,以 sys.executable -m <module> <request.json> 启动 worker(core/process.py:自己的进程组,stdout 与 stderr 合并,PYTHONUNBUFFERED=1)。原版的 --pycmd 参数因此不再需要。
  2. worker 的入口经过 workers/base.py 的 run_worker():它把真正的 stdout 复制出来专门写事件,再把文件描述符 1 重定向到 stderr。库函数随手 print 的内容因此只会变成日志文本,不会被误当作事件。
  3. 事件是带前缀 @rvc 的 JSON 行(protocol/jsonl.py),类型定义在 protocol/messages.py:progress、step、log、metric、output、result、error。没有前缀的行一律当作 info 日志。
  4. 失败统一成 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() 把它重建为对应的领域错误。
  5. 退出码:0 成功;EXIT_CANCELLED = 130 表示已取消(SIGTERM 也映射到它);EXIT_RETRY = 75 请求在新进程中重试(DirectML 下分离先试 fp16,失败再用 fp32)。
  6. 取消即杀掉整个进程树(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 把它变成退出码。

异常codeHTTP退出码何时抛出
NotFoundErrornot_found4042音色、任务、文件或实验不存在
ConflictErrorconflict4093名称冲突,导入覆盖已有模型;有任务或 Live 在运行时请求释放显存(detail.reason = "busy")
BusyError(继承 ConflictError)busy4093Live 已在运行,同一实验已有阶段在运行
InvalidPathErrorinvalid_path4004路径越出根目录,或服务器路径不在浏览根目录内
ValidationErrorinvalid_input4004参数越界、设置无效
ModelFormatError(继承 ValidationError)invalid_model4004.pth 既不是 RVC 小模型也不是 G 检查点
AssetMissingErrorasset_missing4097HuBERT、RMVPE、FCPE 或底模未安装;detail.assets 列出资源 id,界面据此提供下载
DeviceErrordevice_unavailable4098音频设备缺失、被占用或拒绝该格式;detail.reason 为 missing / busy / format / permission
ComputeErrorcompute_unavailable5039GPU 不可用或显存耗尽(detail.reason = "out_of_memory")
RvcNextError(基类)internal_error5001其他

此外,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 表明另一个服务器正在使用这个数据目录,就跳过这一步,以免把别人的任务误判为中断。

本页目录