Преглед на файлове

封装火山引擎-音色管理Api

cgp преди 1 седмица
родител
ревизия
daa4261bbc

+ 41 - 0
fs-admin/src/main/java/com/fs/aiVoice/VolcEngineSpeechController.java

@@ -0,0 +1,41 @@
+package com.fs.aiVoice;
+
+import com.fs.aiSoundReplication.entity.dto.*;
+import com.fs.aiSoundReplication.service.impl.VolcEngineSpeechServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 火山引擎音色管理控制器
+ */
+@RestController
+@RequestMapping("/volcengine/speech")
+public class VolcEngineSpeechController {
+
+    @Autowired
+    private VolcEngineSpeechServiceImpl speechService;
+
+    /**
+     * 分页查询音色状态
+     */
+    @PostMapping("/batchListMegaTTSTrainStatus")
+    public BatchListMegaTTSTrainStatusResp batchListMegaTTSTrainStatus(@RequestBody BatchListMegaTTSTrainStatusReq req) {
+        return speechService.batchListMegaTTSTrainStatus(req);
+    }
+
+    /**
+     * 音色下单
+     */
+    @PostMapping("/orderAccessResourcePacks")
+    public OrderAccessResourcePacksResp orderAccessResourcePacks(@RequestBody OrderAccessResourcePacksReq req) {
+        return speechService.orderAccessResourcePacks(req);
+    }
+
+    /**
+     * 音色续费
+     */
+    @PostMapping("/renewAccessResourcePacks")
+    public OrderAccessResourcePacksResp renewAccessResourcePacks(@RequestBody RenewAccessResourcePacksReq req) {
+        return speechService.renewAccessResourcePacks(req);
+    }
+}

+ 209 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/client/VolcEngineApiClient.java

