From d097e9a20e6f63a26bbda7fdec150dbc834dff27 Mon Sep 17 00:00:00 2001 From: cjy Date: Thu, 15 Jan 2026 16:35:47 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=A2=9E=E5=8A=A0ai=E7=94=9F?= =?UTF-8?q?=E6=88=90=E7=AB=9E=E5=93=81=E6=8A=A5=E5=91=8A=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/router/media.go | 1 + pkg/service/ai/video_vl.go | 216 +++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+) diff --git a/pkg/router/media.go b/pkg/router/media.go index 8f8cb5c..04e8265 100644 --- a/pkg/router/media.go +++ b/pkg/router/media.go @@ -92,6 +92,7 @@ func MediaRouter(r *gin.RouterGroup) { { aiAuth.POST("one-text", serviceAI.OneText) aiAuth.POST("more-text", serviceAI.MoreText) + aiAuth.POST("generate-report", serviceAI.AICompetitorReport) } social := noAuth.Group("social") diff --git a/pkg/service/ai/video_vl.go b/pkg/service/ai/video_vl.go index bbe5707..8cdd615 100644 --- a/pkg/service/ai/video_vl.go +++ b/pkg/service/ai/video_vl.go @@ -2,6 +2,7 @@ package ai import ( "errors" + "fmt" "fonchain-fiee/pkg/common/qwen" "fonchain-fiee/pkg/service" "strings" @@ -59,3 +60,218 @@ func AIVideoVL(ctx *gin.Context) { func contains(s, substr string) bool { return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) } + +// CompetitorReportRequest 竞品报告请求参数 +type CompetitorReportRequest struct { + Videos []string `json:"videos"` // 视频URL列表 + Images []string `json:"images"` // 图片URL列表 + TextPrompt string `json:"textPrompt"` // 竞品报告要求文本 + ImagePrompt string `json:"imagePrompt"` // 图片URL + Model string `json:"model"` // 可选的模型名称,默认使用 qwen3-vl-plus +} + +// CompetitorReportResponse 竞品报告响应数据 +type CompetitorReportResponse struct { + ImageURL string `json:"image_url,omitempty"` // 生成的图片URL(1024*1024),非必须返回 + Text string `json:"text,omitempty"` // 竞品报告文本内容,非必须返回 +} + +// AICompetitorReport 生成竞品报告接口 +func AICompetitorReport(ctx *gin.Context) { + var req CompetitorReportRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + service.Error(ctx, errors.New("参数错误")) + return + } + + if req.TextPrompt == "" && req.ImagePrompt == "" { + service.Error(ctx, errors.New("文本和图片提示词不能同时为空")) + return + } + + // 检查是否至少提供了视频或图片 + if len(req.Videos) == 0 && len(req.Images) == 0 { + service.Error(ctx, errors.New("至少需要提供一个视频或图片")) + return + } + + if len(req.Videos) > 1 { + service.Error(ctx, errors.New("当前只能选一个视频")) + return + } + + // 第一步:调用AI理解视频/图片内容 + vlPrompt := "请你详细描述视频和图片中的内容分别是什么" + vlResult, err := qwen.VL(req.Videos, req.Images, vlPrompt, req.Model) + if err != nil { + // 检查是否是文件下载超时错误(内容过大) + errMsg := err.Error() + if contains(errMsg, "Download multimodal file timed out") || contains(errMsg, "timed out") { + service.Error(ctx, errors.New("报错内容过大,请重新选择")) + } else { + service.Error(ctx, fmt.Errorf("AI理解视频图片失败: %v", err)) + } + return + } + + // 获取理解后的内容 + if len(vlResult.Choices) == 0 { + service.Error(ctx, errors.New("AI理解返回结果为空")) + return + } + vlContent := vlResult.Choices[0].Message.Content + + // 定义协程结果结构 + type textResult struct { + text string + err error + } + + type imageResult struct { + imageURL string + err error + } + + // 根据 TextPrompt 和 ImagePrompt 是否为空决定启动哪些协程 + needText := req.TextPrompt != "" + needImage := req.ImagePrompt != "" + + var textChan chan textResult + var imageChan chan imageResult + + // 如果需要生成文本,启动文本生成协程 + if needText { + textChan = make(chan textResult, 1) + go func() { + // 构建文本生成提示词:理解内容 + 用户要求 + textPrompt := fmt.Sprintf("基于以下视频和图片的内容描述:\n%s\n\n请根据以下要求生成竞品报告:\n%s", vlContent, req.TextPrompt) + + chatReq, err := buildChatRequest(textPrompt, nil) + if err != nil { + textChan <- textResult{err: err} + return + } + + chatResp, err := qwen.Chat(*chatReq) + if err != nil { + textChan <- textResult{err: err} + return + } + + if len(chatResp.Choices) == 0 { + textChan <- textResult{err: errors.New("文本生成返回结果为空")} + return + } + + textChan <- textResult{text: chatResp.Choices[0].Message.Content} + }() + } + + // 如果需要生成图片,启动图片生成协程 + if needImage { + imageChan = make(chan imageResult, 1) + go func() { + // 先请求聊天获取图片提示词 + imagePromptText := fmt.Sprintf("基于以下视频和图片的内容描述:\n%s\n\n请根据以下要求生成竞品报告图片的提示词:\n%s", vlContent, req.ImagePrompt) + + chatReq, err := buildChatRequest(imagePromptText, nil) + if err != nil { + imageChan <- imageResult{err: err} + return + } + + chatResp, err := qwen.Chat(*chatReq) + if err != nil { + imageChan <- imageResult{err: err} + return + } + + if len(chatResp.Choices) == 0 { + imageChan <- imageResult{err: errors.New("图片提示词生成返回结果为空")} + return + } + + imagePrompt := chatResp.Choices[0].Message.Content + + // 生成图片(1024*1024),基于理解后的内容使用文生图 + size := "1024*1024" + resultTask, err := qwen.GenerateTextImage(imagePrompt, size) + + if err != nil { + imageChan <- imageResult{err: err} + return + } + + if resultTask.Code != "" { + imageChan <- imageResult{err: errors.New("文生图失败: " + resultTask.Message)} + return + } + + // 等待图片生成完成 + result, err := qwen.ImgTaskResult(resultTask.Output.TaskID) + if err != nil { + imageChan <- imageResult{err: err} + return + } + + if result == nil || len(result.Output.Results) == 0 { + imageChan <- imageResult{err: errors.New("图片生成失败")} + return + } + + // 返回第一张图片的URL + imageChan <- imageResult{imageURL: result.Output.Results[0].URL} + }() + } + + // 等待所有启动的协程完成 + var textRes textResult + var imageRes imageResult + + // 根据实际启动的协程数量等待结果 + if needText && needImage { + // 两个协程都启动了,使用循环等待两个都完成 + completed := 0 + for completed < 2 { + select { + case textRes = <-textChan: + completed++ + case imageRes = <-imageChan: + completed++ + } + } + } else if needText { + // 只启动文本生成协程 + textRes = <-textChan + } else if needImage { + // 只启动图片生成协程 + imageRes = <-imageChan + } + + // 处理文本结果(如果生成了文本) + if needText { + if textRes.err != nil { + service.Error(ctx, fmt.Errorf("生成竞品报告文本失败: %v", textRes.err)) + return + } + } + + // 处理图片结果(如果生成了图片) + if needImage { + if imageRes.err != nil { + service.Error(ctx, fmt.Errorf("生成竞品报告图片失败: %v", imageRes.err)) + return + } + } + + // 返回结果(只返回实际生成的内容) + result := CompetitorReportResponse{} + if needText { + result.Text = textRes.text + } + if needImage { + result.ImageURL = imageRes.imageURL + } + + service.Success(ctx, result) +}