mirror of
https://github.com/jahruz67/wisp-open.git
synced 2026-08-08 18:14:08 +00:00
DUMP
This commit is contained in:
@@ -0,0 +1,452 @@
|
||||
// Package whisper provides local offline transcription using whisper.cpp
|
||||
package whisper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"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
|
||||
}
|
||||
|
||||
modelPath := filepath.Join(m.installDir, Models[info.Model].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) (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)
|
||||
}
|
||||
|
||||
logger.Info("Running whisper: %s -m %s -f %s", binaryPath, modelPath, audioPath)
|
||||
|
||||
// Run whisper with simple arguments: ./main -m model.bin -f audio.wav
|
||||
// Vulkan build uses GPU by default, no need for -ngl
|
||||
cmd := exec.Command(binaryPath, "-m", modelPath, "-f", audioPath)
|
||||
|
||||
// Set working directory to the binary's location so it can find DLLs
|
||||
cmd.Dir = filepath.Dir(binaryPath)
|
||||
|
||||
// Hide the console window
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
HideWindow: true,
|
||||
CreationFlags: 0x08000000, // CREATE_NO_WINDOW
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user