Upload 48 files
Browse files- docker-compose.yml +27 -0
- package-lock.json +0 -0
- src/api/consts/common.ts +9 -0
- src/api/consts/dreamina.ts +3 -0
- src/api/controllers/core.ts +47 -10
- src/api/controllers/images.ts +48 -27
- src/api/controllers/videos.ts +142 -173
docker-compose.yml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
jimeng-api:
|
| 3 |
+
build:
|
| 4 |
+
context: .
|
| 5 |
+
dockerfile: Dockerfile
|
| 6 |
+
image: jimeng
|
| 7 |
+
container_name: jimeng-api
|
| 8 |
+
restart: unless-stopped
|
| 9 |
+
ports:
|
| 10 |
+
- "5100:5100"
|
| 11 |
+
#volumes:
|
| 12 |
+
# # 挂载日志目录(确保权限正确)
|
| 13 |
+
# - ./logs:/app/logs
|
| 14 |
+
# # 挂载临时目录
|
| 15 |
+
# - ./tmp:/app/tmp
|
| 16 |
+
healthcheck:
|
| 17 |
+
test: ["CMD", "wget", "-q", "--spider", "http://localhost:5100/ping"]
|
| 18 |
+
interval: 15s
|
| 19 |
+
timeout: 5s
|
| 20 |
+
retries: 3
|
| 21 |
+
start_period: 20s
|
| 22 |
+
# 日志配置
|
| 23 |
+
logging:
|
| 24 |
+
driver: "json-file"
|
| 25 |
+
options:
|
| 26 |
+
max-size: "10m"
|
| 27 |
+
max-file: "3"
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/api/consts/common.ts
CHANGED
|
@@ -6,14 +6,22 @@
|
|
| 6 |
export const BASE_URL_CN = "https://jimeng.jianying.com";
|
| 7 |
|
| 8 |
export const BASE_URL_US_COMMERCE = "https://commerce.us.capcut.com";
|
|
|
|
|
|
|
| 9 |
|
| 10 |
// 默认助手ID
|
| 11 |
export const DEFAULT_ASSISTANT_ID_CN = 513695;
|
| 12 |
export const DEFAULT_ASSISTANT_ID_US = 513641;
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
// 地区
|
| 15 |
export const REGION_CN = "cn";
|
| 16 |
export const REGION_US = "US";
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
// 平台代码
|
| 19 |
export const PLATFORM_CODE = "7";
|
|
@@ -51,6 +59,7 @@ export const IMAGE_MODEL_MAP_US = {
|
|
| 51 |
export const VIDEO_MODEL_MAP = {
|
| 52 |
"jimeng-video-3.0-pro": "dreamina_ic_generate_video_model_vgfm_3.0_pro",
|
| 53 |
"jimeng-video-3.0": "dreamina_ic_generate_video_model_vgfm_3.0",
|
|
|
|
| 54 |
"jimeng-video-2.0": "dreamina_ic_generate_video_model_vgfm_lite",
|
| 55 |
"jimeng-video-2.0-pro": "dreamina_ic_generate_video_model_vgfm1.0"
|
| 56 |
};
|
|
|
|
| 6 |
export const BASE_URL_CN = "https://jimeng.jianying.com";
|
| 7 |
|
| 8 |
export const BASE_URL_US_COMMERCE = "https://commerce.us.capcut.com";
|
| 9 |
+
export const BASE_URL_HK_COMMERCE = "https://commerce-api-sg.capcut.com";
|
| 10 |
+
export const BASE_URL_HK = "https://mweb-api-sg.capcut.com";
|
| 11 |
|
| 12 |
// 默认助手ID
|
| 13 |
export const DEFAULT_ASSISTANT_ID_CN = 513695;
|
| 14 |
export const DEFAULT_ASSISTANT_ID_US = 513641;
|
| 15 |
+
export const DEFAULT_ASSISTANT_ID_HK = 513641;
|
| 16 |
+
export const DEFAULT_ASSISTANT_ID_JP = 513641;
|
| 17 |
+
export const DEFAULT_ASSISTANT_ID_SG = 513641;
|
| 18 |
|
| 19 |
// 地区
|
| 20 |
export const REGION_CN = "cn";
|
| 21 |
export const REGION_US = "US";
|
| 22 |
+
export const REGION_HK = "HK";
|
| 23 |
+
export const REGION_JP = "JP";
|
| 24 |
+
export const REGION_SG = "SG";
|
| 25 |
|
| 26 |
// 平台代码
|
| 27 |
export const PLATFORM_CODE = "7";
|
|
|
|
| 59 |
export const VIDEO_MODEL_MAP = {
|
| 60 |
"jimeng-video-3.0-pro": "dreamina_ic_generate_video_model_vgfm_3.0_pro",
|
| 61 |
"jimeng-video-3.0": "dreamina_ic_generate_video_model_vgfm_3.0",
|
| 62 |
+
"jimeng-video-3.0-fast": "dreamina_ic_generate_video_model_vgfm_3.0_fast",
|
| 63 |
"jimeng-video-2.0": "dreamina_ic_generate_video_model_vgfm_lite",
|
| 64 |
"jimeng-video-2.0-pro": "dreamina_ic_generate_video_model_vgfm1.0"
|
| 65 |
};
|
src/api/consts/dreamina.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
| 3 |
export const BASE_URL_DREAMINA_US = "https://dreamina-api.us.capcut.com";
|
| 4 |
export const BASE_URL_IMAGEX_US = "https://imagex16-normal-us-ttp.capcutapi.us";
|
| 5 |
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
export const WEB_VERSION = "7.5.0";
|
| 8 |
export const DA_VERSION = "3.3.2";
|
|
|
|
| 3 |
export const BASE_URL_DREAMINA_US = "https://dreamina-api.us.capcut.com";
|
| 4 |
export const BASE_URL_IMAGEX_US = "https://imagex16-normal-us-ttp.capcutapi.us";
|
| 5 |
|
| 6 |
+
export const BASE_URL_DREAMINA_HK = "https://mweb-api-sg.capcut.com";
|
| 7 |
+
export const BASE_URL_IMAGEX_HK = "https://imagex16-normal-sg-ttp.capcutapi.sg";
|
| 8 |
+
|
| 9 |
|
| 10 |
export const WEB_VERSION = "7.5.0";
|
| 11 |
export const DA_VERSION = "3.3.2";
|
src/api/controllers/core.ts
CHANGED
|
@@ -10,15 +10,23 @@ import { createParser } from "eventsource-parser";
|
|
| 10 |
import logger from "@/lib/logger.ts";
|
| 11 |
import util from "@/lib/util.ts";
|
| 12 |
import { JimengErrorHandler, JimengErrorResponse } from "@/lib/error-handler.ts";
|
| 13 |
-
import { BASE_URL_DREAMINA_US } from "@/api/consts/dreamina.ts";
|
| 14 |
-
import {
|
| 15 |
BASE_URL_CN,
|
| 16 |
BASE_URL_US_COMMERCE,
|
|
|
|
|
|
|
| 17 |
DEFAULT_ASSISTANT_ID_CN,
|
| 18 |
DEFAULT_ASSISTANT_ID_US,
|
|
|
|
|
|
|
|
|
|
| 19 |
PLATFORM_CODE,
|
| 20 |
REGION_CN,
|
| 21 |
REGION_US,
|
|
|
|
|
|
|
|
|
|
| 22 |
VERSION_CODE,
|
| 23 |
RETRY_CONFIG
|
| 24 |
} from "@/api/consts/common.ts";
|
|
@@ -70,11 +78,21 @@ export async function acquireToken(refreshToken: string): Promise<string> {
|
|
| 70 |
*/
|
| 71 |
export function generateCookie(refreshToken: string) {
|
| 72 |
const isUS = refreshToken.toLowerCase().startsWith('us-');
|
| 73 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
return [
|
| 75 |
`_tea_web_id=${WEB_ID}`,
|
| 76 |
`is_staff_user=false`,
|
| 77 |
-
`store-region=${
|
| 78 |
`store-region-src=uid`,
|
| 79 |
`sid_guard=${token}%7C${util.unixTimestamp()}%7C5184000%7CMon%2C+03-Feb-2025+08%3A17%3A09+GMT`,
|
| 80 |
`uid_tt=${USER_ID}`,
|
|
@@ -137,8 +155,6 @@ export async function receiveCredit(refreshToken: string) {
|
|
| 137 |
* @param params 请求参数
|
| 138 |
* @param headers 请求头
|
| 139 |
*/
|
| 140 |
-
import { BASE_URL_DREAMINA_US } from "@/api/consts/dreamina.ts";
|
| 141 |
-
|
| 142 |
export async function request(
|
| 143 |
method: string,
|
| 144 |
uri: string,
|
|
@@ -146,14 +162,17 @@ export async function request(
|
|
| 146 |
options: AxiosRequestConfig & { noDefaultParams?: boolean } = {}
|
| 147 |
) {
|
| 148 |
const isUS = refreshToken.toLowerCase().startsWith('us-');
|
| 149 |
-
const
|
|
|
|
|
|
|
|
|
|
| 150 |
const deviceTime = util.unixTimestamp();
|
| 151 |
const sign = util.md5(
|
| 152 |
`9e2c|${uri.slice(-7)}|${PLATFORM_CODE}|${VERSION_CODE}|${deviceTime}||11ac`
|
| 153 |
);
|
| 154 |
|
| 155 |
let baseUrl: string;
|
| 156 |
-
let aid:
|
| 157 |
let region: string;
|
| 158 |
|
| 159 |
if (isUS) {
|
|
@@ -164,7 +183,25 @@ export async function request(
|
|
| 164 |
}
|
| 165 |
aid = DEFAULT_ASSISTANT_ID_US;
|
| 166 |
region = REGION_US;
|
| 167 |
-
} else
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
baseUrl = BASE_URL_CN;
|
| 169 |
aid = DEFAULT_ASSISTANT_ID_CN;
|
| 170 |
region = REGION_CN;
|
|
@@ -177,7 +214,7 @@ export async function request(
|
|
| 177 |
aid: aid,
|
| 178 |
device_platform: "web",
|
| 179 |
region: region,
|
| 180 |
-
...(isUS ? {} : { webId: WEB_ID }),
|
| 181 |
da_version: "3.3.2",
|
| 182 |
web_component_open_flag: 1,
|
| 183 |
web_version: "7.5.0",
|
|
|
|
| 10 |
import logger from "@/lib/logger.ts";
|
| 11 |
import util from "@/lib/util.ts";
|
| 12 |
import { JimengErrorHandler, JimengErrorResponse } from "@/lib/error-handler.ts";
|
| 13 |
+
import { BASE_URL_DREAMINA_US, BASE_URL_DREAMINA_HK } from "@/api/consts/dreamina.ts";
|
| 14 |
+
import {
|
| 15 |
BASE_URL_CN,
|
| 16 |
BASE_URL_US_COMMERCE,
|
| 17 |
+
BASE_URL_HK_COMMERCE,
|
| 18 |
+
BASE_URL_HK,
|
| 19 |
DEFAULT_ASSISTANT_ID_CN,
|
| 20 |
DEFAULT_ASSISTANT_ID_US,
|
| 21 |
+
DEFAULT_ASSISTANT_ID_HK,
|
| 22 |
+
DEFAULT_ASSISTANT_ID_JP,
|
| 23 |
+
DEFAULT_ASSISTANT_ID_SG,
|
| 24 |
PLATFORM_CODE,
|
| 25 |
REGION_CN,
|
| 26 |
REGION_US,
|
| 27 |
+
REGION_HK,
|
| 28 |
+
REGION_JP,
|
| 29 |
+
REGION_SG,
|
| 30 |
VERSION_CODE,
|
| 31 |
RETRY_CONFIG
|
| 32 |
} from "@/api/consts/common.ts";
|
|
|
|
| 78 |
*/
|
| 79 |
export function generateCookie(refreshToken: string) {
|
| 80 |
const isUS = refreshToken.toLowerCase().startsWith('us-');
|
| 81 |
+
const isHK = refreshToken.toLowerCase().startsWith('hk-');
|
| 82 |
+
const isJP = refreshToken.toLowerCase().startsWith('jp-');
|
| 83 |
+
const isSG = refreshToken.toLowerCase().startsWith('sg-');
|
| 84 |
+
const token = (isUS || isHK || isJP || isSG) ? refreshToken.substring(3) : refreshToken;
|
| 85 |
+
|
| 86 |
+
let storeRegion = 'cn-gd';
|
| 87 |
+
if (isUS) storeRegion = 'us';
|
| 88 |
+
else if (isHK) storeRegion = 'hk';
|
| 89 |
+
else if (isJP) storeRegion = 'hk'; // JP uses HK store region
|
| 90 |
+
else if (isSG) storeRegion = 'hk'; // SG uses HK store region
|
| 91 |
+
|
| 92 |
return [
|
| 93 |
`_tea_web_id=${WEB_ID}`,
|
| 94 |
`is_staff_user=false`,
|
| 95 |
+
`store-region=${storeRegion}`,
|
| 96 |
`store-region-src=uid`,
|
| 97 |
`sid_guard=${token}%7C${util.unixTimestamp()}%7C5184000%7CMon%2C+03-Feb-2025+08%3A17%3A09+GMT`,
|
| 98 |
`uid_tt=${USER_ID}`,
|
|
|
|
| 155 |
* @param params 请求参数
|
| 156 |
* @param headers 请求头
|
| 157 |
*/
|
|
|
|
|
|
|
| 158 |
export async function request(
|
| 159 |
method: string,
|
| 160 |
uri: string,
|
|
|
|
| 162 |
options: AxiosRequestConfig & { noDefaultParams?: boolean } = {}
|
| 163 |
) {
|
| 164 |
const isUS = refreshToken.toLowerCase().startsWith('us-');
|
| 165 |
+
const isHK = refreshToken.toLowerCase().startsWith('hk-');
|
| 166 |
+
const isJP = refreshToken.toLowerCase().startsWith('jp-');
|
| 167 |
+
const isSG = refreshToken.toLowerCase().startsWith('sg-');
|
| 168 |
+
const token = await acquireToken((isUS || isHK || isJP || isSG) ? refreshToken.substring(3) : refreshToken);
|
| 169 |
const deviceTime = util.unixTimestamp();
|
| 170 |
const sign = util.md5(
|
| 171 |
`9e2c|${uri.slice(-7)}|${PLATFORM_CODE}|${VERSION_CODE}|${deviceTime}||11ac`
|
| 172 |
);
|
| 173 |
|
| 174 |
let baseUrl: string;
|
| 175 |
+
let aid: number;
|
| 176 |
let region: string;
|
| 177 |
|
| 178 |
if (isUS) {
|
|
|
|
| 183 |
}
|
| 184 |
aid = DEFAULT_ASSISTANT_ID_US;
|
| 185 |
region = REGION_US;
|
| 186 |
+
} else if (isHK || isJP || isSG) {
|
| 187 |
+
// HK, JP and SG regions use the same SG base URL
|
| 188 |
+
if (uri.startsWith("/commerce/")) {
|
| 189 |
+
baseUrl = BASE_URL_HK_COMMERCE;
|
| 190 |
+
} else {
|
| 191 |
+
baseUrl = BASE_URL_DREAMINA_HK;
|
| 192 |
+
}
|
| 193 |
+
if (isJP) {
|
| 194 |
+
aid = DEFAULT_ASSISTANT_ID_JP;
|
| 195 |
+
region = REGION_JP;
|
| 196 |
+
} else if (isSG) {
|
| 197 |
+
aid = DEFAULT_ASSISTANT_ID_SG;
|
| 198 |
+
region = REGION_SG;
|
| 199 |
+
} else {
|
| 200 |
+
aid = DEFAULT_ASSISTANT_ID_HK;
|
| 201 |
+
region = REGION_HK;
|
| 202 |
+
}
|
| 203 |
+
} else {
|
| 204 |
+
// CN region
|
| 205 |
baseUrl = BASE_URL_CN;
|
| 206 |
aid = DEFAULT_ASSISTANT_ID_CN;
|
| 207 |
region = REGION_CN;
|
|
|
|
| 214 |
aid: aid,
|
| 215 |
device_platform: "web",
|
| 216 |
region: region,
|
| 217 |
+
...(isUS || isHK || isJP || isSG ? {} : { webId: WEB_ID }),
|
| 218 |
da_version: "3.3.2",
|
| 219 |
web_component_open_flag: 1,
|
| 220 |
web_version: "7.5.0",
|
src/api/controllers/images.ts
CHANGED
|
@@ -7,8 +7,8 @@ import util from "@/lib/util.ts";
|
|
| 7 |
import { getCredit, receiveCredit, request } from "./core.ts";
|
| 8 |
import logger from "@/lib/logger.ts";
|
| 9 |
import { SmartPoller, PollingStatus } from "@/lib/smart-poller.ts";
|
| 10 |
-
import { DEFAULT_ASSISTANT_ID_CN, DEFAULT_ASSISTANT_ID_US, DEFAULT_IMAGE_MODEL, DRAFT_VERSION, DRAFT_MIN_VERSION, IMAGE_MODEL_MAP, IMAGE_MODEL_MAP_US, RESOLUTION_OPTIONS } from "@/api/consts/common.ts";
|
| 11 |
-
import { BASE_URL_DREAMINA_US, BASE_URL_IMAGEX_US, WEB_VERSION as DREAMINA_WEB_VERSION, DA_VERSION as DREAMINA_DA_VERSION, AIGC_FEATURES as DREAMINA_AIGC_FEATURES } from "@/api/consts/dreamina.ts";
|
| 12 |
import { createSignature } from "@/lib/aws-signature.ts";
|
| 13 |
|
| 14 |
export const DEFAULT_MODEL = DEFAULT_IMAGE_MODEL;
|
|
@@ -33,9 +33,9 @@ function getResolutionParams(resolution: string = '2k', ratio: string = '1:1'):
|
|
| 33 |
resolution_type: resolution,
|
| 34 |
};
|
| 35 |
}
|
| 36 |
-
export function getModel(model: string,
|
| 37 |
-
const modelMap =
|
| 38 |
-
if (
|
| 39 |
const supportedModels = Object.keys(modelMap).join(', ');
|
| 40 |
throw new Error(`国际版不支持模型 "${model}"。支持的模型: ${supportedModels}`);
|
| 41 |
}
|
|
@@ -60,9 +60,9 @@ function calculateCRC32(buffer: ArrayBuffer): string {
|
|
| 60 |
return ((crc ^ (-1)) >>> 0).toString(16).padStart(8, '0');
|
| 61 |
}
|
| 62 |
|
| 63 |
-
async function uploadImageFromUrl(imageUrl: string, refreshToken: string,
|
| 64 |
try {
|
| 65 |
-
logger.info(`开始上传图片: ${imageUrl} (
|
| 66 |
|
| 67 |
const imageResponse = await fetch(imageUrl);
|
| 68 |
if (!imageResponse.ok) {
|
|
@@ -70,7 +70,7 @@ async function uploadImageFromUrl(imageUrl: string, refreshToken: string, isUS:
|
|
| 70 |
}
|
| 71 |
const imageBuffer = await imageResponse.arrayBuffer();
|
| 72 |
|
| 73 |
-
return await uploadImageFromBuffer(Buffer.from(imageBuffer), refreshToken,
|
| 74 |
|
| 75 |
} catch (error) {
|
| 76 |
logger.error(`图片上传失败: ${error.message}`);
|
|
@@ -78,16 +78,21 @@ async function uploadImageFromUrl(imageUrl: string, refreshToken: string, isUS:
|
|
| 78 |
}
|
| 79 |
}
|
| 80 |
|
| 81 |
-
async function uploadImageFromBuffer(imageBuffer: Buffer, refreshToken: string,
|
| 82 |
try {
|
| 83 |
-
logger.info(`开始通过Buffer上传图片... (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
const tokenResult = await request("post", "/mweb/v1/get_upload_token", refreshToken, {
|
| 86 |
data: {
|
| 87 |
scene: 2,
|
| 88 |
},
|
| 89 |
-
params:
|
| 90 |
-
aid: DEFAULT_ASSISTANT_ID_US,
|
| 91 |
web_version: DREAMINA_WEB_VERSION,
|
| 92 |
da_version: DREAMINA_DA_VERSION,
|
| 93 |
aigc_features: DREAMINA_AIGC_FEATURES,
|
|
@@ -96,13 +101,13 @@ async function uploadImageFromBuffer(imageBuffer: Buffer, refreshToken: string,
|
|
| 96 |
},
|
| 97 |
});
|
| 98 |
const { access_key_id, secret_access_key, session_token } = tokenResult;
|
| 99 |
-
const service_id =
|
| 100 |
|
| 101 |
if (!access_key_id || !secret_access_key || !session_token) {
|
| 102 |
throw new Error("获取上传令牌失败");
|
| 103 |
}
|
| 104 |
|
| 105 |
-
const actualServiceId = service_id || (isUS ? "wopfjsm1ax" : "tb4s082cfz");
|
| 106 |
|
| 107 |
logger.info(`获取上传令牌成功: service_id=${actualServiceId}`);
|
| 108 |
|
|
@@ -114,9 +119,9 @@ async function uploadImageFromBuffer(imageBuffer: Buffer, refreshToken: string,
|
|
| 114 |
const now = new Date();
|
| 115 |
const timestamp = now.toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
| 116 |
const randomStr = Math.random().toString(36).substring(2, 12);
|
| 117 |
-
|
| 118 |
-
const applyUrlHost = isUS ? BASE_URL_IMAGEX_US : 'https://imagex.bytedanceapi.com';
|
| 119 |
-
const applyUrl = `${applyUrlHost}/?Action=ApplyImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}&FileSize=${fileSize}&s=${randomStr}${
|
| 120 |
|
| 121 |
const requestHeaders = {
|
| 122 |
'x-amz-date': timestamp,
|
|
@@ -125,7 +130,7 @@ async function uploadImageFromBuffer(imageBuffer: Buffer, refreshToken: string,
|
|
| 125 |
|
| 126 |
const authorization = createSignature('GET', applyUrl, requestHeaders, access_key_id, secret_access_key, session_token);
|
| 127 |
|
| 128 |
-
const origin = isUS ? new URL(BASE_URL_DREAMINA_US).origin : 'https://jimeng.jianying.com';
|
| 129 |
|
| 130 |
const applyResponse = await fetch(applyUrl, {
|
| 131 |
method: 'GET',
|
|
@@ -247,7 +252,11 @@ export async function generateImageComposition(
|
|
| 247 |
refreshToken: string
|
| 248 |
) {
|
| 249 |
const isUS = refreshToken.toLowerCase().startsWith('us-');
|
| 250 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
let width, height, image_ratio, resolution_type;
|
| 253 |
|
|
@@ -283,10 +292,10 @@ export async function generateImageComposition(
|
|
| 283 |
let imageId: string;
|
| 284 |
if (typeof image === 'string') {
|
| 285 |
logger.info(`正在处理第 ${i + 1}/${imageCount} 张图片 (URL)...`);
|
| 286 |
-
imageId = await uploadImageFromUrl(image, refreshToken,
|
| 287 |
} else {
|
| 288 |
logger.info(`正在处理第 ${i + 1}/${imageCount} 张图片 (Buffer)...`);
|
| 289 |
-
imageId = await uploadImageFromBuffer(image, refreshToken,
|
| 290 |
}
|
| 291 |
uploadedImageIds.push(imageId);
|
| 292 |
logger.info(`图片 ${i + 1}/${imageCount} 上传成功: ${imageId}`);
|
|
@@ -403,7 +412,7 @@ export async function generateImageComposition(
|
|
| 403 |
],
|
| 404 |
}),
|
| 405 |
http_common_info: {
|
| 406 |
-
aid: isUS ? DEFAULT_ASSISTANT_ID_US : DEFAULT_ASSISTANT_ID_CN
|
| 407 |
}
|
| 408 |
},
|
| 409 |
}
|
|
@@ -521,7 +530,11 @@ export async function generateImages(
|
|
| 521 |
refreshToken: string
|
| 522 |
) {
|
| 523 |
const isUS = refreshToken.toLowerCase().startsWith('us-');
|
| 524 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 525 |
logger.info(`使用模型: ${_model} 映射模型: ${model} 分辨率: ${resolution} 比例: ${ratio} 精细度: ${sampleStrength}`);
|
| 526 |
|
| 527 |
return await generateImagesInternal(_model, prompt, { ratio, resolution, sampleStrength, negativePrompt }, refreshToken);
|
|
@@ -544,7 +557,11 @@ async function generateImagesInternal(
|
|
| 544 |
refreshToken: string
|
| 545 |
) {
|
| 546 |
const isUS = refreshToken.toLowerCase().startsWith('us-');
|
| 547 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
|
| 549 |
let width, height, image_ratio, resolution_type;
|
| 550 |
|
|
@@ -655,7 +672,7 @@ async function generateImagesInternal(
|
|
| 655 |
],
|
| 656 |
}),
|
| 657 |
http_common_info: {
|
| 658 |
-
aid: isUS ? DEFAULT_ASSISTANT_ID_US : DEFAULT_ASSISTANT_ID_CN
|
| 659 |
}
|
| 660 |
},
|
| 661 |
}
|
|
@@ -775,7 +792,11 @@ async function generateJimeng40MultiImages(
|
|
| 775 |
refreshToken: string
|
| 776 |
) {
|
| 777 |
const isUS = refreshToken.toLowerCase().startsWith('us-');
|
| 778 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 779 |
const { width, height, image_ratio, resolution_type } = getResolutionParams(resolution, ratio);
|
| 780 |
|
| 781 |
const targetImageCount = prompt.match(/(\d+)张/) ? parseInt(prompt.match(/(\d+)张/)[1]) : 4;
|
|
@@ -858,7 +879,7 @@ async function generateJimeng40MultiImages(
|
|
| 858 |
],
|
| 859 |
}),
|
| 860 |
http_common_info: {
|
| 861 |
-
aid: isUS ? DEFAULT_ASSISTANT_ID_US : DEFAULT_ASSISTANT_ID_CN
|
| 862 |
}
|
| 863 |
},
|
| 864 |
}
|
|
|
|
| 7 |
import { getCredit, receiveCredit, request } from "./core.ts";
|
| 8 |
import logger from "@/lib/logger.ts";
|
| 9 |
import { SmartPoller, PollingStatus } from "@/lib/smart-poller.ts";
|
| 10 |
+
import { DEFAULT_ASSISTANT_ID_CN, DEFAULT_ASSISTANT_ID_US, DEFAULT_ASSISTANT_ID_HK, DEFAULT_ASSISTANT_ID_JP, DEFAULT_ASSISTANT_ID_SG, DEFAULT_IMAGE_MODEL, DRAFT_VERSION, DRAFT_MIN_VERSION, IMAGE_MODEL_MAP, IMAGE_MODEL_MAP_US, RESOLUTION_OPTIONS } from "@/api/consts/common.ts";
|
| 11 |
+
import { BASE_URL_DREAMINA_US, BASE_URL_DREAMINA_HK, BASE_URL_IMAGEX_US, BASE_URL_IMAGEX_HK, WEB_VERSION as DREAMINA_WEB_VERSION, DA_VERSION as DREAMINA_DA_VERSION, AIGC_FEATURES as DREAMINA_AIGC_FEATURES } from "@/api/consts/dreamina.ts";
|
| 12 |
import { createSignature } from "@/lib/aws-signature.ts";
|
| 13 |
|
| 14 |
export const DEFAULT_MODEL = DEFAULT_IMAGE_MODEL;
|
|
|
|
| 33 |
resolution_type: resolution,
|
| 34 |
};
|
| 35 |
}
|
| 36 |
+
export function getModel(model: string, isInternational: boolean) {
|
| 37 |
+
const modelMap = isInternational ? IMAGE_MODEL_MAP_US : IMAGE_MODEL_MAP;
|
| 38 |
+
if (isInternational && !modelMap[model]) {
|
| 39 |
const supportedModels = Object.keys(modelMap).join(', ');
|
| 40 |
throw new Error(`国际版不支持模型 "${model}"。支持的模型: ${supportedModels}`);
|
| 41 |
}
|
|
|
|
| 60 |
return ((crc ^ (-1)) >>> 0).toString(16).padStart(8, '0');
|
| 61 |
}
|
| 62 |
|
| 63 |
+
async function uploadImageFromUrl(imageUrl: string, refreshToken: string, isInternational: boolean): Promise<string> {
|
| 64 |
try {
|
| 65 |
+
logger.info(`开始上传图片: ${imageUrl} (isInternational: ${isInternational})`);
|
| 66 |
|
| 67 |
const imageResponse = await fetch(imageUrl);
|
| 68 |
if (!imageResponse.ok) {
|
|
|
|
| 70 |
}
|
| 71 |
const imageBuffer = await imageResponse.arrayBuffer();
|
| 72 |
|
| 73 |
+
return await uploadImageFromBuffer(Buffer.from(imageBuffer), refreshToken, isInternational);
|
| 74 |
|
| 75 |
} catch (error) {
|
| 76 |
logger.error(`图片上传失败: ${error.message}`);
|
|
|
|
| 78 |
}
|
| 79 |
}
|
| 80 |
|
| 81 |
+
async function uploadImageFromBuffer(imageBuffer: Buffer, refreshToken: string, isInternational: boolean): Promise<string> {
|
| 82 |
try {
|
| 83 |
+
logger.info(`开始通过Buffer上传图片... (isInternational: ${isInternational})`);
|
| 84 |
+
|
| 85 |
+
const isUS = refreshToken.toLowerCase().startsWith('us-');
|
| 86 |
+
const isHK = refreshToken.toLowerCase().startsWith('hk-');
|
| 87 |
+
const isJP = refreshToken.toLowerCase().startsWith('jp-');
|
| 88 |
+
const isSG = refreshToken.toLowerCase().startsWith('sg-');
|
| 89 |
|
| 90 |
const tokenResult = await request("post", "/mweb/v1/get_upload_token", refreshToken, {
|
| 91 |
data: {
|
| 92 |
scene: 2,
|
| 93 |
},
|
| 94 |
+
params: isInternational ? {
|
| 95 |
+
aid: isUS ? DEFAULT_ASSISTANT_ID_US : (isJP ? DEFAULT_ASSISTANT_ID_JP : (isSG ? DEFAULT_ASSISTANT_ID_SG : DEFAULT_ASSISTANT_ID_HK)),
|
| 96 |
web_version: DREAMINA_WEB_VERSION,
|
| 97 |
da_version: DREAMINA_DA_VERSION,
|
| 98 |
aigc_features: DREAMINA_AIGC_FEATURES,
|
|
|
|
| 101 |
},
|
| 102 |
});
|
| 103 |
const { access_key_id, secret_access_key, session_token } = tokenResult;
|
| 104 |
+
const service_id = isInternational ? tokenResult.space_name : tokenResult.service_id;
|
| 105 |
|
| 106 |
if (!access_key_id || !secret_access_key || !session_token) {
|
| 107 |
throw new Error("获取上传令牌失败");
|
| 108 |
}
|
| 109 |
|
| 110 |
+
const actualServiceId = service_id || (isUS ? "wopfjsm1ax" : (isHK || isJP || isSG) ? "wopfjsm1ax" : "tb4s082cfz");
|
| 111 |
|
| 112 |
logger.info(`获取上传令牌成功: service_id=${actualServiceId}`);
|
| 113 |
|
|
|
|
| 119 |
const now = new Date();
|
| 120 |
const timestamp = now.toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
| 121 |
const randomStr = Math.random().toString(36).substring(2, 12);
|
| 122 |
+
|
| 123 |
+
const applyUrlHost = isUS ? BASE_URL_IMAGEX_US : (isHK || isJP || isSG) ? BASE_URL_IMAGEX_HK : 'https://imagex.bytedanceapi.com';
|
| 124 |
+
const applyUrl = `${applyUrlHost}/?Action=ApplyImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}&FileSize=${fileSize}&s=${randomStr}${isInternational ? '&device_platform=web' : ''}`;
|
| 125 |
|
| 126 |
const requestHeaders = {
|
| 127 |
'x-amz-date': timestamp,
|
|
|
|
| 130 |
|
| 131 |
const authorization = createSignature('GET', applyUrl, requestHeaders, access_key_id, secret_access_key, session_token);
|
| 132 |
|
| 133 |
+
const origin = isUS ? new URL(BASE_URL_DREAMINA_US).origin : (isHK || isJP || isSG) ? new URL(BASE_URL_DREAMINA_HK).origin : 'https://jimeng.jianying.com';
|
| 134 |
|
| 135 |
const applyResponse = await fetch(applyUrl, {
|
| 136 |
method: 'GET',
|
|
|
|
| 252 |
refreshToken: string
|
| 253 |
) {
|
| 254 |
const isUS = refreshToken.toLowerCase().startsWith('us-');
|
| 255 |
+
const isHK = refreshToken.toLowerCase().startsWith('hk-');
|
| 256 |
+
const isJP = refreshToken.toLowerCase().startsWith('jp-');
|
| 257 |
+
const isSG = refreshToken.toLowerCase().startsWith('sg-');
|
| 258 |
+
const isInternational = isUS || isHK || isJP || isSG;
|
| 259 |
+
const model = getModel(_model, isInternational);
|
| 260 |
|
| 261 |
let width, height, image_ratio, resolution_type;
|
| 262 |
|
|
|
|
| 292 |
let imageId: string;
|
| 293 |
if (typeof image === 'string') {
|
| 294 |
logger.info(`正在处理第 ${i + 1}/${imageCount} 张图片 (URL)...`);
|
| 295 |
+
imageId = await uploadImageFromUrl(image, refreshToken, isInternational);
|
| 296 |
} else {
|
| 297 |
logger.info(`正在处理第 ${i + 1}/${imageCount} 张图片 (Buffer)...`);
|
| 298 |
+
imageId = await uploadImageFromBuffer(image, refreshToken, isInternational);
|
| 299 |
}
|
| 300 |
uploadedImageIds.push(imageId);
|
| 301 |
logger.info(`图片 ${i + 1}/${imageCount} 上传成功: ${imageId}`);
|
|
|
|
| 412 |
],
|
| 413 |
}),
|
| 414 |
http_common_info: {
|
| 415 |
+
aid: isInternational ? (isUS ? DEFAULT_ASSISTANT_ID_US : (isJP ? DEFAULT_ASSISTANT_ID_JP : (isSG ? DEFAULT_ASSISTANT_ID_SG : DEFAULT_ASSISTANT_ID_HK))) : DEFAULT_ASSISTANT_ID_CN
|
| 416 |
}
|
| 417 |
},
|
| 418 |
}
|
|
|
|
| 530 |
refreshToken: string
|
| 531 |
) {
|
| 532 |
const isUS = refreshToken.toLowerCase().startsWith('us-');
|
| 533 |
+
const isHK = refreshToken.toLowerCase().startsWith('hk-');
|
| 534 |
+
const isJP = refreshToken.toLowerCase().startsWith('jp-');
|
| 535 |
+
const isSG = refreshToken.toLowerCase().startsWith('sg-');
|
| 536 |
+
const isInternational = isUS || isHK || isJP || isSG;
|
| 537 |
+
const model = getModel(_model, isInternational);
|
| 538 |
logger.info(`使用模型: ${_model} 映射模型: ${model} 分辨率: ${resolution} 比例: ${ratio} 精细度: ${sampleStrength}`);
|
| 539 |
|
| 540 |
return await generateImagesInternal(_model, prompt, { ratio, resolution, sampleStrength, negativePrompt }, refreshToken);
|
|
|
|
| 557 |
refreshToken: string
|
| 558 |
) {
|
| 559 |
const isUS = refreshToken.toLowerCase().startsWith('us-');
|
| 560 |
+
const isHK = refreshToken.toLowerCase().startsWith('hk-');
|
| 561 |
+
const isJP = refreshToken.toLowerCase().startsWith('jp-');
|
| 562 |
+
const isSG = refreshToken.toLowerCase().startsWith('sg-');
|
| 563 |
+
const isInternational = isUS || isHK || isJP || isSG;
|
| 564 |
+
const model = getModel(_model, isInternational);
|
| 565 |
|
| 566 |
let width, height, image_ratio, resolution_type;
|
| 567 |
|
|
|
|
| 672 |
],
|
| 673 |
}),
|
| 674 |
http_common_info: {
|
| 675 |
+
aid: isInternational ? (isUS ? DEFAULT_ASSISTANT_ID_US : (isJP ? DEFAULT_ASSISTANT_ID_JP : (isSG ? DEFAULT_ASSISTANT_ID_SG : DEFAULT_ASSISTANT_ID_HK))) : DEFAULT_ASSISTANT_ID_CN
|
| 676 |
}
|
| 677 |
},
|
| 678 |
}
|
|
|
|
| 792 |
refreshToken: string
|
| 793 |
) {
|
| 794 |
const isUS = refreshToken.toLowerCase().startsWith('us-');
|
| 795 |
+
const isHK = refreshToken.toLowerCase().startsWith('hk-');
|
| 796 |
+
const isJP = refreshToken.toLowerCase().startsWith('jp-');
|
| 797 |
+
const isSG = refreshToken.toLowerCase().startsWith('sg-');
|
| 798 |
+
const isInternational = isUS || isHK || isJP || isSG;
|
| 799 |
+
const model = getModel(_model, isInternational);
|
| 800 |
const { width, height, image_ratio, resolution_type } = getResolutionParams(resolution, ratio);
|
| 801 |
|
| 802 |
const targetImageCount = prompt.match(/(\d+)张/) ? parseInt(prompt.match(/(\d+)张/)[1]) : 4;
|
|
|
|
| 879 |
],
|
| 880 |
}),
|
| 881 |
http_common_info: {
|
| 882 |
+
aid: isInternational ? (isUS ? DEFAULT_ASSISTANT_ID_US : (isJP ? DEFAULT_ASSISTANT_ID_JP : (isSG ? DEFAULT_ASSISTANT_ID_SG : DEFAULT_ASSISTANT_ID_HK))) : DEFAULT_ASSISTANT_ID_CN
|
| 883 |
}
|
| 884 |
},
|
| 885 |
}
|
src/api/controllers/videos.ts
CHANGED
|
@@ -8,7 +8,8 @@ import util from "@/lib/util.ts";
|
|
| 8 |
import { getCredit, receiveCredit, request } from "./core.ts";
|
| 9 |
import logger from "@/lib/logger.ts";
|
| 10 |
import { SmartPoller, PollingStatus } from "@/lib/smart-poller.ts";
|
| 11 |
-
import { DEFAULT_ASSISTANT_ID_CN, DEFAULT_VIDEO_MODEL, DRAFT_VERSION, VIDEO_MODEL_MAP } from "@/api/consts/common.ts";
|
|
|
|
| 12 |
|
| 13 |
export const DEFAULT_MODEL = DEFAULT_VIDEO_MODEL;
|
| 14 |
|
|
@@ -24,16 +25,16 @@ function createSignature(
|
|
| 24 |
accessKeyId: string,
|
| 25 |
secretAccessKey: string,
|
| 26 |
sessionToken?: string,
|
| 27 |
-
payload: string = ''
|
|
|
|
| 28 |
) {
|
| 29 |
const urlObj = new URL(url);
|
| 30 |
const pathname = urlObj.pathname || '/';
|
| 31 |
const search = urlObj.search;
|
| 32 |
-
|
| 33 |
// 创建规范请求
|
| 34 |
const timestamp = headers['x-amz-date'];
|
| 35 |
const date = timestamp.substr(0, 8);
|
| 36 |
-
const region = 'cn-north-1';
|
| 37 |
const service = 'imagex';
|
| 38 |
|
| 39 |
// 规范化查询参数
|
|
@@ -128,40 +129,54 @@ function calculateCRC32(buffer: ArrayBuffer): string {
|
|
| 128 |
|
| 129 |
// 核心上传逻辑:上传二进制buffer到ImageX
|
| 130 |
async function _uploadImageBuffer(imageBuffer: ArrayBuffer, refreshToken: string): Promise<string> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
// 第一步:获取上传令牌
|
| 132 |
const tokenResult = await request("post", "/mweb/v1/get_upload_token", refreshToken, {
|
| 133 |
data: {
|
| 134 |
scene: 2, // AIGC 图片上传场景
|
| 135 |
},
|
| 136 |
});
|
| 137 |
-
|
| 138 |
const { access_key_id, secret_access_key, session_token, service_id } = tokenResult;
|
| 139 |
if (!access_key_id || !secret_access_key || !session_token) {
|
| 140 |
throw new Error("获取上传令牌失败");
|
| 141 |
}
|
| 142 |
-
|
| 143 |
-
const actualServiceId = service_id || "tb4s082cfz";
|
| 144 |
logger.info(`获取上传令牌成功: service_id=${actualServiceId}`);
|
| 145 |
|
| 146 |
const fileSize = imageBuffer.byteLength;
|
| 147 |
const crc32 = calculateCRC32(imageBuffer);
|
| 148 |
|
| 149 |
logger.info(`图片Buffer准备完成: 大小=${fileSize}字节, CRC32=${crc32}`);
|
| 150 |
-
|
| 151 |
// 第二步:申请图片上传权限
|
| 152 |
const now = new Date();
|
| 153 |
const timestamp = now.toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
| 154 |
-
|
| 155 |
const randomStr = Math.random().toString(36).substring(2, 12);
|
| 156 |
-
const
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
| 158 |
const requestHeaders = {
|
| 159 |
'x-amz-date': timestamp,
|
| 160 |
'x-amz-security-token': session_token
|
| 161 |
};
|
| 162 |
-
|
| 163 |
-
const authorization = createSignature('GET', applyUrl, requestHeaders, access_key_id, secret_access_key, session_token);
|
| 164 |
-
|
|
|
|
|
|
|
| 165 |
logger.info(`申请上传权限: ${applyUrl}`);
|
| 166 |
|
| 167 |
const applyResponse = await fetch(applyUrl, {
|
|
@@ -170,8 +185,8 @@ async function _uploadImageBuffer(imageBuffer: ArrayBuffer, refreshToken: string
|
|
| 170 |
'accept': '*/*',
|
| 171 |
'accept-language': 'zh-CN,zh;q=0.9',
|
| 172 |
'authorization': authorization,
|
| 173 |
-
'origin':
|
| 174 |
-
'referer':
|
| 175 |
'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
|
| 176 |
'sec-ch-ua-mobile': '?0',
|
| 177 |
'sec-ch-ua-platform': '"Windows"',
|
|
@@ -240,26 +255,26 @@ async function _uploadImageBuffer(imageBuffer: ArrayBuffer, refreshToken: string
|
|
| 240 |
}
|
| 241 |
|
| 242 |
logger.info(`图片文件上传成功`);
|
| 243 |
-
|
| 244 |
// 第四步:提交上传
|
| 245 |
-
const commitUrl = `
|
| 246 |
-
|
| 247 |
const commitTimestamp = new Date().toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
| 248 |
const commitPayload = JSON.stringify({
|
| 249 |
SessionKey: uploadAddress.SessionKey,
|
| 250 |
SuccessActionStatus: "200"
|
| 251 |
});
|
| 252 |
-
|
| 253 |
const payloadHash = crypto.createHash('sha256').update(commitPayload, 'utf8').digest('hex');
|
| 254 |
-
|
| 255 |
const commitRequestHeaders = {
|
| 256 |
'x-amz-date': commitTimestamp,
|
| 257 |
'x-amz-security-token': session_token,
|
| 258 |
'x-amz-content-sha256': payloadHash
|
| 259 |
};
|
| 260 |
-
|
| 261 |
-
const commitAuthorization = createSignature('POST', commitUrl, commitRequestHeaders, access_key_id, secret_access_key, session_token, commitPayload);
|
| 262 |
-
|
| 263 |
const commitResponse = await fetch(commitUrl, {
|
| 264 |
method: 'POST',
|
| 265 |
headers: {
|
|
@@ -267,8 +282,8 @@ async function _uploadImageBuffer(imageBuffer: ArrayBuffer, refreshToken: string
|
|
| 267 |
'accept-language': 'zh-CN,zh;q=0.9',
|
| 268 |
'authorization': commitAuthorization,
|
| 269 |
'content-type': 'application/json',
|
| 270 |
-
'origin':
|
| 271 |
-
'referer':
|
| 272 |
'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
|
| 273 |
'sec-ch-ua-mobile': '?0',
|
| 274 |
'sec-ch-ua-platform': '"Windows"',
|
|
@@ -372,6 +387,15 @@ export async function generateVideo(
|
|
| 372 |
},
|
| 373 |
refreshToken: string
|
| 374 |
) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
const model = getModel(_model);
|
| 376 |
|
| 377 |
// 将秒转换为毫秒,只支持5秒和10秒
|
|
@@ -513,12 +537,12 @@ export async function generateVideo(
|
|
| 513 |
{
|
| 514 |
params: {
|
| 515 |
aigc_features: "app_lip_sync",
|
| 516 |
-
web_version: "
|
| 517 |
da_version: DRAFT_VERSION,
|
| 518 |
},
|
| 519 |
data: {
|
| 520 |
"extend": {
|
| 521 |
-
"root_model": end_frame_image ?
|
| 522 |
"m_video_commerce_info": {
|
| 523 |
benefit_type: "basic_video_operation_vgfm_v_three",
|
| 524 |
resource_id: "generate_video",
|
|
@@ -590,7 +614,9 @@ export async function generateVideo(
|
|
| 590 |
}],
|
| 591 |
}),
|
| 592 |
http_common_info: {
|
| 593 |
-
aid:
|
|
|
|
|
|
|
| 594 |
},
|
| 595 |
},
|
| 596 |
}
|
|
@@ -600,158 +626,103 @@ export async function generateVideo(
|
|
| 600 |
if (!historyId)
|
| 601 |
throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录ID不存在");
|
| 602 |
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
const maxRetries = 60; // 增加重试次数,支持约20分钟的总重试时间
|
| 607 |
-
|
| 608 |
-
// 首次查询前等待更长时间,让服务器有时间处理请求
|
| 609 |
await new Promise((resolve) => setTimeout(resolve, 5000));
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 620 |
history_ids: [historyId],
|
| 621 |
-
}
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
}
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
// 标准API请求方式
|
| 648 |
-
logger.info(`发送请求获取视频生成结果,URL: ${requestUrl}, 历史ID: ${historyId}, 重试次数: ${retryCount + 1}/${maxRetries}`);
|
| 649 |
-
result = await request("post", requestUrl, refreshToken, {
|
| 650 |
-
data: requestData,
|
| 651 |
-
});
|
| 652 |
-
const responseStr = JSON.stringify(result);
|
| 653 |
-
logger.info(`标准API响应摘要: ${responseStr.substring(0, 300)}...`);
|
| 654 |
-
|
| 655 |
-
// 尝试直接从响应中提取视频URL
|
| 656 |
-
const videoUrlMatch = responseStr.match(/https:\/\/v[0-9]+-artist\.vlabvod\.com\/[^"\s]+/);
|
| 657 |
-
if (videoUrlMatch && videoUrlMatch[0]) {
|
| 658 |
-
logger.info(`从标准API响应中直接提取到视频URL: ${videoUrlMatch[0]}`);
|
| 659 |
-
// 提前返回找到的URL
|
| 660 |
-
return videoUrlMatch[0];
|
| 661 |
}
|
| 662 |
-
}
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
logger.info(`等待 ${waitTime}ms 后进行第 ${retryCount + 1} 次重试`);
|
| 685 |
-
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
| 686 |
-
continue;
|
| 687 |
-
}
|
| 688 |
-
|
| 689 |
-
// 记录获取到的结果详情
|
| 690 |
-
logger.info(`获取到历史记录结果: ${JSON.stringify(historyData)}`);
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
// 从历史数据中提取状态和结果
|
| 694 |
-
status = historyData.status;
|
| 695 |
-
failCode = historyData.fail_code;
|
| 696 |
-
item_list = historyData.item_list || [];
|
| 697 |
-
|
| 698 |
-
logger.info(`视频生成状态: ${status}, 失败代码: ${failCode || '无'}, 项目列表长度: ${item_list.length}`);
|
| 699 |
-
|
| 700 |
-
// 如果有视频URL,提前记录
|
| 701 |
-
let tempVideoUrl = item_list?.[0]?.video?.transcoded_video?.origin?.video_url;
|
| 702 |
-
if (!tempVideoUrl) {
|
| 703 |
-
// 尝试从其他可能的路径获取
|
| 704 |
-
tempVideoUrl = item_list?.[0]?.video?.play_url ||
|
| 705 |
-
item_list?.[0]?.video?.download_url ||
|
| 706 |
-
item_list?.[0]?.video?.url;
|
| 707 |
-
}
|
| 708 |
-
|
| 709 |
if (tempVideoUrl) {
|
| 710 |
logger.info(`检测到视频URL: ${tempVideoUrl}`);
|
| 711 |
}
|
| 712 |
-
|
| 713 |
-
if (status === 30) {
|
| 714 |
-
let error;
|
| 715 |
-
if (failCode === 2038 || failCode === '2038') {
|
| 716 |
-
error = new APIException(EX.API_CONTENT_FILTERED, "内容被过滤");
|
| 717 |
-
} else if (failCode === 1001 || failCode === '1001') {
|
| 718 |
-
error = new APIException(EX.API_IMAGE_GENERATION_FAILED, `生成失败,错误码: ${failCode}。可能原因:文本或图片中包含敏感内容`);
|
| 719 |
-
} else {
|
| 720 |
-
error = new APIException(EX.API_IMAGE_GENERATION_FAILED, `生成失败,错误码: ${failCode}`);
|
| 721 |
-
}
|
| 722 |
-
// 添加历史ID到错误对象,以便在chat.ts中显示
|
| 723 |
-
error.historyId = historyId;
|
| 724 |
-
throw error;
|
| 725 |
-
}
|
| 726 |
-
|
| 727 |
-
// 如果状态仍在处理中,等待后继续
|
| 728 |
-
if (status === 20) {
|
| 729 |
-
const waitTime = 2000 * (Math.min(retryCount + 1, 5)); // 随着重试次数增加等待时间,但最多10秒
|
| 730 |
-
logger.info(`视频生成中,状态码: ${status},等待 ${waitTime}ms 后继续查询`);
|
| 731 |
-
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
| 732 |
-
}
|
| 733 |
-
} catch (error) {
|
| 734 |
-
logger.error(`轮询视频生成结果出错: ${error.message}`);
|
| 735 |
-
retryCount++;
|
| 736 |
-
await new Promise((resolve) => setTimeout(resolve, 2000 * (retryCount + 1)));
|
| 737 |
}
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 748 |
|
| 749 |
// 提取视频URL
|
| 750 |
let videoUrl = item_list?.[0]?.video?.transcoded_video?.origin?.video_url;
|
| 751 |
-
|
| 752 |
// 如果通过常规路径无法获取视频URL,尝试其他可能的路径
|
| 753 |
if (!videoUrl) {
|
| 754 |
-
// 尝试从item_list中的其他可能位置获取
|
| 755 |
if (item_list?.[0]?.video?.play_url) {
|
| 756 |
videoUrl = item_list[0].video.play_url;
|
| 757 |
logger.info(`从play_url获取到视频URL: ${videoUrl}`);
|
|
@@ -762,15 +733,13 @@ export async function generateVideo(
|
|
| 762 |
videoUrl = item_list[0].video.url;
|
| 763 |
logger.info(`从url获取到视频URL: ${videoUrl}`);
|
| 764 |
} else {
|
| 765 |
-
// 如果仍然找不到,记录错误并抛出异常
|
| 766 |
logger.error(`未能获取视频URL,item_list: ${JSON.stringify(item_list)}`);
|
| 767 |
-
const error = new APIException(EX.API_IMAGE_GENERATION_FAILED, "未能获取视频URL,请稍后
|
| 768 |
-
// 添加历史ID到错误对象,以便在chat.ts中显示
|
| 769 |
error.historyId = historyId;
|
| 770 |
throw error;
|
| 771 |
}
|
| 772 |
}
|
| 773 |
|
| 774 |
-
logger.info(`视频生成成功,URL: ${videoUrl}`);
|
| 775 |
return videoUrl;
|
| 776 |
}
|
|
|
|
| 8 |
import { getCredit, receiveCredit, request } from "./core.ts";
|
| 9 |
import logger from "@/lib/logger.ts";
|
| 10 |
import { SmartPoller, PollingStatus } from "@/lib/smart-poller.ts";
|
| 11 |
+
import { DEFAULT_ASSISTANT_ID_CN, DEFAULT_ASSISTANT_ID_US, DEFAULT_ASSISTANT_ID_HK, DEFAULT_ASSISTANT_ID_JP, DEFAULT_ASSISTANT_ID_SG, DEFAULT_VIDEO_MODEL, DRAFT_VERSION, VIDEO_MODEL_MAP } from "@/api/consts/common.ts";
|
| 12 |
+
import { BASE_URL_DREAMINA_US, BASE_URL_DREAMINA_HK, BASE_URL_IMAGEX_US, BASE_URL_IMAGEX_HK } from "@/api/consts/dreamina.ts";
|
| 13 |
|
| 14 |
export const DEFAULT_MODEL = DEFAULT_VIDEO_MODEL;
|
| 15 |
|
|
|
|
| 25 |
accessKeyId: string,
|
| 26 |
secretAccessKey: string,
|
| 27 |
sessionToken?: string,
|
| 28 |
+
payload: string = '',
|
| 29 |
+
region: string = 'cn-north-1'
|
| 30 |
) {
|
| 31 |
const urlObj = new URL(url);
|
| 32 |
const pathname = urlObj.pathname || '/';
|
| 33 |
const search = urlObj.search;
|
| 34 |
+
|
| 35 |
// 创建规范请求
|
| 36 |
const timestamp = headers['x-amz-date'];
|
| 37 |
const date = timestamp.substr(0, 8);
|
|
|
|
| 38 |
const service = 'imagex';
|
| 39 |
|
| 40 |
// 规范化查询参数
|
|
|
|
| 129 |
|
| 130 |
// 核心上传逻辑:上传二进制buffer到ImageX
|
| 131 |
async function _uploadImageBuffer(imageBuffer: ArrayBuffer, refreshToken: string): Promise<string> {
|
| 132 |
+
// 检测区域
|
| 133 |
+
const isUS = refreshToken.toLowerCase().startsWith('us-');
|
| 134 |
+
const isHK = refreshToken.toLowerCase().startsWith('hk-');
|
| 135 |
+
const isJP = refreshToken.toLowerCase().startsWith('jp-');
|
| 136 |
+
const isSG = refreshToken.toLowerCase().startsWith('sg-');
|
| 137 |
+
const isInternational = isUS || isHK || isJP || isSG;
|
| 138 |
+
|
| 139 |
+
logger.info(`开始上传视频图片... (isInternational: ${isInternational}, isUS: ${isUS}, isHK: ${isHK}, isJP: ${isJP}, isSG: ${isSG})`);
|
| 140 |
+
|
| 141 |
// 第一步:获取上传令牌
|
| 142 |
const tokenResult = await request("post", "/mweb/v1/get_upload_token", refreshToken, {
|
| 143 |
data: {
|
| 144 |
scene: 2, // AIGC 图片上传场景
|
| 145 |
},
|
| 146 |
});
|
| 147 |
+
|
| 148 |
const { access_key_id, secret_access_key, session_token, service_id } = tokenResult;
|
| 149 |
if (!access_key_id || !secret_access_key || !session_token) {
|
| 150 |
throw new Error("获取上传令牌失败");
|
| 151 |
}
|
| 152 |
+
|
| 153 |
+
const actualServiceId = service_id || (isUS ? "wopfjsm1ax" : (isHK || isJP || isSG) ? "wopfjsm1ax" : "tb4s082cfz");
|
| 154 |
logger.info(`获取上传令牌成功: service_id=${actualServiceId}`);
|
| 155 |
|
| 156 |
const fileSize = imageBuffer.byteLength;
|
| 157 |
const crc32 = calculateCRC32(imageBuffer);
|
| 158 |
|
| 159 |
logger.info(`图片Buffer准备完成: 大小=${fileSize}字节, CRC32=${crc32}`);
|
| 160 |
+
|
| 161 |
// 第二步:申请图片上传权限
|
| 162 |
const now = new Date();
|
| 163 |
const timestamp = now.toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
| 164 |
+
|
| 165 |
const randomStr = Math.random().toString(36).substring(2, 12);
|
| 166 |
+
const applyUrlHost = isUS ? BASE_URL_IMAGEX_US : (isHK || isJP || isSG) ? BASE_URL_IMAGEX_HK : 'https://imagex.bytedanceapi.com';
|
| 167 |
+
const applyUrl = `${applyUrlHost}/?Action=ApplyImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}&FileSize=${fileSize}&s=${randomStr}${isInternational ? '&device_platform=web' : ''}`;
|
| 168 |
+
|
| 169 |
+
const region = isUS ? 'us-east-1' : (isHK || isJP || isSG) ? 'ap-southeast-1' : 'cn-north-1';
|
| 170 |
+
|
| 171 |
const requestHeaders = {
|
| 172 |
'x-amz-date': timestamp,
|
| 173 |
'x-amz-security-token': session_token
|
| 174 |
};
|
| 175 |
+
|
| 176 |
+
const authorization = createSignature('GET', applyUrl, requestHeaders, access_key_id, secret_access_key, session_token, '', region);
|
| 177 |
+
|
| 178 |
+
const origin = isUS ? new URL(BASE_URL_DREAMINA_US).origin : (isHK || isJP || isSG) ? new URL(BASE_URL_DREAMINA_HK).origin : 'https://jimeng.jianying.com';
|
| 179 |
+
|
| 180 |
logger.info(`申请上传权限: ${applyUrl}`);
|
| 181 |
|
| 182 |
const applyResponse = await fetch(applyUrl, {
|
|
|
|
| 185 |
'accept': '*/*',
|
| 186 |
'accept-language': 'zh-CN,zh;q=0.9',
|
| 187 |
'authorization': authorization,
|
| 188 |
+
'origin': origin,
|
| 189 |
+
'referer': `${origin}/ai-tool/video/generate`,
|
| 190 |
'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
|
| 191 |
'sec-ch-ua-mobile': '?0',
|
| 192 |
'sec-ch-ua-platform': '"Windows"',
|
|
|
|
| 255 |
}
|
| 256 |
|
| 257 |
logger.info(`图片文件上传成功`);
|
| 258 |
+
|
| 259 |
// 第四步:提交上传
|
| 260 |
+
const commitUrl = `${applyUrlHost}/?Action=CommitImageUpload&Version=2018-08-01&ServiceId=${actualServiceId}`;
|
| 261 |
+
|
| 262 |
const commitTimestamp = new Date().toISOString().replace(/[:\-]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
| 263 |
const commitPayload = JSON.stringify({
|
| 264 |
SessionKey: uploadAddress.SessionKey,
|
| 265 |
SuccessActionStatus: "200"
|
| 266 |
});
|
| 267 |
+
|
| 268 |
const payloadHash = crypto.createHash('sha256').update(commitPayload, 'utf8').digest('hex');
|
| 269 |
+
|
| 270 |
const commitRequestHeaders = {
|
| 271 |
'x-amz-date': commitTimestamp,
|
| 272 |
'x-amz-security-token': session_token,
|
| 273 |
'x-amz-content-sha256': payloadHash
|
| 274 |
};
|
| 275 |
+
|
| 276 |
+
const commitAuthorization = createSignature('POST', commitUrl, commitRequestHeaders, access_key_id, secret_access_key, session_token, commitPayload, region);
|
| 277 |
+
|
| 278 |
const commitResponse = await fetch(commitUrl, {
|
| 279 |
method: 'POST',
|
| 280 |
headers: {
|
|
|
|
| 282 |
'accept-language': 'zh-CN,zh;q=0.9',
|
| 283 |
'authorization': commitAuthorization,
|
| 284 |
'content-type': 'application/json',
|
| 285 |
+
'origin': origin,
|
| 286 |
+
'referer': `${origin}/ai-tool/video/generate`,
|
| 287 |
'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
|
| 288 |
'sec-ch-ua-mobile': '?0',
|
| 289 |
'sec-ch-ua-platform': '"Windows"',
|
|
|
|
| 387 |
},
|
| 388 |
refreshToken: string
|
| 389 |
) {
|
| 390 |
+
// 检测区域
|
| 391 |
+
const isUS = refreshToken.toLowerCase().startsWith('us-');
|
| 392 |
+
const isHK = refreshToken.toLowerCase().startsWith('hk-');
|
| 393 |
+
const isJP = refreshToken.toLowerCase().startsWith('jp-');
|
| 394 |
+
const isSG = refreshToken.toLowerCase().startsWith('sg-');
|
| 395 |
+
const isInternational = isUS || isHK || isJP || isSG;
|
| 396 |
+
|
| 397 |
+
logger.info(`视频生成区域检测: isUS=${isUS}, isHK=${isHK}, isJP=${isJP}, isSG=${isSG}, isInternational=${isInternational}`);
|
| 398 |
+
|
| 399 |
const model = getModel(_model);
|
| 400 |
|
| 401 |
// 将秒转换为毫秒,只支持5秒和10秒
|
|
|
|
| 537 |
{
|
| 538 |
params: {
|
| 539 |
aigc_features: "app_lip_sync",
|
| 540 |
+
web_version: "7.5.0",
|
| 541 |
da_version: DRAFT_VERSION,
|
| 542 |
},
|
| 543 |
data: {
|
| 544 |
"extend": {
|
| 545 |
+
"root_model": end_frame_image ? VIDEO_MODEL_MAP['jimeng-video-3.0'] : model,
|
| 546 |
"m_video_commerce_info": {
|
| 547 |
benefit_type: "basic_video_operation_vgfm_v_three",
|
| 548 |
resource_id: "generate_video",
|
|
|
|
| 614 |
}],
|
| 615 |
}),
|
| 616 |
http_common_info: {
|
| 617 |
+
aid: isInternational
|
| 618 |
+
? (isUS ? DEFAULT_ASSISTANT_ID_US : (isJP ? DEFAULT_ASSISTANT_ID_JP : (isSG ? DEFAULT_ASSISTANT_ID_SG : DEFAULT_ASSISTANT_ID_HK)))
|
| 619 |
+
: DEFAULT_ASSISTANT_ID_CN
|
| 620 |
},
|
| 621 |
},
|
| 622 |
}
|
|
|
|
| 626 |
if (!historyId)
|
| 627 |
throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录ID不存在");
|
| 628 |
|
| 629 |
+
logger.info(`视频生成任务已提交,history_id: ${historyId},等待生成完成...`);
|
| 630 |
+
|
| 631 |
+
// 首次查询前等待,让服务器有时间处理请求
|
|
|
|
|
|
|
|
|
|
| 632 |
await new Promise((resolve) => setTimeout(resolve, 5000));
|
| 633 |
+
|
| 634 |
+
// 使用 SmartPoller 进行智能轮询
|
| 635 |
+
const maxPollCount = 900; // 增加轮询次数,支持更长的生成时间
|
| 636 |
+
let pollAttempts = 0;
|
| 637 |
+
|
| 638 |
+
const poller = new SmartPoller({
|
| 639 |
+
maxPollCount,
|
| 640 |
+
pollInterval: 2000, // 2秒基础间隔
|
| 641 |
+
expectedItemCount: 1,
|
| 642 |
+
type: 'video',
|
| 643 |
+
timeoutSeconds: 1200 // 20分钟超时
|
| 644 |
+
});
|
| 645 |
+
|
| 646 |
+
const { result: pollingResult, data: finalHistoryData } = await poller.poll(async () => {
|
| 647 |
+
pollAttempts++;
|
| 648 |
+
|
| 649 |
+
// 使用标准API请求方式
|
| 650 |
+
const result = await request("post", "/mweb/v1/get_history_by_ids", refreshToken, {
|
| 651 |
+
data: {
|
| 652 |
history_ids: [historyId],
|
| 653 |
+
},
|
| 654 |
+
});
|
| 655 |
+
|
| 656 |
+
// 尝试直接从响应中提取视频URL
|
| 657 |
+
const responseStr = JSON.stringify(result);
|
| 658 |
+
const videoUrlMatch = responseStr.match(/https:\/\/v[0-9]+-artist\.vlabvod\.com\/[^"\s]+/);
|
| 659 |
+
if (videoUrlMatch && videoUrlMatch[0]) {
|
| 660 |
+
logger.info(`从API响应中直接提取到视频URL: ${videoUrlMatch[0]}`);
|
| 661 |
+
// 构造成功状态并返回
|
| 662 |
+
return {
|
| 663 |
+
status: {
|
| 664 |
+
status: 10,
|
| 665 |
+
itemCount: 1,
|
| 666 |
+
historyId
|
| 667 |
+
} as PollingStatus,
|
| 668 |
+
data: {
|
| 669 |
+
status: 10,
|
| 670 |
+
item_list: [{
|
| 671 |
+
video: {
|
| 672 |
+
transcoded_video: {
|
| 673 |
+
origin: {
|
| 674 |
+
video_url: videoUrlMatch[0]
|
| 675 |
+
}
|
| 676 |
+
}
|
| 677 |
+
}
|
| 678 |
+
}]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 679 |
}
|
| 680 |
+
};
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
// 检查响应中是否有该 history_id 的数据
|
| 684 |
+
if (!result[historyId]) {
|
| 685 |
+
logger.warn(`API未返回历史记录,historyId: ${historyId}`);
|
| 686 |
+
throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录不存在");
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
const historyData = result[historyId];
|
| 690 |
+
|
| 691 |
+
const currentStatus = historyData.status;
|
| 692 |
+
const currentFailCode = historyData.fail_code;
|
| 693 |
+
const currentItemList = historyData.item_list || [];
|
| 694 |
+
const finishTime = historyData.task?.finish_time || 0;
|
| 695 |
+
|
| 696 |
+
// 记录详细信息
|
| 697 |
+
if (currentItemList.length > 0) {
|
| 698 |
+
const tempVideoUrl = currentItemList[0]?.video?.transcoded_video?.origin?.video_url ||
|
| 699 |
+
currentItemList[0]?.video?.play_url ||
|
| 700 |
+
currentItemList[0]?.video?.download_url ||
|
| 701 |
+
currentItemList[0]?.video?.url;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
if (tempVideoUrl) {
|
| 703 |
logger.info(`检测到视频URL: ${tempVideoUrl}`);
|
| 704 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 705 |
}
|
| 706 |
+
|
| 707 |
+
return {
|
| 708 |
+
status: {
|
| 709 |
+
status: currentStatus,
|
| 710 |
+
failCode: currentFailCode,
|
| 711 |
+
itemCount: currentItemList.length,
|
| 712 |
+
finishTime,
|
| 713 |
+
historyId
|
| 714 |
+
} as PollingStatus,
|
| 715 |
+
data: historyData
|
| 716 |
+
};
|
| 717 |
+
}, historyId);
|
| 718 |
+
|
| 719 |
+
const item_list = finalHistoryData.item_list || [];
|
| 720 |
|
| 721 |
// 提取视频URL
|
| 722 |
let videoUrl = item_list?.[0]?.video?.transcoded_video?.origin?.video_url;
|
| 723 |
+
|
| 724 |
// 如果通过常规路径无法获取视频URL,尝试其他可能的路径
|
| 725 |
if (!videoUrl) {
|
|
|
|
| 726 |
if (item_list?.[0]?.video?.play_url) {
|
| 727 |
videoUrl = item_list[0].video.play_url;
|
| 728 |
logger.info(`从play_url获取到视频URL: ${videoUrl}`);
|
|
|
|
| 733 |
videoUrl = item_list[0].video.url;
|
| 734 |
logger.info(`从url获取到视频URL: ${videoUrl}`);
|
| 735 |
} else {
|
|
|
|
| 736 |
logger.error(`未能获取视频URL,item_list: ${JSON.stringify(item_list)}`);
|
| 737 |
+
const error = new APIException(EX.API_IMAGE_GENERATION_FAILED, "未能获取视频URL,请稍后查看");
|
|
|
|
| 738 |
error.historyId = historyId;
|
| 739 |
throw error;
|
| 740 |
}
|
| 741 |
}
|
| 742 |
|
| 743 |
+
logger.info(`视频生成成功,URL: ${videoUrl},总耗时: ${pollingResult.elapsedTime}秒`);
|
| 744 |
return videoUrl;
|
| 745 |
}
|