ImageGenerationController.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. <?php
  2. namespace App\Http\Controllers\AIGeneration;
  3. use Illuminate\Routing\Controller as BaseController;
  4. use App\Libs\Utils;
  5. use App\Services\AIGeneration\AIImageGenerationService;
  6. use App\Transformer\AIGeneration\AIGenerationTransformer;
  7. use Illuminate\Http\Request;
  8. use Illuminate\Http\JsonResponse;
  9. use Illuminate\Support\Facades\Validator;
  10. use App\Libs\ApiResponse;
  11. use App\Consts\BaseConst;
  12. class ImageGenerationController extends BaseController
  13. {
  14. use ApiResponse;
  15. private $aiImageGenerationService;
  16. public function __construct(
  17. AIImageGenerationService $aiImageGenerationService
  18. ) {
  19. $this->aiImageGenerationService = $aiImageGenerationService;
  20. }
  21. /**
  22. * 创建图片生成任务
  23. *
  24. * @param Request $request
  25. * @return JsonResponse
  26. */
  27. public function createTask(Request $request): JsonResponse
  28. {
  29. $data = $request->all();
  30. // 首先验证基本参数
  31. $baseRules = [
  32. 'prompt' => 'required|string|max:2000',
  33. 'width' => 'required|numeric|between:1024,4096',
  34. 'height' => 'required|numeric|between:1024,4096',
  35. 'image_num' => 'required|numeric|between:1,4',
  36. 'scale' => 'required|numeric|between:0,100',
  37. 'model' => 'nullable|string|in:jimeng_4.0,' . implode(',', BaseConst::VOLC_PIC_MODELS),
  38. ];
  39. $baseMessages = [
  40. 'prompt.required' => '提示词不能为空',
  41. 'prompt.max' => '提示词不能超过2000个字符',
  42. 'width.required' => '宽不能为空',
  43. 'width.numeric' => '宽必须是数字',
  44. 'width.between' => '宽必须在1024到4096之间',
  45. 'height.required' => '高不能为空',
  46. 'height.numeric' => '高必须是数字',
  47. 'height.between' => '高必须在1024到4096之间',
  48. 'image_num.required' => '生成图片数量不能为空',
  49. 'image_num.numeric' => '生成图片数量必须是数字',
  50. 'image_num.between' => '生成图片数量必须在1到4之间',
  51. 'scale.required' => '缩放比例不能为空',
  52. 'scale.numeric' => '缩放比例必须是数字',
  53. 'scale.between' => '缩放比例必须在0到100之间',
  54. 'model.in' => '模型类型无效',
  55. ];
  56. // 根据传入参数决定验证规则
  57. if (isset($data['ref_img_url'])) {
  58. // URL 方式 - 支持单个或多个URL(逗号分隔或数组)
  59. $validationRules = array_merge($baseRules, [
  60. 'ref_img_url' => 'required',
  61. ]);
  62. $validationMessages = array_merge($baseMessages, [
  63. 'ref_img_url.required' => '参考图片URL不能为空',
  64. ]);
  65. } else {
  66. // 上传文件方式 - 支持多个文件
  67. $validationRules = array_merge($baseRules, [
  68. 'ref_img_file' => 'required',
  69. 'ref_img_file.*' => 'image|mimes:jpeg,png|max:4800',
  70. ]);
  71. $validationMessages = array_merge($baseMessages, [
  72. 'ref_img_file.required' => '参考图片不能为空',
  73. 'ref_img_file.*.image' => '参考图片必须是图片文件',
  74. 'ref_img_file.*.mimes' => '参考图片必须是jpeg,png格式',
  75. 'ref_img_file.*.max' => '参考图片大小不能超过4.7MB',
  76. ]);
  77. }
  78. $validator = Validator::make($data, $validationRules, $validationMessages);
  79. if ($validator->fails()) {
  80. Utils::throwError('1002:'.$validator->errors()->first());
  81. }
  82. $refImgUrls = [];
  83. $refImgBase64List = [];
  84. // 处理上传的图片
  85. if ($request->hasFile('ref_img_file')) {
  86. $refImgFiles = is_array($request->file('ref_img_file'))
  87. ? $request->file('ref_img_file')
  88. : [$request->file('ref_img_file')];
  89. foreach ($refImgFiles as $index => $refImgFile) {
  90. // 验证图片分辨率和比例
  91. $refImgInfo = @getimagesize($refImgFile->getPathname());
  92. if (!$refImgInfo) {
  93. Utils::throwError('1003:图片' . ($index + 1) . '格式无效');
  94. }
  95. $refImg = [
  96. 'width' => $refImgInfo[0],
  97. 'height' => $refImgInfo[1],
  98. 'size' => $refImgFile->getSize(),
  99. ];
  100. // 验证图片规格
  101. $this->validateImageSpecs($refImg, $index + 1);
  102. // 上传图片到服务器并获取URL
  103. $filename = randStr(10) . '.' . $refImgFile->getClientOriginalExtension();
  104. $uploaded_url = uploadStreamByTos('image', file_get_contents($refImgFile->getPathname()), $filename);
  105. if (!$uploaded_url) {
  106. Utils::throwError('1003:图片' . ($index + 1) . '保存失败');
  107. }
  108. $refImgUrls[] = $uploaded_url;
  109. }
  110. } else {
  111. // 使用URL方式,支持逗号分隔的字符串或数组
  112. $urls = is_array($data['ref_img_url'])
  113. ? $data['ref_img_url']
  114. : array_map('trim', explode(',', $data['ref_img_url']));
  115. foreach ($urls as $index => $url) {
  116. if (empty($url)) {
  117. continue;
  118. }
  119. // 验证URL格式
  120. if (!filter_var($url, FILTER_VALIDATE_URL)) {
  121. Utils::throwError('1003:图片URL' . ($index + 1) . '格式无效');
  122. }
  123. // 验证远程图片
  124. $refImg = Utils::getRemoteImageInfo($url, true);
  125. if (!$refImg) {
  126. Utils::throwError('1003:图片' . ($index + 1) . '获取失败');
  127. }
  128. // 验证图片规格
  129. $this->validateImageSpecs($refImg, $index + 1);
  130. $refImgUrls[] = $url;
  131. }
  132. }
  133. if (empty($refImgUrls)) {
  134. Utils::throwError('1003:至少需要一张参考图片');
  135. }
  136. // 将处理后的URL存入data(支持多个)
  137. $data['ref_img_urls'] = $refImgUrls;
  138. $data['ref_img_url'] = $refImgUrls[0]; // 保持向后兼容
  139. try {
  140. $task = $this->aiImageGenerationService->createImageGenerationTask($data);
  141. return $this->success(['task_id' => $task->id, 'status' => $task->status]);
  142. } catch (\Exception $e) {
  143. Utils::throwError('1001:'.$e->getMessage());
  144. }
  145. }
  146. /**
  147. * 图片超清(超分辨率)放大
  148. *
  149. * 支持传入图片URL或上传图片文件,放大到 1080p / 2k / 4k 或指定宽高。
  150. * 默认使用 ffmpeg(本地环境或未安装时自动回退 gd)。
  151. *
  152. * @param Request $request
  153. * @return JsonResponse
  154. */
  155. public function upscaleImage(Request $request): JsonResponse
  156. {
  157. $data = $request->all();
  158. // 参数验证
  159. $rules = [
  160. 'image_url' => 'nullable|string',
  161. 'image_file' => 'nullable|image|mimes:jpeg,png,webp|max:13312',
  162. 'target_size' => 'nullable|string|in:1080p,2k,4k',
  163. 'target_width' => 'nullable|numeric|between:1,8192',
  164. 'target_height' => 'nullable|numeric|between:1,8192',
  165. 'format' => 'nullable|string|in:jpg,jpeg,png,webp',
  166. 'quality' => 'nullable|numeric|between:1,100',
  167. 'sharpen' => 'nullable|boolean',
  168. 'sharpen_amount' => 'nullable|numeric|between:0,2',
  169. 'driver' => 'nullable|string|in:gd,ffmpeg',
  170. ];
  171. $messages = [
  172. 'image_url.string' => '图片URL格式不正确',
  173. 'image_file.image' => '上传文件必须是图片',
  174. 'image_file.mimes' => '图片格式必须是jpeg、png或webp',
  175. 'image_file.max' => '图片大小不能超过13MB',
  176. 'target_size.in' => '目标档位必须是1080p、2k或4k',
  177. 'target_width.numeric' => '宽度必须是数字',
  178. 'target_width.between' => '宽度必须在1到8192之间',
  179. 'target_height.numeric' => '高度必须是数字',
  180. 'target_height.between' => '高度必须在1到8192之间',
  181. 'format.in' => '输出格式必须是jpg、png或webp',
  182. 'quality.numeric' => '输出质量必须是数字',
  183. 'quality.between' => '输出质量必须在1到100之间',
  184. 'sharpen.boolean' => 'sharpen必须是布尔值',
  185. 'sharpen_amount.numeric' => '锐化强度必须是数字',
  186. 'sharpen_amount.between' => '锐化强度必须在0到2之间',
  187. 'driver.in' => '处理驱动必须是gd或ffmpeg',
  188. ];
  189. $validator = Validator::make($data, $rules, $messages);
  190. if ($validator->fails()) {
  191. Utils::throwError('1002:'.$validator->errors()->first());
  192. }
  193. // 获取图片URL:优先使用上传文件,其次使用传入URL
  194. $imageUrl = trim((string)($data['image_url'] ?? ''));
  195. if ($request->hasFile('image_file')) {
  196. $imageFile = $request->file('image_file');
  197. $imageInfo = @getimagesize($imageFile->getPathname());
  198. if (!$imageInfo) {
  199. Utils::throwError('1003:图片格式无效');
  200. }
  201. // 复用已有图片规格校验
  202. $this->validateImageSpecs([
  203. 'width' => $imageInfo[0],
  204. 'height' => $imageInfo[1],
  205. 'size' => $imageFile->getSize(),
  206. ], 1);
  207. $filename = randStr(10) . '.' . $imageFile->getClientOriginalExtension();
  208. $imageUrl = uploadStreamByTos('image', file_get_contents($imageFile->getPathname()), $filename);
  209. if (!$imageUrl) {
  210. Utils::throwError('1003:图片保存失败');
  211. }
  212. }
  213. if ($imageUrl === '') {
  214. Utils::throwError('1002:image_url或image_file不能为空');
  215. }
  216. if (!filter_var($imageUrl, FILTER_VALIDATE_URL)) {
  217. Utils::throwError('1002:图片URL格式无效');
  218. }
  219. // 仅允许 http/https,防止通过API读取服务器本地文件
  220. $scheme = strtolower((string)parse_url($imageUrl, PHP_URL_SCHEME));
  221. if (!in_array($scheme, ['http', 'https'], true)) {
  222. Utils::throwError('1002:图片URL格式无效');
  223. }
  224. // 组装超分参数(null 自动走默认值)
  225. $upscaleParams = [
  226. 'image_url' => $imageUrl,
  227. 'target' => $data['target_size'] ?? null,
  228. 'width' => isset($data['target_width']) ? (int)$data['target_width'] : null,
  229. 'height' => isset($data['target_height']) ? (int)$data['target_height'] : null,
  230. 'format' => $data['format'] ?? null,
  231. 'quality' => isset($data['quality']) ? (int)$data['quality'] : null,
  232. 'sharpen' => isset($data['sharpen']) ? filter_var($data['sharpen'], FILTER_VALIDATE_BOOLEAN) : null,
  233. 'sharpen_amount' => isset($data['sharpen_amount']) ? (float)$data['sharpen_amount'] : null,
  234. 'driver' => $data['driver'] ?? null,
  235. ];
  236. try {
  237. $result = upscaleImage($upscaleParams);
  238. return $this->success([
  239. 'url' => $result['url'],
  240. 'width' => $result['width'],
  241. 'height' => $result['height'],
  242. 'src_width' => $result['src_width'],
  243. 'src_height' => $result['src_height'],
  244. 'upscaled' => $result['upscaled'],
  245. 'driver' => $result['driver'],
  246. ]);
  247. } catch (\Exception $e) {
  248. Utils::throwError('1001:'.$e->getMessage());
  249. }
  250. }
  251. /**
  252. * 验证图片规格
  253. *
  254. * @param array $refImg
  255. * @param int $index
  256. * @return void
  257. */
  258. private function validateImageSpecs(array $refImg, int $index): void
  259. {
  260. // 判断图片是否满足以下条件:
  261. // 1.图片文件最大4.7MB
  262. // 2.图片分辨率最大: 4096*4096, 最短边不低于320
  263. // 3.图片长边与短边比例在3以内
  264. if ($refImg['size'] > 60 * 1024 * 1024) {
  265. Utils::throwError('1003:图片' . $index . '大小不能超过50MB');
  266. }
  267. $minDimension = min($refImg['width'], $refImg['height']);
  268. $maxDimension = max($refImg['width'], $refImg['height']);
  269. if ($maxDimension > 4096 || $minDimension < 320) {
  270. Utils::throwError('1003:图片' . $index . '分辨率不能超过4096*4096,且最短边不能低于320');
  271. }
  272. if ($maxDimension / $minDimension > 3) {
  273. Utils::throwError('1003:图片' . $index . '长边与短边比例不能超过3');
  274. }
  275. }
  276. /**
  277. * 查询任务状态
  278. *
  279. * @param string $taskId
  280. * @return JsonResponse
  281. */
  282. public function taskStatus(string $taskId): JsonResponse
  283. {
  284. $task = \App\Models\MpGeneratePicTask::where('id', $taskId)->first();
  285. if (!$task) {
  286. return response()->json([
  287. 'success' => false,
  288. 'message' => '任务不存在'
  289. ], 404);
  290. }
  291. return response()->json([
  292. 'success' => true,
  293. 'data' => [
  294. 'task_id' => $task->id,
  295. 'status' => $task->status,
  296. 'result_url' => $task->result_url,
  297. 'error_message' => $task->error_message,
  298. 'created_at' => $task->created_at,
  299. 'started_at' => $task->started_at,
  300. 'completed_at' => $task->completed_at
  301. ]
  302. ]);
  303. }
  304. /**
  305. * 获取任务列表
  306. *
  307. * @param Request $request
  308. * @return mixed
  309. */
  310. public function taskList(Request $request)
  311. {
  312. $data = $request->all();
  313. $result = $this->aiImageGenerationService->getTaskList($data);
  314. return $this->success($result, [new AIGenerationTransformer(), 'newBuildTaskList']);
  315. }
  316. /**
  317. * 批量上传图片或视频
  318. *
  319. * @param Request $request
  320. * @return void
  321. */
  322. public function batchUploadImg(Request $request) {
  323. $refImgUrls = [];
  324. $refVideoUrls = [];
  325. $refAudioUrls = [];
  326. // 处理上传的图片文件
  327. if ($request->hasFile('ref_img_file')) {
  328. $refImgFiles = is_array($request->file('ref_img_file'))
  329. ? $request->file('ref_img_file')
  330. : [$request->file('ref_img_file')];
  331. foreach ($refImgFiles as $index => $refImgFile) {
  332. // 验证图片分辨率和比例
  333. $refImgInfo = @getimagesize($refImgFile->getPathname());
  334. if (!$refImgInfo) {
  335. Utils::throwError('1003:图片' . ($index + 1) . '格式无效');
  336. }
  337. $refImg = [
  338. 'width' => $refImgInfo[0],
  339. 'height' => $refImgInfo[1],
  340. 'size' => $refImgFile->getSize(),
  341. ];
  342. // 验证图片规格
  343. $this->validateImageSpecs($refImg, $index + 1);
  344. // 上传图片到服务器并获取URL
  345. $filename = randStr(10) . '.' . $refImgFile->getClientOriginalExtension();
  346. $uploaded_url = uploadStreamByTos('image', file_get_contents($refImgFile->getPathname()), $filename);
  347. if (!$uploaded_url) {
  348. Utils::throwError('1003:图片' . ($index + 1) . '保存失败');
  349. }
  350. $refImgUrls[] = $uploaded_url;
  351. }
  352. }
  353. // 处理图片二进制内容(ref_img数组)
  354. if ($request->has('ref_img')) {
  355. $refImgs = $request->input('ref_img');
  356. // 确保是数组格式
  357. if (!is_array($refImgs)) {
  358. $refImgs = [$refImgs];
  359. }
  360. foreach ($refImgs as $index => $refImgBinary) {
  361. $tempFile = null;
  362. $tempPath = null;
  363. try {
  364. // 解码base64(如果是base64格式)
  365. if (preg_match('/^data:image\/(\w+);base64,/', $refImgBinary, $matches)) {
  366. $refImgBinary = base64_decode(substr($refImgBinary, strpos($refImgBinary, ',') + 1));
  367. }
  368. // 创建临时文件以验证图片
  369. $tempFile = tmpfile();
  370. $tempPath = stream_get_meta_data($tempFile)['uri'];
  371. fwrite($tempFile, $refImgBinary);
  372. // 验证图片分辨率和比例
  373. $refImgInfo = @getimagesize($tempPath);
  374. if (!$refImgInfo) {
  375. Utils::throwError('1003:图片' . ($index + 1) . '格式无效');
  376. }
  377. $refImg = [
  378. 'width' => $refImgInfo[0],
  379. 'height' => $refImgInfo[1],
  380. 'size' => strlen($refImgBinary),
  381. ];
  382. // 验证图片规格
  383. $this->validateImageSpecs($refImg, $index + 1);
  384. // 上传图片到服务器并获取URL(默认使用.jpg格式)
  385. $filename = randStr(10) . '.jpg';
  386. $uploaded_url = uploadStreamByTos('image', $refImgBinary, $filename);
  387. if (!$uploaded_url) {
  388. Utils::throwError('1003:图片' . ($index + 1) . '保存失败');
  389. }
  390. $refImgUrls[] = $uploaded_url;
  391. } finally {
  392. // 确保临时文件被关闭和删除
  393. if ($tempFile) {
  394. fclose($tempFile);
  395. }
  396. }
  397. }
  398. }
  399. // 处理上传的视频文件
  400. if ($request->hasFile('ref_video_file')) {
  401. $refVideoFiles = is_array($request->file('ref_video_file'))
  402. ? $request->file('ref_video_file')
  403. : [$request->file('ref_video_file')];
  404. foreach ($refVideoFiles as $index => $refVideoFile) {
  405. // 验证视频文件大小(最大100MB)
  406. if ($refVideoFile->getSize() > 100 * 1024 * 1024) {
  407. Utils::throwError('1003:视频' . ($index + 1) . '大小不能超过100MB');
  408. }
  409. // 验证视频文件类型
  410. $allowedMimeTypes = ['video/mp4', 'video/mpeg', 'video/quicktime', 'video/x-msvideo'];
  411. if (!in_array($refVideoFile->getMimeType(), $allowedMimeTypes)) {
  412. Utils::throwError('1003:视频' . ($index + 1) . '格式无效,仅支持mp4、mpeg、mov、avi格式');
  413. }
  414. // 上传视频到服务器并获取URL
  415. $filename = randStr(10) . '.' . $refVideoFile->getClientOriginalExtension();
  416. $uploaded_url = uploadStreamByTos('video', file_get_contents($refVideoFile->getPathname()), $filename);
  417. if (!$uploaded_url) {
  418. Utils::throwError('1003:视频' . ($index + 1) . '保存失败');
  419. }
  420. $refVideoUrls[] = $uploaded_url;
  421. }
  422. }
  423. // 处理视频二进制内容(ref_video数组)
  424. if ($request->has('ref_video')) {
  425. $refVideos = $request->input('ref_video');
  426. // 确保是数组格式
  427. if (!is_array($refVideos)) {
  428. $refVideos = [$refVideos];
  429. }
  430. foreach ($refVideos as $index => $refVideoBinary) {
  431. try {
  432. // 解码base64(如果是base64格式)
  433. if (preg_match('/^data:video\/(\w+);base64,/', $refVideoBinary, $matches)) {
  434. $extension = $matches[1];
  435. $refVideoBinary = base64_decode(substr($refVideoBinary, strpos($refVideoBinary, ',') + 1));
  436. } else {
  437. $extension = 'mp4'; // 默认扩展名
  438. }
  439. // 验证视频大小(最大100MB)
  440. if (strlen($refVideoBinary) > 100 * 1024 * 1024) {
  441. Utils::throwError('1003:视频' . ($index + 1) . '大小不能超过100MB');
  442. }
  443. // 上传视频到服务器并获取URL
  444. $filename = randStr(10) . '.' . $extension;
  445. $uploaded_url = uploadStreamByTos('video', $refVideoBinary, $filename);
  446. if (!$uploaded_url) {
  447. Utils::throwError('1003:视频' . ($index + 1) . '保存失败');
  448. }
  449. $refVideoUrls[] = $uploaded_url;
  450. } catch (\Exception $e) {
  451. Utils::throwError('1003:视频' . ($index + 1) . '处理失败:' . $e->getMessage());
  452. }
  453. }
  454. }
  455. // 处理上传的音频文件(音色等)
  456. if ($request->hasFile('ref_audio_file')) {
  457. $refAudioFiles = is_array($request->file('ref_audio_file'))
  458. ? $request->file('ref_audio_file')
  459. : [$request->file('ref_audio_file')];
  460. foreach ($refAudioFiles as $index => $refAudioFile) {
  461. // 验证音频文件大小(最大50MB)
  462. if ($refAudioFile->getSize() > 50 * 1024 * 1024) {
  463. Utils::throwError('1003:音频' . ($index + 1) . '大小不能超过50MB');
  464. }
  465. // 验证音频文件格式
  466. $extension = strtolower($refAudioFile->getClientOriginalExtension());
  467. if (!in_array($extension, $this->allowedAudioExtensions())) {
  468. Utils::throwError('1003:音频' . ($index + 1) . '格式无效,仅支持mp3、wav、m4a、aac、ogg、flac、amr、wma格式');
  469. }
  470. // 上传音频到服务器并获取URL
  471. $filename = randStr(10) . '.' . $extension;
  472. $uploaded_url = uploadStreamByTos('audio', file_get_contents($refAudioFile->getPathname()), $filename);
  473. if (!$uploaded_url) {
  474. Utils::throwError('1003:音频' . ($index + 1) . '保存失败');
  475. }
  476. $refAudioUrls[] = $uploaded_url;
  477. }
  478. }
  479. // 处理音频二进制内容(ref_audio数组)
  480. if ($request->has('ref_audio')) {
  481. $refAudios = $request->input('ref_audio');
  482. // 确保是数组格式
  483. if (!is_array($refAudios)) {
  484. $refAudios = [$refAudios];
  485. }
  486. foreach ($refAudios as $index => $refAudioBinary) {
  487. // 解码base64(如果是base64格式)
  488. if (is_string($refAudioBinary) && preg_match('/^data:audio\/([\w.+-]+);base64,/', $refAudioBinary, $matches)) {
  489. $extension = $this->normalizeAudioExtension($matches[1]);
  490. $refAudioBinary = base64_decode(substr($refAudioBinary, strpos($refAudioBinary, ',') + 1));
  491. } else {
  492. $extension = 'mp3'; // 默认扩展名
  493. }
  494. if (!in_array($extension, $this->allowedAudioExtensions())) {
  495. Utils::throwError('1003:音频' . ($index + 1) . '格式无效,仅支持mp3、wav、m4a、aac、ogg、flac、amr、wma格式');
  496. }
  497. // 验证音频大小(最大50MB)
  498. if (strlen($refAudioBinary) > 50 * 1024 * 1024) {
  499. Utils::throwError('1003:音频' . ($index + 1) . '大小不能超过50MB');
  500. }
  501. // 上传音频到服务器并获取URL
  502. $filename = randStr(10) . '.' . $extension;
  503. $uploaded_url = uploadStreamByTos('audio', $refAudioBinary, $filename);
  504. if (!$uploaded_url) {
  505. Utils::throwError('1003:音频' . ($index + 1) . '保存失败');
  506. }
  507. $refAudioUrls[] = $uploaded_url;
  508. }
  509. }
  510. return $this->success([
  511. 'image_urls' => $refImgUrls,
  512. 'video_urls' => $refVideoUrls,
  513. 'audio_urls' => $refAudioUrls
  514. ]);
  515. }
  516. /**
  517. * 允许上传的音频扩展名
  518. *
  519. * @return array
  520. */
  521. private function allowedAudioExtensions() {
  522. return ['mp3', 'wav', 'm4a', 'aac', 'ogg', 'oga', 'flac', 'amr', 'wma', 'weba'];
  523. }
  524. /**
  525. * 归一化音频扩展名
  526. *
  527. * @param string $extension
  528. * @return string
  529. */
  530. private function normalizeAudioExtension($extension) {
  531. $extension = strtolower(trim($extension));
  532. $map = [
  533. 'mpeg' => 'mp3',
  534. 'mpga' => 'mp3',
  535. 'x-wav' => 'wav',
  536. 'wave' => 'wav',
  537. 'x-m4a' => 'm4a',
  538. 'mp4' => 'm4a',
  539. 'x-aac' => 'aac',
  540. 'x-flac' => 'flac',
  541. 'x-ms-wma' => 'wma',
  542. 'x-ogg' => 'ogg',
  543. 'opus' => 'ogg',
  544. 'x-weba' => 'weba',
  545. ];
  546. return $map[$extension] ?? $extension;
  547. }
  548. }