使用java实现即梦文生视频、图生视频,火山引擎「即梦 AI - 视频生成 3.0 Pro」调用 Demo(原生 HTTP 签名版)。

一、即梦视频生成能力申请

进入即梦AI进行能力申请:账号登录-火山引擎

二、文档地址

官方文档地址:即梦AI-视频生成3.0 Pro-接口文档--即梦AI-火山引擎

java代码调用demo:

java 复制代码
package cn.iocoder.yudao.module.iscs.utils;

import com.alibaba.fastjson.JSONObject;
import com.google.common.io.ByteStreams;
import com.volcengine.helper.Utils;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * 火山引擎「即梦 AI - 视频生成 3.0 Pro」调用 Demo(原生 HTTP 签名版)。
 * <p>
 * 官方文档:https://www.volcengine.com/docs/85621/1777001
 * <p>
 * 模型标识(req_key):{@code jimeng_ti2v_v30_pro}
 * <ul>
 *   <li>支持<strong>文生视频</strong>:仅传 prompt,由文字描述生成 1080P 视频</li>
 *   <li>支持<strong>图生视频(首帧)</strong>:首帧图片 + prompt,让静态图「动起来」</li>
 * </ul>
 * <p>
 * 调用流程(异步,与文生图相同):
 * <pre>
 *   1. CVSync2AsyncSubmitTask  --- 提交任务,返回 task_id
 *   2. CVSync2AsyncGetResult    --- 按 task_id 轮询,直到 status=done
 *   3. 从 data.video_url 下载 MP4 并保存到本地
 * </pre>
 * <p>
 * 注意:本类为本地调试 Demo,AK/SK 硬编码仅用于测试,生产环境请改用配置中心或环境变量。
 */
public class Sign {

    // ======================== 认证与连接配置 ========================

    /** 火山引擎 Access Key */
    private static final String AK = "你的AK";

    /** 火山引擎 Secret Key(Base64 编码,末尾通常带 ==) */
    private static final String SK = "你的SK";

    /** 视觉智能 API 固定 endpoint,不可使用 open.volcengineapi.com */
    private static final String HOST = "visual.volcengineapi.com";

    private static final String REGION = "cn-north-1";
    private static final String SERVICE = "cv";
    private static final String SCHEMA = "https";
    private static final String PATH = "/";
    private static final String API_VERSION = "2022-08-31";

    // ======================== 模型与轮询配置 ========================

    /**
     * 能力标识(req_key)。
     * 即梦视频生成 3.0 Pro 同时支持文生视频和图生视频,提交与查询时均需携带同一 req_key。
     */
    private static final String REQ_KEY = "jimeng_ti2v_v30_pro";

    /** 轮询间隔(毫秒);视频生成通常需 1~5 分钟,建议 5 秒查一次 */
    private static final long POLL_INTERVAL_MS = 5_000;

    /** 最大轮询次数,120 × 5s = 10 分钟,超时则放弃 */
    private static final int MAX_POLL_TIMES = 120;

    // ======================== 运行模式开关(改这里切换文生/图生) ========================

    /**
     * 当前演示模式。
     * <ul>
     *   <li>{@link VideoMode#TEXT_TO_VIDEO}  --- 文生视频,仅需 prompt</li>
     *   <li>{@link VideoMode#IMAGE_TO_VIDEO} --- 图生视频,需首帧图片 + prompt</li>
     * </ul>
     */
    private static final VideoMode CURRENT_MODE = VideoMode.TEXT_TO_VIDEO;

    /** 生成视频保存目录(可按本机路径修改) */
    private static final String OUTPUT_DIR = "C:\\Users\\HP\\Pictures\\Saved Pictures\\";

    // ======================== 编码工具(签名用,无需修改) ========================

    private static final BitSet URLENCODER = new BitSet(256);
    private static final String CONST_ENCODE = "0123456789ABCDEF";
    public static final Charset UTF_8 = StandardCharsets.UTF_8;

    static {
        int i;
        for (i = 97; i <= 122; ++i) {
            URLENCODER.set(i);
        }
        for (i = 65; i <= 90; ++i) {
            URLENCODER.set(i);
        }
        for (i = 48; i <= 57; ++i) {
            URLENCODER.set(i);
        }
        URLENCODER.set('-');
        URLENCODER.set('_');
        URLENCODER.set('.');
        URLENCODER.set('~');
    }

    // ======================== 程序入口 ========================

