Skip to content

Commit

Permalink
clean ups
Browse files Browse the repository at this point in the history
  • Loading branch information
C-Loftus committed Oct 12, 2024
1 parent 62119f3 commit bded0b9
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 45 deletions.
15 changes: 12 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [

{
"name": "Launch Package",
"name": "Convert Remote",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}",
"program": "${workspaceFolder}",
"args": ["https://example-files.online-convert.com/document/txt/example.txt"],
}
},
{
"name": "Convert Chinese",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}",
"args": ["--model=zh_CN-huayan-medium.onnx", "lib/test_chinese.txt", "--speak-utf-8"]
},
]
}
5 changes: 2 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
## This dockerfile is primarily for testing. It can be used like the following:
# This dockerfile can be used to build a binary for use with the QuickPiperAudiobook command.
# You can use it for testing, or other architectures that don't have a piper build.
# docker build -t quickpiperaudiobook .
# docker run quickpiperaudiobook /app/examples/lorem_ipsum.txt

FROM --platform=linux/amd64 golang:1.22 as build

WORKDIR /app

# Copy all the code from the current directory
COPY . .

# Install Go dependencies and build the binary
RUN go mod tidy && \
go build -o QuickPiperAudiobook .

# Final stage
FROM --platform=linux/amd64 ubuntu:latest

# Install runtime dependencies
Expand Down
63 changes: 29 additions & 34 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import (
)

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"`
Input string `arg:"" help:"Local path or URL to the input file"`
Output string `help:"The directory in which to save the output audiobook"`
Model string `help:"Local path to the onnx model for piper to use"`
SpeakUTF8 bool `help:"Speak UTF-8 characters; Necessary for many non-English languages."`
ListModels bool `help:"List piper models which are installed locally"`
}

