代码初始化

This commit is contained in:
2026-05-20 11:32:39 +08:00
parent 219b7e39c7
commit e76bf57d54
20 changed files with 1585 additions and 309 deletions

View File

@@ -2,105 +2,64 @@ package audio
import (
"context"
"encoding/json"
"strings"
common "media/controller/common"
dto "media/model/dto/audio"
service "media/service/asr"
"gitea.com/red-future/common/beans"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
)
type audio struct{}
var AudioExtract = new(audio)
// safeResult 对外输出的识别结果(隐藏内部路径)
type safeResult struct {
Text string `json:"text"`
Model string `json:"model"`
Language string `json:"language"`
AudioSize int64 `json:"audioSize"`
AudioDuration string `json:"audioDuration"`
Scenes *dto.SceneSummaryDTO `json:"scenes,omitempty"`
}
// safeItem 对外输出的单视频结果
type safeItem struct {
FileName string `json:"fileName"`
Result *safeResult `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
// TranscribeHandler 语音转文字+分镜分析
// 支持两种入参方式:
// 1. JSON body: {"video_urls":[...], "model":"medium", "language":"zh", "threshold":0.3}
// 2. 文件上传: files 参数(兼容单/多文件)
func (c *audio) TranscribeHandler(r *ghttp.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin"})
// 优先尝试 JSON bodyURL 列表模式)
body := r.GetBody()
if len(body) > 0 && body[0] == '{' {
var req dto.TranscribeReq
if json.Unmarshal(body, &req) == nil && len(req.VideoURLs) > 0 {
// 填充默认值
if req.Model == "" {
req.Model = g.Cfg().MustGet(ctx, "whisper.model", "medium").String()
}
if req.Language == "" {
req.Language = g.Cfg().MustGet(ctx, "whisper.language", "zh").String()
}
if req.Threshold <= 0 {
req.Threshold = 0.3
}
res, svcErr := service.VideoTranscribe.TranscribeWithURLs(ctx, &req)
if svcErr != nil {
r.Response.WriteJson(g.Map{"code": 500, "message": svcErr.Error()})
return
}
r.Response.WriteJson(g.Map{"code": 200, "message": "success", "data": toSafeItems(res.Results)})
return
}
// Create 创建语音转文字异步任务 POST /audio/transcribe
func (c *audio) Create(ctx context.Context, req *dto.TranscribeReq) (res *dto.CreateTaskRes, err error) {
ctx = withUser(ctx)
fileNames := make([]string, len(req.VideoURLs))
for i, u := range req.VideoURLs {
parts := strings.Split(u, "/")
fileNames[i] = parts[len(parts)-1]
}
// 文件上传模式
savePaths, err := common.SaveUploadedFiles(r)
if err != nil || len(savePaths) == 0 {
r.Response.WriteJson(g.Map{"code": 400, "message": "请上传视频文件( multipart )或提供 video_urls( JSON )"})
return
g.Log().Infof(ctx, "收到转写请求, 回调URL: %s", req.CallbackURL)
params := &service.CreateTaskParams{
InputData: req.VideoURLs,
FileNames: fileNames,
Model: req.Model,
Language: req.Language,
Threshold: req.Threshold,
CallbackURL: req.CallbackURL,
}
results := service.VideoTranscribe.TranscribeUpload(ctx, savePaths,
r.Get("model", g.Cfg().MustGet(ctx, "whisper.model", "medium").String()).String(),
r.Get("language", g.Cfg().MustGet(ctx, "whisper.language", "zh").String()).String(),
r.Get("threshold", 0.3).Float64())
r.Response.WriteJson(g.Map{"code": 200, "message": "success", "data": toSafeItems(results)})
return service.AudioTask.Create(ctx, params)
}
// toSafeItems 将结果转为安全的响应格式(移除 audioPath 等内部路径)
func toSafeItems(results []dto.TranscribeItem) []safeItem {
var items []safeItem
for _, item := range results {
si := safeItem{FileName: item.FileName, Error: item.Error}
if item.Result != nil {
if r, ok := item.Result.(*dto.TranscribeResult); ok {
si.Result = &safeResult{
Text: r.Text,
Model: r.Model,
Language: r.Language,
AudioSize: r.AudioSize,
AudioDuration: r.AudioDuration,
Scenes: r.Scenes,
}
}
}
items = append(items, si)
// GetTask 获取任务详情 GET /audio/task/{taskId}
func (c *audio) GetTask(ctx context.Context, req *dto.GetTaskReq) (res *dto.GetTaskRes, err error) {
ctx = withUser(ctx)
return service.AudioTask.GetTask(ctx, req)
}
// GetProgress 获取任务进度 GET /audio/task/{taskId}/progress
func (c *audio) GetProgress(ctx context.Context, req *dto.GetProgressReq) (res *dto.GetProgressRes, err error) {
ctx = withUser(ctx)
return service.AudioTask.GetProgress(ctx, req)
}
// ListTasks 获取任务列表 GET /audio/tasks
func (c *audio) ListTasks(ctx context.Context, req *dto.ListTaskReq) (res *dto.ListTaskRes, err error) {
ctx = withUser(ctx)
return service.AudioTask.ListTasks(ctx, req)
}
// withUser 为 context 注入默认用户(无认证基础设施时使用)
func withUser(ctx context.Context) context.Context {
if ctx.Value("user") == nil {
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1})
}
return items
return ctx
}

View File

@@ -55,6 +55,15 @@ func SaveUploadedFiles(r *ghttp.Request) ([]string, error) {
return saved, nil
}
// SaveUploadedFilesFromCtx 从请求上下文中获取上传文件并保存
func SaveUploadedFilesFromCtx(ctx context.Context) ([]string, error) {
r := g.RequestFromCtx(ctx)
if r == nil {
return nil, fmt.Errorf("无法获取请求上下文")
}
return SaveUploadedFiles(r)
}
func getTempDir(ctx context.Context) string {
tempDir := g.Cfg().MustGet(ctx, "ffmpeg.temp_dir", "resource/temp").String()
if tempDir == "" {

View File

@@ -2,7 +2,6 @@ package video
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
@@ -18,99 +17,98 @@ import (
"gitea.com/red-future/common/beans"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
)
type video struct{}
var Concat = new(video)
// ConcatVideosHandler 视频拼接
// 支持两种入参方式:
// 1. JSON body: {"video_urls":[...], "method":"auto"}
// 2. 文件上传: files 参数至少2个视频
func (c *video) ConcatVideosHandler(r *ghttp.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin"})
// 优先尝试 JSON bodyURL 列表模式)
body := r.GetBody()
if len(body) > 0 && body[0] == '{' {
var req dto.ConcatReq
if json.Unmarshal(body, &req) == nil && len(req.VideoURLs) >= 2 {
if req.Method == "" {
req.Method = "auto"
}
tempDir := g.Cfg().MustGet(ctx, "ffmpeg.temp_dir", "resource/temp").String()
if !filepath.IsAbs(tempDir) {
absDir, _ := filepath.Abs(tempDir)
tempDir = absDir
}
os.MkdirAll(tempDir, 0755)
var savePaths []string
for _, videoURL := range req.VideoURLs {
savePath, dlErr := downloadFromURL(ctx, videoURL, tempDir)
if dlErr != nil {
continue
}
savePaths = append(savePaths, savePath)
}
if len(savePaths) < 2 {
cleanupConcat(savePaths)
r.Response.WriteJson(g.Map{"code": 400, "message": "成功下载的视频不足2个"})
return
}
svcRes, svcErr := service.Concat.Concat(ctx, &service.ConcatReq{
VideoPaths: savePaths,
Method: req.Method,
})
cleanupConcat(savePaths)
if svcErr != nil {
r.Response.WriteJson(g.Map{"code": 500, "message": "视频拼接失败: " + svcErr.Error()})
return
}
r.Response.WriteJson(g.Map{
"code": 200,
"message": "success",
"data": g.Map{
"outputPath": svcRes.OutputPath,
"fileSize": svcRes.FileSize,
"duration": svcRes.Duration,
"durationStr": svcRes.DurationStr,
"methodUsed": svcRes.MethodUsed,
"inputFiles": svcRes.InputFiles,
},
})
return
}
// Concat 视频拼接URL模式 POST /video/concat
func (c *video) Concat(ctx context.Context, req *dto.ConcatReq) (res *dto.ConcatRes, err error) {
ctx = withUser(ctx)
if req.Method == "" {
req.Method = "auto"
}
// 文件上传模式
savePaths, err := common.SaveUploadedFiles(r)
if err != nil || len(savePaths) < 2 {
r.Response.WriteJson(g.Map{"code": 400, "message": fmt.Sprintf("至少需要2个视频当前%d个", len(savePaths))})
return
savePaths, err := downloadVideos(ctx, req.VideoURLs)
if err != nil {
return nil, err
}
defer cleanupConcat(savePaths)
svcRes, svcErr := service.Concat.Concat(ctx, &service.ConcatReq{
svcRes, err := service.Concat.Concat(ctx, &service.ConcatReq{
VideoPaths: savePaths,
Method: r.Get("method", "auto").String(),
Method: req.Method,
Upload: req.Upload,
})
service.CleanupConcat(savePaths)
if svcErr != nil {
r.Response.WriteJson(g.Map{"code": 500, "message": "视频拼接失败: " + svcErr.Error()})
return
if err != nil {
return nil, err
}
return toDTORes(svcRes), nil
}
// ConcatUpload 视频拼接(文件上传模式) POST /video/concat/upload
func (c *video) ConcatUpload(ctx context.Context, req *dto.ConcatUploadReq) (res *dto.ConcatRes, err error) {
ctx = withUser(ctx)
savePaths, err := common.SaveUploadedFilesFromCtx(ctx)
if err != nil || len(savePaths) < 2 {
return nil, fmt.Errorf("至少需要2个视频当前%d个", len(savePaths))
}
defer service.CleanupConcat(savePaths)
if req.Method == "" {
req.Method = "auto"
}
r.Response.ServeFile(svcRes.OutputPath)
go func(path string) {
time.Sleep(5 * time.Second)
os.Remove(path)
}(svcRes.OutputPath)
svcRes, err := service.Concat.Concat(ctx, &service.ConcatReq{
VideoPaths: savePaths,
Method: req.Method,
Upload: req.Upload,
})
if err != nil {
return nil, err
}
return toDTORes(svcRes), nil
}
// withUser 为 context 注入默认用户(无认证基础设施时使用)
func withUser(ctx context.Context) context.Context {
if ctx.Value("user") == nil {
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1})
}
return ctx
}
// toDTORes 将 Service 内部响应类型转换为 DTO 响应类型
func toDTORes(svcRes *service.ConcatRes) *dto.ConcatRes {
return &dto.ConcatRes{
OutputPath: svcRes.OutputPath,
FileSize: svcRes.FileSize,
Duration: svcRes.Duration,
DurationStr: svcRes.DurationStr,
MethodUsed: svcRes.MethodUsed,
InputFiles: svcRes.InputFiles,
FileURL: svcRes.FileURL,
}
}
// downloadVideos 下载视频URL列表
func downloadVideos(ctx context.Context, videoURLs []string) ([]string, error) {
tempDir := getTempDir(ctx)
os.MkdirAll(tempDir, 0755)
var savePaths []string
for _, videoURL := range videoURLs {
savePath, dlErr := downloadFromURL(ctx, videoURL, tempDir)
if dlErr != nil {
continue
}
savePaths = append(savePaths, savePath)
}
if len(savePaths) < 2 {
return savePaths, fmt.Errorf("成功下载的视频不足2个")
}
return savePaths, nil
}
func downloadFromURL(ctx context.Context, rawURL, tempDir string) (string, error) {
@@ -154,3 +152,15 @@ func cleanupConcat(paths []string) {
os.Remove(p)
}
}
func getTempDir(ctx context.Context) string {
tempDir := g.Cfg().MustGet(ctx, "ffmpeg.temp_dir", "resource/temp").String()
if tempDir == "" {
tempDir = "resource/temp"
}
if !filepath.IsAbs(tempDir) {
absDir, _ := filepath.Abs(tempDir)
tempDir = absDir
}
return tempDir
}