    public static void main(String[] args) throws Exception {
        Sign sign = new Sign(REGION, SERVICE, SCHEMA, HOST, PATH, AK, SK);

        // ---------- 1. 按模式构建提交请求体 ----------
        JSONObject submitReq = buildSubmitRequest(CURRENT_MODE);
        System.out.println("当前模式:" + CURRENT_MODE.label);
        System.out.println("提交参数:" + submitReq.toJSONString());

        // ---------- 2. 提交异步任务 ----------
        // Action=CVSync2AsyncSubmitTask(注意:不是 CVProcess!CVProcess 为同步接口,视频生成不支持)
        String submitRespBody = sign.doRequest("POST", new HashMap<>(),
                submitReq.toJSONString().getBytes(StandardCharsets.UTF_8),
                new Date(), "CVSync2AsyncSubmitTask", API_VERSION);
        System.out.println("提交任务返回:" + submitRespBody);

        JSONObject submitResp = JSONObject.parseObject(submitRespBody);
        checkResponse(submitResp, "提交任务");

        String taskId = submitResp.getJSONObject("data").getString("task_id");
        if (StringUtils.isBlank(taskId)) {
            System.out.println("task_id 为空,无法查询结果");
            return;
        }
        System.out.println("task_id:" + taskId);

        // ---------- 3. 轮询直到任务完成 ----------
        JSONObject resultData = pollTaskResult(sign, taskId);
        if (resultData == null) {
            System.out.println("任务超时或失败,未获取到视频");
            return;
        }

        // ---------- 4. 下载并保存视频 ----------
        String videoUrl = resultData.getString("video_url");
        if (StringUtils.isBlank(videoUrl)) {
            System.out.println("status=done 但 video_url 为空,完整 data:" + resultData.toJSONString());
            System.out.println("可能原因:生成内部异常、内容审核未通过,或需稍后重试查询");
            return;
        }

        String outputPath = OUTPUT_DIR + System.currentTimeMillis() + ".mp4";
        saveVideoFromUrl(videoUrl, outputPath);
        System.out.println("视频保存成功:" + new File(outputPath).getAbsolutePath());
        System.out.println("视频地址:" + videoUrl);
    }

    // ======================== 文生视频 / 图生视频 传参方案 ========================

    /**
     * 演示模式枚举。
     */
    enum VideoMode {
        /** 文生视频:仅文本提示词 */
        TEXT_TO_VIDEO("文生视频"),
        /** 图生视频(首帧):首帧图片 + 文本提示词 */
        IMAGE_TO_VIDEO("图生视频-首帧");

        final String label;

        VideoMode(String label) {
            this.label = label;
        }
    }

    /**
     * 按模式构建「提交任务」请求体。
     *
     * @param mode 文生视频 或 图生视频
     * @return 可直接 POST 的 JSON 请求体
     */
    private static JSONObject buildSubmitRequest(VideoMode mode) {
        switch (mode) {
            case TEXT_TO_VIDEO:
                return buildTextToVideoRequest();
            case IMAGE_TO_VIDEO:
                return buildImageToVideoRequest();
            default:
                throw new IllegalArgumentException("未知模式: " + mode);
        }
    }

    /**
     * 【文生视频】传参方案
     * <p>
     * 仅需文字描述,由模型直接生成 1080P 视频。
     * <p>
     * 必填参数:
     * <ul>
     *   <li>{@code req_key} --- 固定 {@code jimeng_ti2v_v30_pro}</li>
     *   <li>{@code prompt}  --- 视频内容描述,支持中文;建议按镜头分段描述动作与场景</li>
     * </ul>
     * 可选参数:
     * <ul>
     *   <li>{@code frames}       --- 总帧数,{@code 121}≈5秒,{@code 241}≈10秒,默认 121</li>
     *   <li>{@code aspect_ratio} --- 宽高比:{@code 16:9}、{@code 4:3}、{@code 1:1}、{@code 3:4}、{@code 9:16}、{@code 21:9},默认 16:9</li>
     *   <li>{@code seed}         --- 随机种子,{@code -1} 表示随机,固定正整数可复现结果</li>
     * </ul>
     * 不需要传的参数:
     * <ul>
     *   <li>{@code image_urls}、{@code binary_data_base64} --- 文生视频不传图片</li>
     * </ul>
     * 请求示例:
     * <pre>{@code
     * {
     *   "req_key": "jimeng_ti2v_v30_pro",
     *   "prompt": "一只橘猫趴在键盘上打哈欠,温暖的台灯光线,镜头缓缓推进",
     *   "frames": 121,
     *   "aspect_ratio": "16:9",
     *   "seed": -1
     * }
     * }</pre>
     */
    private static JSONObject buildTextToVideoRequest() {
        JSONObject req = new JSONObject();
        req.put("req_key", REQ_KEY);
        req.put("prompt", "新疆风情浓郁的肖像视频,一位神秘女子留着齐腰波浪长发,脸上妆容现代精致,女子头转向侧面,随着微风轻拂,几缕发丝飘逸飞舞,女子侧脸对镜头微笑,深邃的背景色调烘托出温暖而静谧的氛围。高清写实风格,动作自然流畅,光影层次丰");
        req.put("frames", 121);          // 121 帧 ≈ 5 秒;241 帧 ≈ 10 秒
        req.put("aspect_ratio", "16:9");  // 文生视频可指定画幅比例
        req.put("seed", -1);              // -1 = 每次随机;填正整数可固定随机种子
        return req;
    }

