소스 검색

新增应用有声数据和获取图片任务信息

lh 3 달 전
부모
커밋
9d0c5bdfbc
3개의 변경된 파일416개의 추가작업 그리고 11개의 파일을 삭제
  1. 20 0
      app/Http/Controllers/Anime/AnimeController.php
  2. 395 11
      app/Services/Anime/AnimeService.php
  3. 1 0
      routes/api.php

+ 20 - 0
app/Http/Controllers/Anime/AnimeController.php

@@ -877,4 +877,24 @@ class AnimeController extends BaseController
         return $this->success($result);
     }
 
+    // 获取分集生成图片信息
+    public function episodePicsInfo(Request $request) {
+        // 忽略所有超时限制
+        set_time_limit(0);
+        ini_set('max_execution_time', '0');
+
+        $data = $request->all();
+
+        // 获取生成器函数
+        $streamGenerator = $this->AnimeService->episodePicsInfo($data);
+
+        // 返回 SSE 响应
+        return response()->stream($streamGenerator, 200, [
+            'Content-Type' => 'text/event-stream',
+            'Cache-Control' => 'no-cache',
+            'Connection' => 'keep-alive',
+            'X-Accel-Buffering' => 'no', // 禁用 Nginx 缓冲
+        ]);
+    }
+
 }

+ 395 - 11
app/Services/Anime/AnimeService.php

@@ -278,7 +278,7 @@ class AnimeService
             Utils::throwError('1002:请选择剧集');
         }
 
-        $episode = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('id', $episode_id)->where('is_default', 1)->select('id as episode_id', 'anime_id', 'episode_number', 'title', 'intro', 'art_style', 'roles', 'scenes')->first();
+        $episode = DB::table('mp_anime_episodes')->where('anime_id', $anime_id)->where('id', $episode_id)->select('id as episode_id', 'anime_id', 'episode_number', 'title', 'intro', 'art_style', 'roles', 'scenes')->first();
         if (!$episode) Utils::throwError('20003: 该剧集不存在!');
         $episode = (array)$episode;
         $episode['roles'] = json_decode($episode['roles'], true);
@@ -1528,28 +1528,412 @@ class AnimeService
         $global_data = getProp($data, 'global_data');
         $segment_data = getProp($data, 'segment_data');
 
