diff --git a/service/video/scene_split_service.go b/service/video/scene_split_service.go index ed785d5..784704c 100644 --- a/service/video/scene_split_service.go +++ b/service/video/scene_split_service.go @@ -248,11 +248,56 @@ func (s *sceneSplitService) detectScenes(ctx context.Context, videoPath, outputJ } // lookupPython 查找真实可用的 Python 可执行文件 -// Windows 上 %LOCALAPPDATA%\Microsoft\WindowsApps\ 下存在 Store 占位(假 Python), -// 需要跳过该目录,优先检查常见安装路径 +// 优先级:PATH 中的 python3/python(容器镜像内 venv 已置于 PATH 首位,命中带 scenedetect 的解释器) +// → 硬编码常见安装路径(Windows 用户/系统目录、/usr/bin/python3 等) +// 每个候选都必须同时通过「真实 Python」与「可导入 scenedetect」验证,两者缺一即跳过, +// 禁止只按 --version 选中解释器(否则会命中系统 python,其无 scenedetect,运行时报 ModuleNotFoundError)。 func (s *sceneSplitService) lookupPython(ctx context.Context) (string, error) { - // 1. 常见 Python 安装路径(Windows) - commonPaths := []string{ + for _, p := range s.collectPythonCandidates(ctx) { + if s.verifyPython(ctx, p) { + g.Log().Infof(ctx, "[场景检测] 使用 Python: %s", p) + return p, nil + } + } + return "", fmt.Errorf("未找到可用的 Python 3(需能导入 scenedetect,安装参考: pip install scenedetect[opencv,ffmpeg])") +} + +// collectPythonCandidates 收集候选 Python 路径,PATH 优先,硬编码路径回退,去重 +func (s *sceneSplitService) collectPythonCandidates(ctx context.Context) []string { + var candidates []string + seen := make(map[string]bool) + add := func(p string) { + if p != "" && !seen[p] { + seen[p] = true + candidates = append(candidates, p) + } + } + + // 1. PATH 中的 python3 / python(容器镜像内 venv 已置于 PATH 首位) + for _, name := range []string{"python3", "python"} { + if path, err := exec.LookPath(name); err == nil { + // 跳过 Microsoft Store 假占位 + if strings.Contains(path, "WindowsApps") { + g.Log().Warningf(ctx, "[场景检测] 跳过 %s(%s): Microsoft Store 占位,非真实 Python", name, path) + continue + } + add(path) + } + } + + // 2. 常见安装路径(Windows/Linux/macOS),仅收集实际存在的 + for _, p := range commonPythonPaths() { + if _, err := os.Stat(p); err == nil { + add(p) + } + } + + return candidates +} + +// commonPythonPaths 硬编码的常见 Python 安装路径,作为 PATH 查找的回退 +func commonPythonPaths() []string { + return []string{ // 用户安装 filepath.Join(os.Getenv("LOCALAPPDATA"), "Programs", "Python", "Python314", "python.exe"), filepath.Join(os.Getenv("LOCALAPPDATA"), "Programs", "Python", "Python313", "python.exe"), @@ -275,43 +320,33 @@ func (s *sceneSplitService) lookupPython(ctx context.Context) (string, error) { filepath.Join("C:", "Python312", "python.exe"), filepath.Join("C:", "Python311", "python.exe"), filepath.Join("C:", "Python310", "python.exe"), - // WSL / Git Bash 环境 + // WSL / Git Bash / Linux 环境 "/usr/bin/python3", "/usr/bin/python", } - for _, p := range commonPaths { - if _, err := os.Stat(p); err == nil { - if s.verifyPython(ctx, p) { - return p, nil - } - } - } - - // 2. 从 PATH 查找,排除 WindowsApps 目录 - for _, name := range []string{"python3", "python"} { - if path, err := exec.LookPath(name); err == nil { - // 跳过 Microsoft Store 假占位 - if strings.Contains(path, "WindowsApps") { - g.Log().Warningf(ctx, "[场景检测] 跳过 %s(%s): Microsoft Store 占位,非真实 Python", name, path) - continue - } - if s.verifyPython(ctx, path) { - return path, nil - } - } - } - - return "", fmt.Errorf("未找到可用的 Python 3") } -// verifyPython 执行 python --version 验证是否真实可用 -func (s *sceneSplitService) verifyPython(ctx context.Context, path string) bool { - ctx, cancel := context.WithTimeout(ctx, 5*time.Second) +// checkPythonImport 探测候选 Python 能否执行指定 Python 代码 +func checkPythonImport(ctx context.Context, path, code string, timeout time.Duration) bool { + ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - verCmd := exec.CommandContext(ctx, path, "--version") - output, err := verCmd.CombinedOutput() - if err != nil { - g.Log().Warningf(ctx, "[场景检测] 尝试 %s 失败(%v): %s", path, err, string(output)) + cmd := exec.CommandContext(ctx, path, "-c", code) + if err := cmd.Run(); err != nil { + g.Log().Warningf(ctx, "[场景检测] %s 执行 python -c %q 失败: %v", path, code, err) + return false + } + return true +} + +// verifyPython 验证候选 Python:真实可用 且 能导入 scenedetect +func (s *sceneSplitService) verifyPython(ctx context.Context, path string) bool { + // 1. 确认是真实可用的 Python(排除 Microsoft Store 假占位等) + if !checkPythonImport(ctx, path, "import sys", 5*time.Second) { + return false + } + // 2. 确认能导入 scenedetect(场景分割实际依赖,缺失则运行时报 ModuleNotFoundError) + if !checkPythonImport(ctx, path, "import scenedetect", 30*time.Second) { + g.Log().Warningf(ctx, "[场景检测] %s 无法导入 scenedetect,跳过", path) return false } return true