Files
media/service/setup/setup_service.go

564 lines
16 KiB
Go
Raw Normal View History

2026-05-19 14:33:06 +08:00
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
)
2026-05-20 11:32:39 +08:00
func init() {
ensureDependencies()
}
// ensureDependencies 启动时检查并安装 ffmpeg 和 whisper
func ensureDependencies() {
ctx := context.Background()
2026-05-19 14:33:06 +08:00
g.Log().Info(ctx, "========== 检查依赖环境 ==========")
2026-05-20 11:32:39 +08:00
// 打印当前运行环境信息
g.Log().Infof(ctx, "平台: %s/%s, Docker: %v", runtime.GOOS, runtime.GOARCH, isRunningInContainer())
2026-05-19 14:33:06 +08:00
ensureFFmpeg(ctx)
ensureWhisper(ctx)
resolveWhisperPath(ctx)
if envConfigured {
g.Log().Info(ctx, "依赖检查完成,新环境变量已配置,建议重启终端")
} else {
g.Log().Info(ctx, "依赖检查完成,所有依赖已就绪")
}
g.Log().Info(ctx, "===================================")
}
2026-05-20 11:32:39 +08:00
// 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()
2026-05-19 14:33:06 +08:00
// 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":
2026-05-20 11:32:39 +08:00
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))
2026-05-19 14:33:06 +08:00
return
}
2026-05-20 11:32:39 +08:00
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")
2026-05-19 14:33:06 +08:00
output, err := cmd.CombinedOutput()
if err != nil {
2026-05-20 11:32:39 +08:00
g.Log().Errorf(ctx, "[ffmpeg] ❌ apk 安装失败: %v\n%s", err, string(output))
2026-05-19 14:33:06 +08:00
return
}
g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功")
2026-05-20 11:32:39 +08:00
return
}
2026-05-19 14:33:06 +08:00
2026-05-20 11:32:39 +08:00
// 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 {
2026-05-19 14:33:06 +08:00
g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功")
return
}
2026-05-20 11:32:39 +08:00
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 {
2026-05-19 14:33:06 +08:00
g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功")
return
}
2026-05-20 11:32:39 +08:00
g.Log().Warningf(ctx, "[ffmpeg] ⚠ choco 安装失败: %v\n%s", err, string(output))
}
2026-05-19 14:33:06 +08:00
2026-05-20 11:32:39 +08:00
// 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))
2026-05-19 14:33:06 +08:00
}
2026-05-20 11:32:39 +08:00
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`)
2026-05-19 14:33:06 +08:00
}
// ensureWhisper 确保 whisper 可用(优先安装 C++ 版,速度更快)
func ensureWhisper(ctx context.Context) {
// 1. 检查是否已有 whisper-cppC++ 版,最快)
2026-05-20 11:32:39 +08:00
// exec.LookPath 在 Windows 上会自动查找 .exe 后缀
2026-05-19 14:33:06 +08:00
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
}
2026-05-20 11:32:39 +08:00
// 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
}
2026-05-19 14:33:06 +08:00
}
2026-05-20 11:32:39 +08:00
// 3. 仅在 macOS 上尝试使用 Homebrew 安装 C++ 版
2026-05-19 14:33:06 +08:00
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++ 版安装成功")
2026-05-20 11:32:39 +08:00
if !inContainer {
addToShellPath(ctx, getHomebrewBinDir())
}
2026-05-19 14:33:06 +08:00
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"
}
2026-05-20 11:32:39 +08:00
// pip install --user 可能在某些环境下不兼容,尝试先不加 --user失败后再加
cmd := exec.CommandContext(ctx, pipCmd, "install", "openai-whisper")
2026-05-19 14:33:06 +08:00
output, err := cmd.CombinedOutput()
if err != nil {
2026-05-20 11:32:39 +08:00
// 尝试 --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
}
2026-05-19 14:33:06 +08:00
}
g.Log().Info(ctx, "[whisper] ✔ Python 版安装成功")
2026-05-20 11:32:39 +08:00
// 安装后自动配置 PATH仅在非容器、非 Windows 环境)
if !inContainer && runtime.GOOS != "windows" {
configureWhisperPath(ctx)
}
2026-05-19 14:33:06 +08:00
}
// 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 倍)
2026-05-20 11:32:39 +08:00
// exec.LookPath 在 Windows 上自动查找 .exe 后缀
2026-05-19 14:33:06 +08:00
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
}
}
2026-05-20 11:32:39 +08:00
// 2. 仅在 macOS 上查找 Homebrew 目录下的 C++ 版本
if runtime.GOOS == "darwin" {
if p := findHomebrewWhisperCpp(); p != "" {
DetectedWhisperPath = p
g.Log().Infof(ctx, "[whisper] ✔ C++ 版(自动检测): %s", p)
return
}
2026-05-19 14:33:06 +08:00
}
// 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"))
2026-05-20 11:32:39 +08:00
// Windows 上 pip 安装的可执行文件是 .exe
if runtime.GOOS == "windows" {
candidates = append(candidates, filepath.Join(p, "whisper.exe"))
}
2026-05-19 14:33:06 +08:00
}
// 常见 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"),
)
2026-05-20 11:32:39 +08:00
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"),
)
}
2026-05-19 14:33:06 +08:00
}
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
}
2026-05-20 11:32:39 +08:00
// 容器环境不修改 shell 配置(无意义)
if inContainer {
return
}
// Windows 环境不修改 shell rc 文件(使用系统环境变量)
if runtime.GOOS == "windows" {
g.Log().Infof(ctx, "[setup] Windows 环境,请手动将 %s 添加到系统 PATH 环境变量", dir)
return
}
2026-05-19 14:33:06 +08:00
// 检查是否已在 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
}
}