要实现首尾帧模式的视频生成(即通过首帧、尾帧图片,由AI生成中间过渡帧并拼接为完整视频),核心是借助 豆包AI开放平台的视频生成API(支持图像到视频的生成),结合C#完成API调用、参数配置、结果处理。以下是完整实现方案:

一、前置准备

1. 开通豆包AI开放平台服务

  1. 注册并登录 豆包AI开放平台
  2. 创建应用,获取 API KeySecret Key(在「应用管理」-「密钥管理」中查看)
  3. 开通「视频生成」相关接口权限(在「接口管理」中找到「图像生成视频」或「首尾帧视频生成」接口,申请开通)

2. 环境与依赖

  • .NET Framework 4.8+ 或 .NET Core 3.1+
  • 必要NuGet包(右键项目 → 管理NuGet程序包):
    • Newtonsoft.Json(JSON序列化/反序列化)
    • System.Net.Http(HTTP请求)
    • System.Drawing.Common(图片Base64编码,.NET Core需单独安装)

二、核心原理

首尾帧视频生成的核心流程:

  1. 将本地首帧、尾帧图片转为 Base64编码(API要求的图片传输格式)
  2. 调用豆包AI的「首尾帧视频生成API」,传入Base64、视频参数(时长、帧率等)
  3. API返回任务ID(视频生成是异步任务,需轮询查询结果)
  4. 轮询「任务查询API」,直到生成完成,获取视频URL
  5. 下载视频到本地或直接使用

三、完整C#实现代码

1. 配置类与工具类