@@ -0,0 +1,209 @@
+package com.fs.aiSoundReplication.client;
+
+import com.fs.aiSoundReplication.config.VolcEngineSpeechConfig;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.DataInputStream;
+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.*;
+
+/**
+ * 火山引擎API客户端 - 负责HTTP请求发送和签名验证
+ */
+@Slf4j
+@Component
+public class VolcEngineApiClient {
+
+    @Autowired
+    private VolcEngineSpeechConfig config;
+
+    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('~');
+    }
+
+    /**
+     * 执行API请求
+     */
+    public String doRequest(String method, Object requestBody,
+                            Date date, String action, String version) throws Exception {
+        ObjectMapper objectMapper = new ObjectMapper();
+        String bodyJson = objectMapper.writeValueAsString(requestBody);
+        byte[] body = bodyJson.getBytes();
+
+        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; charset=utf-8";
+        String signHeader = "content-type;host;x-content-sha256;x-date";
+        SortedMap<String, String> realQueryList = new TreeMap<>();
+        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" + config.getPath() + "\n" + querySB + "\n" +
+                "content-type:" + contentType + "\n" +
+                "host:" + config.getEndpoint() + "\n" +
+                "x-content-sha256:" + xContentSha256 + "\n" +
+                "x-date:" + xDate + "\n" +
+                "\n" +
+                signHeader + "\n" +
+                xContentSha256;
+
+        log.debug("Canonical Request: {}", canonicalStringBuilder);
+
+        String hashcanonicalString = hashSHA256(canonicalStringBuilder.getBytes());
+        String credentialScope = shortXDate + "/" + config.getRegion() + "/" + config.getService() + "/request";
+        String signString = "HMAC-SHA256" + "\n" + xDate + "\n" + credentialScope + "\n" + hashcanonicalString;
+
+        byte[] signKey = genSigningSecretKeyV4(config.getSecretAccessKey(), shortXDate, config.getRegion(), config.getService());
+        String signature = bytesToHex(hmacSHA256(signKey, signString));
+        log.debug("Signature: {}", signature);
+
+        String urlString = config.getSchema() + "://" + config.getEndpoint() + config.getPath() + "?" + querySB;
+        log.info("Request URL: {}", urlString);
+
+        URL url = new URL(urlString);
+
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod(method);
+        conn.setRequestProperty("Host", config.getEndpoint());
+        conn.setRequestProperty("X-Date", xDate);
+        conn.setRequestProperty("X-Content-Sha256", xContentSha256);
+        conn.setRequestProperty("Content-Type", contentType);
+        conn.setRequestProperty("Authorization", "HMAC-SHA256" +
+                " Credential=" + config.getAccessKeyId() + "/" + credentialScope +
+                ", SignedHeaders=" + signHeader +
+                ", Signature=" + signature);
+
+        if (!Objects.equals(conn.getRequestMethod(), "GET")) {
+            conn.setDoOutput(true);
+            OutputStream os = conn.getOutputStream();
+            os.write(body);
+            os.flush();
+            os.close();
+        }
+        conn.connect();
+
+        int responseCode = conn.getResponseCode();
+
+        InputStream is;
+        if (responseCode == 200) {
+            is = conn.getInputStream();
+        } else {
+            is = conn.getErrorStream();
+        }
+        byte[] bytes = new byte[is.available()];
+        DataInputStream dataInputStream = new DataInputStream(is);
+        dataInputStream.readFully(bytes);
+        String responseBody = new String(bytes);
+        is.close();
+
+        log.info("Response Code: {}", responseCode);
+        log.debug("Response Body: {}", 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 {
+        try {
+            MessageDigest md = MessageDigest.getInstance("SHA-256");
+
+            return bytesToHex(md.digest(content));
+        } catch (Exception e) {
+            throw new Exception(
+                    "Unable to compute hash while signing request: "
+                            + e.getMessage(), e);
+        }
+    }
+
+    public static byte[] hmacSHA256(byte[] key, String content) throws Exception {
+        try {
+            Mac mac = Mac.getInstance("HmacSHA256");
+            mac.init(new SecretKeySpec(key, "HmacSHA256"));
+            return mac.doFinal(content.getBytes());
+        } catch (Exception e) {
+            throw new Exception(
+                    "Unable to calculate a request signature: "
+                            + e.getMessage(), e);
+        }
+    }
+
+    private byte[] genSigningSecretKeyV4(String secretKey, String date, String region, String service) throws Exception {
+        byte[] kDate = hmacSHA256((secretKey).getBytes(), date);
+        byte[] kRegion = hmacSHA256(kDate, region);
+        byte[] kService = hmacSHA256(kRegion, service);
+        return hmacSHA256(kService, "request");
+    }
+
+    public static String bytesToHex(byte[] bytes) {
+        char[] hexChars = new char[bytes.length * 2];
+        for (int j = 0; j < bytes.length; j++) {
+            int v = bytes[j] & 0xFF;
+            hexChars[j * 2] = CONST_ENCODE.toCharArray()[v >>> 4];
+            hexChars[j * 2 + 1] = CONST_ENCODE.toCharArray()[v & 0x0F];
+        }
+        return new String(hexChars);
+    }
+}

+ 48 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/config/VolcEngineSpeechConfig.java

@@ -0,0 +1,48 @@
+package com.fs.aiSoundReplication.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 配置类 - 火山引擎配置
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "volcengine.speech")
+public class VolcEngineSpeechConfig {
+    /**
+     * Access Key ID - 火山引擎访问密钥ID
+     */
+    private String accessKeyId="AKLTMzhiNjY4Yjg5ODljNGFlZDljNTIwOWM2MDQzYzBmYmY";//测试数据
+
+    /**
+     * Secret Access Key - 火山引擎访问密钥
+     */
+    private String secretAccessKey="zxcvbnmzxcvbnmxcvbnm";//测试数据
+
+    /**
+     * 区域 - 默认值: cn-north-1
+     */
+    private String region = "cn-north-1";
+
+    /**
+     * 服务名称 - 默认值: speech_saas_prod
+     */
+    private String service = "speech_saas_prod";
+
+    /**
+     * API端点域名 - 默认值: open.volcengineapi.com
+     */
+    private String endpoint = "open.volcengineapi.com";
+
+    /**
+     * 协议 - 默认值: https
+     */
+    private String schema = "https";
+
+    /**
+     * 请求路径 - 默认值: /
+     */
+    private String path = "/";
+}

+ 78 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/entity/dto/BatchListMegaTTSTrainStatusReq.java

