|
|
@@ -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();
|
|
|
+ }
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
// 轮训获取图片任务结果
|