mirror of
https://github.com/jahruz67/wisp-open.git
synced 2026-08-08 18:14:08 +00:00
457 lines
11 KiB
Go
457 lines
11 KiB
Go
// Package whisper provides local offline transcription using whisper.cpp
|
|
package whisper
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"wis-free-v3/internal/logger"
|
|
)
|
|
|
|
// Download URLs
|
|
const (
|
|
// Whisper.cpp Windows binary from ggml-org GitHub releases
|
|
// Using Vulkan build to support Intel iGPU, AMD, and Nvidia
|
|
whisperBinaryURL = "https://github.com/jerryshell/whisper.cpp-windows-vulkan-bin/releases/download/v1.0.0/whisper.cpp-windows-vulkan.zip"
|
|
// Model URLs from Hugging Face
|
|
modelBaseURL = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main"
|
|
)
|
|
|
|
// Available models
|
|
var Models = map[string]struct {
|
|
Filename string
|
|
Size string
|
|
}{
|
|
"tiny": {"ggml-tiny.bin", "75 MB"},
|
|
"base": {"ggml-base.bin", "150 MB"},
|
|
"small": {"ggml-small.bin", "500 MB"},
|
|
"medium": {"ggml-medium.bin", "1.5 GB"},
|
|
}
|
|
|
|
// InstalledInfo stores information about the installed whisper
|
|
type InstalledInfo struct {
|
|
Version string `json:"version"`
|
|
Model string `json:"model"`
|
|
InstallPath string `json:"install_path"`
|
|
}
|
|
|
|
// Manager handles whisper installation and execution
|
|
type Manager struct {
|
|
installDir string
|
|
}
|
|
|
|
// NewManager creates a new whisper manager
|
|
func NewManager() (*Manager, error) {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get home directory: %w", err)
|
|
}
|
|
|
|
installDir := filepath.Join(homeDir, ".wis-free-v3", "whisper")
|
|
|
|
return &Manager{
|
|
installDir: installDir,
|
|
}, nil
|
|
}
|
|
|
|
// IsInstalled checks if whisper is installed
|
|
func (m *Manager) IsInstalled() bool {
|
|
infoPath := filepath.Join(m.installDir, "installed.json")
|
|
if _, err := os.Stat(infoPath); os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
|
|
// Check if model exists
|
|
info, err := m.GetInstalledInfo()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
modelInfo, ok := Models[info.Model]
|
|
if !ok || modelInfo.Filename == "" {
|
|
return false
|
|
}
|
|
|
|
modelPath := filepath.Join(m.installDir, modelInfo.Filename)
|
|
if _, err := os.Stat(modelPath); os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// GetInstalledInfo returns information about the installed whisper
|
|
func (m *Manager) GetInstalledInfo() (*InstalledInfo, error) {
|
|
infoPath := filepath.Join(m.installDir, "installed.json")
|
|
data, err := os.ReadFile(infoPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var info InstalledInfo
|
|
if err := json.Unmarshal(data, &info); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &info, nil
|
|
}
|
|
|
|
// Install downloads and installs whisper with the specified model
|
|
// It opens a terminal window to show progress
|
|
func (m *Manager) Install(model string) error {
|
|
if _, ok := Models[model]; !ok {
|
|
return fmt.Errorf("unknown model: %s", model)
|
|
}
|
|
|
|
// Create install directory
|
|
if err := os.MkdirAll(m.installDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create install directory: %w", err)
|
|
}
|
|
|
|
// Create install script
|
|
scriptPath := filepath.Join(m.installDir, "install.bat")
|
|
script := m.generateInstallScript(model)
|
|
if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
|
|
return fmt.Errorf("failed to write install script: %w", err)
|
|
}
|
|
|
|
// Run install script in a visible terminal
|
|
cmd := exec.Command("cmd", "/c", "start", "cmd", "/k", scriptPath)
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to start installer: %w", err)
|
|
}
|
|
|
|
logger.Info("Whisper installation started in terminal")
|
|
return nil
|
|
}
|
|
|
|
// generateInstallScript creates a batch script for installation
|
|
func (m *Manager) generateInstallScript(model string) string {
|
|
modelInfo := Models[model]
|
|
modelURL := fmt.Sprintf("%s/%s", modelBaseURL, modelInfo.Filename)
|
|
modelPath := filepath.Join(m.installDir, modelInfo.Filename)
|
|
infoPath := filepath.Join(m.installDir, "installed.json")
|
|
zipPath := filepath.Join(m.installDir, "whisper.zip")
|
|
|
|
script := fmt.Sprintf(`@echo off
|
|
title wis-free-v3 - Installing Offline Whisper
|
|
color 0A
|
|
echo.
|
|
echo ===============================================
|
|
echo wis-free-v3 - Offline Whisper Installer
|
|
echo ===============================================
|
|
echo.
|
|
echo Install directory: %s
|
|
echo Model: %s (%s)
|
|
echo.
|
|
|
|
echo Checking prerequisites...
|
|
if not exist "C:\Windows\System32\msvcp140.dll" (
|
|
echo.
|
|
echo ERROR: Microsoft Visual C++ Redistributable is missing.
|
|
echo This is required for the offline model to run.
|
|
echo.
|
|
echo Please download and install it from:
|
|
echo https://aka.ms/vs/17/release/vc_redist.x64.exe
|
|
echo.
|
|
echo After installing, restart wis-free-v3 and try again.
|
|
echo.
|
|
pause
|
|
exit /b 1
|
|
)
|
|
|
|
echo.
|
|
echo [1/4] Creating directories...
|
|
mkdir "%s" 2>nul
|
|
|
|
echo.
|
|
echo [2/4] Downloading whisper.cpp binary...
|
|
echo URL: %s
|
|
curl -L --retry 3 --progress-bar -o "%s" "%s"
|
|
if %%ERRORLEVEL%% neq 0 (
|
|
echo.
|
|
echo ERROR: Failed to download whisper.cpp
|
|
pause
|
|
exit /b 1
|
|
)
|
|
|
|
echo.
|
|
echo [3/4] Extracting whisper.cpp...
|
|
powershell -Command "Expand-Archive -Path '%s' -DestinationPath '%s' -Force"
|
|
if %%ERRORLEVEL%% neq 0 (
|
|
echo ERROR: Failed to extract whisper.cpp
|
|
pause
|
|
exit /b 1
|
|
)
|
|
del "%s"
|
|
|
|
echo.
|
|
echo [4/4] Downloading Whisper %s model (%s)...
|
|
echo URL: %s
|
|
echo.
|
|
echo This may take several minutes depending on your internet speed...
|
|
echo.
|
|
curl -L --retry 3 --progress-bar -o "%s" "%s"
|
|
if %%ERRORLEVEL%% neq 0 (
|
|
echo.
|
|
echo ERROR: Failed to download model
|
|
pause
|
|
exit /b 1
|
|
)
|
|
|
|
echo.
|
|
echo Verifying installation...
|
|
if not exist "%s" (
|
|
echo ERROR: Model file not found
|
|
pause
|
|
exit /b 1
|
|
)
|
|
|
|
echo.
|
|
echo Creating installation info...
|
|
echo {"version":"1.8.2","model":"%s","install_path":"%s"} > "%s"
|
|
|
|
echo.
|
|
echo ===============================================
|
|
echo Installation Complete!
|
|
echo ===============================================
|
|
echo.
|
|
echo Whisper.cpp and %s model installed successfully!
|
|
echo.
|
|
echo NEXT STEPS:
|
|
echo 1. Close this window
|
|
echo 2. Restart wis-free-v3
|
|
echo 3. Select "Local Whisper" in settings
|
|
echo.
|
|
pause
|
|
exit
|
|
`,
|
|
m.installDir,
|
|
model, modelInfo.Size,
|
|
m.installDir,
|
|
whisperBinaryURL,
|
|
zipPath, whisperBinaryURL,
|
|
zipPath, m.installDir,
|
|
zipPath,
|
|
model, modelInfo.Size,
|
|
modelURL,
|
|
modelPath, modelURL,
|
|
modelPath,
|
|
model, strings.ReplaceAll(m.installDir, `\`, `\\`), infoPath,
|
|
model,
|
|
)
|
|
|
|
return script
|
|
}
|
|
|
|
// Uninstall removes whisper installation
|
|
func (m *Manager) Uninstall() error {
|
|
if err := os.RemoveAll(m.installDir); err != nil {
|
|
return fmt.Errorf("failed to remove whisper directory: %w", err)
|
|
}
|
|
logger.Info("Whisper uninstalled")
|
|
return nil
|
|
}
|
|
|
|
// Transcribe runs local whisper transcription
|
|
func (m *Manager) Transcribe(audioPath string, language string) (string, error) {
|
|
if !m.IsInstalled() {
|
|
return "", fmt.Errorf("whisper is not installed")
|
|
}
|
|
|
|
info, err := m.GetInstalledInfo()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get installation info: %w", err)
|
|
}
|
|
|
|
binaryPath := m.getBinaryPath()
|
|
modelFilename := Models[info.Model].Filename
|
|
|
|
// Check for model in Release directory first, then main directory
|
|
modelPath := filepath.Join(m.installDir, "Release", modelFilename)
|
|
if _, err := os.Stat(modelPath); os.IsNotExist(err) {
|
|
modelPath = filepath.Join(m.installDir, modelFilename)
|
|
}
|
|
|
|
if language == "" {
|
|
language = "auto"
|
|
}
|
|
|
|
logger.Info("Running whisper: %s -m %s -l %s -f %s", binaryPath, modelPath, language, audioPath)
|
|
|
|
// Run whisper with simple arguments: ./main -m model.bin -l language -f audio.wav
|
|
// Vulkan build uses GPU by default, no need for -ngl
|
|
cmd := exec.Command(binaryPath, "-m", modelPath, "-l", language, "-f", audioPath)
|
|
|
|
// Set working directory to the binary's location so it can find DLLs
|
|
cmd.Dir = filepath.Dir(binaryPath)
|
|
|
|
// Hide the console window on Windows
|
|
hideWindowContext(cmd)
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
outputStr := string(output)
|
|
|
|
if err != nil {
|
|
logger.Error("Whisper failed: %v, output: %s", err, outputStr)
|
|
return "", fmt.Errorf("transcription failed: %w", err)
|
|
}
|
|
|
|
// Parse the output - whisper.cpp outputs transcribed text to stdout
|
|
lines := strings.Split(outputStr, "\n")
|
|
var textLines []string
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
|
|
// Skip empty lines
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
// Skip all system/debug info
|
|
if strings.Contains(line, "whisper_") ||
|
|
strings.Contains(line, "main:") ||
|
|
strings.Contains(line, "system_info:") ||
|
|
strings.Contains(line, "ld_") ||
|
|
strings.Contains(line, "cuda_") ||
|
|
strings.Contains(line, "ggml_vulkan") {
|
|
continue
|
|
}
|
|
|
|
// Handle timestamped lines like: [00:00:00.000 --> 00:00:07.280] Text
|
|
if strings.HasPrefix(line, "[") && strings.Contains(line, "]") {
|
|
// Extract text after the timestamp
|
|
parts := strings.SplitN(line, "]", 2)
|
|
if len(parts) > 1 {
|
|
text := strings.TrimSpace(parts[1])
|
|
if text != "" {
|
|
textLines = append(textLines, text)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Fallback for non-prefixed text lines (if any)
|
|
textLines = append(textLines, line)
|
|
}
|
|
|
|
result := strings.Join(textLines, " ")
|
|
result = strings.TrimSpace(result)
|
|
|
|
if result == "" {
|
|
// If still no text, return simple fallback or raw output if short
|
|
if len(outputStr) < 200 {
|
|
return strings.TrimSpace(outputStr), nil
|
|
}
|
|
// Return empty string rather than spamming user with massive logs
|
|
return "", nil
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// getBinaryPath returns the path to the whisper binary
|
|
func (m *Manager) getBinaryPath() string {
|
|
// whisper.cpp extracts to a Release subdirectory
|
|
releaseDir := filepath.Join(m.installDir, "Release")
|
|
|
|
// Try different possible binary names
|
|
// whisper-cli.exe is the new standard (main.exe is deprecated)
|
|
possibleNames := []string{
|
|
"whisper-cli.exe",
|
|
"main.exe",
|
|
}
|
|
|
|
// First check in Release subdirectory
|
|
for _, name := range possibleNames {
|
|
path := filepath.Join(releaseDir, name)
|
|
if _, err := os.Stat(path); err == nil {
|
|
return path
|
|
}
|
|
}
|
|
|
|
// Then check in main install directory
|
|
for _, name := range possibleNames {
|
|
path := filepath.Join(m.installDir, name)
|
|
if _, err := os.Stat(path); err == nil {
|
|
return path
|
|
}
|
|
}
|
|
|
|
// Default to Release/main.exe
|
|
return filepath.Join(releaseDir, "main.exe")
|
|
}
|
|
|
|
// CheckOnline checks if internet is available
|
|
func CheckOnline() bool {
|
|
client := &http.Client{
|
|
Timeout: 3 * time.Second,
|
|
}
|
|
|
|
resp, err := client.Get("https://api.groq.com")
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return true
|
|
}
|
|
|
|
// DownloadProgress represents download progress
|
|
type DownloadProgress struct {
|
|
Downloaded int64
|
|
Total int64
|
|
Percent float64
|
|
}
|
|
|
|
// downloadFile downloads a file with progress reporting
|
|
func downloadFile(url, dest string, progress chan<- DownloadProgress) error {
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
out, err := os.Create(dest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
total := resp.ContentLength
|
|
var downloaded int64
|
|
|
|
buf := make([]byte, 32*1024)
|
|
for {
|
|
n, err := resp.Body.Read(buf)
|
|
if n > 0 {
|
|
out.Write(buf[:n])
|
|
downloaded += int64(n)
|
|
if progress != nil && total > 0 {
|
|
progress <- DownloadProgress{
|
|
Downloaded: downloaded,
|
|
Total: total,
|
|
Percent: float64(downloaded) / float64(total) * 100,
|
|
}
|
|
}
|
|
}
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|