Compare commits
1
Commits
dev
...
dev-HyperFrames
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff0beb0f34 |
@@ -87,3 +87,8 @@ hyperframes:
|
|||||||
# 是否启用 headless 模式
|
# 是否启用 headless 模式
|
||||||
headless: true
|
headless: true
|
||||||
|
|
||||||
|
# HyperFrames 远程渲染服务配置(独立微服务)
|
||||||
|
hyperframes_service:
|
||||||
|
# 服务地址,留空则使用默认服务名 (http://127.0.0.1:3029)
|
||||||
|
address: "http://127.0.0.1:3029"
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package video
|
package video
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"io"
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -205,10 +207,10 @@ func (s *captionService) processTask(user *beans.User, taskID string, videoURLs
|
|||||||
}
|
}
|
||||||
g.Log().Infof(bgCtx, "[字幕 %s] HTML生成完成: %s", taskID, htmlPath)
|
g.Log().Infof(bgCtx, "[字幕 %s] HTML生成完成: %s", taskID, htmlPath)
|
||||||
|
|
||||||
// 10. 执行 HyperFrames 渲染
|
// 10. 远程渲染:打包项目 → 上传 OSS → 调用 hyperframes-service → 下载结果
|
||||||
outputPath := filepath.Join(projectDir, "output.mp4")
|
outputPath := filepath.Join(projectDir, "hf_output.mp4")
|
||||||
if err := s.runHyperFramesRender(bgCtx, projectDir, outputPath); err != nil {
|
if err := s.renderViaRemoteService(bgCtx, taskID, projectDir, outputPath); err != nil {
|
||||||
errMsg := fmt.Sprintf("视频渲染失败: %v", err)
|
errMsg := fmt.Sprintf("远程渲染失败: %v", err)
|
||||||
g.Log().Errorf(bgCtx, "[字幕 %s] %s", taskID, errMsg)
|
g.Log().Errorf(bgCtx, "[字幕 %s] %s", taskID, errMsg)
|
||||||
if err := dao.CaptionTask.UpdateError(bgCtx, taskID, errMsg); err != nil {
|
if err := dao.CaptionTask.UpdateError(bgCtx, taskID, errMsg); err != nil {
|
||||||
g.Log().Errorf(bgCtx, "[字幕 %s] 更新失败状态到数据库出错: %v", taskID, err)
|
g.Log().Errorf(bgCtx, "[字幕 %s] 更新失败状态到数据库出错: %v", taskID, err)
|
||||||
@@ -266,56 +268,128 @@ func (s *captionService) processTask(user *beans.User, taskID string, videoURLs
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// runHyperFramesRender 执行 HyperFrames 渲染
|
// ---------- 远程渲染(通过 hyperframes-service 微服务) ----------
|
||||||
func (s *captionService) runHyperFramesRender(ctx context.Context, projectDir, outputPath string) error {
|
|
||||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Minute)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 使用全局 hyperframes 命令(已在 setup 中安装检查)
|
// renderViaRemoteService 通过远程 hyperframes-service 执行渲染
|
||||||
hyperframesPath, lookErr := exec.LookPath("hyperframes")
|
// 流程:打包项目 ZIP → 直接 POST 给 hyperframes-service(multipart)→ 保存返回的 MP4
|
||||||
if lookErr != nil {
|
func (s *captionService) renderViaRemoteService(ctx context.Context, taskID, projectDir, outputPath string) error {
|
||||||
return fmt.Errorf("hyperframes 未安装, 请运行: npm install -g hyperframes")
|
// 1. 打包项目目录为 ZIP
|
||||||
|
zipPath := filepath.Join(projectDir, "project.zip")
|
||||||
|
if err := s.zipProjectDir(projectDir, zipPath); err != nil {
|
||||||
|
return fmt.Errorf("打包项目文件失败: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(zipPath)
|
||||||
|
|
||||||
|
// 2. 获取 hyperframes-service 地址
|
||||||
|
serviceAddr := g.Cfg().MustGet(ctx, "hyperframes_service.address", "").String()
|
||||||
|
if serviceAddr == "" {
|
||||||
|
serviceAddr = "http://127.0.0.1:3029"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接在工作目录运行 hyperframes render,输出默认 output.mp4
|
// 3. 将 ZIP 作为 multipart 发送到渲染服务(同步渲染)
|
||||||
cmd := exec.CommandContext(ctxWithTimeout, hyperframesPath, "render")
|
var reqBuf bytes.Buffer
|
||||||
cmd.Dir = projectDir
|
mw := multipart.NewWriter(&reqBuf)
|
||||||
cmd.Env = append(os.Environ(),
|
|
||||||
"HYPERFRAMES_HEADLESS=true",
|
|
||||||
"HYPERFRAMES_BROWSER_PATH=/usr/bin/chromium",
|
|
||||||
)
|
|
||||||
|
|
||||||
output, err := cmd.CombinedOutput()
|
fw, err := mw.CreateFormFile("file", "project.zip")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("hyperframes render 失败: %v\n%s", err, string(output))
|
return fmt.Errorf("创建表单文件字段失败: %v", err)
|
||||||
}
|
}
|
||||||
g.Log().Infof(ctx, "[HyperFrames] render 完成: %s", strings.TrimSpace(string(output)))
|
|
||||||
|
|
||||||
// 查找输出文件:优先 renders/ 目录(HyperFrames 默认输出位置)
|
zipFile, err := os.Open(zipPath)
|
||||||
rendersDir := filepath.Join(projectDir, "renders")
|
if err != nil {
|
||||||
if entries, readErr := os.ReadDir(rendersDir); readErr == nil {
|
return fmt.Errorf("打开ZIP文件失败: %v", err)
|
||||||
for _, entry := range entries {
|
}
|
||||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".mp4") {
|
if _, err = io.Copy(fw, zipFile); err != nil {
|
||||||
src := filepath.Join(rendersDir, entry.Name())
|
zipFile.Close()
|
||||||
if err := os.Rename(src, outputPath); err != nil {
|
mw.Close()
|
||||||
return fmt.Errorf("移动输出文件失败: %v", err)
|
return fmt.Errorf("写入ZIP内容失败: %v", err)
|
||||||
}
|
}
|
||||||
g.Log().Infof(ctx, "[HyperFrames] 找到输出文件: %s", entry.Name())
|
zipFile.Close()
|
||||||
return nil
|
mw.Close()
|
||||||
}
|
|
||||||
|
g.Log().Infof(ctx, "[字幕 %s] 发送渲染请求, ZIP大小=%d", taskID, reqBuf.Len())
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 60 * time.Minute}
|
||||||
|
resp, err := client.Post(serviceAddr+"/render", mw.FormDataContentType(), &reqBuf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("调用渲染服务失败: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("渲染服务返回错误(%d): %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 保存响应体(MP4)到本地
|
||||||
|
outFile, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建输出文件失败: %v", err)
|
||||||
|
}
|
||||||
|
defer outFile.Close()
|
||||||
|
|
||||||
|
written, err := io.Copy(outFile, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("保存渲染结果失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Log().Infof(ctx, "[字幕 %s] ✅ 远程渲染完成, 输出=%s (%d bytes)", taskID, outputPath, written)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// zipProjectDir 将项目目录打包为 ZIP 文件
|
||||||
|
func (s *captionService) zipProjectDir(sourceDir, targetPath string) error {
|
||||||
|
zipFile, err := os.Create(targetPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建ZIP文件失败: %v", err)
|
||||||
|
}
|
||||||
|
defer zipFile.Close()
|
||||||
|
|
||||||
|
writer := zip.NewWriter(zipFile)
|
||||||
|
defer writer.Close()
|
||||||
|
|
||||||
|
return filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 回退:检查 projectDir 下的 output.mp4
|
// 跳过 ZIP 文件自身
|
||||||
fallback := filepath.Join(projectDir, "output.mp4")
|
if path == targetPath {
|
||||||
if _, statErr := os.Stat(fallback); statErr == nil {
|
return nil
|
||||||
if err := os.Rename(fallback, outputPath); err != nil {
|
|
||||||
return fmt.Errorf("移动输出文件失败: %v", err)
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("未找到输出文件(已在 %s 和 %s 中查找)", rendersDir, fallback)
|
// 计算相对路径
|
||||||
|
relPath, err := filepath.Rel(sourceDir, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
// 跳过空目录(HyperFrames 渲染只需要文件)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := zip.FileInfoHeader(info)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建ZIP头失败: %v", err)
|
||||||
|
}
|
||||||
|
header.Name = relPath
|
||||||
|
header.Method = zip.Deflate
|
||||||
|
|
||||||
|
fw, err := writer.CreateHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建ZIP条目失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("打开文件失败: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(fw, file)
|
||||||
|
return err
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildHTML 生成 HyperFrames HTML 模板
|
// buildHTML 生成 HyperFrames HTML 模板
|
||||||
|
|||||||
Reference in New Issue
Block a user