395 lines
11 KiB
Go
395 lines
11 KiB
Go
|
|
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-cpp(C++ 版,最快)
|
|||
|
|
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-cpp(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++ 版安装成功")
|
|||
|
|
// 装好后把 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
|
|||
|
|
}
|
|||
|
|
}
|