Skip to content

生图、视频与配音

需要 client 加上对应能力的 feature(image / video / tts)。

media 模块负责一次 HTTP 调用怎么发、怎么解析:生图 generate、视频 submit / poll、配音 synthesize。 实现整体来自 StoryLoom 在生产环境跑过的代码,各家协议细节、错误翻译、超时策略都是实测踩出来的。

边界:任务编排归你

本模块调用方
按地址识别协议、组装请求、解析响应建任务记录、写数据库
生图结果立即下载成字节(临时链接常 1 小时失效)落盘、生成缩略图
视频的一次提交、一次查询轮询循环、取消、断点续跑、进度事件
把上游报错翻成可操作的中文决定展示在哪、要不要重试

视频尤其如此:轮询间隔、超时、用户点「停止」后怎么办、应用重启后怎么续跑, 都和你的存储与界面绑定,本库不替你决定。

协议按地址识别

各家协议不同,却都只给一个 base_url,所以协议从地址(和供应商的 extra)里认。 改预置地址之前先看这张表 —— 地址决定走哪套协议。

能力识别规则函数
生图dashscope → 通义万相异步任务;其余 → OpenAI /images/generationsImageProtocol::detect
视频minimaxi → 海螺;dashscope → Vidu;bigmodel / zhipu → 智谱;siliconflow → 硅基流动;extra.video_api = "newapi" → New API 中转站;其余 → 火山方舟VideoProtocol::detect
配音openspeech → 火山语音专有协议;火山方舟地址 → 直接报配置错误;其余 → OpenAI /audio/speechTtsProtocol::detect

New API 中转站只认显式标记

New API / one-api 是通用的中转架构,站点千千万。本库不按域名猜 —— 需要走这套协议的配置要在 extra 里带 {"video_api":"newapi"}。 预置可以用 default_extra 自动写上。

302.AI 这类聚合站走透传地址(api.302.ai/minimaxi/v1),靠路径里的厂商名命中对应协议。

生图

rust
use ai_profile::media::image::{AnyImageProvider, ImageGenConfig, ImageGenParams};

let provider = AnyImageProvider::from_config(ImageGenConfig {
    endpoint: "https://api.siliconflow.cn/v1".into(),
    model: "Kwai-Kolors/Kolors".into(),
    api_key: key,
});

let img = provider
    .generate(&ImageGenParams {
        prompt: "竖屏漫画分镜:少女站在雨中的站台".into(),
        size: "720x1280".into(),
        ..Default::default()
    })
    .await?;

std::fs::write(format!("shot.{}", img.ext), &img.bytes)?;
参数说明
prompt / negative_prompt提示词 / 反向提示词
seed同一角色复用同一 seed 求一致性;None 由服务端随机
size宽x高,默认 720x1280(9:16 竖屏)
images参考图(URL 或 base64 data URL):0 张纯文生图,1 张图生图,多张多图融合

返回的 ImageResult 已经把图片下载成字节,ext 按响应推断(png / jpg / webp)。 小于 MIN_IMAGE_BYTES(1 KB)的响应一律当失败 —— 中转站异常时常返回空内容或错误页,却给 200。

视频

视频必然是异步的:提交拿任务号,再轮询到完成。

rust
use ai_profile::media::video::{AnyVideoProvider, VideoGenConfig, VideoGenParams, VideoProvider, VideoTaskStatus};

let provider = AnyVideoProvider::from_config(
    VideoGenConfig { endpoint, model, api_key },
    &provider_extra,           // 供应商的 extra JSON,可为空串
);

let task_id = provider
    .submit(&VideoGenParams {
        prompt: "镜头缓慢推进,人物自然眨眼".into(),
        image: first_frame_data_url,     // 首帧
        last_image: String::new(),       // 尾帧,空 = 纯首帧模式
        duration: 5,
        ..Default::default()
    })
    .await?;

// 轮询循环由调用方负责
loop {
    match provider.poll(&task_id).await? {
        VideoTaskStatus::Pending => tokio::time::sleep(std::time::Duration::from_secs(5)).await,
        VideoTaskStatus::Succeeded(url) => break download(url).await?,
        VideoTaskStatus::Failed(msg) => return Err(explain_error(&msg).into()),
    }
}

AnyVideoProvider 实现了 VideoProvider,可以直接 move 进 tokio::spawn 做后台轮询。

首尾帧

给了 last_image 就走首尾帧模式:AI 在首帧和尾帧之间补出过程,终点被钉死。 不是每家都支持,发请求前先问 supports_last_frame(endpoint, extra)

服务商支持方式
火山方舟 Seedancecontent 里第二个 image_urlrole: "last_frame"
海螺顶层 last_frame_image(Hailuo-02 及以上)
智谱 CogVideoXimage_url[首帧, 尾帧]
Vidu / 硅基流动 / New API 中转站宁可不给,也不发一个会被忽略或报错的字段

错误翻译

explain_error(raw) 把审核类拦截翻成可操作的中文(「首帧被判为真人 / 敏感内容,建议换画风或换供应商」), 其余原样截断到 300 字。提交阶段被拒和轮询阶段失败都可以过它一遍再展示。

配音

rust
use ai_profile::media::tts::{audio_format_for, synthesize, TtsParams};

let bytes = synthesize(
    &provider.endpoint,
    &provider.model,
    &provider.extra,          // 火山语音从这里取 appid / cluster
    key,
    &TtsParams {
        text: "你终于来了。".into(),
        voice: "alex".into(),
        format: audio_format_for(&provider.model).into(),
        ..Default::default()
    },
)
.await?;
函数用途
audio_format_for(model)glm-tts 只出 wav,其余 mp3 —— 合成格式、落盘扩展名、播放 mime 三者要对齐
voice_catalog(model)按模型给出可选音色(火山 / CosyVoice / glm-tts 各一套)
parse_volc_extra(extra)从 extra 解析火山语音的 appidcluster(缺省 volcano_tts

火山方舟没有语音合成

配音填了火山方舟的地址(ark.cn-beijing.volces.com)会被直接拦下,不会发出请求。 语音合成在「火山语音」(openspeech.bytedance.com),是另一条产品线、另一个密钥。

代理与证书

每个 provider 都会自己建 HTTP 客户端。要走代理时传一个底座进来:

rust
use ai_profile::media::MediaHttp;

let http = MediaHttp::from_fn(move || {
    let b = ai_profile::reqwest::Client::builder();
    match ai_profile::reqwest::Proxy::all(&proxy_url) {
        Ok(p) => b.proxy(p),
        Err(_) => b,
    }
});

let img = AnyImageProvider::from_config_with(config, &http);
let video = AnyVideoProvider::from_config_with(config, &extra, &http);
let audio = synthesize_with(endpoint, model, extra, key, &params, &http).await?;

底座由你配,超时策略由本库在其后施加,覆盖不掉:出图用「读超时」而不是整体超时 —— 整体超时会在算图慢时误杀请求,而中转站此时已经算完并照常计费,用户得到「有扣费却没图」。

错误

所有调用返回 MediaError,只有两类:

变体含义界面动作
InvalidInput入参或配置不对(文本为空、缺 App ID、端点填错产品线)让用户改配置,重试没用
Failed其余一切(网络、上游拒绝、解析失败),消息已是可读中文展示并允许重试

MediaError::message() 取不带前缀的原文。

相关章节

MIT 协议开源 · 文档同样欢迎 PR