Переглянути джерело

资产库新增音色url字段管理

lh 3 днів тому
батько
коміт
d4de44bc94

+ 104 - 1
app/Http/Controllers/AIGeneration/ImageGenerationController.php

@@ -373,6 +373,7 @@ class ImageGenerationController extends BaseController
     public function batchUploadImg(Request $request) {
         $refImgUrls = [];
         $refVideoUrls = [];
+        $refAudioUrls = [];
         
         // 处理上传的图片文件
         if ($request->hasFile('ref_img_file')) {
@@ -528,9 +529,111 @@ class ImageGenerationController extends BaseController
             }
         }
 
+        // 处理上传的音频文件(音色等)
+        if ($request->hasFile('ref_audio_file')) {
+            $refAudioFiles = is_array($request->file('ref_audio_file'))
+                ? $request->file('ref_audio_file')
+                : [$request->file('ref_audio_file')];
+
+            foreach ($refAudioFiles as $index => $refAudioFile) {
+                // 验证音频文件大小(最大50MB)
+                if ($refAudioFile->getSize() > 50 * 1024 * 1024) {
+                    Utils::throwError('1003:音频' . ($index + 1) . '大小不能超过50MB');
+                }
+
+                // 验证音频文件格式
+                $extension = strtolower($refAudioFile->getClientOriginalExtension());
+                if (!in_array($extension, $this->allowedAudioExtensions())) {
+                    Utils::throwError('1003:音频' . ($index + 1) . '格式无效,仅支持mp3、wav、m4a、aac、ogg、flac、amr、wma格式');
+                }
+
+                // 上传音频到服务器并获取URL
+                $filename = randStr(10) . '.' . $extension;
+                $uploaded_url = uploadStreamByTos('audio', file_get_contents($refAudioFile->getPathname()), $filename);
+                if (!$uploaded_url) {
+                    Utils::throwError('1003:音频' . ($index + 1) . '保存失败');
+                }
+                $refAudioUrls[] = $uploaded_url;
+            }
+        }
+
+        // 处理音频二进制内容(ref_audio数组)
+        if ($request->has('ref_audio')) {
+            $refAudios = $request->input('ref_audio');
+
+            // 确保是数组格式
+            if (!is_array($refAudios)) {
+                $refAudios = [$refAudios];
+            }
+
+            foreach ($refAudios as $index => $refAudioBinary) {
+                // 解码base64(如果是base64格式)
+                if (is_string($refAudioBinary) && preg_match('/^data:audio\/([\w.+-]+);base64,/', $refAudioBinary, $matches)) {
+                    $extension = $this->normalizeAudioExtension($matches[1]);
+                    $refAudioBinary = base64_decode(substr($refAudioBinary, strpos($refAudioBinary, ',') + 1));
+                } else {
+                    $extension = 'mp3'; // 默认扩展名
+                }
+
+                if (!in_array($extension, $this->allowedAudioExtensions())) {
+                    Utils::throwError('1003:音频' . ($index + 1) . '格式无效,仅支持mp3、wav、m4a、aac、ogg、flac、amr、wma格式');
+                }
+
+                // 验证音频大小(最大50MB)
+                if (strlen($refAudioBinary) > 50 * 1024 * 1024) {
+                    Utils::throwError('1003:音频' . ($index + 1) . '大小不能超过50MB');
+                }
+
+                // 上传音频到服务器并获取URL
+                $filename = randStr(10) . '.' . $extension;
+                $uploaded_url = uploadStreamByTos('audio', $refAudioBinary, $filename);
+                if (!$uploaded_url) {
+                    Utils::throwError('1003:音频' . ($index + 1) . '保存失败');
+                }
+                $refAudioUrls[] = $uploaded_url;
+            }
+        }
+
         return $this->success([
             'image_urls' => $refImgUrls,
-            'video_urls' => $refVideoUrls
+            'video_urls' => $refVideoUrls,
+            'audio_urls' => $refAudioUrls
         ]);
     }