@@ -0,0 +1,78 @@
+package com.fs.aiSoundReplication.entity.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 批量查询音色状态请求
+ */
+@Data
+public class BatchListMegaTTSTrainStatusReq {
+    /**
+     * 应用ID
+     */
+    @JsonProperty("AppID")
+    private String appId;
+    
+    /**
+     * SpeakerID的列表,传空为返回指定APPID下的全部SpeakerID
+     */
+    @JsonProperty("SpeakerIDs")
+    private List<String> speakerIds;
+    
+    /**
+     * 音色状态,支持取值:Unknown、Training、Success、Active、Expired、Reclaimed
+     */
+    @JsonProperty("State")
+    private String state;
+    
+    /**
+     * 页数, 需大于0, 默认为1
+     */
+    @JsonProperty("PageNumber")
+    private Integer pageNumber = 1;
+    
+    /**
+     * 每页条数, 必须在范围[1, 100]内, 默认为10
+     */
+    @JsonProperty("PageSize")
+    private Integer pageSize = 10;
+    
+    /**
+     * 上次请求返回的字符串; 如果不为空的话, 将覆盖PageNumber及PageSize的值
+     */
+    @JsonProperty("NextToken")
+    private String nextToken;
+    
+    /**
+     * 与NextToken相配合控制返回结果的最大数量; 如果不为空则必须在范围[1, 100]内, 默认为10
+     */
+    @JsonProperty("MaxResults")
+    private Integer maxResults = 10;
+    
+    /**
+     * 下单时间检索上边界毫秒级时间戳,受实例交付速度影响,可能比支付完成的时间晚
+     */
+    @JsonProperty("OrderTimeStart")
+    private Long orderTimeStart;
+    
+    /**
+     * 下单时间检索下边界毫秒级时间戳,受实例交付速度影响,可能比支付完成的时间晚
+     */
+    @JsonProperty("OrderTimeEnd")
+    private Long orderTimeEnd;
+    
+    /**
+     * 实例到期时间的检索上边界毫秒级时间戳
+     */
+    @JsonProperty("ExpireTimeStart")
+    private Long expireTimeStart;
+    
+    /**
+     * 实例到期时间的检索下边界毫秒级时间戳
+     */
+    @JsonProperty("ExpireTimeEnd")
+    private Long expireTimeEnd;
+}

+ 213 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/entity/dto/BatchListMegaTTSTrainStatusResp.java

@@ -0,0 +1,213 @@
+package com.fs.aiSoundReplication.entity.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 批量查询音色状态响应
+ */
+@Data
+public class BatchListMegaTTSTrainStatusResp {
+    @JsonProperty("ResponseMetadata")
+    private ResponseMetadata responseMetadata;
+    
+    @JsonProperty("Result")
+    private ResultData result;
+    
+    @Data
+    public static class ResponseMetadata {
+        /**
+         * 请求ID
+         */
+        @JsonProperty("RequestId")
+        private String requestId;
+        
+        /**
+         * 动作
+         */
+        @JsonProperty("Action")
+        private String action;
+        
+        /**
+         * 版本
+         */
+        @JsonProperty("Version")
+        private String version;
+        
+        /**
+         * 服务
+         */
+        @JsonProperty("Service")
+        private String service;
+        
+        /**
+         * 区域
+         */
+        @JsonProperty("Region")
+        private String region;
+        
+        /**
+         * 错误信息
+         */
+        @JsonProperty("Error")
+        private ErrorInfo error;
+    }
+    
+    @Data
+    public static class ResultData {
+        /**
+         * 应用ID
+         */
+        @JsonProperty("AppID")
+        private String appId;
+        
+        /**
+         * SpeakerIDs总数量
+         */
+        @JsonProperty("TotalCount")
+        private Integer totalCount;
+        
+        /**
+         * NextToken字符串,可发送请求后面的结果; 如果没有更多结果将为空
+         */
+        @JsonProperty("NextToken")
+        private String nextToken;
+        
+        /**
+         * 使用分页参数时的当前页数
+         */
+        @JsonProperty("PageNumber")
+        private Integer pageNumber;
+        
+        /**
+         * 使用分页参数时当前页包含的条数
+         */
+        @JsonProperty("PageSize")
+        private Integer pageSize;
+        
+        /**
+         * 状态列表
+         */
+        @JsonProperty("Statuses")
+        private List<SpeakerStatus> statuses;
+    }
+    
+    @Data
+    public static class SpeakerStatus {
+        /**
+         * 创建时间,unix epoch格式,单位ms
+         */
+        @JsonProperty("CreateTime")
+        private Long createTime;
+        
+        /**
+         * demo音频链接
+         */
+        @JsonProperty("DemoAudio")
+        private String demoAudio;
+        
+        /**
+         * 火山引擎实例Number
+         */
+        @JsonProperty("InstanceNO")
+        private String instanceNo;
+        
+        /**
+         * 是否可激活
+         */
+        @JsonProperty("IsActivable")
+        private Boolean isActivable;
+        
+        /**
+         * SpeakerID
+         */
+        @JsonProperty("SpeakerID")
+        private String speakerId;
+        
+        /**
+         * SpeakerID的状态
+         */
+        @JsonProperty("State")
+        private String state;
+        
+        /**
+         * SpeakerID已训练过的次数
+         */
+        @JsonProperty("Version")
+        private String version;
+        
+        /**
+         * 到期时间
+         */
+        @JsonProperty("ExpireTime")
+        private Long expireTime;
+        
+        /**
+         * 下单时间
+         */
+        @JsonProperty("OrderTime")
+        private Long orderTime;
+        
+        /**
+         * 别名,和控制台同步
+         */
+        @JsonProperty("Alias")
+        private String alias;
+        
+        /**
+         * 剩余训练次数
+         */
+        @JsonProperty("AvailableTrainingTimes")
+        private Integer availableTrainingTimes;
+        
+        /**
+         * 模型类型详情列表
+         */
+        @JsonProperty("ModelTypeDetails")
+        private List<ModelTypeDetail> modelTypeDetails;
+    }
+    
+    @Data
+    public static class ModelTypeDetail {
+        /**
+         * 模型类型
+         */
+        @JsonProperty("ModelType")
+        private Integer modelType;
+        
+        /**
+         * demo音频链接
+         */
+        @JsonProperty("DemoAudio")
+        private String demoAudio;
+        
+        /**
+         * ICL Speaker ID
+         */
+        @JsonProperty("IclSpeakerId")
+        private String iclSpeakerId;
+        
+        /**
+         * 资源ID
+         */
+        @JsonProperty("ResourceID")
+        private String resourceId;
+    }
+    
+    @Data
+    public static class ErrorInfo {
+        /**
+         * 错误码
+         */
+        @JsonProperty("Code")
+        private String code;
+        
+        /**
+         * 错误消息
+         */
+        @JsonProperty("Message")
+        private String message;
+    }
+}

