|
@@ -5076,159 +5076,112 @@ class AnimeService
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 预估视频生成所需积分(支持单一/批量)
|
|
|
|
|
|
|
+ * 预估生成所需积分(mode=chat/image/video,支持多数量总额)
|
|
|
*
|
|
*
|
|
|
- * 传参与对应创建接口一致,另支持 type 参数区分场景:
|
|
|
|
|
- * segment-单分镜转视频、act-单片段转视频、batch_segment-分镜批量、batch_act-片段批量;
|
|
|
|
|
- * type 缺省时按 segment_id / act_id 自动推断,批量场景需显式传入。
|
|
|
|
|
|
|
+ * chat : count(默认1,AI对话单次10积分);批量分集可传 anime_id + generate_episode_number 按实际待创建集数估算
|
|
|
|
|
+ * image : model 必传;resolution(1k/2k/4k)或 width/height 自动归一化;image_num(默认1)
|
|
|
|
|
+ * video : model 必传;video_resolution(默认720p);video_duration(默认5秒);count(默认1);scene(默认video_generation)
|
|
|
*
|
|
*
|
|
|
* @param array $data
|
|
* @param array $data
|
|
|
* @return array
|
|
* @return array
|
|
|
*/
|
|
*/
|
|
|
- public function previewVideoCharge(array $data): array
|
|
|
|
|
|
|
+ public function previewCharge(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', '');
|
|
|
|
|
+ if (!in_array($mode, ['chat', 'image', 'video'])) {
|
|
|
|
|
+ Utils::throwError('1003:请传入mode参数(chat/image/video)');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- $mode = (string)getProp($data, 'mode', 'video_generation');
|
|
|
|
|
|
|
+ // 视频计费场景(video_generation/video_to_video),默认视频生成
|
|
|
|
|
+ $scene = (string)getProp($data, 'scene', 'video_generation');
|
|
|
$items = [];
|
|
$items = [];
|
|
|
$model = '';
|
|
$model = '';
|
|
|
- $resolution = '720p';
|
|
|
|
|
|
|
+ $resolution = '';
|
|
|
|
|
|
|
|
- $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';
|
|
|
|
|
|
|
+ if ($mode === 'video') {
|
|
|
|
|
+ // 通用视频预估:单价/秒 × 时长 × 数量(多个视频计算总额)
|
|
|
|
|
+ $model = (string)getProp($data, 'model', '');
|
|
|
|
|
+ if (!$model) {
|
|
|
|
|
+ Utils::throwError('1002:model参数不能为空');
|
|
|
}
|
|
}
|
|
|
- $duration = (int)ceil((float)$segment->audio_duration);
|
|
|
|
|
|
|
+ $resolution = strtolower((string)getProp($data, 'video_resolution', '720p'));
|
|
|
|
|
+ $duration = (int)getProp($data, 'video_duration', 5);
|
|
|
if ($duration <= 0) {
|
|
if ($duration <= 0) {
|
|
|
$duration = 5;
|
|
$duration = 5;
|
|
|
}
|
|
}
|
|
|
|
|
+ $count = max(1, (int)getProp($data, 'count', 1));
|
|
|
|
|
+ $pointsPerVideo = $this->pointsService->getVideoChargePoints([
|
|
|
|
|
+ 'model' => $model,
|
|
|
|
|
+ 'video_resolution' => $resolution,
|
|
|
|
|
+ 'video_duration' => $duration,
|
|
|
|
|
+ 'mode' => $scene,
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $points = $pointsPerVideo * $count;
|
|
|
$items[] = [
|
|
$items[] = [
|
|
|
- 'id' => $segment->segment_id,
|
|
|
|
|
- 'duration' => $duration,
|
|
|
|
|
- 'points' => $buildCharge($segment->segment_id, $duration, $model, '720p', $mode),
|
|
|
|
|
|
|
+ 'type' => 'video',
|
|
|
|
|
+ 'model' => $model,
|
|
|
|
|
+ 'video_resolution' => $resolution,
|
|
|
|
|
+ 'video_duration' => $duration,
|
|
|
|
|
+ 'count' => $count,
|
|
|
|
|
+ 'points' => $points,
|
|
|
];
|
|
];
|
|
|
- } 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';
|
|
|
|
|
|
|
+ } elseif ($mode === 'image') {
|
|
|
|
|
+ // 通用图片预估:单张价格 × 张数(resolution 可直传,或传 width/height 自动归一化)
|
|
|
|
|
+ $model = (string)getProp($data, 'model', '');
|
|
|
|
|
+ if (!$model) {
|
|
|
|
|
+ Utils::throwError('1002:model参数不能为空');
|
|
|
}
|
|
}
|
|
|
- $resolution = strtolower((string)getProp($data, 'video_resolution', ''));
|
|
|
|
|
|
|
+ $resolution = strtolower((string)getProp($data, 'resolution', ''));
|
|
|
if (!$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;
|
|
|
|
|
|
|
+ $width = (int)getProp($data, 'width', 0);
|
|
|
|
|
+ $height = (int)getProp($data, 'height', 0);
|
|
|
|
|
+ if ($width <= 0 || $height <= 0) {
|
|
|
|
|
+ $width = 1600;
|
|
|
|
|
+ $height = 2848;
|
|
|
|
|
+ }
|
|
|
|
|
+ $resolution = $this->pointsService->normalizeImageResolutionKey($width, $height);
|
|
|
}
|
|
}
|
|
|
|
|
+ $imageNum = max(1, (int)getProp($data, 'image_num', 1));
|
|
|
|
|
+ $points = $this->pointsService->getImageChargePoints([
|
|
|
|
|
+ 'model' => $model,
|
|
|
|
|
+ 'resolution' => $resolution,
|
|
|
|
|
+ ]) * $imageNum;
|
|
|
$items[] = [
|
|
$items[] = [
|
|
|
- 'id' => $act->id,
|
|
|
|
|
- 'duration' => $duration,
|
|
|
|
|
- 'points' => $buildCharge($act->id, $duration, $model, $resolution, $mode),
|
|
|
|
|
|
|
+ 'type' => 'image',
|
|
|
|
|
+ 'model' => $model,
|
|
|
|
|
+ 'resolution' => $resolution,
|
|
|
|
|
+ 'image_num' => $imageNum,
|
|
|
|
|
+ 'points' => $points,
|
|
|
];
|
|
];
|
|
|
- } elseif (in_array($type, ['batch_segment', 'batch_act'])) {
|
|
|
|
|
- // 批量场景
|
|
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // AI对话预估:单次10积分;批量生成分集传 anime_id + generate_episode_number 时按实际待创建集数估算
|
|
|
|
|
+ $count = max(1, (int)getProp($data, 'count', 1));
|
|
|
$animeId = getProp($data, 'anime_id');
|
|
$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')
|
|
|
|
|
|
|
+ $generateEpisodes = (int)getProp($data, 'generate_episode_number', 0);
|
|
|
|
|
+ if ($animeId && $generateEpisodes > 0) {
|
|
|
|
|
+ $currentMaxEpisode = DB::table('mp_anime_episodes')
|
|
|
->where('anime_id', $animeId)
|
|
->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('is_default', 1)
|
|
|
|
|
+ ->max('episode_number');
|
|
|
|
|
+ $currentMaxEpisode = $currentMaxEpisode ?: 0;
|
|
|
|
|
+ $startEpisodeNumber = $currentMaxEpisode + 1;
|
|
|
|
|
+ $endEpisodeNumber = $startEpisodeNumber + $generateEpisodes - 1;
|
|
|
|
|
+ $existingSkipped = DB::table('mp_batch_episode_generation_details')
|
|
|
->where('anime_id', $animeId)
|
|
->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),
|
|
|
|
|
- ];
|
|
|
|
|
|
|
+ ->whereBetween('episode_number', [$startEpisodeNumber, $endEpisodeNumber])
|
|
|
|
|
+ ->whereIn('status', ['pending', 'processing', 'completed'])
|
|
|
|
|
+ ->count();
|
|
|
|
|
+ $count = ($endEpisodeNumber - $startEpisodeNumber + 1) - $existingSkipped;
|
|
|
|
|
+ if ($count < 0) {
|
|
|
|
|
+ $count = 0;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- } else {
|
|
|
|
|
- Utils::throwError('1003:type参数不正确');
|
|
|
|
|
|
|
+ $points = PointsService::CHAT_CHARGE_POINTS * $count;
|
|
|
|
|
+ $items[] = [
|
|
|
|
|
+ 'type' => 'chat',
|
|
|
|
|
+ 'count' => $count,
|
|
|
|
|
+ 'points' => $points,
|
|
|
|
|
+ ];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
$totalPoints = 0;
|
|
$totalPoints = 0;
|
|
@@ -5237,15 +5190,11 @@ class AnimeService
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return [
|
|
return [
|
|
|
- 'type' => $type,
|
|
|
|
|
- 'model' => $model,
|
|
|
|
|
- 'video_resolution' => $resolution,
|
|
|
|
|
'mode' => $mode,
|
|
'mode' => $mode,
|
|
|
- 'count' => count($items),
|
|
|
|
|
'total_points' => $totalPoints,
|
|
'total_points' => $totalPoints,
|
|
|
'items' => $items,
|
|
'items' => $items,
|
|
|
'points_balance' => $this->pointsService->getUserPointsBalance(),
|
|
'points_balance' => $this->pointsService->getUserPointsBalance(),
|
|
|
- 'remark' => '积分为预估值,按预估时长计算;实际扣费以视频生成完成后的实际时长为准',
|
|
|
|
|
|
|
+ 'remark' => '积分为预估值,实际扣费以生成完成后的实际消耗为准',
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
|
|
|
|