    /**
     * 【图生视频-首帧】传参方案
     * <p>
     * 以一张静态图片作为视频首帧,配合 prompt 描述「画面如何动起来」。
     * <p>
     * 必填参数:
     * <ul>
     *   <li>{@code req_key} --- 固定 {@code jimeng_ti2v_v30_pro}</li>
     *   <li>{@code prompt}  --- 描述动态效果,如表情变化、镜头运动、风吹发丝等</li>
     *   <li>首帧图片(二选一,不可同时传):
     *     <ul>
     *       <li>{@code image_urls}         --- 公网可访问的图片 URL 数组,仅 1 张</li>
     *       <li>{@code binary_data_base64} --- 图片 Base64 编码数组,支持 JPEG/PNG,仅 1 张</li>
     *     </ul>
     *   </li>
     * </ul>
     * 可选参数:
     * <ul>
     *   <li>{@code frames} --- 总帧数,{@code 121}≈5秒,{@code 241}≈10秒,默认 121</li>
     *   <li>{@code seed}   --- 随机种子,{@code -1} 表示随机</li>
     * </ul>
     * 说明:
     * <ul>
     *   <li>图生视频时画幅由首帧图片决定,一般<strong>无需传 aspect_ratio</strong></li>
     *   <li>图片须清晰、主体明确;URL 须公网可访问,火山服务端需能下载</li>
     *   <li>本地图片可先转 Base64 放入 {@code binary_data_base64},避免上传图床</li>
     * </ul>
     * 请求示例(image_urls 方式):
     * <pre>{@code
     * {
     *   "req_key": "jimeng_ti2v_v30_pro",
     *   "prompt": "女孩缓缓睁开眼睛,头发被风吹动,镜头缓缓拉出",
     *   "image_urls": ["https://example.com/first_frame.png"],
     *   "frames": 121,
     *   "seed": -1
     * }
     * }</pre>
     * 请求示例(binary_data_base64 方式):
     * <pre>{@code
     * {
     *   "req_key": "jimeng_ti2v_v30_pro",
     *   "prompt": "女孩缓缓睁开眼睛,头发被风吹动",
     *   "binary_data_base64": ["<图片文件的Base64字符串>"],
     *   "frames": 121,
     *   "seed": -1
     * }
     * }</pre>
     */
    private static JSONObject buildImageToVideoRequest() {
        JSONObject req = new JSONObject();
        req.put("req_key", REQ_KEY);
        req.put("prompt", "女孩抱着狐狸,女孩睁开眼,温柔地看向镜头,狐狸友善地抱着,镜头缓缓拉出,女孩的头发被风吹动");

        // 方式一(推荐调试):公网图片 URL,火山官方示例图
        List<String> imageUrls = new ArrayList<>();
        imageUrls.add("https://ark-project.tos-cn-beijing.volces.com/doc_image/i2v_foxrgirl.png");
        req.put("image_urls", imageUrls);

        // 方式二:本地图片转 Base64(与 image_urls 二选一,调试时取消下面注释并注释掉 image_urls)
        // byte[] imageBytes = FileUtils.readFileToByteArray(new File("C:\\path\\to\\your\\image.png"));
        // List<String> base64List = new ArrayList<>();
        // base64List.add(Base64.getEncoder().encodeToString(imageBytes));
        // req.put("binary_data_base64", base64List);

        req.put("frames", 121);
        req.put("seed", -1);
        return req;
    }

    // ======================== 轮询与结果处理 ========================