+ 73 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/entity/dto/OrderAccessResourcePacksReq.java

@@ -0,0 +1,73 @@
+package com.fs.aiSoundReplication.entity.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * 音色下单请求
+ */
+@Data
+public class OrderAccessResourcePacksReq {
+    /**
+     * 应用ID
+     */
+    @JsonProperty("AppID")
+    private Integer appId;
+    
+    /**
+     * 平台的服务类型资源标识,固定值:volc.megatts.voiceclone
+     */
+    @JsonProperty("ResourceID")
+    private String resourceId = "volc.megatts.voiceclone";
+    
+    /**
+     * 平台的计费项标识,固定值:Model_storage 声音复刻
+     */
+    @JsonProperty("Code")
+    private String code = "Model_storage";
+    
+    /**
+     * 下单单个音色的时长,单位为月
+     */
+    @JsonProperty("Times")
+    private Integer times;
+    
+    /**
+     * 下单音色的个数,如100,即为购买100个音色
+     */
+    @JsonProperty("Quantity")
+    private Integer quantity;
+    
+    /**
+     * 是否自动使用代金券,默认false
+     */
+    @JsonProperty("AutoUseCoupon")
+    private Boolean autoUseCoupon = false;
+    
+    /**
+     * 代金券ID,通过代金券管理获取
+     */
+    @JsonProperty("CouponID")
+    private String couponId;
+    
+    /**
+     * 项目&标签账单配置
+     */
+    @JsonProperty("ResourceTag")
+    private ResourceTag resourceTag;
+}
+
+@Data
+class ResourceTag {
+    /**
+     * 标签,通过标签管理获取
+     */
+    @JsonProperty("CustomTags")
+    private java.util.Map<String, String> customTags;
+    
+    /**
+     * 项目名称,通过项目管理获取
+     */
+    @JsonProperty("ProjectName")
+    private String projectName;
+}

+ 27 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/entity/dto/OrderAccessResourcePacksResp.java

@@ -0,0 +1,27 @@
+package com.fs.aiSoundReplication.entity.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 下单/续费响应
+ */
+@Data
+public class OrderAccessResourcePacksResp {
+    @JsonProperty("ResponseMetadata")
+    private BatchListMegaTTSTrainStatusResp.ResponseMetadata responseMetadata;
+    
+    @JsonProperty("Result")
+    private OrderResult result;
+    
+    @Data
+    public static class OrderResult {
+        /**
+         * 购买成功返回的订单号ID列表
+         */
+        @JsonProperty("OrderIDs")
+        private List<String> orderIds;
+    }
+}

