package ai import ( "errors" "fmt" "fonchain-fiee/pkg/common/qwen" "fonchain-fiee/pkg/service" "strings" "github.com/gin-gonic/gin" ) // VideoVLRequest 视频/图片理解请求参数 type VideoVLRequest struct { Videos []string `json:"videos"` // 视频URL列表 Images []string `json:"images"` // 图片URL列表 Text string `json:"text"` // 可选的文本提示 Model string `json:"model"` // 可选的模型名称,默认使用 qwen3-vl-plus } // AIVideoVL AI理解视频/图片接口 func AIVideoVL(ctx *gin.Context) { var req VideoVLRequest if err := ctx.ShouldBindJSON(&req); err != nil { 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 } Prompt := "请你详细描述视频和图片中的内容分别是什么" // 调用VL函数进行AI理解 result, err := qwen.VL(req.Videos, req.Images, Prompt, 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, errors.New("ai分析帖子内容失败")) } return } // 返回AI返回的数据 service.Success(ctx, result) } // contains 检查字符串是否包含子字符串(不区分大小写) 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请根据以下要求生成竞品报告:首先除非我在下面特殊要求里面特别要求,否则不要输出markdown格式,直接输出纯文本\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) }