先封装API配置、签名生成(豆包API需签名鉴权)、图片Base64转换等通用逻辑:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace DoubaoVideoGenerator
{
    /// <summary>
    /// 豆包AI API配置
    /// </summary>
    public class DoubaoConfig
    {
        // 替换为你的API Key和Secret Key(从豆包开放平台获取)
        public string ApiKey { get; set; } = "your-api-key";
        public string SecretKey { get; set; } = "your-secret-key";
        
        // 首尾帧视频生成API地址(参考豆包官方文档,以最新地址为准)
        public string CreateTaskUrl { get; set; } = "https://aquasearch.ai/api/v1/video/generate/frame-to-frame";
        
        // 任务查询API地址
        public string QueryTaskUrl { get; set; } = "https://aquasearch.ai/api/v1/video/task/query";
    }

    /// <summary>
    /// 视频生成参数
    /// </summary>
    public class VideoGenerateParams
    {
        /// <summary>
        /// 首帧图片Base64编码
        /// </summary>
        [JsonProperty("first_frame")]
        public string FirstFrameBase64 { get; set; }
        
        /// <summary>
        /// 尾帧图片Base64编码
        /// </summary>
        [JsonProperty("last_frame")]
        public string LastFrameBase64 { get; set; }
        
        /// <summary>
        /// 视频时长(秒),范围:3-60
        /// </summary>
        [JsonProperty("duration")]
        public int Duration { get; set; } = 10;
        
        /// <summary>
        /// 帧率(fps),可选:15/24/30
        /// </summary>
        [JsonProperty("fps")]
        public int Fps { get; set; } = 24;
        
        /// <summary>
        /// 视频分辨率,可选:720p(1280x720)/1080p(1920x1080)
        /// </summary>
        [JsonProperty("resolution")]
        public string Resolution { get; set; } = "720p";
        
        /// <summary>
        /// 生成风格(可选,参考API文档)
        /// </summary>
        [JsonProperty("style")]
        public string Style { get; set; } = "realistic";
    }

    /// <summary>
    /// 豆包AI视频生成工具类
    /// </summary>
    public class DoubaoVideoGenerator
    {
        private readonly DoubaoConfig _config;
        private readonly HttpClient _httpClient;

        public DoubaoVideoGenerator(DoubaoConfig config)
        {
            _config = config ?? throw new ArgumentNullException(nameof(config));
            _httpClient = new HttpClient();
            _httpClient.Timeout = TimeSpan.FromMinutes(5); // 延长超时时间(视频生成耗时)
        }

        /// <summary>
        /// 本地图片转为Base64编码(带格式前缀)
        /// </summary>
        /// <param name="imagePath">图片路径(支持JPG/PNG)</param>
        /// <returns>Base64字符串</returns>
        public string ImageToBase64(string imagePath)
        {
            if (!File.Exists(imagePath))
                throw new FileNotFoundException("图片文件不存在", imagePath);

            using (var image = Image.FromFile(imagePath))
            using (var ms = new MemoryStream())
            {
                // 保留原图片格式
                var extension = Path.GetExtension(imagePath).ToLower();
                var format = extension switch
                {
                    ".jpg" or ".jpeg" => ImageFormat.Jpeg,
                    ".png" => ImageFormat.Png,
                    _ => throw new NotSupportedException("仅支持JPG/PNG格式图片")
                };

                image.Save(ms, format);
                byte[] imageBytes = ms.ToArray();
                return $"data:image/{extension.TrimStart('.')};base64,{Convert.ToBase64String(imageBytes)}";
            }
        }

        /// <summary>
        /// 生成API请求签名(豆包API鉴权要求)
        /// 签名规则:HMAC-SHA256(secretKey, timestamp + nonce + apiKey + 请求体JSON)
        /// </summary>
        private string GenerateSignature(string requestBody, out long timestamp, out string nonce)
        {
            timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
            nonce = Guid.NewGuid().ToString("N"); // 随机字符串

            // 拼接签名原文
            string signText = $"{timestamp}{nonce}{_config.ApiKey}{requestBody}";
            
            // HMAC-SHA256加密
            using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_config.SecretKey)))
            {
                byte[] hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(signText));
                return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
            }
        }

        /// <summary>
        /// 创建首尾帧视频生成任务
        /// </summary>
        /// <param name="params">生成参数</param>
        /// <returns>任务ID</returns>
        public async Task<string> CreateFrameToFrameTaskAsync(VideoGenerateParams @params)
        {
            if (@params == null)
                throw new ArgumentNullException(nameof(@params));

            // 序列化请求体
            string requestBody = JsonConvert.SerializeObject(@params);
            
            // 生成签名
            string signature = GenerateSignature(requestBody, out long timestamp, out string nonce);

            // 构造请求头
            var headers = new Dictionary<string, string>
            {
                ["Content-Type"] = "application/json",
                ["X-API-Key"] = _config.ApiKey,
                ["X-Timestamp"] = timestamp.ToString(),
                ["X-Nonce"] = nonce,
                ["X-Signature"] = signature
            };

            // 发送POST请求
            var request = new HttpRequestMessage(HttpMethod.Post, _config.CreateTaskUrl)
            {
                Content = new StringContent(requestBody, Encoding.UTF8, "application/json")
            };
            foreach (var header in headers)
                request.Headers.TryAddWithoutValidation(header.Key, header.Value);

            var response = await _httpClient.SendAsync(request);
            response.EnsureSuccessStatusCode(); // 抛出HTTP错误

            // 解析响应
            string responseJson = await response.Content.ReadAsStringAsync();
            dynamic result = JsonConvert.DeserializeObject(responseJson);
            
            if (result.code != 0)
                throw new Exception($"创建任务失败:{result.msg}");

            return result.data.task_id; // 返回任务ID
        }

        /// <summary>
        /// 轮询查询任务状态
        /// </summary>
        /// <param name="taskId">任务ID</param>
        /// <param name="pollInterval">轮询间隔(默认5秒)</param>
        /// <returns>生成的视频URL</returns>
        public async Task<string> PollTaskStatusAsync(string taskId, int pollInterval = 5000)
        {
            if (string.IsNullOrEmpty(taskId))
                throw new ArgumentException("任务ID不能为空");

            while (true)
            {
                // 构造查询参数
                var queryParams = new Dictionary<string, string>
                {
                    ["task_id"] = taskId
                };
                string queryString = string.Join("&", queryParams.Keys.Select(k => $"{k}={Uri.EscapeDataString(queryParams[k])}"));
                string queryUrl = $"{_config.QueryTaskUrl}?{queryString}";

                // 生成签名(查询请求体为空,requestBody传空字符串)
                string signature = GenerateSignature("", out long timestamp, out string nonce);

                // 构造请求头
                var request = new HttpRequestMessage(HttpMethod.Get, queryUrl);
                request.Headers.TryAddWithoutValidation("X-API-Key", _config.ApiKey);
                request.Headers.TryAddWithoutValidation("X-Timestamp", timestamp.ToString());
                request.Headers.TryAddWithoutValidation("X-Nonce", nonce);
                request.Headers.TryAddWithoutValidation("X-Signature", signature);

                // 发送查询请求
                var response = await _httpClient.SendAsync(request);
                response.EnsureSuccessStatusCode();
                string responseJson = await response.Content.ReadAsStringAsync();
                dynamic result = JsonConvert.DeserializeObject(responseJson);

                if (result.code != 0)
                    throw new Exception($"查询任务失败:{result.msg}");

                // 解析任务状态(参考豆包API文档的状态码定义)
                string status = result.data.status;
                Console.WriteLine($"任务状态:{status}(任务ID:{taskId})");

                switch (status)
                {
                    case "SUCCESS": // 生成成功
                        return result.data.video_url; // 返回视频URL
                    case "FAILED": // 生成失败
                        throw new Exception($"任务失败:{result.data.fail_reason}");
                    case "PROCESSING": // 处理中,继续轮询
                        await Task.Delay(pollInterval);
                        break;
                    default:
                        throw new Exception($"未知任务状态:{status}");
                }
            }
        }

        /// <summary>
        /// 下载视频到本地
        /// </summary>
        /// <param name="videoUrl">视频URL</param>
        /// <param name="savePath">保存路径(含文件名,如:output.mp4)</param>
        public async Task DownloadVideoAsync(string videoUrl, string savePath)
        {
            if (string.IsNullOrEmpty(videoUrl))
                throw new ArgumentException("视频URL不能为空");

            using (var response = await _httpClient.GetAsync(videoUrl, HttpCompletionOption.ResponseHeadersRead))
            {
                response.EnsureSuccessStatusCode();
                using (var stream = await response.Content.ReadAsStreamAsync())
                using (var fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write))
                {
                    await stream.CopyToAsync(fileStream);
                    Console.WriteLine($"视频已保存到:{savePath}");
                }
            }
        }
    }
}

