|
|
@@ -7,6 +7,7 @@ use App\Facade\Site;
|
|
|
use App\Libs\Utils;
|
|
|
use App\Models\MpGeneratePicTask;
|
|
|
use App\Services\AIGeneration\AIImageGenerationService;
|
|
|
+use App\Services\AIGeneration\AIVideoGenerationService;
|
|
|
use Dflydev\DotAccessData\Util;
|
|
|
use GuzzleHttp\Client;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
@@ -18,12 +19,17 @@ use OSS\OssClient;
|
|
|
class AnimeService
|
|
|
{
|
|
|
protected $aiImageGenerationService;
|
|
|
+ protected $aiVideoGenerationService;
|
|
|
private $url;
|
|
|
private $api_key;
|
|
|
private $headers;
|
|
|
|
|
|
- public function __construct(AIImageGenerationService $aiImageGenerationService) {
|
|
|
+ public function __construct(
|
|
|
+ AIImageGenerationService $aiImageGenerationService,
|
|
|
+ AIVideoGenerationService $aiVideoGenerationService
|
|
|
+ ) {
|
|
|
$this->aiImageGenerationService = $aiImageGenerationService;
|
|
|
+ $this->aiVideoGenerationService = $aiVideoGenerationService;
|
|
|
$this->url = 'https://api.deepseek.com/chat/completions';
|
|
|
$this->api_key = env('DEEPSEEK_API_KEY');
|
|
|
$this->headers = [
|
|
|
@@ -1390,6 +1396,238 @@ class AnimeService
|
|
|
}
|
|
|
|
|
|
public function createSegmentVideoTask($data) {
|
|
|
+ $segment_id = getProp($data, 'segment_id');
|
|
|
+ $tail_frame = getProp($data, 'tail_frame');
|
|
|
+
|
|
|
+ if (!$segment_id) {
|
|
|
+ Utils::throwError('1002:分镜ID不能为空');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取分镜信息
|
|
|
+ $segment = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->first();
|
|
|
+ if (!$segment) {
|
|
|
+ Utils::throwError('20003:分镜不存在');
|
|
|
+ }
|
|
|
+
|
|
|
+ $segment = (array)$segment;
|
|
|
+
|
|
|
+ // 获取分镜内容
|
|
|
+ $segmentContent = getProp($segment, 'segment_content', '');
|
|
|
+
|
|
|
+ // 如果没有传入尾帧描述,使用分镜表中的尾帧描述
|
|
|
+ if (!$tail_frame) {
|
|
|
+ $tail_frame = getProp($segment, 'tail_frame', '');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建完整的提示词
|
|
|
+ $fullPrompt = $segmentContent;
|
|
|
+ if ($tail_frame) {
|
|
|
+ $fullPrompt .= "\n尾帧描述:$tail_frame";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 智能选择视频时长
|
|
|
+ $videoDuration = $this->calculateOptimalVideoDuration($segmentContent, $tail_frame);
|
|
|
+
|
|
|
+ // 构建视频生成参数
|
|
|
+ $videoParams = [
|
|
|
+ 'model' => 'doubao-seedance-1-5-pro-251215',
|
|
|
+ 'prompt' => $fullPrompt,
|
|
|
+ 'video_duration' => $videoDuration,
|
|
|
+ 'video_resolution' => '720P',
|
|
|
+ 'seed' => -1,
|
|
|
+ 'ratio' => '16:9',
|
|
|
+ 'generate_audio' => false,
|
|
|
+ 'draft' => false,
|
|
|
+ 'watermark' => false,
|
|
|
+ 'camera_fixed' => false,
|
|
|
+ 'callback_url' => 'http://mpaudio.yqsd.cn'
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 如果分镜有图片,作为首帧
|
|
|
+ if (getProp($segment, 'img_url')) {
|
|
|
+ $videoParams['first_frame_url'] = getProp($segment, 'img_url');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建content数组
|
|
|
+ $videoParams['content'] = [
|
|
|
+ [
|
|
|
+ 'type' => 'text',
|
|
|
+ 'text' => $videoParams['prompt'],
|
|
|
+ ]
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 如果有首帧图片,添加到content中
|
|
|
+ if (isset($videoParams['first_frame_url'])) {
|
|
|
+ $videoParams['content'][] = [
|
|
|
+ 'type' => 'image_url',
|
|
|
+ 'image_url' => [
|
|
|
+ 'url' => $videoParams['first_frame_url'],
|
|
|
+ ],
|
|
|
+ 'role' => 'first_frame',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // dd($videoParams);
|
|
|
+
|
|
|
+ // 创建视频生成任务
|
|
|
+ $task = $this->aiVideoGenerationService->createSeedanceTask($videoParams);
|
|
|
+
|
|
|
+ // 更新分镜表的视频任务信息
|
|
|
+ DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([
|
|
|
+ 'video_task_id' => $task->id,
|
|
|
+ 'video_task_status' => '生成中',
|
|
|
+ 'updated_at' => date('Y-m-d H:i:s')
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'task_id' => $task->id,
|
|
|
+ 'status' => $task->status,
|
|
|
+ 'segment_id' => $segment_id,
|
|
|
+ 'video_duration' => $videoDuration
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据提示词内容智能计算最优视频时长
|
|
|
+ *
|
|
|
+ * @param string $segmentContent 分镜内容
|
|
|
+ * @param string $tailFrame 尾帧描述
|
|
|
+ * @return int 视频时长(2-12秒)
|
|
|
+ */
|
|
|
+ private function calculateOptimalVideoDuration($segmentContent, $tailFrame = '') {
|
|
|
+ // 合并所有文本内容
|
|
|
+ $fullText = trim($segmentContent . ' ' . $tailFrame);
|
|
|
+
|
|
|
+ // 如果没有内容,返回默认时长
|
|
|
+ if (empty($fullText)) {
|
|
|
+ return 5;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算文本长度(中文字符按2个字符计算)
|
|
|
+ $textLength = mb_strlen($fullText, 'UTF-8');
|
|
|
+ $chineseCharCount = preg_match_all('/[\x{4e00}-\x{9fff}]/u', $fullText);
|
|
|
+ $adjustedLength = $textLength + $chineseCharCount; // 中文字符权重更高
|
|
|
+
|
|
|
+ // 分析内容复杂度
|
|
|
+ $complexityScore = $this->analyzeContentComplexity($fullText);
|
|
|
+
|
|
|
+ // 基础时长计算(根据文本长度)
|
|
|
+ $baseDuration = 3; // 默认2秒
|
|
|
+
|
|
|
+ // if ($adjustedLength <= 20) {
|
|
|
+ // $baseDuration = 3;
|
|
|
+ // } elseif ($adjustedLength <= 40) {
|
|
|
+ // $baseDuration = 4;
|
|
|
+ // } elseif ($adjustedLength <= 60) {
|
|
|
+ // $baseDuration = 5;
|
|
|
+ // } elseif ($adjustedLength <= 80) {
|
|
|
+ // $baseDuration = 6;
|
|
|
+ // } elseif ($adjustedLength <= 100) {
|
|
|
+ // $baseDuration = 7;
|
|
|
+ // } elseif ($adjustedLength <= 120) {
|
|
|
+ // $baseDuration = 8;
|
|
|
+ // } else {
|
|
|
+ // $baseDuration = 9;
|
|
|
+ // }
|
|
|
+
|
|
|
+ // 根据内容复杂度调整时长
|
|
|
+ $finalDuration = $baseDuration + $complexityScore;
|
|
|
+
|
|
|
+ // 确保时长在2-12秒范围内
|
|
|
+ return max(2, min(12, $finalDuration));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 分析内容复杂度,返回时长调整值
|
|
|
+ *
|
|
|
+ * @param string $text 文本内容
|
|
|
+ * @return int 调整值(-2到+3)
|
|
|
+ */
|
|
|
+ private function analyzeContentComplexity($text) {
|
|
|
+ $score = 0;
|
|
|
+
|
|
|
+ // 动作关键词(需要更长时间展现)
|
|
|
+ $actionKeywords = [
|
|
|
+ '跑步', '奔跑', '飞行', '跳跃', '战斗', '打斗', '追逐', '逃跑', '舞蹈', '表演',
|
|
|
+ '变化', '变身', '爆炸', '崩塌', '建造', '制作', '烹饪', '绘画', '写字',
|
|
|
+ '移动', '旋转', '翻滚', '攀爬', '游泳', '潜水', '滑行', '飞翔'
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 静态关键词(可以用较短时间)
|
|
|
+ $staticKeywords = [
|
|
|
+ '站立', '坐着', '躺着', '静止', '凝视', '思考', '沉思', '观察', '等待',
|
|
|
+ '睡觉', '休息', '静坐', '冥想', '阅读', '听音乐'
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 复杂场景关键词(需要更长时间)
|
|
|
+ $complexSceneKeywords = [
|
|
|
+ '人群', '聚会', '会议', '战争', '庆典', '仪式', '表演', '比赛', '竞技',
|
|
|
+ '多人', '群体', '团队', '合作', '互动', '对话', '争论', '讨论'
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 情感关键词(需要适中时间表现)
|
|
|
+ $emotionKeywords = [
|
|
|
+ '哭泣', '笑容', '愤怒', '惊讶', '恐惧', '悲伤', '喜悦', '兴奋', '紧张',
|
|
|
+ '感动', '震惊', '困惑', '失望', '希望', '绝望', '温柔', '严肃'
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 检查动作关键词
|
|
|
+ foreach ($actionKeywords as $keyword) {
|
|
|
+ if (strpos($text, $keyword) !== false) {
|
|
|
+ $score += 1;
|
|
|
+ break; // 避免重复加分
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查静态关键词
|
|
|
+ foreach ($staticKeywords as $keyword) {
|
|
|
+ if (strpos($text, $keyword) !== false) {
|
|
|
+ $score -= 1;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查复杂场景关键词
|
|
|
+ foreach ($complexSceneKeywords as $keyword) {
|
|
|
+ if (strpos($text, $keyword) !== false) {
|
|
|
+ $score += 1;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查情感关键词
|
|
|
+ foreach ($emotionKeywords as $keyword) {
|
|
|
+ if (strpos($text, $keyword) !== false) {
|
|
|
+ $score += 1;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查时间相关词汇
|
|
|
+ if (preg_match('/缓慢|慢慢|渐渐|逐渐|慢动作/', $text)) {
|
|
|
+ $score += 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (preg_match('/快速|迅速|急速|瞬间|立刻|马上/', $text)) {
|
|
|
+ $score -= 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查转场词汇
|
|
|
+ if (preg_match('/然后|接着|随后|紧接着|同时|与此同时/', $text)) {
|
|
|
+ $score += 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查环境描述(复杂环境需要更多时间)
|
|
|
+ if (preg_match('/风景|景色|环境|背景|氛围|光线|阴影|色彩|细节/', $text)) {
|
|
|
+ $score += 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查对话内容(对话需要更多时间)
|
|
|
+ if (preg_match('/说|讲|告诉|回答|询问|问|对话|交谈/', $text)) {
|
|
|
+ $score += 1;
|
|
|
+ }
|
|
|
|
|
|
+ // 限制调整范围
|
|
|
+ return max(-1, min(4, $score));
|
|
|
}
|
|
|
}
|