+ 36 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/entity/dto/RenewAccessResourcePacksReq.java

@@ -0,0 +1,36 @@
+package com.fs.aiSoundReplication.entity.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 音色续费请求
+ */
+@Data
+public class RenewAccessResourcePacksReq {
+    /**
+     * 续费音色的时长,单位为月
+     */
+    @JsonProperty("Times")
+    private Integer times;
+    
+    /**
+     * 要续费的SpeakerID的列表,可以通过BatchListMegaTTSTrainStatus接口过滤获取
+     */
+    @JsonProperty("SpeakerIDs")
+    private List<String> speakerIds;
+    
+    /**
+     * 是否自动使用代金券,默认false
+     */
+    @JsonProperty("AutoUseCoupon")
+    private Boolean autoUseCoupon = false;
+    
+    /**
+     * 代金券ID,通过代金券管理获取
+     */
+    @JsonProperty("CouponID")
+    private String couponId;
+}

+ 21 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/service/VolcEngineSpeechService.java

@@ -0,0 +1,21 @@
+package com.fs.aiSoundReplication.service;
+
+import com.fs.aiSoundReplication.entity.dto.*;
+
+
+public interface VolcEngineSpeechService {
+    /**
+     * 分页查询SpeakerID状态
+     */
+    public BatchListMegaTTSTrainStatusResp batchListMegaTTSTrainStatus(BatchListMegaTTSTrainStatusReq req);
+
+    /**
+     * 音色下单
+     */
+    public OrderAccessResourcePacksResp orderAccessResourcePacks(OrderAccessResourcePacksReq req);
+
+    /**
+     * 音色续费
+     */
+    public OrderAccessResourcePacksResp renewAccessResourcePacks(RenewAccessResourcePacksReq req);
+}

+ 83 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/service/impl/VolcEngineSpeechServiceImpl.java

@@ -0,0 +1,83 @@
+package com.fs.aiSoundReplication.service.impl;
+
+import com.fs.aiSoundReplication.client.VolcEngineApiClient;
+import com.fs.aiSoundReplication.entity.dto.*;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.aiSoundReplication.service.VolcEngineSpeechService;
+import com.fs.common.exception.CustomException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+
+@Slf4j
+@Service
+public class VolcEngineSpeechServiceImpl implements VolcEngineSpeechService {
+
+    @Autowired
+    private VolcEngineApiClient apiClient;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    /**
+     * 分页查询SpeakerID状态
+     */
+    public BatchListMegaTTSTrainStatusResp batchListMegaTTSTrainStatus(BatchListMegaTTSTrainStatusReq req) {
+        try {
+            //火山引擎-音色管理API固定公共参数
+            String response = apiClient.doRequest(
+                    "POST",
+                    req,
+                    new Date(),
+                    "BatchListMegaTTSTrainStatus",
+                    "2023-11-07"
+            );
+            
+            return objectMapper.readValue(response, BatchListMegaTTSTrainStatusResp.class);
+        } catch (Exception e) {
+            log.error("批量查询音色状态失败", e);
+            throw new CustomException("批量查询音色状态失败", e);
+        }
+    }
+
+    /**
+     * 音色下单
+     */
+    public OrderAccessResourcePacksResp orderAccessResourcePacks(OrderAccessResourcePacksReq req) {
+        try {
+            String response = apiClient.doRequest(
+                    "POST",
+                    req,
+                    new Date(),
+                    "OrderAccessResourcePacks",
+                    "2023-11-07"
+            );
+            
+            return objectMapper.readValue(response, OrderAccessResourcePacksResp.class);
+        } catch (Exception e) {
+            log.error("音色下单失败", e);
+            throw new CustomException("音色下单失败", e);
+        }
+    }
+
+    /**
+     * 音色续费
+     */
+    public OrderAccessResourcePacksResp renewAccessResourcePacks(RenewAccessResourcePacksReq req) {
+        try {
+            String response = apiClient.doRequest(
+                    "POST",
+                    req,
+                    new Date(),
+                    "RenewAccessResourcePacks",
+                    "2023-11-07"
+            );
+            
+            return objectMapper.readValue(response, OrderAccessResourcePacksResp.class);
+        } catch (Exception e) {
+            log.error("音色续费失败", e);
+            throw new CustomException("音色续费失败", e);
+        }
+    }
+}