+
+    /**
+     * 允许上传的音频扩展名
+     *
+     * @return array
+     */
+    private function allowedAudioExtensions() {
+        return ['mp3', 'wav', 'm4a', 'aac', 'ogg', 'oga', 'flac', 'amr', 'wma', 'weba'];
+    }
+
+    /**
+     * 归一化音频扩展名
+     *
+     * @param string $extension
+     * @return string
+     */
+    private function normalizeAudioExtension($extension) {
+        $extension = strtolower(trim($extension));
+
+        $map = [
+            'mpeg'   => 'mp3',
+            'mpga'   => 'mp3',
+            'x-wav'  => 'wav',
+            'wave'   => 'wav',
+            'x-m4a'  => 'm4a',
+            'mp4'    => 'm4a',
+            'x-aac'  => 'aac',
+            'x-flac' => 'flac',
+            'x-ms-wma' => 'wma',
+            'x-ogg'  => 'ogg',
+            'opus'   => 'ogg',
+            'x-weba' => 'weba',
+        ];
+
+        return $map[$extension] ?? $extension;
+    }
 }

+ 69 - 2
app/Services/Anime/AnimeService.php

@@ -6842,7 +6842,7 @@ class AnimeService
                 $query->where('product', $product);
             }
             
-            $result = $query->select('id', 'type', 'parent_id', 'level', 'product_name', 'url', 'pic_prompt', 'three_view_image_url', 'versions', 'product', 'user_id', 'created_at')
+            $result = $query->select('id', 'type', 'parent_id', 'level', 'product_name', 'url', 'pic_prompt', 'three_view_image_url', 'timbre_url', 'versions', 'product', 'user_id', 'created_at')
                 ->orderByRaw('type DESC, CASE WHEN user_id = '.$uid.' THEN 0 ELSE 1 END ASC, sort_order DESC, created_at DESC')
                 ->get()
                 ->map(function($value) {
@@ -6853,6 +6853,7 @@ class AnimeService
                     $value['level'] = (int)$value['level'];
                     $value['product'] = (int)($value['product'] ?? 1);
                     $value['is_public'] = (int)$value['user_id'] === 0 ? 1 : 0;
+                    $value['timbre_url'] = $value['timbre_url'] ?? '';
                     $value['versions'] = json_decode($value['versions'], true) ?: [];
                     unset($value['user_id']);
                     return $value;
@@ -6896,7 +6897,7 @@ class AnimeService
         // 1. 文件夹在前(type DESC)
         // 2. 个人库在前,公共库在后(CASE WHEN user_id = current_uid)
         // 3. 其他排序规则
-        $items = $query->select('id', 'type', 'parent_id', 'level', 'product_name', 'url', 'pic_prompt', 'three_view_image_url', 'versions', 'product', 'user_id', 'created_at')
+        $items = $query->select('id', 'type', 'parent_id', 'level', 'product_name', 'url', 'pic_prompt', 'three_view_image_url', 'timbre_url', 'versions', 'product', 'user_id', 'created_at')
             ->orderByRaw('type DESC, CASE WHEN user_id = '.$current_uid.' THEN 0 ELSE 1 END ASC, sort_order DESC, created_at DESC')
             ->get();
         
@@ -6912,6 +6913,7 @@ class AnimeService
                 'url' => $item->url,
                 'pic_prompt' => $item->pic_prompt ?? '',
                 'three_view_image_url' => $item->three_view_image_url,
+                'timbre_url' => $item->timbre_url ?? '',
                 'product' => (int)($item->product ?? 1),
                 'is_public' => (int)$item->user_id === 0 ? 1 : 0,
                 'created_at' => transDate($item->created_at, 'Y-m-d')
@@ -7357,6 +7359,12 @@ class AnimeService
             }
         }
 
+        // 选填音色音频地址(兼容 timbre_url/audio_url/timbre 传参)
+        $timbre_url = $this->getProductTimbreUrl($data);
+        if ($timbre_url) {
+            $this->validateProductAudioUrl($timbre_url);
+        }
+
         $pic_prompt = trim(getProp($data, 'pic_prompt'));
         if (!$pic_prompt) Utils::throwError('20003:提示词不能为空');
         