// package level variables we want to expose for testing
Expand All @@ -32,15 +32,15 @@ const defaultModel = "en_US-hfc_male-medium.onnx"
func RunCLI() {

if homedir_err != nil {
fmt.Printf("Error getting user home directory: %v\n", homedir_err)
return
fmt.Fprintf(os.Stderr, "Error getting user home directory: %v\n", homedir_err)
os.Exit(1)
}

var config CLI

if err := lib.CreateConfigIfNotExists(configFile, configDir, defaultModel); err != nil {
fmt.Printf("Error: %v\n", err)
return
fmt.Fprintf(os.Stderr, "Error creating default config file: %v\n", err)
os.Exit(1)
}

parser, _ := kong.New(&config, kong.Configuration(kongyaml.Loader, configFile))
Expand All @@ -49,8 +49,8 @@ func RunCLI() {
_, err := parser.Parse([]string{name})

if err != nil {
fmt.Println("Error parsing the value for", name, "in your config file at:", configFile)
return
fmt.Fprintf(os.Stderr, "Error parsing the value for %s in your config file at: %s\n", name, configFile)
os.Exit(1)
}
}

Expand All @@ -60,9 +60,7 @@ func RunCLI() {
if cli.ListModels {
models, err := lib.FindModels(configDir)
if err != nil {
fmt.Printf("Error: %v\n", err)
ctx.FatalIfErrorf(err)
return
}

if len(models) == 0 {
Expand All @@ -74,7 +72,7 @@ func RunCLI() {
}

if cli.Output == "" && config.Output != "" {
fmt.Println("No output value specified, default from config file: " + config.Output)
fmt.Println("No output directory 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 == "" {
Expand All @@ -98,9 +96,7 @@ func RunCLI() {
if _, err := os.Stat(cli.Output); os.IsNotExist(err) {
err := os.MkdirAll(cli.Output, os.ModePerm)
if err != nil {
fmt.Printf("Error: %v\n", err)
ctx.FatalIfErrorf(err)
return
}
}

Expand All @@ -109,55 +105,54 @@ func RunCLI() {
if err := lib.CheckEbookConvertInstalled(); err != nil {
fmt.Printf("Error: %v\n", err)
ctx.FatalIfErrorf(err)
return
}
}

if !lib.PiperIsInstalled(configDir) {
if err := lib.InstallPiper(configDir); err != nil {
ctx.FatalIfErrorf(err)
return
}
} else {
slog.Debug("Piper install detected in " + configDir)
}

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

if err != nil {
// Some errors above are fine; (we can just download the corresponding model)
// but others that pertain to having the model but not the corresponding metadata are
// an error that should be fatal
if modelPathErr != nil && strings.Contains(modelPathErr.Error(), "but the corresponding") {
ctx.FatalIfErrorf(modelPathErr)
}

if modelPathErr != nil {
// if the path can't be expanded, it doesn't exist and we need to download it
err := lib.DownloadModelIfNotExists(cli.Model, configDir)

if err != nil && modelPathErr != nil {
fmt.Printf("Error: %v\n", modelPathErr)
}

if err != nil {
fmt.Printf("Error: %v\n", err)
ctx.FatalIfErrorf(err)
return
}
modelPath, err = lib.ExpandModelPath(cli.Model, configDir)

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

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

if err != nil {
fmt.Printf("Error: %v\n", err)
ctx.FatalIfErrorf(err)
} else if data == nil {
fmt.Println("After converting" + cli.Input + "to txt, no data was generated.")
return
} else {
fmt.Println("Text conversion completed successfully.")
ctx.FatalIfErrorf(fmt.Errorf("after converting %s to txt, no data was generated", cli.Input))
}

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

}
Expand Down
31 changes: 30 additions & 1 deletion cli/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

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

Expand All @@ -17,10 +18,38 @@ func TestCLIWithDiacritics(t *testing.T) {
// reset all cli args, since the golang testing framework changes them
os.RemoveAll(configDir)
origArgs := os.Args
os.Args = append(origArgs[:1], "https://example-files.online-convert.com/document/txt/example.txt", "--speak-diacritics")
os.Args = append(origArgs[:1], "https://example-files.online-convert.com/document/txt/example.txt", "--speak-utf-8")
RunCLI()

// make sure that after running you can run the list models command and it will work
os.Args = append(origArgs[:1], "https://example-files.online-convert.com/document/txt/example.txt", "--list-models")
RunCLI()
}

// Test that the cli works with chinese language text
func TestChinese(t *testing.T) {
// reset all cli args, since the golang testing framework changes them
os.RemoveAll(configDir)
origArgs := os.Args
// get the file located at ../lib/test_chinese.txt
os.Args = append(origArgs[:1], "../lib/test_chinese.txt", "--model=zh_CN-huayan-medium.onnx", "--speak-utf-8")
RunCLI()

// check if there is a file at ~/Audiobooks/test_chinese.wav and make sure it's not empty
homedir, err := os.UserHomeDir()
if err != nil {
t.Fatalf("error getting user home directory: %v", err)
}
testFile := filepath.Join(homedir, "Audiobooks", "test_chinese.wav")
defer os.Remove(testFile)

if info, err := os.Stat(testFile); err != nil {
if os.IsNotExist(err) {
t.Fatalf("file not created: %v", err)
}
t.Fatalf("error getting file info: %v", err)
} else if info.Size() == 0 {
t.Fatalf("file is empty")
}

}
11 changes: 8 additions & 3 deletions lib/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ var ModelToURL = map[string]string{
"en_US-hfc_female-medium.onnx": "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/hfc_female/medium/en_US-hfc_female-medium.onnx",
"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",
// Below is an example of a non-English model
// I happily accept PRs for others here. It is just a bit tedious to enumerate them all
// since some do not follow the same pattern.
"zh_CN-huayan-medium.onnx": "https://huggingface.co/rhasspy/piper-voices/resolve/main/zh/zh_CN/huayan/medium/zh_CN-huayan-medium.onnx",
}

func ExpandModelPath(modelName string, defaultModelDir string) (string, error) {
Expand All @@ -25,14 +29,15 @@ func ExpandModelPath(modelName string, defaultModelDir string) (string, error) {
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)
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("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)
return "", fmt.Errorf("model: %s", modelName)
}
51 changes: 51 additions & 0 deletions lib/models_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package lib

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

func TestExpandModelPath(t *testing.T) {
// Create a temporary directory for testing
tempDir := t.TempDir()
modelName := "test_model"
modelPath := filepath.Join(tempDir, modelName)
modelJSONPath := modelPath + ".json"

// Test case 1: Both ONNX and JSON files are present
os.WriteFile(modelPath, []byte("dummy ONNX model"), 0644)
os.WriteFile(modelJSONPath, []byte("dummy JSON"), 0644)

result, err := ExpandModelPath(modelName, tempDir)
if err != nil || result != modelPath {
t.Errorf("Expected %s, got %s, error: %v", modelPath, result, err)
}

// Test case 2: ONNX file is present, but JSON file is missing
os.Remove(modelJSONPath) // remove the JSON file

result, err = ExpandModelPath(modelName, tempDir)
if err == nil || result != "" {
t.Errorf("Expected error for missing JSON file, got: %v, result: %s", err, result)
}

// Test case 3: Model not found
result, err = ExpandModelPath("non_existent_model", tempDir)
if err == nil || result != "" {
t.Errorf("Expected error for non-existent model, got: %v, result: %s", err, result)
}

// Test case 4: Model found in the default model directory
modelNameInDir := "another_model"
modelPathInDir := filepath.Join(tempDir, modelNameInDir)
modelJSONPathInDir := modelPathInDir + ".json"

os.WriteFile(modelPathInDir, []byte("dummy ONNX model"), 0644)
os.WriteFile(modelJSONPathInDir, []byte("dummy JSON"), 0644)

result, err = ExpandModelPath(modelNameInDir, tempDir)
if err != nil || result != modelPathInDir {
t.Errorf("Expected %s, got %s, error: %v", modelPathInDir, result, err)
}
}
1 change: 1 addition & 0 deletions lib/test_chinese.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
彩虹,又稱天弓、天虹、絳等,簡稱虹,是氣象中的一種光學現象,當太陽 光照射到半空中的水滴,光線被折射及反射,在天空上形成拱形的七彩光譜,由外 圈至内圈呈紅、橙、黃、綠、蓝、靛蓝、堇紫七种颜色(霓虹則相反)。
1 change: 0 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ Listen to sample output [ here ](./examples/)
> You don't need to have piper installed. This program manages piper and the associated models


## Usage

* Pass in either a local file or a remote URL to generate an audiobook:
Expand Down

0 comments on commit bded0b9

Please sign in to comment.