package setup import ( "context" "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "github.com/gogf/gf/v2/frame/g" ) var ( envConfigured bool // DetectedWhisperPath 自动检测到的 whisper 命令行路径(空则使用 python -m whisper) DetectedWhisperPath string ) func init() { ensureDependencies() } // ensureDependencies 启动时检查并安装 ffmpeg 和 whisper func ensureDependencies() { ctx := context.Background() g.Log().Info(ctx, "========== 检查依赖环境 ==========") // 打印当前运行环境信息 g.Log().Infof(ctx, "平台: %s/%s, Docker: %v", runtime.GOOS, runtime.GOARCH, isRunningInContainer()) ensureFFmpeg(ctx) ensureWhisper(ctx) resolveWhisperPath(ctx) if envConfigured { g.Log().Info(ctx, "依赖检查完成,新环境变量已配置,建议重启终端") } else { g.Log().Info(ctx, "依赖检查完成,所有依赖已就绪") } g.Log().Info(ctx, "===================================") } // isRunningInContainer 检测是否运行在 Docker 容器中 func isRunningInContainer() bool { // 方法1: 检查 /.dockerenv 文件 if _, err := os.Stat("/.dockerenv"); err == nil { return true } // 方法2: 检查 /proc/1/cgroup 是否包含 docker 关键字 if data, err := os.ReadFile("/proc/1/cgroup"); err == nil { if strings.Contains(string(data), "docker") || strings.Contains(string(data), "kubepods") || strings.Contains(string(data), "containerd") { return true } } return false } // inContainer 是否为容器环境(简化调用) var inContainer = isRunningInContainer() // ensureFFmpeg 确保 ffmpeg 可用 func ensureFFmpeg(ctx context.Context) { if _, err := exec.LookPath("ffmpeg"); err == nil { g.Log().Info(ctx, "[ffmpeg] ✔ 已安装") return } g.Log().Infof(ctx, "[ffmpeg] 未找到,尝试自动安装...") switch runtime.GOOS { case "darwin": installFFmpegOnMac(ctx) case "linux": installFFmpegOnLinux(ctx) case "windows": installFFmpegOnWindows(ctx) default: g.Log().Warningf(ctx, "[ffmpeg] ⚠ 不支持的平台(%s),请手动安装 ffmpeg", runtime.GOOS) } } // installFFmpegOnMac 通过 Homebrew 安装 ffmpeg func installFFmpegOnMac(ctx context.Context) { if _, err := exec.LookPath("brew"); err != nil { g.Log().Warningf(ctx, "[ffmpeg] ⚠ 未检测到 Homebrew,请手动安装:\n brew install ffmpeg") return } cmd := exec.CommandContext(ctx, "brew", "install", "ffmpeg") output, err := cmd.CombinedOutput() if err != nil { g.Log().Errorf(ctx, "[ffmpeg] ❌ 安装失败: %v\n%s", err, string(output)) return } g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功") } // installFFmpegOnLinux 在 Linux(含 Docker)上安装 ffmpeg func installFFmpegOnLinux(ctx context.Context) { // Docker 容器通常以 root 运行,不需要 sudo sudoPrefix := "" if !inContainer { // 非容器环境,检查是否需要 sudo if _, err := exec.LookPath("sudo"); err == nil { sudoPrefix = "sudo" } } // 1. 尝试 apt (Debian/Ubuntu) if _, err := exec.LookPath("apt-get"); err == nil { args := []string{"install", "-y", "ffmpeg"} if sudoPrefix != "" { args = append([]string{sudoPrefix}, args...) } cmd := exec.CommandContext(ctx, "apt-get", args...) output, err := cmd.CombinedOutput() if err != nil { g.Log().Errorf(ctx, "[ffmpeg] ❌ apt-get 安装失败: %v\n%s", err, string(output)) return } g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功") // 更新库缓存(Debian/Ubuntu 会用 ldconfig 更新) return } // 2. 尝试 apk (Alpine Linux,常见于 Docker 精简镜像) if _, err := exec.LookPath("apk"); err == nil { // Alpine 的 apk 不需要 sudo(默认以 root 运行) cmd := exec.CommandContext(ctx, "apk", "add", "ffmpeg") output, err := cmd.CombinedOutput() if err != nil { g.Log().Errorf(ctx, "[ffmpeg] ❌ apk 安装失败: %v\n%s", err, string(output)) return } g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功") return } // 3. 尝试 yum (CentOS/RHEL) if _, err := exec.LookPath("yum"); err == nil { args := []string{"install", "-y", "ffmpeg"} if sudoPrefix != "" { args = append([]string{sudoPrefix}, args...) } cmd := exec.CommandContext(ctx, "yum", args...) output, err := cmd.CombinedOutput() if err != nil { g.Log().Errorf(ctx, "[ffmpeg] ❌ yum 安装失败: %v\n%s", err, string(output)) return } g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功") return } if inContainer { g.Log().Warningf(ctx, "[ffmpeg] ⚠ 容器中未找到 apt-get/apk/yum,请将 ffmpeg 预装在 Docker 镜像中") } else { g.Log().Warningf(ctx, "[ffmpeg] ⚠ 请手动安装: sudo apt-get install ffmpeg") } } // installFFmpegOnWindows 在 Windows 上安装 ffmpeg func installFFmpegOnWindows(ctx context.Context) { // 1. 尝试 winget (Windows 10/11 内置) if _, err := exec.LookPath("winget"); err == nil { g.Log().Infof(ctx, "[ffmpeg] 通过 winget 安装...") cmd := exec.CommandContext(ctx, "winget", "install", "--id", "FFmpeg.FFmpeg", "-e", "--accept-package-agreements") output, err := cmd.CombinedOutput() if err == nil { g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功") return } g.Log().Warningf(ctx, "[ffmpeg] ⚠ winget 安装失败: %v\n%s", err, string(output)) } // 2. 尝试 choco (Chocolatey) if _, err := exec.LookPath("choco"); err == nil { // choco 安装可能需要管理员权限 g.Log().Infof(ctx, "[ffmpeg] 通过 choco 安装...") cmd := exec.CommandContext(ctx, "choco", "install", "ffmpeg", "-y") output, err := cmd.CombinedOutput() if err == nil { g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功") return } g.Log().Warningf(ctx, "[ffmpeg] ⚠ choco 安装失败: %v\n%s", err, string(output)) } // 3. 尝试 scoop if _, err := exec.LookPath("scoop"); err == nil { g.Log().Infof(ctx, "[ffmpeg] 通过 scoop 安装...") cmd := exec.CommandContext(ctx, "scoop", "install", "ffmpeg") output, err := cmd.CombinedOutput() if err == nil { g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功") return } g.Log().Warningf(ctx, "[ffmpeg] ⚠ scoop 安装失败: %v\n%s", err, string(output)) } g.Log().Warningf(ctx, `[ffmpeg] ⚠ 请手动安装 ffmpeg,推荐方式: 1. winget install --id FFmpeg.FFmpeg -e 2. choco install ffmpeg -y 3. 从 https://ffmpeg.org/download.html 下载并加入 PATH`) } // ensureWhisper 确保 whisper 可用(优先安装 C++ 版,速度更快) func ensureWhisper(ctx context.Context) { // 1. 检查是否已有 whisper-cpp(C++ 版,最快) // exec.LookPath 在 Windows 上会自动查找 .exe 后缀 if path, err := exec.LookPath("whisper-cpp"); err == nil { g.Log().Infof(ctx, "[whisper] ✔ C++ 版已安装: %s", path) return } if path, err := exec.LookPath("whisper-cli"); err == nil { g.Log().Infof(ctx, "[whisper] ✔ C++ 版已安装: %s", path) return } // 2. 仅在 macOS 上检查 Homebrew 安装目录(即使不在 PATH 也能找到) if runtime.GOOS == "darwin" { if p := findHomebrewWhisperCpp(); p != "" { DetectedWhisperPath = p if !inContainer { addToShellPath(ctx, filepath.Dir(p)) } g.Log().Infof(ctx, "[whisper] ✔ C++ 版已安装(自动检测): %s", p) return } } // 3. 仅在 macOS 上尝试使用 Homebrew 安装 C++ 版 if runtime.GOOS == "darwin" { if _, err := exec.LookPath("brew"); err == nil { g.Log().Infof(ctx, "[whisper] 安装 C++ 版 (brew install whisper-cpp)...") cmd := exec.CommandContext(ctx, "brew", "install", "whisper-cpp") output, err := cmd.CombinedOutput() if err == nil { g.Log().Info(ctx, "[whisper] ✔ C++ 版安装成功") if !inContainer { addToShellPath(ctx, getHomebrewBinDir()) } if p := findHomebrewWhisperCpp(); p != "" { DetectedWhisperPath = p } return } g.Log().Warningf(ctx, "[whisper] ⚠ brew 安装失败: %v\n%s", err, string(output)) g.Log().Infof(ctx, "[whisper] 降级安装 Python 版...") } } // 4. 降级:检查 python -m whisper 是否可用 if pythonWhisperAvailable() { g.Log().Info(ctx, "[whisper] ✔ Python 版已安装 (python3 -m whisper)") return } // 5. 降级:pip 安装 Python 版 if _, err := exec.LookPath("pip3"); err != nil { if _, err2 := exec.LookPath("pip"); err2 != nil { g.Log().Warningf(ctx, "[whisper] ⚠ 未找到 pip,请手动安装:\n pip3 install openai-whisper") return } } g.Log().Infof(ctx, "[whisper] 安装 Python 版 (pip install openai-whisper)...") pipCmd := "pip3" if _, err := exec.LookPath("pip3"); err != nil { pipCmd = "pip" } // pip install --user 可能在某些环境下不兼容,尝试先不加 --user,失败后再加 cmd := exec.CommandContext(ctx, pipCmd, "install", "openai-whisper") output, err := cmd.CombinedOutput() if err != nil { // 尝试 --user 模式 g.Log().Warningf(ctx, "[whisper] pip 全局安装失败: %v,尝试 --user 模式...", err) cmd = exec.CommandContext(ctx, pipCmd, "install", "--user", "openai-whisper") output, err = cmd.CombinedOutput() if err != nil { g.Log().Errorf(ctx, "[whisper] ❌ pip 安装失败: %v\n%s", err, string(output)) return } } g.Log().Info(ctx, "[whisper] ✔ Python 版安装成功") // 安装后自动配置 PATH(仅在非容器、非 Windows 环境) if !inContainer && runtime.GOOS != "windows" { configureWhisperPath(ctx) } } // resolveWhisperPath 自动找到 whisper 二进制路径并存储 func resolveWhisperPath(ctx context.Context) { // 0. 如果已经通过 ensure 检测到了路径,直接使用 if DetectedWhisperPath != "" { if _, err := os.Stat(DetectedWhisperPath); err == nil { g.Log().Infof(ctx, "[whisper] ✔ 路径: %s", DetectedWhisperPath) return } } // 1. 优先检测 C++ 版本(快 3-5 倍) // exec.LookPath 在 Windows 上自动查找 .exe 后缀 for _, name := range []string{"whisper-cpp", "whisper-cli"} { if path, err := exec.LookPath(name); err == nil { DetectedWhisperPath = path g.Log().Infof(ctx, "[whisper] ✔ C++ 版: %s", path) return } } // 2. 仅在 macOS 上查找 Homebrew 目录下的 C++ 版本 if runtime.GOOS == "darwin" { if p := findHomebrewWhisperCpp(); p != "" { DetectedWhisperPath = p g.Log().Infof(ctx, "[whisper] ✔ C++ 版(自动检测): %s", p) return } } // 3. 从 PATH 查找 Python 版 whisper if path, err := exec.LookPath("whisper"); err == nil { DetectedWhisperPath = path g.Log().Infof(ctx, "[whisper] ✔ Python 版: %s", path) return } // 4. 尝试常见 pip user bin 路径 for _, p := range getWhisperCandidates() { if info, err := os.Stat(p); err == nil && !info.IsDir() { DetectedWhisperPath = p g.Log().Infof(ctx, "[whisper] ✔ Python 版(自动检测): %s", p) return } } g.Log().Info(ctx, "[whisper] ✔ 使用 python3 -m whisper 方式") } // getWhisperCandidates 返回可能的 whisper 二进制路径 func getWhisperCandidates() []string { var candidates []string // 通过 python 探针获取 user-site bin 目录 if p := getUserPythonBin(); p != "" { candidates = append(candidates, filepath.Join(p, "whisper")) // Windows 上 pip 安装的可执行文件是 .exe if runtime.GOOS == "windows" { candidates = append(candidates, filepath.Join(p, "whisper.exe")) } } // 常见 pip user base 路径 userHome, _ := os.UserHomeDir() switch runtime.GOOS { case "darwin": // macOS 常见的 Python 版本路径 pythonVersions := []string{"3.9", "3.10", "3.11", "3.12", "3.13"} for _, ver := range pythonVersions { candidates = append(candidates, filepath.Join(userHome, "Library", "Python", ver, "bin", "whisper"), ) } case "linux": candidates = append(candidates, filepath.Join(userHome, ".local", "bin", "whisper"), ) case "windows": // Windows 上 pip --user 安装的脚本路径 candidates = append(candidates, filepath.Join(userHome, "AppData", "Roaming", "Python", "Scripts", "whisper.exe"), filepath.Join(userHome, "AppData", "Roaming", "Python", "Scripts", "whisper"), filepath.Join(userHome, "AppData", "Local", "Programs", "Python", "Scripts", "whisper.exe"), filepath.Join(userHome, "AppData", "Local", "Programs", "Python", "Scripts", "whisper"), ) // Python 版本特定路径 pythonVersions := []string{"39", "310", "311", "312", "313"} for _, ver := range pythonVersions { candidates = append(candidates, filepath.Join(userHome, "AppData", "Roaming", "Python", "Python"+ver, "Scripts", "whisper.exe"), filepath.Join(userHome, "AppData", "Roaming", "Python", "Python"+ver, "Scripts", "whisper"), ) } } return candidates } // getUserPythonBin 通过 python 获取 user bin 目录 func getUserPythonBin() string { pythonCandidates := []string{"python3", "python"} for _, py := range pythonCandidates { path, err := exec.LookPath(py) if err != nil { continue } cmd := exec.Command(path, "-m", "site", "--user-base") output, err := cmd.Output() if err != nil { continue } base := strings.TrimSpace(string(output)) if base != "" { return filepath.Join(base, "bin") } } return "" } // configureWhisperPath 将 pip user bin 目录加到 shell 配置 func configureWhisperPath(ctx context.Context) { binDir := getUserPythonBin() if binDir == "" { return } // 检查是否已经在 PATH 中 currentPath := os.Getenv("PATH") if strings.Contains(currentPath, binDir) { return } // 配置到 .zshrc 或 .bashrc home, _ := os.UserHomeDir() rcFiles := []string{".zshrc", ".bashrc", ".bash_profile"} for _, rc := range rcFiles { rcPath := filepath.Join(home, rc) // 文件不存在则跳过 if _, err := os.Stat(rcPath); os.IsNotExist(err) { continue } // 检查是否已添加 data, _ := os.ReadFile(rcPath) if strings.Contains(string(data), binDir) { continue } // 追加 line := fmt.Sprintf("\nexport PATH=\"%s:$PATH\"\n", binDir) f, err := os.OpenFile(rcPath, os.O_APPEND|os.O_WRONLY, 0644) if err != nil { g.Log().Warningf(ctx, "[whisper] 写入 %s 失败: %v", rc, err) continue } f.WriteString(line) f.Close() g.Log().Infof(ctx, "[whisper] 已将 %s 添加到 %s,请执行: source ~/%s", binDir, rc, rc) envConfigured = true break } } // pythonWhisperAvailable 检查 python -m whisper 是否可用 func pythonWhisperAvailable() bool { pythonCandidates := []string{"python3", "python"} for _, py := range pythonCandidates { if path, err := exec.LookPath(py); err == nil { cmd := exec.Command(path, "-m", "whisper", "--help") if cmd.Run() == nil { return true } } } return false } // findHomebrewWhisperCpp 在 Homebrew 安装目录查找 whisper-cpp func findHomebrewWhisperCpp() string { dirs := getHomebrewBinDirs() for _, dir := range dirs { for _, name := range []string{"whisper-cpp", "whisper-cli"} { p := filepath.Join(dir, name) if info, err := os.Stat(p); err == nil && !info.IsDir() { return p } } } return "" } // getHomebrewBinDirs 返回 Homebrew 可能的 bin 目录 func getHomebrewBinDirs() []string { userHome, _ := os.UserHomeDir() return []string{ "/opt/homebrew/bin", // Apple Silicon "/usr/local/bin", // Intel filepath.Join(userHome, ".homebrew", "bin"), } } // getHomebrewBinDir 返回当前系统的 Homebrew bin 目录 func getHomebrewBinDir() string { dirs := getHomebrewBinDirs() for _, dir := range dirs { if _, err := os.Stat(filepath.Join(dir, "brew")); err == nil { return dir } // 也检查 brew 命令路径 if path, err := exec.LookPath("brew"); err == nil { return filepath.Dir(path) } } return "/opt/homebrew/bin" // 默认 Apple Silicon 路径 } // addToShellPath 将目录添加到 shell rc 文件的 PATH 中 func addToShellPath(ctx context.Context, dir string) { if dir == "" { return } // 容器环境不修改 shell 配置(无意义) if inContainer { return } // Windows 环境不修改 shell rc 文件(使用系统环境变量) if runtime.GOOS == "windows" { g.Log().Infof(ctx, "[setup] Windows 环境,请手动将 %s 添加到系统 PATH 环境变量", dir) return } // 检查是否已在 PATH 中 currentPath := os.Getenv("PATH") if strings.Contains(currentPath, dir) { return } home, _ := os.UserHomeDir() rcFiles := []string{".zshrc", ".bashrc", ".bash_profile"} for _, rc := range rcFiles { rcPath := filepath.Join(home, rc) if _, err := os.Stat(rcPath); os.IsNotExist(err) { continue } data, _ := os.ReadFile(rcPath) if strings.Contains(string(data), dir) { continue } line := fmt.Sprintf("\nexport PATH=\"%s:$PATH\"\n", dir) f, err := os.OpenFile(rcPath, os.O_APPEND|os.O_WRONLY, 0644) if err != nil { g.Log().Warningf(ctx, "[setup] 写入 %s 失败: %v", rc, err) continue } f.WriteString(line) f.Close() g.Log().Infof(ctx, "[setup] 已将 %s 添加到 %s", dir, rc) envConfigured = true break } }