Skip to content

Commit

Permalink
Merge pull request #1 from C-Loftus/iconv
Browse files Browse the repository at this point in the history
Improve Testing; Don't speak diacritics by default
  • Loading branch information
C-Loftus authored Sep 15, 2024
2 parents abf282e + 6750563 commit d535608
Show file tree
Hide file tree
Showing 17 changed files with 761 additions and 412 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"request": "launch",
"mode": "auto",
"program": "${fileDirname}",
"args": ["https://example-files.online-convert.com/document/txt/example.txt"]
"args": ["https://example-files.online-convert.com/document/txt/example.txt"],
}
]
}
150 changes: 150 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package cli

import (
"fmt"
"os/user"
"path/filepath"
"strings"

"QuickPiperAudiobook/lib"

"github.com/alecthomas/kong"
kongyaml "github.com/alecthomas/kong-yaml"
"github.com/fatih/color"
)

type CLI struct {
Input string `arg:"" help:"Local path or URL to the input file"`
Output string `help:"Directory in which to save the converted ebook file"`
Model string `help:"Local path to the onnx model for piper to use"`
SpeakDiacritics bool `help:"Speak diacritics from the input file"`
ListModels bool `help:"List available models"`
}

// package level variables we want to expose for testing
var usr, _ = user.Current()
var configDir = filepath.Join(usr.HomeDir, ".config", "QuickPiperAudiobook")
var configFile = filepath.Join(configDir, "config.yaml")

const defaultModel = "en_US-hfc_male-medium.onnx"

// All cli code is outside of the main package for testing purposes
func RunCLI() {

var config CLI

modelDirectory, _ := filepath.Abs(filepath.Join(usr.HomeDir, ".config", "QuickPiperAudiobook"))

if err := lib.CreateConfigIfNotExists(configFile, configDir, defaultModel); err != nil {
fmt.Printf("Error: %v\n", err)
return
}

parser, _ := kong.New(&config, kong.Configuration(kongyaml.Loader, configFile))

for _, name := range []string{"output", "model"} {
_, err := parser.Parse([]string{name})

if err != nil {
fmt.Println("Error parsing the value for", name, "in your config file at:", configFile)
return
}
}

var cli CLI
ctx := kong.Parse(&cli, kong.Description("Covert a text file to an audiobook using a managed piper install"))

if cli.ListModels {
models, err := lib.FindModels(modelDirectory)
if err != nil {
fmt.Printf("Error: %v\n", err)
ctx.FatalIfErrorf(err)
return
}

if len(models) == 0 {
fmt.Println("No models found in " + modelDirectory)
} else {
fmt.Println("Found models:\n" + strings.TrimSpace(strings.Join(models, "\n")))
}
return
}

if cli.Output == "" && config.Output != "" {
fmt.Println("No output value specified, default from config file: " + config.Output)
cli.Output = config.Output
// if output is not set and config is not set, default to current directory
} else if cli.Output == "" && config.Output == "" {
cli.Output = "."
}

if cli.Model == "" && config.Model != "" {
fmt.Println("Using model specified in config file: " + config.Model)
cli.Model = config.Model
} else if cli.Model == "" && config.Model == "" {
println("No model specified. Defaulting to " + defaultModel)
cli.Model = defaultModel
}

if strings.HasPrefix(cli.Output, "~/") {
// if it starts with ~, then we need to expand it
cli.Output = filepath.Join(usr.HomeDir, cli.Output[2:])
}

if (filepath.Ext(cli.Input)) != ".txt" {
// if it is not already a .txt file, then we need to convert it to .txt and thus need ebook-convert
if err := lib.CheckEbookConvertInstalled(); err != nil {
fmt.Printf("Error: %v\n", err)
ctx.FatalIfErrorf(err)
return
}
}

if !lib.PiperIsInstalled(modelDirectory) {
if err := lib.InstallPiper(modelDirectory); err != nil {
ctx.FatalIfErrorf(err)
return
}
}

modelPath, err := lib.ExpandModelPath(cli.Model, modelDirectory)

if err != nil {
// if the path can't be expanded, it doesn't exist and we need to download it
err := lib.DownloadModelIfNotExists(cli.Model, modelDirectory)
if err != nil {
fmt.Printf("Error: %v\n", err)
ctx.FatalIfErrorf(err)
return
}
modelPath, err = lib.ExpandModelPath(cli.Model, modelDirectory)

if err != nil {
fmt.Printf("Error could not find the model path after downloading it: %v\n", err)
ctx.FatalIfErrorf(err)
return
}
}

data, err := lib.GetConvertedRawText(cli.Input)

if err != nil {
fmt.Printf("Error: %v\n", err)
ctx.FatalIfErrorf(err)
} else {
fmt.Println("Text conversion completed successfully.")
}

if !cli.SpeakDiacritics {
if data, err = lib.RemoveDiacritics(data); err != nil {
fmt.Printf("Error: %v\n", err)
ctx.FatalIfErrorf(err)
return
}

}

if err := lib.RunPiper(cli.Input, modelPath, data, cli.Output); err != nil {
color.Red("Error: %v", err)
}
}
14 changes: 14 additions & 0 deletions cli/e2e_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package cli

import (
"os"
"testing"
)

