|
@@ -262,15 +262,14 @@ class WorkOrder extends Api{
|
|
|
/**
|
|
/**
|
|
|
* 即梦AI--创建视频任务接口
|
|
* 即梦AI--创建视频任务接口
|
|
|
* 支持:单张首帧图 / 首尾双帧图
|
|
* 支持:单张首帧图 / 首尾双帧图
|
|
|
- * 图片入参:form-data 文件(first_image/last_image)、base64、或 http(s) URL
|
|
|
|
|
|
|
+ * 图片入参:JSON/base64(first_image/last_image)、form-data 文件、或 http(s) URL
|
|
|
*/
|
|
*/
|
|
|
public function Create_ImgToVideo()
|
|
public function Create_ImgToVideo()
|
|
|
{
|
|
{
|
|
|
$apiUrl = 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks';
|
|
$apiUrl = 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks';
|
|
|
$apiKey = 'ark-1ca8aa97-3663-4bc7-8c53-d4ab516883f1-d2339';
|
|
$apiKey = 'ark-1ca8aa97-3663-4bc7-8c53-d4ab516883f1-d2339';
|
|
|
|
|
|
|
|
- $params = $this->request->param();
|
|
|
|
|
- // halt($params);
|
|
|
|
|
|
|
+ $params = $this->mergeRequestParams();
|
|
|
$prompt = trim((string)($params['prompt'] ?? ''));
|
|
$prompt = trim((string)($params['prompt'] ?? ''));
|
|
|
if ($prompt === '') {
|
|
if ($prompt === '') {
|
|
|
return json(['code' => 0, 'msg' => 'prompt 不能为空']);
|
|
return json(['code' => 0, 'msg' => 'prompt 不能为空']);
|
|
@@ -280,10 +279,18 @@ class WorkOrder extends Api{
|
|
|
$firstFrame = $this->resolveFrameImagePayload($params, 'first', $firstError);
|
|
$firstFrame = $this->resolveFrameImagePayload($params, 'first', $firstError);
|
|
|
if (($firstFrame['api_url'] ?? '') === '') {
|
|
if (($firstFrame['api_url'] ?? '') === '') {
|
|
|
$hint = Common::isOssEnabled()
|
|
$hint = Common::isOssEnabled()
|
|
|
- ? '请用 form-data 上传 first_image(类型选文件),并查看 runtime/log'
|
|
|
|
|
|
|
+ ? '请传 first_image(JSON 内 data:image/...;base64,... 或 form-data 文件),并查看 runtime/log'
|
|
|
: '请在 application/config.php 配置 oss(accessKeyId、endpoint、bucket、host)';
|
|
: '请在 application/config.php 配置 oss(accessKeyId、endpoint、bucket、host)';
|
|
|
$detail = $firstError !== '' ? '(' . $firstError . ')' : '';
|
|
$detail = $firstError !== '' ? '(' . $firstError . ')' : '';
|
|
|
- return json(['code' => 0, 'msg' => '首帧图片无效或上传 OSS 失败。' . $hint . $detail]);
|
|
|
|
|
|
|
+ return json([
|
|
|
|
|
+ 'code' => 0,
|
|
|
|
|
+ 'msg' => '首帧图片无效或上传失败。' . $hint . $detail,
|
|
|
|
|
+ 'data' => [
|
|
|
|
|
+ 'has_first_image' => isset($params['first_image']) && $params['first_image'] !== '',
|
|
|
|
|
+ 'content_type' => $this->request->contentType(),
|
|
|
|
|
+ 'upload_env' => $this->getPhpUploadEnvInfo(),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
$lastFrame = $this->resolveFrameImagePayload($params, 'last');
|
|
$lastFrame = $this->resolveFrameImagePayload($params, 'last');
|
|
@@ -295,8 +302,8 @@ class WorkOrder extends Api{
|
|
|
'model' => 'doubao-seedance-1-5-pro-251215',
|
|
'model' => 'doubao-seedance-1-5-pro-251215',
|
|
|
'content' => $content,
|
|
'content' => $content,
|
|
|
'generate_audio' => filter_var($params['generate_audio'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
|
'generate_audio' => filter_var($params['generate_audio'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
|
|
- 'ratio' => $params['ratio'] ?? $params['aspect_ratio'] ?? 'adaptive',
|
|
|
|
|
- 'duration' => (int)($params['duration'] ?? 5),
|
|
|
|
|
|
|
+ 'ratio' => $params['ratio'] ?? $params['aspect_ratio'] ?? $params['size'] ?? 'adaptive',
|
|
|
|
|
+ 'duration' => (int)($params['duration'] ?? $params['seconds'] ?? 5),
|
|
|
'watermark' => filter_var($params['watermark'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
|
'watermark' => filter_var($params['watermark'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
|
|
];
|
|
];
|
|
|
|
|
|
|
@@ -1410,11 +1417,6 @@ class WorkOrder extends Api{
|
|
|
if ($raw === '' || strlen($raw) < 50) {
|
|
if ($raw === '' || strlen($raw) < 50) {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
- preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $raw, $bm);
|
|
|
|
|
- if (empty($bm)) {
|
|
|
|
|
- $error = ($prefix === 'first' ? '首帧图' : '尾帧图') . '未找到图片数据';
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
$result = $this->uploadBase64ImageToOss($raw, $prefix, $ossTaskId, $error);
|
|
$result = $this->uploadBase64ImageToOss($raw, $prefix, $ossTaskId, $error);
|
|
|
if (($result['url'] ?? '') !== '') {
|
|
if (($result['url'] ?? '') !== '') {
|
|
|
return [
|
|
return [
|
|
@@ -1609,6 +1611,15 @@ class WorkOrder extends Api{
|
|
|
$objectKey = $this->buildTaskMediaObjectKey($taskId, $fileName);
|
|
$objectKey = $this->buildTaskMediaObjectKey($taskId, $fileName);
|
|
|
$upload = $this->uploadToOSS($localFullPath, $objectKey);
|
|
$upload = $this->uploadToOSS($localFullPath, $objectKey);
|
|
|
if (!$upload['success']) {
|
|
if (!$upload['success']) {
|
|
|
|
|
+ $fallbackUrl = $this->buildPublicUploadUrl($localFullPath);
|
|
|
|
|
+ if ($fallbackUrl !== '') {
|
|
|
|
|
+ Log::write('[uploadBase64ImageToOss] OSS失败,回退本站URL: ' . $fallbackUrl, 'warning');
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'url' => $fallbackUrl,
|
|
|
|
|
+ 'local_path' => $localFullPath,
|
|
|
|
|
+ 'object_key' => $objectKey,
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
Log::write('[uploadBase64ImageToOss] OSS上传失败: ' . $objectKey, 'error');
|
|
Log::write('[uploadBase64ImageToOss] OSS上传失败: ' . $objectKey, 'error');
|
|
|
return $empty;
|
|
return $empty;
|
|
|
}
|
|
}
|
|
@@ -1622,7 +1633,7 @@ class WorkOrder extends Api{
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 解析 base64 图片(与 ImageToImageJob 一致:data:image/(png|jpg|jpeg);base64,...)
|
|
|
|
|
|
|
+ * 解析 base64 图片(避免对大字符串整段正则,防止服务器 PCRE 超限)
|
|
|
* @return array{0:string,1:string}|null [扩展名, 二进制内容]
|
|
* @return array{0:string,1:string}|null [扩展名, 二进制内容]
|
|
|
*/
|
|
*/
|
|
|
private function parseBase64Image(string $base64Input): ?array
|
|
private function parseBase64Image(string $base64Input): ?array
|
|
@@ -1632,15 +1643,30 @@ class WorkOrder extends Api{
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $base64Input, $m);
|
|
|
|
|
- if (empty($m)) {
|
|
|
|
|
|
|
+ $ext = 'jpg';
|
|
|
|
|
+ $rawBase64 = $base64Input;
|
|
|
|
|
+ $prefix = 'data:image/';
|
|
|
|
|
+ if (stripos($base64Input, $prefix) === 0) {
|
|
|
|
|
+ $semi = stripos($base64Input, ';base64,');
|
|
|
|
|
+ if ($semi === false) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ $mimePart = strtolower(substr($base64Input, strlen($prefix), $semi - strlen($prefix)));
|
|
|
|
|
+ if ($mimePart === 'jpeg') {
|
|
|
|
|
+ $mimePart = 'jpg';
|
|
|
|
|
+ }
|
|
|
|
|
+ if (in_array($mimePart, ['jpg', 'png', 'gif', 'webp', 'bmp'], true)) {
|
|
|
|
|
+ $ext = $mimePart;
|
|
|
|
|
+ }
|
|
|
|
|
+ $rawBase64 = substr($base64Input, $semi + 8);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $rawBase64 = preg_replace('/\s+/', '', $rawBase64);
|
|
|
|
|
+ if ($rawBase64 === '') {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- $rawBase64 = preg_replace('/\s+/', '', $m[2]);
|
|
|
|
|
- $ext = $m[1];
|
|
|
|
|
$imageData = base64_decode($rawBase64, true);
|
|
$imageData = base64_decode($rawBase64, true);
|
|
|
-
|
|
|
|
|
if ($imageData === false || strlen($imageData) < 100) {
|
|
if ($imageData === false || strlen($imageData) < 100) {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
@@ -1648,6 +1674,29 @@ class WorkOrder extends Api{
|
|
|
return [$ext, $imageData];
|
|
return [$ext, $imageData];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 合并请求参数(兼容浏览器 application/json 提交 first_image/last_image base64)
|
|
|
|
|
+ */
|
|
|
|
|
+ private function mergeRequestParams(): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $params = $this->request->param();
|
|
|
|
|
+ if (!empty($params['first_image']) || !empty($params['last_image']) || !empty($params['prompt'])) {
|
|
|
|
|
+ return $params;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $content = $this->request->getInput();
|
|
|
|
|
+ if (!is_string($content) || $content === '') {
|
|
|
|
|
+ return $params;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $json = json_decode($content, true);
|
|
|
|
|
+ if (!is_array($json)) {
|
|
|
|
|
+ return $params;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return array_merge($params, $json);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 将本地文件上传到阿里云 OSS
|
|
* 将本地文件上传到阿里云 OSS
|
|
|
*
|
|
*
|
|
@@ -1675,7 +1724,7 @@ class WorkOrder extends Api{
|
|
|
$ossObjectKey = implode('/', $segments);
|
|
$ossObjectKey = implode('/', $segments);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- $success = $this->uploadLocalFileToAliyunOss($localFullPath, $ossObjectKey);
|
|
|
|
|
|
|
+ $success = Common::uploadLocalFileToOss($localFullPath, $ossObjectKey);
|
|
|
return [
|
|
return [
|
|
|
'success' => $success,
|
|
'success' => $success,
|
|
|
'object_key' => $ossObjectKey,
|
|
'object_key' => $ossObjectKey,
|
|
@@ -1684,95 +1733,39 @@ class WorkOrder extends Api{
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 上传本地文件到阿里云 OSS(仅 WorkOrder 内实现,不修改 Common)
|
|
|
|
|
|
|
+ * 将 public 下本地文件转为可公网访问的站点 URL(OSS 不可用时的回退)
|
|
|
*/
|
|
*/
|
|
|
- private function uploadLocalFileToAliyunOss(string $localFullPath, string $objectKey): bool
|
|
|
|
|
|
|
+ private function buildPublicUploadUrl(string $localFullPath): string
|
|
|
{
|
|
{
|
|
|
- if (!Common::isOssEnabled() || !is_file($localFullPath)) {
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
- $objectKey = Common::normalizeOssObjectKey($objectKey);
|
|
|
|
|
- if ($objectKey === '') {
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (class_exists(\OSS\OssClient::class, true)) {
|
|
|
|
|
- try {
|
|
|
|
|
- $config = Common::getOssConfig();
|
|
|
|
|
- $client = new \OSS\OssClient(
|
|
|
|
|
- $config['accessKeyId'],
|
|
|
|
|
- $config['accessKeySecret'],
|
|
|
|
|
- $config['endpoint']
|
|
|
|
|
- );
|
|
|
|
|
- $client->uploadFile($config['bucket'], $objectKey, $localFullPath);
|
|
|
|
|
- return true;
|
|
|
|
|
- } catch (\Throwable $e) {
|
|
|
|
|
- Log::write('[uploadLocalFileToAliyunOss SDK] ' . $e->getMessage() . ' | ' . $objectKey, 'error');
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ $publicRoot = str_replace('\\', '/', ROOT_PATH . 'public/');
|
|
|
|
|
+ $path = str_replace('\\', '/', $localFullPath);
|
|
|
|
|
+ if (strpos($path, $publicRoot) !== 0) {
|
|
|
|
|
+ return '';
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- return $this->putLocalFileToAliyunOssByCurl($localFullPath, $objectKey);
|
|
|
|
|
|
|
+ $relative = ltrim(substr($path, strlen($publicRoot)), '/');
|
|
|
|
|
+ return $relative === '' ? '' : rtrim($this->request->domain(), '/') . '/' . $relative;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 无 OSS SDK 时通过 REST PUT 上传
|
|
|
|
|
|
|
+ * @return array{summary:string,content_length:int,post_max_size:string,upload_max_filesize:string,has_first_image_file:bool}
|
|
|
*/
|
|
*/
|
|
|
- private function putLocalFileToAliyunOssByCurl(string $localFullPath, string $objectKey): bool
|
|
|
|
|
|
|
+ private function getPhpUploadEnvInfo(): array
|
|
|
{
|
|
{
|
|
|
- $config = Common::getOssConfig();
|
|
|
|
|
- $content = file_get_contents($localFullPath);
|
|
|
|
|
- if ($content === false) {
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $mime = function_exists('mime_content_type') ? (mime_content_type($localFullPath) ?: '') : '';
|
|
|
|
|
- if ($mime === '') {
|
|
|
|
|
- $ext = strtolower(pathinfo($localFullPath, PATHINFO_EXTENSION));
|
|
|
|
|
- $mimeMap = [
|
|
|
|
|
- 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png',
|
|
|
|
|
- 'gif' => 'image/gif', 'webp' => 'image/webp', 'mp4' => 'video/mp4',
|
|
|
|
|
- ];
|
|
|
|
|
- $mime = $mimeMap[$ext] ?? 'application/octet-stream';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $date = gmdate('D, d M Y H:i:s \G\M\T');
|
|
|
|
|
- $bucket = $config['bucket'];
|
|
|
|
|
- $endpoint = ltrim((string)$config['endpoint'], 'https://');
|
|
|
|
|
- $endpoint = ltrim($endpoint, 'http://');
|
|
|
|
|
- $canonicalizedResource = '/' . $bucket . '/' . $objectKey;
|
|
|
|
|
- $stringToSign = "PUT\n\n{$mime}\n{$date}\n{$canonicalizedResource}";
|
|
|
|
|
- $signature = base64_encode(hash_hmac('sha1', $stringToSign, $config['accessKeySecret'], true));
|
|
|
|
|
- $urlPath = implode('/', array_map('rawurlencode', explode('/', $objectKey)));
|
|
|
|
|
- $url = 'https://' . $bucket . '.' . $endpoint . '/' . $urlPath;
|
|
|
|
|
-
|
|
|
|
|
- $ch = curl_init($url);
|
|
|
|
|
- curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
|
|
|
|
|
- curl_setopt($ch, CURLOPT_POSTFIELDS, $content);
|
|
|
|
|
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
|
|
|
- curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
|
|
|
- curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
|
|
|
- 'Date: ' . $date,
|
|
|
|
|
- 'Content-Type: ' . $mime,
|
|
|
|
|
- 'Authorization: OSS ' . $config['accessKeyId'] . ':' . $signature,
|
|
|
|
|
- ]);
|
|
|
|
|
- curl_setopt($ch, CURLOPT_TIMEOUT, 300);
|
|
|
|
|
- $response = curl_exec($ch);
|
|
|
|
|
- $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
|
- $curlError = curl_error($ch);
|
|
|
|
|
- curl_close($ch);
|
|
|
|
|
-
|
|
|
|
|
- if ($httpCode >= 200 && $httpCode < 300) {
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- Log::write(
|
|
|
|
|
- '[putLocalFileToAliyunOssByCurl] http=' . $httpCode
|
|
|
|
|
- . ' err=' . $curlError
|
|
|
|
|
- . ' resp=' . substr((string)$response, 0, 500)
|
|
|
|
|
- . ' | objectKey=' . $objectKey,
|
|
|
|
|
- 'error'
|
|
|
|
|
- );
|
|
|
|
|
- return false;
|
|
|
|
|
|
|
+ $contentLength = (int)($_SERVER['CONTENT_LENGTH'] ?? 0);
|
|
|
|
|
+ $postMax = (string)ini_get('post_max_size');
|
|
|
|
|
+ $uploadMax = (string)ini_get('upload_max_filesize');
|
|
|
|
|
+ $hasFile = !empty($this->request->file('first_image'));
|
|
|
|
|
+ $summary = 'CONTENT_LENGTH=' . $contentLength
|
|
|
|
|
+ . ';post_max_size=' . $postMax
|
|
|
|
|
+ . ';upload_max_filesize=' . $uploadMax
|
|
|
|
|
+ . ';has_first_image_file=' . ($hasFile ? 'yes' : 'no');
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'summary' => $summary,
|
|
|
|
|
+ 'content_length' => $contentLength,
|
|
|
|
|
+ 'post_max_size' => $postMax,
|
|
|
|
|
+ 'upload_max_filesize' => $uploadMax,
|
|
|
|
|
+ 'has_first_image_file' => $hasFile,
|
|
|
|
|
+ ];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|