Skip to content
Open
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
31 changes: 31 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: ci

on:
push:
branches:
- "main"

jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/clockbox:latest
31 changes: 31 additions & 0 deletions .github/workflows/main2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: ci

on:
push:
branches:
- "master"

jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/espeak-ipa:latest
15 changes: 7 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
FROM gliderlabs/alpine:3.1
RUN apk-install espeak opus lame flac wget && \
apk del libstdc++

FROM alpine:3.17.2
RUN apk add --no-cache espeak opus lame flac wget && \
apk del libstdc++
RUN cd /tmp && \
wget https://downloads.xiph.org/releases/opus/opus-tools-0.1.9.tar.gz --no-check-certificate && \
tar xzf opus-tools-0.1.9.tar.gz && \
wget https://downloads.xiph.org/releases/opus/opus-tools-0.1.9.tar.gz --no-check-certificate && \
tar xzf opus-tools-0.1.9.tar.gz && \
cd opus-tools-0.1.9/ && \
apk-install build-base flac-dev opus-dev libogg-dev && \
apk add --no-cache build-base flac-dev opus-dev libogg-dev && \
./configure && \
make && \
make install && \
rm -rf /tmp/* && \
apk del build-base flac-dev opus-dev libogg-dev
COPY server.go /tmp/
RUN cd /tmp && \
apk-install go && \
apk add --no-cache go && \
go build -o /server server.go && \
apk del go && \
rm /tmp/server.go
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ GET /speech?text=<utterance>
[&pitch=<0,99; default 50>]
[&speed=<80,450; default 175 wpm>]
[&voice=<name; default en>]
[&encoding=<mp3|opus; default mp3>]
[&encoding=<mp3|opus|wav; default mp3>]
```

Response:
Expand Down
41 changes: 38 additions & 3 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func buildEncodeCmd(values *url.Values, w *http.ResponseWriter) (cmd *exec.Cmd,
}

// Handles a request for synthesized speech. Build the synthesizer and encoder
// comands. Pipe the first to the second. Pipe the encoded stream out as the
// commands. Pipe the first to the second. Pipe the encoded stream out as the
// response. Supports URL arguments:
// text: string to synthesize (required)
// pitch: [0, 99] default: 50
Expand Down Expand Up @@ -198,8 +198,8 @@ func voicesHandler(w http.ResponseWriter, r *http.Request) {
}

voices := new(Voices)
// leave out the header value
voices.Names = strings.Split(voicesBuf.String(), "\n")[1:]
// leave out the header value and trailing empty line
voices.Names = strings.Split(strings.TrimSpace(voicesBuf.String()), "\n")[1:]

js, err := json.Marshal(voices)
if err != nil {
Expand All @@ -214,9 +214,44 @@ func voicesHandler(w http.ResponseWriter, r *http.Request) {
w.Write(cachedVoicesJSON)
}

// Handles a request to get the ipa of a word.
func ipaHandler(w http.ResponseWriter, r *http.Request) {

args := []string{"--ipa"}

values := r.URL.Query()

// text is the only required parameter
text := values.Get("text")
if len(text) == 0 {
err := errors.New("Missing required parameter: text")
http.Error(w, err.Error(), 400)
return
} else {
args = append(args, text)
}

// voice
voice := values.Get("voice")
if len(voice) > 0 {
args = append(args, "-v")
args = append(args, voice)
}

var ipaStr, err = exec.Command("espeak", args...).Output()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")

w.Write(ipaStr)
}

func main() {
http.HandleFunc("/speech", speechHandler)
http.HandleFunc("/voices", voicesHandler)
http.HandleFunc("/ipa", ipaHandler)

err := http.ListenAndServe(":8080", nil)
if err != nil {
Expand Down
37 changes: 37 additions & 0 deletions server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)

func TestVoicesHandler(t *testing.T) {
// Avoid external command execution by setting the cached response
cachedVoicesJSON = []byte(`{"names": ["en"]}`)

mux := http.NewServeMux()
mux.HandleFunc("/voices", voicesHandler)
ts := httptest.NewServer(mux)
defer ts.Close()

resp, err := http.Get(ts.URL + "/voices")
if err != nil {
t.Fatalf("failed to GET /voices: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
t.Fatalf("expected status 200, got %d", resp.StatusCode)
}

var data map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
t.Fatalf("failed to decode JSON: %v", err)
}

if _, ok := data["names"]; !ok {
t.Fatalf("response JSON missing 'names' key")
}
}