fonchain-fiee/pkg/service/ai/video_vl.go

278 lines
7.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"` // 生成的图片URL1024*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)
}