构建视频生成提供者插件

视频生成(video-gen)提供者插件注册一个后端,服务于每次 video_generate 工具调用。内置提供者(xAI、FAL)作为插件提供。通过将目录放入 plugins/video_gen/<name>/,即可添加新插件或覆盖内置插件。

:::tip 视频生成几乎与图像生成提供者插件逐行对应——如果你已经构建过图像生成后端,那么你已经熟悉了模式。主要区别在于:一个 capabilities() 方法用于声明模态/宽高比/时长,以及一个路由约定(传入 image_url 使用图像到视频,省略则使用文本到视频——提供者内部选择正确的端点)。 :::

统一接口(一个工具,两种模态)

video_generate 工具通过一个参数暴露两种模态:

  • 文本到视频(Text-to-video) — 仅使用 prompt 调用。提供者路由到其文本到视频端点。
  • 图像到视频(Image-to-video) — 使用 prompt + image_url 调用。提供者路由到其图像到视频端点。

编辑和扩展功能有意不在范围之内。大多数后端不支持它们,不一致性会迫使将每个后端的描述塞入代理的工具描述中。

发现机制

Hermes 在三个位置扫描视频生成后端:

  1. 内置<repo>/plugins/video_gen/<name>/(自动加载,kind: backend
  2. 用户~/.hermes/plugins/video_gen/<name>/(通过 plugins.enabled 选择加入)
  3. Pip — 声明了 hermes_agent.plugins 入口点的包

每个插件的 register(ctx) 函数调用 ctx.register_video_gen_provider(...)。活动的提供者由 config.yaml 中的 video_gen.provider 选择;hermes tools → Video Generation 引导用户完成选择。与 image_generate 不同,没有树内遗留后端——每个提供者都是一个插件。

目录结构

plugins/video_gen/my-backend/
├── __init__.py      # VideoGenProvider 子类 + register()
└── plugin.yaml      # 清单文件,kind: backend

VideoGenProvider ABC

继承 agent.video_gen_provider.VideoGenProvider。必需:name 属性和 generate() 方法。

# plugins/video_gen/my-backend/__init__.py
from typing import Any, Dict, List, Optional
import os
 
from agent.video_gen_provider import (
    VideoGenProvider,
    error_response,
    success_response,
)
 
 
class MyVideoGenProvider(VideoGenProvider):
    @property
    def name(self) -> str:
        return "my-backend"
 
    @property
    def display_name(self) -> str:
        return "My Backend"
 
    def is_available(self) -> bool:
        return bool(os.environ.get("MY_API_KEY"))
 
    def list_models(self) -> List[Dict[str, Any]]:
        # 每个条目是一个模型家族(FAMILY)——用户只需选择一次的名称。
        # 你的提供者的 generate() 根据是否传入了 image_url
        # 在家族内部进行路由。
        return [
            {
                "id": "fast",
                "display": "Fast",
                "speed": "~30s",
                "strengths": "Cheapest tier",
                "price": "$0.05/s",
                "modalities": ["text", "image"],  # 供参考
            },
        ]
 
    def default_model(self) -> Optional[str]:
        return "fast"
 
    def capabilities(self) -> Dict[str, Any]:
        return {
            "modalities": ["text", "image"],
            "aspect_ratios": ["16:9", "9:16"],
            "resolutions": ["720p", "1080p"],
            "min_duration": 1,
            "max_duration": 10,
            "supports_audio": False,
            "supports_negative_prompt": True,
            "max_reference_images": 0,
        }
 
    def get_setup_schema(self) -> Dict[str, Any]:
        return {
            "name": "My Backend",
            "badge": "paid",
            "tag": "Short description shown in `hermes tools`",
            "env_vars": [
                {
                    "key": "MY_API_KEY",
                    "prompt": "My Backend API key",
                    "url": "https://mybackend.example.com/keys",
                },
            ],
        }
 
    def generate(
        self,
        prompt: str,
        *,
        model: Optional[str] = None,
        image_url: Optional[str] = None,
        reference_image_urls: Optional[List[str]] = None,
        duration: Optional[int] = None,
        aspect_ratio: str = "16:9",
        resolution: str = "720p",
        negative_prompt: Optional[str] = None,
        audio: Optional[bool] = None,
        seed: Optional[int] = None,
        **kwargs: Any,  # 始终忽略未知 kwargs 以保持向前兼容
    ) -> Dict[str, Any]:
        # 路由:image_url 的存在决定端点。
        if image_url:
            endpoint = "my-backend/image-to-video"
            modality_used = "image"
        else:
            endpoint = "my-backend/text-to-video"
            modality_used = "text"
 
        # ... 调用你的 API ...
 
        return success_response(
            video="https://your-cdn/output.mp4",
            model=model or "fast",
            prompt=prompt,
            modality=modality_used,
            aspect_ratio=aspect_ratio,
            duration=duration or 5,
            provider=self.name,
        )
 
 
def register(ctx) -> None:
    ctx.register_video_gen_provider(MyVideoGenProvider())

插件清单

# plugins/video_gen/my-backend/plugin.yaml
name: my-backend
version: 1.0.0
description: "My video generation backend"
author: Your Name
kind: backend
requires_env:
  - MY_API_KEY

video_generate 模式

该工具在所有后端的模式是统一的。提供者会忽略它们不支持的参数。

参数作用
prompt文本指令(必需)
image_url设置时 → 图像到视频;省略时 → 文本到视频
reference_image_urls风格/角色参考(取决于提供者)
duration秒数——提供者会做限定处理
aspect_ratio"16:9""9:16""1:1" 等——提供者会做限定处理
resolution"480p" / "540p" / "720p" / "1080p"——提供者会做限定处理
negative_prompt要避免的内容(仅 Pixverse/Kling)
audio原生音频(Veo3 / Pixverse 定价层级)
seed可重现性
model覆盖活动的模型/家族

提供者的 capabilities() 声明哪些参数被实际支持。代理在工具描述中查看活动后端的 capabilities,当用户通过 hermes tools 更改后端时会动态重建。

模型家族与端点路由(FAL 模式)

当你的后端每个”模型”有多个端点——如 FAL,其中每个家族(Veo 3.1、Pixverse v6、Kling O3)同时有 /text-to-video/image-to-video URL——将每个家族表示为一个目录条目。你的 generate() 根据是否传入了 image_url 选择正确的端点:

FAMILIES = {
    "veo3.1": {
        "text_endpoint": "fal-ai/veo3.1",
        "image_endpoint": "fal-ai/veo3.1/image-to-video",
        # ... 家族特定的能力标志 ...
    },
}
 
def generate(self, prompt, *, image_url=None, model=None, **kwargs):
    family_id, family = _resolve_family(model)
    endpoint = family["image_endpoint"] if image_url else family["text_endpoint"]
    # ... 根据家族声明的能力标志构建载荷,调用端点 ...

用户在 hermes tools 中选择 veo3.1 一次。代理从不考虑端点——它只需传递(或不传递)image_url

选择优先级

对于每个实例的模型旋钮(参见 plugins/video_gen/fal/__init__.py):

  1. 工具调用中的 model= 关键字
  2. <PROVIDER>_VIDEO_MODEL 环境变量
  3. config.yaml 中的 video_gen.<provider>.model
  4. config.yaml 中的 video_gen.model(当其值为你的 ID 之一时)
  5. 提供者的 default_model()

响应形状

success_response()error_response() 生成每个后端返回的字典形状。请使用它们——不要手写字典。

成功键:successvideo(URL 或绝对路径)、modelpromptmodality"text""image")、aspect_ratiodurationprovider,外加 extra

错误键:successvideo(None)、errorerror_typemodelpromptaspect_ratioprovider

工件存储位置

如果你的后端返回 base64,使用 save_b64_video() 写入 $HERMES_HOME/cache/videos/。如果是后续 HTTP 获取的原始字节,使用 save_bytes_video()。否则直接返回上游 URL——gateway 在投递时会解析远程 URL。

测试

tests/plugins/video_gen/test_<name>_plugin.py 下放入冒烟测试。xAI 和 FAL 的测试展示了模式——注册、验证目录、使用和不使用 image_url 测试路由、在缺少认证时断言清晰的错误响应。