|
|
@@ -4650,12 +4650,16 @@ class AnimeService
|
|
|
// 音频传入的前提:必须有至少一张图片或一个视频
|
|
|
$canUseAudio = ($hasImage || $hasVideo) && !empty($audioUrl);
|
|
|
|
|
|
- // 余额预检:积分不足直接报错
|
|
|
+ // 余额预检:按预估时长计算所需积分,不足直接报错
|
|
|
+ $chargeDuration = (int)ceil((float)$audioDuration);
|
|
|
+ if ($chargeDuration <= 0) {
|
|
|
+ $chargeDuration = 5; // 无音频时长时按默认5秒预估
|
|
|
+ }
|
|
|
$this->pointsService->checkUserPointsEnough(
|
|
|
$this->pointsService->getVideoChargePoints([
|
|
|
'model' => $model,
|
|
|
- 'video_resolution' => $videoParams['video_resolution'] ?? '720P',
|
|
|
- 'video_duration' => $videoDuration,
|
|
|
+ 'video_resolution' => strtolower($videoParams['video_resolution'] ?? '720P'),
|
|
|
+ 'video_duration' => $chargeDuration,
|
|
|
'ratio' => $ratio,
|
|
|
'generate_audio' => $current_generate_audio,
|
|
|
])
|
|
|
@@ -4879,12 +4883,17 @@ class AnimeService
|
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
|
]);
|
|
|
|
|
|
- // 余额预检:积分不足直接报错
|
|
|
+ // 余额预检:按预估时长计算所需积分,不足直接报错
|
|
|
+ $chargeDuration = (int)$videoDuration;
|
|
|
+ if ($chargeDuration <= 0) {
|
|
|
+ $chargeDuration = 5; // 无时长时按默认5秒预估
|
|
|
+ }
|
|
|
+
|
|
|
$this->pointsService->checkUserPointsEnough(
|
|
|
$this->pointsService->getVideoChargePoints([
|
|
|
'model' => $model,
|
|
|
- 'video_resolution' => $video_resolution ?? '720P',
|
|
|
- 'video_duration' => $videoDuration,
|
|
|
+ 'video_resolution' => strtolower($video_resolution ?? '720p'),
|
|
|
+ 'video_duration' => $chargeDuration,
|
|
|
'ratio' => $ratio,
|
|
|
'generate_audio' => $current_generate_audio,
|
|
|
])
|
|
|
@@ -5062,6 +5071,180 @@ class AnimeService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 预估视频生成所需积分(支持单一/批量)
|
|
|
+ *
|
|
|
+ * 传参与对应创建接口一致,另支持 type 参数区分场景:
|
|
|
+ * segment-单分镜转视频、act-单片段转视频、batch_segment-分镜批量、batch_act-片段批量;
|
|
|
+ * type 缺省时按 segment_id / act_id 自动推断,批量场景需显式传入。
|
|
|
+ *
|
|
|
+ * @param array $data
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ public function previewVideoCharge(array $data): array
|
|
|
+ {
|
|
|
+ $type = (string)getProp($data, 'type', '');
|
|
|
+ if (!$type) {
|
|
|
+ if (getProp($data, 'segment_id')) {
|
|
|
+ $type = 'segment';
|
|
|
+ } elseif (getProp($data, 'act_id')) {
|
|
|
+ $type = 'act';
|
|
|
+ } else {
|
|
|
+ Utils::throwError('1003:请传入type参数(segment/act/batch_segment/batch_act)');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $mode = (string)getProp($data, 'mode', 'video_generation');
|
|
|
+ $items = [];
|
|
|
+ $model = '';
|
|
|
+ $resolution = '720p';
|
|
|
+
|
|
|
+ $buildCharge = function ($itemId, $duration, $model, $resolution, $mode) {
|
|
|
+ return $this->pointsService->getVideoChargePoints([
|
|
|
+ 'model' => $model,
|
|
|
+ 'video_resolution' => $resolution,
|
|
|
+ 'video_duration' => $duration,
|
|
|
+ 'mode' => $mode,
|
|
|
+ ]);
|
|
|
+ };
|
|
|
+
|
|
|
+ if ($type === 'segment') {
|
|
|
+ // 单分镜转视频
|
|
|
+ $segmentId = getProp($data, 'segment_id');
|
|
|
+ if (!$segmentId) {
|
|
|
+ Utils::throwError('1002:分镜ID不能为空');
|
|
|
+ }
|
|
|
+ $segment = DB::table('mp_episode_segments')->where('segment_id', $segmentId)->first();
|
|
|
+ if (!$segment) {
|
|
|
+ Utils::throwError('20003:分镜不存在');
|
|
|
+ }
|
|
|
+ $episode = DB::table('mp_anime_episodes')->where('id', $segment->episode_id)->first();
|
|
|
+ $model = getProp($data, 'model') ?: getProp($episode, 'video_model', 'doubao-seedance-1-5-pro-251215');
|
|
|
+ if (!DB::table('mp_video_models')->where('model', $model)->where('is_enabled', 1)->exists()) {
|
|
|
+ $model = 'doubao-seedance-1-5-pro-251215';
|
|
|
+ }
|
|
|
+ $duration = (int)ceil((float)$segment->audio_duration);
|
|
|
+ if ($duration <= 0) {
|
|
|
+ $duration = 5;
|
|
|
+ }
|
|
|
+ $items[] = [
|
|
|
+ 'id' => $segment->segment_id,
|
|
|
+ 'duration' => $duration,
|
|
|
+ 'points' => $buildCharge($segment->segment_id, $duration, $model, '720p', $mode),
|
|
|
+ ];
|
|
|
+ } elseif ($type === 'act') {
|
|
|
+ // 单片段转视频
|
|
|
+ $actId = getProp($data, 'act_id');
|
|
|
+ if (!$actId) {
|
|
|
+ Utils::throwError('1002:片段ID不能为空');
|
|
|
+ }
|
|
|
+ $act = DB::table('mp_episode_segments')->where('id', $actId)->first();
|
|
|
+ if (!$act) {
|
|
|
+ Utils::throwError('20003:片段不存在');
|
|
|
+ }
|
|
|
+ $episode = DB::table('mp_anime_episodes')->where('id', $act->episode_id)->first();
|
|
|
+ $model = getProp($data, 'model') ?: getProp($episode, 'video_model', 'zhizhen-20');
|
|
|
+ if (!DB::table('mp_video_models')->where('model', $model)->where('is_enabled', 1)->exists()) {
|
|
|
+ $model = 'zhizhen-20';
|
|
|
+ }
|
|
|
+ $resolution = strtolower((string)getProp($data, 'video_resolution', ''));
|
|
|
+ if (!$resolution) {
|
|
|
+ $resolution = strtolower((string)getProp($episode, 'video_resolution', '720p'));
|
|
|
+ }
|
|
|
+ $duration = (int)getProp($data, 'video_duration', 0);
|
|
|
+ if ($duration <= 0) {
|
|
|
+ $duration = (int)ceil((float)$act->act_duration);
|
|
|
+ }
|
|
|
+ if ($duration <= 0) {
|
|
|
+ $duration = 5;
|
|
|
+ }
|
|
|
+ $items[] = [
|
|
|
+ 'id' => $act->id,
|
|
|
+ 'duration' => $duration,
|
|
|
+ 'points' => $buildCharge($act->id, $duration, $model, $resolution, $mode),
|
|
|
+ ];
|
|
|
+ } elseif (in_array($type, ['batch_segment', 'batch_act'])) {
|
|
|
+ // 批量场景
|
|
|
+ $animeId = getProp($data, 'anime_id');
|
|
|
+ $episodeId = getProp($data, 'episode_id');
|
|
|
+ if (!$animeId || !$episodeId) {
|
|
|
+ Utils::throwError('1002:anime_id和episode_id不能为空');
|
|
|
+ }
|
|
|
+ $episode = DB::table('mp_anime_episodes')->where('id', $episodeId)->where('anime_id', $animeId)->first();
|
|
|
+ if (!$episode) {
|
|
|
+ Utils::throwError('20003:分集不存在');
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($type === 'batch_segment') {
|
|
|
+ $model = getProp($data, 'model') ?: getProp($episode, 'video_model', 'doubao-seedance-1-5-pro-251215');
|
|
|
+ if (!DB::table('mp_video_models')->where('model', $model)->where('is_enabled', 1)->exists()) {
|
|
|
+ $model = 'doubao-seedance-1-5-pro-251215';
|
|
|
+ }
|
|
|
+ $segments = DB::table('mp_episode_segments')
|
|
|
+ ->where('anime_id', $animeId)
|
|
|
+ ->where('episode_id', $episodeId)
|
|
|
+ ->whereNotIn('video_task_status', ['已完成', '生成中'])
|
|
|
+ ->orderBy('segment_number')
|
|
|
+ ->get();
|
|
|
+ foreach ($segments as $segment) {
|
|
|
+ if ((int)$segment->current_type === 2) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $duration = (int)ceil((float)$segment->audio_duration);
|
|
|
+ if ($duration <= 0) {
|
|
|
+ $duration = 5;
|
|
|
+ }
|
|
|
+ $items[] = [
|
|
|
+ 'id' => $segment->segment_id,
|
|
|
+ 'duration' => $duration,
|
|
|
+ 'points' => $buildCharge($segment->segment_id, $duration, $model, '720p', $mode),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 片段批量固定智帧20
|
|
|
+ $model = 'zhizhen-20';
|
|
|
+ $acts = DB::table('mp_episode_segments')
|
|
|
+ ->where('anime_id', $animeId)
|
|
|
+ ->where('episode_id', $episodeId)
|
|
|
+ ->orderBy('act_number')
|
|
|
+ ->get();
|
|
|
+ foreach ($acts as $act) {
|
|
|
+ if ($act->video_task_status === '生成中' || (int)$act->current_type === 2) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $duration = (int)ceil((float)$act->act_duration);
|
|
|
+ if ($duration <= 0) {
|
|
|
+ $duration = 5;
|
|
|
+ }
|
|
|
+ $items[] = [
|
|
|
+ 'id' => $act->id,
|
|
|
+ 'duration' => $duration,
|
|
|
+ 'points' => $buildCharge($act->id, $duration, $model, '720p', $mode),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ Utils::throwError('1003:type参数不正确');
|
|
|
+ }
|
|
|
+
|
|
|
+ $totalPoints = 0;
|
|
|
+ foreach ($items as $item) {
|
|
|
+ $totalPoints += $item['points'];
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'type' => $type,
|
|
|
+ 'model' => $model,
|
|
|
+ 'video_resolution' => $resolution,
|
|
|
+ 'mode' => $mode,
|
|
|
+ 'count' => count($items),
|
|
|
+ 'total_points' => $totalPoints,
|
|
|
+ 'items' => $items,
|
|
|
+ 'points_balance' => $this->pointsService->getUserPointsBalance(),
|
|
|
+ 'remark' => '积分为预估值,按预估时长计算;实际扣费以视频生成完成后的实际时长为准',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 根据提示词内容智能计算最优视频时长
|
|
|
*
|
|
|
* @param string $segmentContent 分镜内容
|