-        // 解析数据
-        
+        if (!$segment_id) Utils::throwError('20003:请选择分镜');
+
+        $global_data = is_array($global_data) ? $global_data : [];
+        $segment_data = is_array($segment_data) ? $segment_data : [];
+        $allow_fields = ['dialogue', 'voice_type', 'speed_ratio', 'loudness_ratio', 'emotion_scale'];
+        $global_update_data = [];
+        $segment_update_data = [];
+        $global_voice_type = '';
+        $target_segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first();
+        if (!$target_segment) Utils::throwError('20003:分镜不存在');
+
+        $episode_id = getProp($target_segment, 'episode_id');
+        $target_voice_actor = trim((string)getProp($target_segment, 'voice_actor'));
+        $episode = DB::table('mp_anime_episodes')->where('id', $episode_id)->first();
+        if (!$episode) Utils::throwError('20003:分集不存在');
+
+        foreach ($global_data as $item) {
+            $field_name = trim((string)getProp($item, 'name'));
+            if (!$field_name || !in_array($field_name, $allow_fields)) {
+                continue;
+            }
+
+            $global_update_data[$field_name] = getProp($item, 'value');
+            if ($field_name === 'voice_type') {
+                $global_voice_type = trim((string)getProp($item, 'value'));
+            }
+        }
+
+        foreach ($segment_data as $item) {
+            $field_name = trim((string)getProp($item, 'name'));
+            if (!$field_name || !in_array($field_name, $allow_fields)) {
+                continue;
+            }
+
+            $segment_update_data[$field_name] = getProp($item, 'value');
+        }
+
+        if ($global_voice_type) {
+            $timbre_info = DB::table('mp_timbres')
+                ->where('timbre_type', $global_voice_type)
+                ->select('audio_url', 'timbre_name')
+                ->first();
+            if ($timbre_info) {
+                $global_update_data['voice_audio_url'] = getProp($timbre_info, 'audio_url');
+                $global_update_data['voice_name'] = getProp($timbre_info, 'timbre_name');
+            }
+        }
 
         try {
             DB::beginTransaction();
 
-            // 更新全局数据
-            $boolen = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([
-                'global_data' => json_encode($global_data),
-                'segment_data' => json_encode($segment_data),
-            ]);
-            if (!$boolen) {
-                Utils::throwError('20003:更新分镜失败');
+            $now = date('Y-m-d H:i:s');
+
+            if (!empty($global_update_data)) {
+                if (!$target_voice_actor) Utils::throwError('20003:角色异常');
+
+                $global_segment_update_data = $global_update_data;
+                $global_segment_update_data['updated_at'] = $now;
+                DB::table('mp_episode_segments')
+                    ->where('episode_id', $episode_id)
+                    ->where('voice_name', $target_voice_actor)
+                    ->update($global_segment_update_data);
+
+                if ($global_voice_type) {
+                    $episode_roles = json_decode((string)getProp($episode, 'roles'), true);
+                    $episode_roles = is_array($episode_roles) ? $episode_roles : [];
+                    $roles_updated = false;
+
+                    foreach ($episode_roles as &$role) {
+                        if (trim((string)getProp($role, 'voice_name')) !== $target_voice_actor) {
+                            continue;
+                        }
+
+                        $role['voice_type'] = $global_voice_type;
+                        $role['voice_audio_url'] = getProp($global_update_data, 'voice_audio_url');
+                        $role['voice_name'] = getProp($global_update_data, 'voice_name');
+                        $roles_updated = true;
+                    }
+                    unset($role);
+
+                    if ($roles_updated) {
+                        DB::table('mp_anime_episodes')->where('id', $episode_id)->update([
+                            'roles' => json_encode($episode_roles, 256),
+                            'updated_at' => $now,
+                        ]);
+                    }
+                }
+            }
+
+            if (!empty($segment_update_data)) {
+                $segment_update_data['updated_at'] = $now;
+                DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update($segment_update_data);
+            }
+
+            // 收集需要创建音频合成任务的segment_id
+            $segment_ids_to_create_task = [];
+            $segments_data = [];
+
+            // 如果有全局更新,获取该角色的所有分镜数据
+            if (!empty($global_update_data)) {
+                $affected_segments = DB::table('mp_episode_segments')
+                    ->where('episode_id', $episode_id)
+                    ->where('voice_name', $target_voice_actor)
+                    ->select('segment_id', 'dialogue', 'voice_type', 'voice_name', 'emotion', 'emotion_type', 'gender', 'speed_ratio', 'loudness_ratio', 'emotion_scale', 'pitch')
+                    ->get();
+                
+                foreach ($affected_segments as $seg) {
+                    $seg = (array)$seg;
+                    $sid = getProp($seg, 'segment_id');
+                    $segment_ids_to_create_task[] = $sid;
+                    $segments_data[$sid] = $seg;
+                }
+            }
+
+            // 如果有单个分镜更新,添加该分镜数据
+            if (!empty($segment_update_data)) {
+                $segment_ids_to_create_task[] = $segment_id;
+                
+                // 如果该分镜数据还未获取,则获取
+                if (!isset($segments_data[$segment_id])) {
+                    $segment = DB::table('mp_episode_segments')
+                        ->where('segment_id', $segment_id)
+                        ->select('segment_id', 'dialogue', 'voice_type', 'voice_name', 'emotion', 'emotion_type', 'gender', 'speed_ratio', 'loudness_ratio', 'emotion_scale', 'pitch')
+                        ->first();
+                    
+                    if ($segment) {
+                        $segments_data[$segment_id] = (array)$segment;
+                    }
+                }
+            }
+
+            // 去重
+            $segment_ids_to_create_task = array_unique($segment_ids_to_create_task);
+
+            // 为所有需要更新的分镜创建音频合成任务
+            if (!empty($segment_ids_to_create_task)) {
+                foreach ($segment_ids_to_create_task as $sid) {
+                    // 从已获取的数据中取出分镜信息
+                    if (!isset($segments_data[$sid])) continue;
+                    
+                    $segment = $segments_data[$sid];
+
+                    // 构建音频生成参数
+                    $generate_json = [
+                        'text' => getProp($segment, 'dialogue'),
+                        'voice_type' => getProp($segment, 'voice_type'),
+                        'voice_name' => getProp($segment, 'voice_name'),
+                        'emotion' => getProp($segment, 'emotion'),
+                        'emotion_type' => getProp($segment, 'emotion_type'),
+                        'gender' => getProp($segment, 'gender'),
+                        'speed_ratio' => getProp($segment, 'speed_ratio', 0),
+                        'loudness_ratio' => getProp($segment, 'loudness_ratio', 0),
+                        'emotion_scale' => getProp($segment, 'emotion_scale', 0),
+                        'pitch' => getProp($segment, 'pitch', 0),
+                    ];
+
+                    // 插入音频合成任务
+                    $task_id = DB::table('mp_dub_video_tasks')->insertGetId([
+                        'alias_segment_id' => $sid,
+                        'generate_status' => '执行中',
+                        'audio_url' => '',
+                        'generate_json' => json_encode($generate_json, 256),
+                        'created_at' => $now,
+                        'updated_at' => $now,
+                    ]);
+
+                    if (!$task_id) {
+                        Utils::throwError('20003:创建音频合成任务失败');
+                    }
+                }
             }
 
             DB::commit();
-            return true;
 
         } catch (\Exception $e) {
             DB::rollBack();
             Utils::throwError('20003:'.$e->getMessage());
         }
+
+        return true;
+    }
+
+    public function episodePicsInfo($data) {
+        $anime_id = getProp($data, 'anime_id');
+        $episode_id = getProp($data, 'episode_id');
+
+        // 参数验证
+        if (!$anime_id && !$episode_id) {
+            Utils::throwError('20003:请提供动漫ID或分集ID');
+        }
+
+        // 根据参数获取数据源
+        if ($episode_id) {
+            // 从剧集表获取
+            $source = DB::table('mp_anime_episodes')->where('id', $episode_id)->first();
+            if (!$source) Utils::throwError('20003:该剧集不存在');
+            $table_name = 'mp_anime_episodes';
+            $id_field = 'id';
+            $id_value = $episode_id;
+        } else {
+            // 从动漫表获取
+            $source = DB::table('mp_animes')->where('id', $anime_id)->where('is_deleted', 0)->first();
+            if (!$source) Utils::throwError('20003:该动漫不存在');
+            $table_name = 'mp_animes';
+            $id_field = 'id';
+            $id_value = $anime_id;
+        }
+
+        $source = (array)$source;
+        $roles = json_decode(getProp($source, 'roles', '[]'), true) ?: [];
+        $scenes = json_decode(getProp($source, 'scenes', '[]'), true) ?: [];
+
+        // 返回生成器函数,用于 SSE 流式输出
+        return function() use ($roles, $scenes, $table_name, $id_field, $id_value) {
+            $startTime = time();
+            $maxDuration = 600; // 10分钟超时
+            $checkInterval = 3; // 每3秒检查一次
+
+            // Redis 缓存键
+            $cacheKey = "anime:pics_info:{$table_name}:{$id_value}";
+            
+            // 生成当前数据的哈希值用于比较
+            $generateHash = function($roles, $scenes) {
+                return md5(json_encode(['roles' => $roles, 'scenes' => $scenes], JSON_UNESCAPED_UNICODE));
+            };
+
+            // 发送初始数据
+            $currentHash = $generateHash($roles, $scenes);
+            Redis::setex($cacheKey, 600, $currentHash); // 缓存10分钟
+
+            echo "data: " . json_encode([
+                'type' => 'init',
+                'data' => [
+                    'roles' => $roles,
+                    'scenes' => $scenes,
+                    'start_time' => $startTime
+                ]
+            ], JSON_UNESCAPED_UNICODE) . "\n\n";
+            ob_flush();
+            flush();
+
+            // 持续轮询任务状态
+            while (time() - $startTime < $maxDuration) {
+                $hasProcessing = false;
+                $updated = false;
+
+                // 检查角色任务状态
+                foreach ($roles as $index => &$role) {
+                    // 如果已经有 URL,跳过检查
+                    $existing_url = getProp($role, 'url');
+                    if (!empty($existing_url)) {
+                        continue;
+                    }
+
+                    $task_id = getProp($role, 'task_id');
+                    $task_status = getProp($role, 'task_status');
+
+                    // 只处理有任务ID且状态为processing的角色
+                    if ($task_id && $task_status === 'processing') {
+                        $hasProcessing = true;
+                        
+                        // 查询任务状态
+                        $task = DB::table('mp_generate_pic_tasks')
+                            ->where('id', $task_id)
+                            ->select('id', 'status', 'result_url', 'error_message')
+                            ->first();
+
+                        if ($task) {
+                            $task = (array)$task;
+                            $status = getProp($task, 'status');
+
+                            // 如果任务完成或失败
+                            if (in_array($status, ['success', 'failed'])) {
+                                $role['task_status'] = $status;
+                                
+                                if ($status === 'success') {
+                                    $result_url = getProp($task, 'result_url');
+                                    if ($result_url) {
+                                        $result_urls = json_decode($result_url, true);
+                                        $role['url'] = is_array($result_urls) ? $result_urls[0] : $result_url;
+                                    }
+                                }
+                                
+                                $updated = true;
+                            }
+                        }
+                    }
+                }
+
+                // 检查场景任务状态
+                foreach ($scenes as $index => &$scene) {
+                    // 如果已经有 URL,跳过检查
+                    $existing_url = getProp($scene, 'url');
+                    if (!empty($existing_url)) {
+                        continue;
+                    }
+
+                    $task_id = getProp($scene, 'task_id');
+                    $task_status = getProp($scene, 'task_status');
+
+                    // 只处理有任务ID且状态为processing的场景
+                    if ($task_id && $task_status === 'processing') {
+                        $hasProcessing = true;
+                        
+                        // 查询任务状态
+                        $task = DB::table('mp_generate_pic_tasks')
+                            ->where('id', $task_id)
+                            ->select('id', 'status', 'result_url', 'error_message')
+                            ->first();
+
+                        if ($task) {
+                            $task = (array)$task;
+                            $status = getProp($task, 'status');
+
+                            // 如果任务完成或失败
+                            if (in_array($status, ['success', 'failed'])) {
+                                $scene['task_status'] = $status;
+                                
+                                if ($status === 'success') {
+                                    $result_url = getProp($task, 'result_url');
+                                    if ($result_url) {
+                                        $result_urls = json_decode($result_url, true);
+                                        $scene['url'] = is_array($result_urls) ? $result_urls[0] : $result_url;
+                                    }
+                                }
+                                
+                                $updated = true;
+                            }
+                        }
+                    }
+                }
+
+                // 如果有更新,检查数据是否真的变化了(通过哈希对比)
+                if ($updated) {
+                    $newHash = $generateHash($roles, $scenes);
+                    $cachedHash = Redis::get($cacheKey);
+
+                    // 只有当数据真正变化时才更新数据库和发送事件
+                    if ($newHash !== $cachedHash) {
+                        try {
+                            // 更新数据库
+                            DB::table($table_name)->where($id_field, $id_value)->update([
+                                'roles' => json_encode($roles, JSON_UNESCAPED_UNICODE),
+                                'scenes' => json_encode($scenes, JSON_UNESCAPED_UNICODE),
+                                'updated_at' => date('Y-m-d H:i:s')
+                            ]);
+
+                            // 更新缓存
+                            Redis::setex($cacheKey, 600, $newHash);
+
+                            // 发送更新事件
+                            echo "data: " . json_encode([
+                                'type' => 'update',
+                                'data' => [
+                                    'roles' => $roles,
+                                    'scenes' => $scenes,
+                                    'elapsed_time' => time() - $startTime
+                                ]
+                            ], JSON_UNESCAPED_UNICODE) . "\n\n";
+                            ob_flush();
+                            flush();
+                        } catch (\Exception $e) {
+                            dLog('anime')->error('更新角色/场景信息失败: ' . $e->getMessage());
+                        }
+                    }
+                }
+
+                // 如果所有任务都已完成,发送完成事件并退出
+                if (!$hasProcessing) {
+                    // 清理缓存
+                    Redis::del($cacheKey);
+
+                    echo "data: " . json_encode([
+                        'type' => 'completed',
+                        'data' => [
+                            'roles' => $roles,
+                            'scenes' => $scenes,
+                            'total_time' => time() - $startTime
+                        ]
+                    ], JSON_UNESCAPED_UNICODE) . "\n\n";
+                    ob_flush();
+                    flush();
+                    break;
+                }
+
+                // 等待下次检查
+                sleep($checkInterval);
+            }
+
+            // 超时处理
+            if (time() - $startTime >= $maxDuration) {
+                // 清理缓存
+                Redis::del($cacheKey);
+
+                echo "data: " . json_encode([
+                    'type' => 'timeout',
+                    'message' => '轮询超时,请刷新页面查看最新状态',
+                    'data' => [
+                        'roles' => $roles,
+                        'scenes' => $scenes
+                    ]
+                ], JSON_UNESCAPED_UNICODE) . "\n\n";
+                ob_flush();
+                flush();
+            }
+        };
     }
 
     // 轮训获取图片任务结果

+ 1 - 0
routes/api.php

@@ -159,6 +159,7 @@ Route::group(['middleware' => ['bindToken', 'bindExportToken', 'checkLogin']], f
         Route::post('createSegmentVideoTask', [AnimeController::class, 'createSegmentVideoTask']);
         Route::post('batchSetSegmentVideos', [AnimeController::class, 'batchSetSegmentVideos']);
         Route::post('applyAudioData', [AnimeController::class, 'applyAudioData']);
+        Route::get('episodePicsInfo', [AnimeController::class, 'episodePicsInfo']);
     });
     
 });