    /**
     * 轮询查询任务结果。
     * <p>
     * 对应 API:Action=CVSync2AsyncGetResult, Version=2022-08-31
     * <p>
     * 查询请求体固定格式:
     * <pre>{@code
     * {
     *   "req_key": "jimeng_ti2v_v30_pro",
     *   "task_id": "<提交任务返回的 task_id>"
     * }
     * }</pre>
     * 任务状态(data.status):
     * <ul>
     *   <li>{@code in_queue}   --- 排队中</li>
     *   <li>{@code generating} --- 生成中</li>
     *   <li>{@code done}       --- 完成,读取 data.video_url</li>
     *   <li>{@code failed}     --- 失败</li>
     * </ul>
     */
    private static JSONObject pollTaskResult(Sign sign, String taskId) throws Exception {
        JSONObject queryReq = new JSONObject();
        queryReq.put("req_key", REQ_KEY);
        queryReq.put("task_id", taskId);

        for (int i = 1; i <= MAX_POLL_TIMES; i++) {
            Thread.sleep(POLL_INTERVAL_MS);

            String queryRespBody = sign.doRequest("POST", new HashMap<>(),
                    queryReq.toJSONString().getBytes(StandardCharsets.UTF_8),
                    new Date(), "CVSync2AsyncGetResult", API_VERSION);

            String logContent = queryRespBody.length() < 1000 ? queryRespBody : "响应过长,已省略";
            System.out.println("第 " + i + " 次查询:" + logContent);

            JSONObject queryResp = JSONObject.parseObject(queryRespBody);
            if (queryResp == null) {
                continue;
            }

            int code = queryResp.getIntValue("code");
            if (code != 10000 && code != 0) {
                throw new RuntimeException("查询失败: " + queryResp.getString("message"));
            }

            JSONObject data = queryResp.getJSONObject("data");
            if (data == null) {
                continue;
            }

            String status = data.getString("status");
            if ("done".equalsIgnoreCase(status) || "success".equalsIgnoreCase(status)) {
                return data;
            }
            if ("failed".equalsIgnoreCase(status) || "error".equalsIgnoreCase(status)) {
                throw new RuntimeException("任务失败: " + data.toJSONString());
            }
        }
        return null;
    }

    private static void checkResponse(JSONObject resp, String step) {
        if (resp == null) {
            throw new RuntimeException(step + "响应为空");
        }
        int code = resp.getIntValue("code");
        if (code != 10000 && code != 0) {
            throw new RuntimeException(step + "失败: " + resp.getString("message"));
        }
        if (resp.getJSONObject("data") == null) {
            throw new RuntimeException(step + " data 字段为空");
        }
    }

    private static void saveVideoFromUrl(String url, String path) throws Exception {
        File file = new File(path);
        FileUtils.forceMkdirParent(file);
        FileUtils.copyURLToFile(new URL(url), file, 10_000, 120_000);
    }

    // ======================== HTTP 签名请求(火山 V4 签名) ========================

    private final String region;
    private final String service;
    private final String schema;
    private final String host;
    private final String path;
    private final String ak;
    private final String sk;

    public Sign(String region, String service, String schema, String host, String path, String ak, String sk) {
        this.region = region;
        this.service = service;
        this.schema = schema;
        this.host = host;
        this.path = path;
        this.ak = ak;
        this.sk = sk;
    }

    /**
     * 发送已签名的 HTTP 请求,返回响应体字符串。
     *
     * @param action 接口名,如 CVSync2AsyncSubmitTask、CVSync2AsyncGetResult
     */
    public String doRequest(String method, Map<String, String> queryList, byte[] body,
                            Date date, String action, String version) throws Exception {
        if (body == null) {
            body = new byte[0];
        }
        String xContentSha256 = hashSHA256(body);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
        sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
        String xDate = sdf.format(date);
        String shortXDate = xDate.substring(0, 8);
        String contentType = "application/json";
        String signHeader = "host;x-date;x-content-sha256;content-type";

        SortedMap<String, String> realQueryList = new TreeMap<>(queryList);
        realQueryList.put("Action", action);
        realQueryList.put("Version", version);
        StringBuilder querySB = new StringBuilder();
        for (String key : realQueryList.keySet()) {
            querySB.append(signStringEncoder(key)).append("=").append(signStringEncoder(realQueryList.get(key))).append("&");
        }
        querySB.deleteCharAt(querySB.length() - 1);

        String canonicalStringBuilder = method + "\n" + path + "\n" + querySB + "\n" +
                "host:" + host + "\n" +
                "x-date:" + xDate + "\n" +
                "x-content-sha256:" + xContentSha256 + "\n" +
                "content-type:" + contentType + "\n" +
                "\n" +
                signHeader + "\n" +
                xContentSha256;

        String hashcanonicalString = hashSHA256(canonicalStringBuilder.getBytes(StandardCharsets.UTF_8));
        String credentialScope = shortXDate + "/" + region + "/" + service + "/request";
        String signString = "HMAC-SHA256" + "\n" + xDate + "\n" + credentialScope + "\n" + hashcanonicalString;

        byte[] signKey = genSigningSecretKeyV4(sk, shortXDate, region, service);
        String signature = Hex.encodeHexString(Utils.hmacSHA256(signKey, signString));

        URL url = new URL(schema + "://" + host + path + "?" + querySB);

        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod(method);
        conn.setRequestProperty("Host", host);
        conn.setRequestProperty("X-Date", xDate);
        conn.setRequestProperty("X-Content-Sha256", xContentSha256);
        conn.setRequestProperty("Content-Type", contentType);
        conn.setRequestProperty("Authorization", "HMAC-SHA256" +
                " Credential=" + ak + "/" + credentialScope +
                ", SignedHeaders=" + signHeader +
                ", Signature=" + signature);
        conn.setDoOutput(true);
        try (OutputStream os = conn.getOutputStream()) {
            os.write(body);
        }
        conn.connect();

        int responseCode = conn.getResponseCode();
        InputStream is = responseCode == 200 ? conn.getInputStream() : conn.getErrorStream();
        String responseBody = new String(ByteStreams.toByteArray(is), StandardCharsets.UTF_8);
        is.close();

        System.out.println("HTTP " + responseCode + " | Action=" + action);
        if (responseCode != 200) {
            System.out.println("错误响应:" + responseBody);
        }
        return responseBody;
    }