@@ -7398,6 +7406,7 @@ class AnimeService
                 'product_name'  => $product_name,
                 'url'           => $url,
                 'three_view_image_url' => $three_view_image_url,
+                'timbre_url'    => $timbre_url,
                 'pic_prompt'    => $pic_prompt,
                 'product'       => $product,
                 'versions'      => json_encode($versions, 256),
@@ -7427,6 +7436,7 @@ class AnimeService
             'product_name'  => $product_name,
             'url'           => $url,
             'three_view_image_url' => $three_view_image_url,
+            'timbre_url'    => $timbre_url,
             'pic_prompt'    => $pic_prompt,
             'product'       => $product,
             'versions'      => $versions
@@ -7513,6 +7523,15 @@ class AnimeService
 
             $updateData['three_view_image_url'] = $three_view_image_url;
         }
+
+        // 音色音频地址(选填,显式传空字符串可清除已有音色)
+        if (array_key_exists('timbre_url', $data) || array_key_exists('audio_url', $data) || array_key_exists('timbre', $data)) {
+            $timbre_url = $this->getProductTimbreUrl($data);
+            if ($timbre_url) {
+                $this->validateProductAudioUrl($timbre_url);
+            }
+            $updateData['timbre_url'] = $timbre_url;
+        }
         
         // 提示词
         $pic_prompt = trim(getProp($data, 'pic_prompt'));
@@ -7607,6 +7626,51 @@ class AnimeService
         
         return $updated;
     }
+
+    /**
+     * 获取资产音色音频地址
+     * 兼容 timbre_url/audio_url/timbre 三种传参,timbre 支持字符串或 {"url":"..."} 结构
+     *
+     * @param array $data
+     * @return string
+     */
+    private function getProductTimbreUrl($data) {
+        foreach (['timbre_url', 'audio_url', 'timbre'] as $key) {
+            if (!array_key_exists($key, $data)) {
+                continue;
+            }
+
+            $value = $data[$key];
+            if (is_array($value)) {
+                $value = getProp($value, 'url', $value[0] ?? '');
+            }
+
+            if (!is_string($value)) {
+                continue;
+            }
+
+            return trim($value);
+        }
+
+        return '';
+    }
+
+    /**
+     * 校验资产音色音频地址格式
+     *
+     * @param string $url 音频地址
+     * @return void
+     */
+    private function validateProductAudioUrl($url) {
+        $allowedExtensions = ['mp3', 'wav', 'm4a', 'aac', 'ogg', 'oga', 'flac', 'amr', 'wma', 'weba'];
+
+        $urlPath = parse_url($url, PHP_URL_PATH);
+        $extension = strtolower(pathinfo($urlPath, PATHINFO_EXTENSION));
+
+        if (!in_array($extension, $allowedExtensions)) {
+            Utils::throwError('20003:音色音频格式不正确,仅支持 mp3、wav、m4a、aac、ogg、flac、amr、wma 格式');
+        }
+    }
     
     /**
      * 保存单一资产与剧本剧集的关联关系
@@ -11034,6 +11098,7 @@ class AnimeService
                 'product.pic_task_id',
                 'product.pic_task_status',
                 'product.three_view_image_url',
+                'product.timbre_url',
                 'mapping.created_at'
             )
             ->orderBy('mapping.product_id', 'asc')
@@ -11053,6 +11118,7 @@ class AnimeService
                     'product' => (int)$item->product,
                     'pic_prompt' => $item->pic_prompt,
                     'url' => $item->url,
+                    'timbre_url' => $item->timbre_url,
                     'pic_task_id' => $item->pic_task_id,
                     'pic_task_status' => $item->pic_task_status,
                     'episode_numbers' => [],
@@ -11082,6 +11148,7 @@ class AnimeService
                 'product' => $product['product'],
                 'pic_prompt' => $product['pic_prompt'],
                 'url' => $product['url'],
+                'timbre_url' => $product['timbre_url'] ?? '',
                 'pic_task_id' => $product['pic_task_id'],
                 'pic_task_status' => $product['pic_task_status'],
                 'episode_numbers' => $episodeNumbersStr,