func TestCLI(t *testing.T) {
// reset all cli args, since the golang testing framework changes them
os.RemoveAll(configDir)
os.Args = os.Args[:1]
os.Args = append(os.Args, "https://example-files.online-convert.com/document/txt/example.txt")
RunCLI()
}
File renamed without changes.
23 changes: 23 additions & 0 deletions lib/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package lib

import (
"fmt"
"os"
)

func CreateConfigIfNotExists(configPath string, configDir string, defaultModel string) error {
if _, err := os.Stat(configPath); os.IsNotExist(err) {
if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("error creating config directory: %v", err)
}

defaultConfig := []byte(fmt.Sprintf("output: ~/Audiobooks\nmodel: %q\n", defaultModel))
if err := os.WriteFile(configPath, defaultConfig, 0644); err != nil {
return fmt.Errorf("error creating config file: %v", err)
}
fmt.Println("New default configuration file created at", configPath)
} else if err != nil {
return fmt.Errorf("error checking if config file exists: %v", err)
}
return nil
}
33 changes: 33 additions & 0 deletions lib/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package lib

import (
"os"
"os/user"
"path/filepath"
"testing"
)

func TestCreateConfigIfNotExists(t *testing.T) {
usr, _ := user.Current()
configDir := filepath.Join(usr.HomeDir, ".config", "QuickPiperAudiobook")
configFile := filepath.Join(configDir, "config.yaml")
defaultModel := "en_US-hfc_male-medium.onnx"

if err := CreateConfigIfNotExists(configFile, configDir, defaultModel); err != nil {
t.Fatalf("error creating config file: %v", err)
}

if _, err := os.Stat(configDir); os.IsNotExist(err) {
t.Fatalf("config directory not created: %v", err)
}

if _, err := os.Stat(configFile); os.IsNotExist(err) {
t.Fatalf("config file not created: %v", err)
}

//teardown
if err := os.Remove(configFile); err != nil {
t.Fatalf("error removing config file: %v", err)
}

}
31 changes: 23 additions & 8 deletions lib/downloads.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,45 @@ import (
"net/http"
"net/url"
"os"
"strings"
)

func IsUrl(str string) bool {
u, err := url.Parse(str)
return err == nil && u.Scheme != "" && u.Host != ""
}

func DownloadIfNotExists(fileURL, fileName string) error {
if _, err := os.Stat(fileName); os.IsNotExist(err) {
if _, err := DownloadFile(fileURL, fileName); err != nil {
func DownloadIfNotExists(fileURL, fileName string, outputDir string) error {

// add the "/" to the end so it the outputdir is a valid path
if !strings.HasSuffix(outputDir, "/") {
outputDir += "/"
}

outputPath := outputDir + fileName

if _, err := os.Stat(outputPath); os.IsNotExist(err) {
if _, err := DownloadFile(fileURL, fileName, outputDir); err != nil {
return err
}
}
return nil
}

func DownloadFile(url string, filename string) (*os.File, error) {
func DownloadFile(url string, outputName string, outputDir string) (*os.File, error) {

if !strings.HasSuffix(outputDir, "/") {
outputDir += "/"
}

outputPath := outputDir + outputName

println("Downloading " + filename)
println("Downloading " + outputName + " to " + outputPath)

// Create the file to save the model
file, err := os.Create(filename)
file, err := os.Create(outputPath)
if err != nil {
return nil, fmt.Errorf("error creating file %s: %v", filename, err)
return nil, fmt.Errorf("error creating file %s: %v", outputPath, err)
}
defer file.Close()

Expand All @@ -48,7 +63,7 @@ func DownloadFile(url string, filename string) (*os.File, error) {
// Copy the response body to the file
_, err = io.Copy(file, resp.Body)
if err != nil {
return nil, fmt.Errorf("error saving file %s: %v", filename, err)
return nil, fmt.Errorf("error saving file %s: %v", outputPath, err)
}

fmt.Println("Finished downloading successfully.")
Expand Down
26 changes: 26 additions & 0 deletions lib/models.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package lib

import (
"fmt"
"os"
"path/filepath"
)

// Piper has hundreds of pretrained models on the sample Website
// These are some of the best ones for English. However, as long
// as you have both the .onnx and .onnx.json files locally, you
Expand All @@ -10,3 +16,23 @@ var ModelToURL = map[string]string{
"en_US-lessac-medium.onnx": "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx",
"en_GB-northern_english_male-medium.onnx": "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_GB/northern_english_male/medium/en_GB-northern_english_male-medium.onnx",
}

func ExpandModelPath(modelName string, defaultModelDir string) (string, error) {
// when given a modelName check if it is present relatively or in the modelDir
// a path should only be valid if both the onnx and onnx.json file is present

if _, err := os.Stat(modelName); err == nil {
if _, err := os.Stat(modelName + ".json"); err == nil {
return modelName, nil
}
return "", fmt.Errorf("onnx for model: %s was found but the corresponding onnx.json was not", modelName)
}
if _, err := os.Stat(filepath.Join(defaultModelDir, modelName)); err == nil {
if _, err := os.Stat(filepath.Join(defaultModelDir, modelName) + ".json"); err == nil {
return filepath.Join(defaultModelDir, modelName), nil
}
return "", fmt.Errorf("onnx for model: %s was found in the model directory: %s but the corresponding onnx.json was not", modelName, defaultModelDir)

}
return "", fmt.Errorf("model not found: %s", modelName)
}
Loading

0 comments on commit d535608

Please sign in to comment.