2. 主程序调用示例

using System;
using System.Threading.Tasks;

namespace DoubaoVideoGenerator
{
    class Program
    {
        static async Task Main(string[] args)
        {
            try
            {
                // 1. 配置豆包API密钥
                var config = new DoubaoConfig
                {
                    ApiKey = "替换为你的API Key",
                    SecretKey = "替换为你的Secret Key"
                };

                // 2. 初始化生成器
                var generator = new DoubaoVideoGenerator(config);

                // 3. 准备首尾帧图片(替换为你的本地图片路径)
                string firstFramePath = @"C:\images\first_frame.jpg";
                string lastFramePath = @"C:\images\last_frame.jpg";

                // 4. 图片转Base64
                Console.WriteLine("正在转换图片为Base64...");
                string firstFrameBase64 = generator.ImageToBase64(firstFramePath);
                string lastFrameBase64 = generator.ImageToBase64(lastFramePath);

                // 5. 配置视频生成参数
                var videoParams = new VideoGenerateParams
                {
                    FirstFrameBase64 = firstFrameBase64,
                    LastFrameBase64 = lastFrameBase64,
                    Duration = 15, // 视频时长15秒
                    Fps = 30, // 帧率30fps
                    Resolution = "1080p", // 1080P分辨率
                    Style = "cinematic" // 电影风格(可选,参考API文档)
                };

                // 6. 创建视频生成任务
                Console.WriteLine("正在创建视频生成任务...");
                string taskId = await generator.CreateFrameToFrameTaskAsync(videoParams);
                Console.WriteLine($"任务创建成功,任务ID:{taskId}");

                // 7. 轮询任务状态,获取视频URL
                Console.WriteLine("正在等待视频生成(可能需要1-5分钟)...");
                string videoUrl = await generator.PollTaskStatusAsync(taskId);
                Console.WriteLine($"视频生成成功,URL:{videoUrl}");

                // 8. 下载视频到本地
                string savePath = @"C:\output\frame_to_frame_video.mp4";
                Console.WriteLine("正在下载视频...");
                await generator.DownloadVideoAsync(videoUrl, savePath);

                Console.WriteLine("所有操作完成!");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"出错:{ex.Message}");
            }
            finally
            {
                Console.WriteLine("按任意键退出...");
                Console.ReadKey();
            }
        }
    }
}

四、关键说明

1. API地址与参数调整

  • 代码中的API地址(CreateTaskUrlQueryTaskUrl)需参考 豆包官方最新文档(可能会更新),请登录开放平台查看「首尾帧视频生成」接口的正式地址。
  • 视频参数(时长、帧率、分辨率、风格)需符合API限制(例如时长3-60秒),具体以官方文档为准。

2. 签名鉴权

豆包API的签名规则可能会调整,核心是:

  • 签名原文 = 时间戳(毫秒级) + 随机字符串 + API Key + 请求体JSON
  • 加密方式:HMAC-SHA256(密钥为Secret Key)
  • 请务必按照官方文档的签名规则修改 GenerateSignature 方法,否则会鉴权失败。

3. 异步任务处理

视频生成是异步任务(耗时1-5分钟),需通过轮询任务ID查询状态,不可直接等待响应(会超时)。代码中PollTaskStatusAsync方法已实现自动轮询。

4. 图片格式要求

  • 支持JPG/PNG格式,建议首尾帧图片分辨率一致(避免生成视频拉伸)。
  • 图片大小建议不超过5MB,过大可能导致API请求失败。

五、常见问题排查

  1. 鉴权失败(401错误)

    • 检查API Key和Secret Key是否正确。
    • 签名生成逻辑是否与官方文档一致(时间戳是否为毫秒级、参数顺序是否正确)。
  2. 图片转换失败

    • 确认图片路径正确,文件未被占用。
    • 仅支持JPG/PNG格式,其他格式需先转换。
  3. 任务生成失败

    • 检查首尾帧图片是否符合分辨率要求(例如API要求最小640x480)。
    • 视频时长、帧率是否超出API限制。
    • 账户是否有足够的调用额度(豆包API可能有免费额度,超出后需付费)。
  4. 下载超时

    • 延长HttpClient的超时时间(代码中已设为5分钟)。
    • 直接复制视频URL到浏览器下载,排查网络问题。

六、扩展功能

  1. 视频格式转换:生成的视频可能为MP4格式,若需其他格式(如AVI、MOV),可结合FFmpeg(C#通过Process调用)进行转换。
  2. 进度显示:在轮询任务时,可根据API返回的进度百分比(若支持)显示生成进度。
  3. 批量生成:循环调用CreateFrameToFrameTaskAsync,批量处理多组首尾帧。
  4. 错误重试:对API请求添加重试机制(使用Polly库),提高稳定性。
Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