Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix --speak-diacritics #12

Merged
merged 5 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
github: [c-loftus]
custom: ["https://www.paypal.com/paypalme/coltonloftus"]
32 changes: 32 additions & 0 deletions .github/workflows/go_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Go Test

on:
push:
# only trigger on branches, not on tags
branches: '**'

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '1.22'

- name: Install Calibre
run: |
sudo apt-get update
sudo apt-get install -y calibre

- name: Ensure ebook-convert is in PATH
run: |
echo "$(dirname $(which ebook-convert)) is in PATH"
ebook-convert --version # This will fail if ebook-convert is not installed correctly

- name: Run Go tests
run: go test ./... -p 1 -count=1
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"]
},
]
}
6 changes: 4 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"editor.formatOnType": true,
// hide git ignored files
"explorer.excludeGitIgnore": false
"explorer.excludeGitIgnore": false,
// disable caching for go tests; don't run in parallel since we need to access
// files on the local filesystem
"go.testFlags": ["-count=1", "-p", "1"]
}
7 changes: 3 additions & 4 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:latest as build
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
62 changes: 30 additions & 32 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,52 +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 {
fmt.Println("Text conversion completed successfully.")
} else if data == nil {
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
41 changes: 41 additions & 0 deletions 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 @@ -12,3 +13,43 @@ func TestCLI(t *testing.T) {
os.Args = append(os.Args, "https://example-files.online-convert.com/document/txt/example.txt")
RunCLI()
}

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-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)
}
}
7 changes: 5 additions & 2 deletions lib/piper.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ import (
"github.com/gen2brain/beeep"
)

func RunPiper(filename string, // we need to have the filename here since the file passed in is a tmp file and a dummy name
model string, file *os.File, outdir string) error {
func RunPiper(
filename string, // filename must be specified since the file passed in is a tmp file and a dummy name
model string, // piper model used
file *os.File, // file with text to convert
outdir string) error {

// Debugging: Read file content to check if it's empty
fileContent, err := io.ReadAll(file)
Expand Down
1 change: 1 addition & 0 deletions lib/test_chinese.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
彩虹,又稱天弓、天虹、絳等,簡稱虹,是氣象中的一種光學現象,當太陽 光照射到半空中的水滴,光線被折射及反射,在天空上形成拱形的七彩光譜,由外 圈至内圈呈紅、橙、黃、綠、蓝、靛蓝、堇紫七种颜色(霓虹則相反)。
Loading
Loading