|
|
@@ -321,7 +321,7 @@ class AnimeController extends BaseController
|
|
|
return response()->stream(function () use ($taskId, $result) {
|
|
|
// 设置 SSE 响应头
|
|
|
echo "data: " . json_encode([
|
|
|
- 'type' => 'task_created',
|
|
|
+ 'type' => 'init',
|
|
|
'data' => $result
|
|
|
]) . "\n\n";
|
|
|
ob_flush();
|
|
|
@@ -423,7 +423,7 @@ class AnimeController extends BaseController
|
|
|
|
|
|
// 发送当前状态
|
|
|
echo "data: " . json_encode([
|
|
|
- 'type' => 'status_update',
|
|
|
+ 'type' => 'update',
|
|
|
'data' => [
|
|
|
'task_id' => $task->id,
|
|
|
'status' => $task->status,
|
|
|
@@ -471,7 +471,7 @@ class AnimeController extends BaseController
|
|
|
// 超时处理
|
|
|
if (time() - $startTime >= $maxDuration) {
|
|
|
echo "data: " . json_encode([
|
|
|
- 'type' => 'timeout',
|
|
|
+ 'type' => 'time_out',
|
|
|
'message' => '任务执行超时,请稍后查询任务状态',
|
|
|
'data' => [
|
|
|
'task_id' => $taskId
|
|
|
@@ -679,7 +679,7 @@ class AnimeController extends BaseController
|
|
|
return response()->stream(function () use ($taskIds, $segmentTasks, $animeId, $episodeNumber, $episodeId, $startTime, $maxDuration) {
|
|
|
// 发送初始任务创建信息
|
|
|
echo "data: " . json_encode([
|
|
|
- 'type' => 'tasks_created',
|
|
|
+ 'type' => 'init',
|
|
|
'data' => [
|
|
|
'anime_id' => $animeId,
|
|
|
'episode_id' => $episodeId,
|
|
|
@@ -770,7 +770,7 @@ class AnimeController extends BaseController
|
|
|
|
|
|
// 发送单个任务完成通知
|
|
|
echo "data: " . json_encode([
|
|
|
- 'type' => 'task_completed',
|
|
|
+ 'type' => 'update',
|
|
|
'data' => [
|
|
|
'segment_id' => $segmentId,
|
|
|
'task_id' => $taskId,
|
|
|
@@ -796,7 +796,7 @@ class AnimeController extends BaseController
|
|
|
|
|
|
// 发送任务处理失败通知
|
|
|
echo "data: " . json_encode([
|
|
|
- 'type' => 'task_process_error',
|
|
|
+ 'type' => 'error',
|
|
|
'data' => [
|
|
|
'segment_id' => $segmentId,
|
|
|
'task_id' => $taskId,
|
|
|
@@ -820,7 +820,7 @@ class AnimeController extends BaseController
|
|
|
|
|
|
// 发送任务失败通知
|
|
|
echo "data: " . json_encode([
|
|
|
- 'type' => 'task_failed',
|
|
|
+ 'type' => 'update',
|
|
|
'data' => [
|
|
|
'segment_id' => $segmentId,
|
|
|
'task_id' => $taskId,
|
|
|
@@ -855,7 +855,7 @@ class AnimeController extends BaseController
|
|
|
|
|
|
// 发送整体进度更新
|
|
|
echo "data: " . json_encode([
|
|
|
- 'type' => 'progress_update',
|
|
|
+ 'type' => 'update',
|
|
|
'data' => [
|
|
|
'anime_id' => $animeId,
|
|
|
'episode_id' => $episodeId,
|
|
|
@@ -882,7 +882,7 @@ class AnimeController extends BaseController
|
|
|
->get();
|
|
|
|
|
|
echo "data: " . json_encode([
|
|
|
- 'type' => 'all_completed',
|
|
|
+ 'type' => 'completed',
|
|
|
'data' => [
|
|
|
'anime_id' => $animeId,
|
|
|
'episode_id' => $episodeId,
|
|
|
@@ -944,7 +944,7 @@ class AnimeController extends BaseController
|
|
|
->get();
|
|
|
|
|
|
echo "data: " . json_encode([
|
|
|
- 'type' => 'timeout',
|
|
|
+ 'type' => 'time_out',
|
|
|
'message' => '批量视频生成已超时30分钟,停止状态查询',
|
|
|
'data' => [
|
|
|
'anime_id' => $animeId,
|
|
|
@@ -1049,4 +1049,331 @@ class AnimeController extends BaseController
|
|
|
$result = $this->AnimeService->generateImg($data);
|
|
|
return $this->success($result);
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 监控分镜图片和音频生成任务进度(SSE)
|
|
|
+ */
|
|
|
+ public function monitorSegmentTasks(Request $request) {
|
|
|
+ // 忽略所有超时限制
|
|
|
+ set_time_limit(0);
|
|
|
+ ini_set('max_execution_time', '0');
|
|
|
+
|
|
|
+ $data = $request->all();
|
|
|
+
|
|
|
+ // 验证参数
|
|
|
+ $validator = Validator::make($data, [
|
|
|
+ 'anime_id' => 'required|string',
|
|
|
+ 'episode_id' => 'required|string',
|
|
|
+ ], [
|
|
|
+ 'anime_id.required' => '动漫ID不能为空',
|
|
|
+ 'episode_id.required' => '剧集ID不能为空',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if ($validator->fails()) {
|
|
|
+ Utils::throwError('1002:' . $validator->errors()->first());
|
|
|
+ }
|
|
|
+
|
|
|
+ $animeId = $data['anime_id'];
|
|
|
+ $episodeId = $data['episode_id'];
|
|
|
+
|
|
|
+ // 获取所有分镜信息
|
|
|
+ $segments = DB::table('mp_episode_segments')
|
|
|
+ ->where('anime_id', $animeId)
|
|
|
+ ->where('episode_id', $episodeId)
|
|
|
+ // ->whereNotNull('pic_task_id')
|
|
|
+ ->orderBy('segment_number')
|
|
|
+ ->select('segment_id', 'segment_number', 'pic_task_id', 'pic_task_status', 'audio_task_id', 'audio_task_status', 'img_url', 'audio_url')
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ if ($segments->isEmpty()) {
|
|
|
+ Utils::throwError('20003:未找到分镜任务数据');
|
|
|
+ }
|
|
|
+
|
|
|
+ $startTime = time();
|
|
|
+ $maxDuration = 900; // 15分钟超时
|
|
|
+ $checkInterval = 5; // 每5秒检查一次
|
|
|
+
|
|
|
+ // 设置 SSE 响应头并开始长连接
|
|
|
+ return response()->stream(function () use ($segments, $animeId, $episodeId, $startTime, $maxDuration, $checkInterval) {
|
|
|
+ // 发送初始任务信息
|
|
|
+ echo "data: " . json_encode([
|
|
|
+ 'type' => 'init',
|
|
|
+ 'data' => [
|
|
|
+ 'anime_id' => $animeId,
|
|
|
+ 'episode_id' => $episodeId,
|
|
|
+ 'total_segments' => count($segments),
|
|
|
+ 'start_time' => $startTime,
|
|
|
+ 'max_duration' => $maxDuration
|
|
|
+ ]
|
|
|
+ ]) . "\n\n";
|
|
|
+ ob_flush();
|
|
|
+ flush();
|
|
|
+
|
|
|
+ $completedSegments = [];
|
|
|
+ $processedSegments = []; // 记录已处理完成的分镜(成功或失败)
|
|
|
+ $notifiedSegments = []; // 记录已通知过的分镜,避免重复通知
|
|
|
+
|
|
|
+ while (time() - $startTime < $maxDuration) {
|
|
|
+ try {
|
|
|
+ $updatedImages = []; // 记录本次循环中更新的图片信息
|
|
|
+
|
|
|
+ foreach ($segments as $segment) {
|
|
|
+ $segmentId = $segment->segment_id;
|
|
|
+ $picTaskId = $segment->pic_task_id;
|
|
|
+
|
|
|
+ // 跳过已处理完成的分镜
|
|
|
+ if (in_array($segmentId, $processedSegments)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $picCompleted = false;
|
|
|
+ $audioCompleted = false;
|
|
|
+ $picProcessed = false;
|
|
|
+ $audioProcessed = false;
|
|
|
+
|
|
|
+ // 检查图片任务状态
|
|
|
+ if ($picTaskId) {
|
|
|
+ $picTask = DB::table('mp_generate_pic_tasks')
|
|
|
+ ->where('id', $picTaskId)
|
|
|
+ ->select('status', 'result_url', 'error_message')
|
|
|
+ ->first();
|
|
|
+
|
|
|
+ if ($picTask && in_array($picTask->status, ['success', 'failed'])) {
|
|
|
+ $updateData = [
|
|
|
+ 'pic_task_status' => $picTask->status === 'success' ? '已完成' : '失败',
|
|
|
+ 'updated_at' => date('Y-m-d H:i:s')
|
|
|
+ ];
|
|
|
+
|
|
|
+ $imgUrl = '';
|
|
|
+ if ($picTask->status === 'success' && !empty($picTask->result_url)) {
|
|
|
+ // 更新分镜表的图片URL
|
|
|
+ $resultUrls = json_decode($picTask->result_url, true);
|
|
|
+ $imgUrl = is_array($resultUrls) ? $resultUrls[0] : $picTask->result_url;
|
|
|
+ $updateData['img_url'] = $imgUrl;
|
|
|
+ $picCompleted = true;
|
|
|
+ } else if ($picTask->status === 'failed') {
|
|
|
+ // 失败时清空图片URL
|
|
|
+ $updateData['img_url'] = '';
|
|
|
+ }
|
|
|
+
|
|
|
+ DB::table('mp_episode_segments')
|
|
|
+ ->where('segment_id', $segmentId)
|
|
|
+ ->update($updateData);
|
|
|
+
|
|
|
+ // 记录更新的图片信息(只有未通知过的才记录)
|
|
|
+ if (!in_array($segmentId, $notifiedSegments)) {
|
|
|
+ $updatedImages[] = [
|
|
|
+ 'segment_id' => $segmentId,
|
|
|
+ 'segment_number' => $segment->segment_number,
|
|
|
+ 'img_url' => $imgUrl,
|
|
|
+ 'status' => $picTask->status === 'success' ? 'success' : 'failed'
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 标记为已通知
|
|
|
+ $notifiedSegments[] = $segmentId;
|
|
|
+ }
|
|
|
+
|
|
|
+ $picProcessed = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查音频任务状态
|
|
|
+ $audioTask = DB::table('mp_dub_video_tasks')
|
|
|
+ ->where('alias_segment_id', $segmentId)
|
|
|
+ ->orderBy('id', 'desc')
|
|
|
+ ->select('generate_status', 'audio_url')
|
|
|
+ ->first();
|
|
|
+
|
|
|
+ if ($audioTask && in_array($audioTask->generate_status, ['执行成功', '执行失败'])) {
|
|
|
+ $updateData = [
|
|
|
+ 'audio_task_status' => $audioTask->generate_status === '执行成功' ? '已完成' : '失败',
|
|
|
+ 'updated_at' => date('Y-m-d H:i:s')
|
|
|
+ ];
|
|
|
+
|
|
|
+ if ($audioTask->generate_status === '执行成功' && !empty($audioTask->audio_url)) {
|
|
|
+ $updateData['audio_url'] = $audioTask->audio_url;
|
|
|
+ $audioCompleted = true;
|
|
|
+ } else if ($audioTask->generate_status === '执行失败') {
|
|
|
+ // 失败时清空音频URL
|
|
|
+ $updateData['audio_url'] = '';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 静默更新分镜表的音频URL(不输出通知)
|
|
|
+ DB::table('mp_episode_segments')
|
|
|
+ ->where('segment_id', $segmentId)
|
|
|
+ ->update($updateData);
|
|
|
+
|
|
|
+ $audioProcessed = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果图片和音频都成功完成,标记为已完成
|
|
|
+ if ($picCompleted && $audioCompleted) {
|
|
|
+ $completedSegments[] = $segmentId;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果图片和音频都已处理完成(成功或失败),标记为已处理
|
|
|
+ if ($picProcessed && $audioProcessed) {
|
|
|
+ $processedSegments[] = $segmentId;
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果有图片更新,发送具体的图片更新信息
|
|
|
+ if (!empty($updatedImages)) {
|
|
|
+ echo "data: " . json_encode([
|
|
|
+ 'type' => 'update',
|
|
|
+ 'data' => [
|
|
|
+ 'anime_id' => $animeId,
|
|
|
+ 'episode_id' => $episodeId,
|
|
|
+ 'updated_images' => $updatedImages,
|
|
|
+ 'completed_count' => count($completedSegments), // 成功完成的数量
|
|
|
+ 'processed_count' => count($processedSegments), // 已处理完成的数量(成功+失败)
|
|
|
+ 'total_count' => count($segments),
|
|
|
+ 'elapsed_time' => time() - $startTime,
|
|
|
+ 'remaining_time' => max(0, $maxDuration - (time() - $startTime))
|
|
|
+ ]
|
|
|
+ ]) . "\n\n";
|
|
|
+ ob_flush();
|
|
|
+ flush();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果所有分镜都已处理完成(成功或失败),获取最终结果并结束连接
|
|
|
+ if (count($processedSegments) === count($segments)) {
|
|
|
+ // 获取剧集信息
|
|
|
+ $episode = DB::table('mp_anime_episodes')
|
|
|
+ ->where('anime_id', $animeId)
|
|
|
+ ->where('id', $episodeId)
|
|
|
+ ->select('id as episode_id', 'anime_id', 'episode_number', 'title', 'intro', 'art_style', 'roles', 'scenes')
|
|
|
+ ->first();
|
|
|
+
|
|
|
+ if ($episode) {
|
|
|
+ $episode = (array)$episode;
|
|
|
+ $episode['roles'] = json_decode($episode['roles'], true);
|
|
|
+ $episode['scenes'] = json_decode($episode['scenes'], true);
|
|
|
+
|
|
|
+ // 获取所有分镜信息
|
|
|
+ $allSegments = DB::table('mp_episode_segments')
|
|
|
+ ->where('anime_id', $animeId)
|
|
|
+ ->where('episode_id', $episodeId)
|
|
|
+ ->orderBy('segment_number')
|
|
|
+ ->select('*')
|
|
|
+ ->get()
|
|
|
+ ->map(function ($value) {
|
|
|
+ return (array)$value;
|
|
|
+ })->toArray();
|
|
|
+
|
|
|
+ // 按照episodeInfo的格式组织数据
|
|
|
+ $acts = [];
|
|
|
+ foreach($allSegments as $segment) {
|
|
|
+ $segmentData = [
|
|
|
+ 'segment_id' => getProp($segment, 'segment_id'),
|
|
|
+ 'segment_number' => getProp($segment, 'segment_number'),
|
|
|
+ 'segment_content' => getProp($segment, 'segment_content'),
|
|
|
+ 'description' => getProp($segment, 'description'),
|
|
|
+ 'composition' => getProp($segment, 'composition'),
|
|
|
+ 'camera_movement' => getProp($segment, 'camera_movement'),
|
|
|
+ 'voice_actor' => getProp($segment, 'voice_actor'),
|
|
|
+ 'dialogue' => getProp($segment, 'dialogue'),
|
|
|
+ 'frame_type' => getProp($segment, 'frame_type'),
|
|
|
+ 'scene' => getProp($segment, 'scene'),
|
|
|
+ 'characters' => getProp($segment, 'characters'),
|
|
|
+ 'tail_frame' => getProp($segment, 'tail_frame'),
|
|
|
+ 'emotion' => getProp($segment, 'emotion'),
|
|
|
+ 'emotion_type' => getProp($segment, 'emotion_type'),
|
|
|
+ 'gender' => getProp($segment, 'gender'),
|
|
|
+ 'speed_ratio' => getProp($segment, 'speed_ratio'),
|
|
|
+ 'loudness_ratio' => getProp($segment, 'loudness_ratio'),
|
|
|
+ 'emotion_scale' => getProp($segment, 'emotion_scale'),
|
|
|
+ 'pitch' => getProp($segment, 'pitch'),
|
|
|
+ 'voice_name' => getProp($segment, 'voice_name'),
|
|
|
+ 'voice_type' => getProp($segment, 'voice_type'),
|
|
|
+ 'voice_audio_url' => getProp($segment, 'voice_audio_url'),
|
|
|
+ 'img_url' => getProp($segment, 'img_url'),
|
|
|
+ 'audio_url' => getProp($segment, 'audio_url'),
|
|
|
+ 'video_url' => getProp($segment, 'video_url'),
|
|
|
+ 'last_frame_url' => getProp($segment, 'last_frame_url'),
|
|
|
+ ];
|
|
|
+
|
|
|
+ $actNumber = getProp($segment, 'act_number');
|
|
|
+ if (isset($acts[$actNumber])) {
|
|
|
+ $acts[$actNumber]['segments'][] = $segmentData;
|
|
|
+ } else {
|
|
|
+ $acts[$actNumber] = [
|
|
|
+ 'act_number' => $actNumber,
|
|
|
+ 'act_title' => getProp($segment, 'act_title'),
|
|
|
+ 'segments' => [$segmentData]
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $episode['acts'] = array_values($acts);
|
|
|
+ }
|
|
|
+
|
|
|
+ echo "data: " . json_encode([
|
|
|
+ 'type' => 'completed',
|
|
|
+ 'data' => [
|
|
|
+ 'anime_id' => $animeId,
|
|
|
+ 'episode_id' => $episodeId,
|
|
|
+ 'completed_count' => count($completedSegments), // 成功完成的数量
|
|
|
+ 'processed_count' => count($processedSegments), // 已处理完成的数量(成功+失败)
|
|
|
+ 'total_count' => count($segments),
|
|
|
+ 'total_elapsed_time' => time() - $startTime,
|
|
|
+ 'episode_info' => $episode ?? null
|
|
|
+ ]
|
|
|
+ ]) . "\n\n";
|
|
|
+ ob_flush();
|
|
|
+ flush();
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否超时
|
|
|
+ if (time() - $startTime >= $maxDuration) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ sleep($checkInterval);
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ echo "data: " . json_encode([
|
|
|
+ 'type' => 'error',
|
|
|
+ 'message' => '查询任务状态失败: ' . $e->getMessage(),
|
|
|
+ 'elapsed_time' => time() - $startTime,
|
|
|
+ 'remaining_time' => max(0, $maxDuration - (time() - $startTime))
|
|
|
+ ]) . "\n\n";
|
|
|
+ ob_flush();
|
|
|
+ flush();
|
|
|
+
|
|
|
+ if (time() - $startTime >= $maxDuration) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ sleep($checkInterval);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 超时处理
|
|
|
+ if (time() - $startTime >= $maxDuration) {
|
|
|
+ echo "data: " . json_encode([
|
|
|
+ 'type' => 'time_out',
|
|
|
+ 'message' => '分镜任务监控已超时15分钟,停止状态查询',
|
|
|
+ 'data' => [
|
|
|
+ 'anime_id' => $animeId,
|
|
|
+ 'episode_id' => $episodeId,
|
|
|
+ 'completed_count' => count($completedSegments), // 成功完成的数量
|
|
|
+ 'processed_count' => count($processedSegments), // 已处理完成的数量(成功+失败)
|
|
|
+ 'total_count' => count($segments),
|
|
|
+ 'total_elapsed_time' => time() - $startTime,
|
|
|
+ 'timeout_duration' => $maxDuration
|
|
|
+ ]
|
|
|
+ ]) . "\n\n";
|
|
|
+ ob_flush();
|
|
|
+ flush();
|
|
|
+ }
|
|
|
+
|
|
|
+ }, 200, [
|
|
|
+ 'Content-Type' => 'text/event-stream',
|
|
|
+ 'Cache-Control' => 'no-cache',
|
|
|
+ 'Connection' => 'keep-alive',
|
|
|
+ 'X-Accel-Buffering' => 'no', // 禁用 Nginx 缓冲
|
|
|
+ ]);
|
|
|
+ }
|
|
|
}
|