    private String signStringEncoder(String source) {
        if (source == null) {
            return null;
        }
        StringBuilder buf = new StringBuilder(source.length());
        ByteBuffer bb = UTF_8.encode(source);
        while (bb.hasRemaining()) {
            int b = bb.get() & 255;
            if (URLENCODER.get(b)) {
                buf.append((char) b);
            } else if (b == 32) {
                buf.append("%20");
            } else {
                buf.append("%");
                char hex1 = CONST_ENCODE.charAt(b >> 4);
                char hex2 = CONST_ENCODE.charAt(b & 15);
                buf.append(hex1);
                buf.append(hex2);
            }
        }
        return buf.toString();
    }

    public static String hashSHA256(byte[] content) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        return Hex.encodeHexString(md.digest(content));
    }

    public static byte[] hmacSHA256(byte[] key, String content) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(key, "HmacSHA256"));
        return mac.doFinal(content.getBytes(StandardCharsets.UTF_8));
    }

    private byte[] genSigningSecretKeyV4(String secretKey, String date, String region, String service) throws Exception {
        byte[] kDate = hmacSHA256(secretKey.getBytes(StandardCharsets.UTF_8), date);
        byte[] kRegion = hmacSHA256(kDate, region);
        byte[] kService = hmacSHA256(kRegion, service);
        return hmacSHA256(kService, "request");
    }
}

运行结果如下:

视频地址:

Saved Pictures1780987693143

相关推荐
段一凡-华北理工大学2 小时前
工业领域的Hadoop架构学习~系列文章22:Hadoop生态展望 - 面向未来的技术演进
大数据·人工智能·hadoop·分布式·学习·架构·高炉炼铁
人工小情绪2 小时前
Antigravity 2.0 更新:它不只是一个 AI IDE 了
ide·人工智能·ai agent·antigratity
七月稻草人2 小时前
用30秒声音复刻自己的音色:Index-TTS远程部署与公网访问实践
人工智能·语音识别
Fatbobman(东坡肘子)2 小时前
WWDC 2026 初印象:符合预期,但更务实 -- 肘子的 Swift 周报 #139
人工智能·macos·ios·swiftui·swift·wwdc
渡码桑2 小时前
英伟达与SK海力士合作,下一代AI内存技术路线解析
大数据·人工智能·音视频
LoserChaser2 小时前
大语言模型基础-与大语言模型交互
人工智能·语言模型·交互
搞科研的小刘选手2 小时前
【国家电网省科学研究院支持】第七届物联网、人工智能与电气能源国际学术会议(IoTAIEE 2026)
人工智能·物联网·机器学习·计算机视觉·自动化·能源·电气
小雨下雨的雨2 小时前
数独算法与求解器鸿蒙PC Electron框架完成深度解析
javascript·人工智能·算法·游戏·华为·electron·鸿蒙系统
沐曦股份MetaX2 小时前
沐曦芯生,开源共创 | 沐曦股份 × SGLang联合举办技术交流Meetup,共同探索AI推理落地新路径
人工智能·开源·sglang