Files
media/service/setup/setup_service.go
2026-05-19 14:33:06 +08:00

395 lines
11 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
)
// EnsureDependencies 启动时检查并安装 ffmpeg 和 whisper
func EnsureDependencies(ctx context.Context) {
g.Log().Info(ctx, "========== 检查依赖环境 ==========")
ensureFFmpeg(ctx)
ensureWhisper(ctx)
resolveWhisperPath(ctx)
if envConfigured {
g.Log().Info(ctx, "依赖检查完成,新环境变量已配置,建议重启终端")
} else {
g.Log().Info(ctx, "依赖检查完成,所有依赖已就绪")
}
g.Log().Info(ctx, "===================================")
}
// 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":
// 检查是否安装了 Homebrew
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] ✔ 安装成功")
case "linux":
// 尝试 apt
if _, err := exec.LookPath("apt"); err == nil {
cmd := exec.CommandContext(ctx, "sudo", "apt", "install", "-y", "ffmpeg")
output, err := cmd.CombinedOutput()
if err != nil {
g.Log().Errorf(ctx, "[ffmpeg] ❌ apt 安装失败: %v\n%s", err, string(output))
return
}
g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功")
return
}
// 尝试 yum
if _, err := exec.LookPath("yum"); err == nil {
cmd := exec.CommandContext(ctx, "sudo", "yum", "install", "-y", "ffmpeg")
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
}
g.Log().Warningf(ctx, "[ffmpeg] ⚠ 请手动安装: sudo apt install ffmpeg")
default:
g.Log().Warningf(ctx, "[ffmpeg] ⚠ 不支持的平台(%s),请手动安装 ffmpeg", runtime.GOOS)
}
}
// ensureWhisper 确保 whisper 可用(优先安装 C++ 版,速度更快)
func ensureWhisper(ctx context.Context) {
// 1. 检查是否已有 whisper-cppC++ 版,最快)
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. 检查 Homebrew 安装目录(即使不在 PATH 也能找到)
if p := findHomebrewWhisperCpp(); p != "" {
DetectedWhisperPath = p
// 自动添加到 PATH 环境变量
addToShellPath(ctx, filepath.Dir(p))
g.Log().Infof(ctx, "[whisper] ✔ C++ 版已安装(自动检测): %s", p)
return
}
// 3. 尝试安装 whisper-cppC++ 版)
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++ 版安装成功")
// 装好后把 Homebrew bin 加到 PATH
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"
}
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
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 倍)
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. 在 Homebrew 目录查找 C++ 版本
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"))
}
// 常见 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"),
)
}
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
}
// 检查是否已在 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
}
}