Files
media/service/setup/setup_service.go
2026-05-20 11:32:39 +08:00

564 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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-cppC++ 版,最快)
// 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
}
}