diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..87042da --- /dev/null +++ b/.github/workflows/main.yml @@ -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 diff --git a/.github/workflows/main2.yml b/.github/workflows/main2.yml new file mode 100644 index 0000000..388306f --- /dev/null +++ b/.github/workflows/main2.yml @@ -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 diff --git a/Dockerfile b/Dockerfile index b6dad78..04c5d4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,11 @@ -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 && \ @@ -14,7 +13,7 @@ wget https://downloads.xiph.org/releases/opus/opus-tools-0.1.9.tar.gz --no-check 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 diff --git a/README.md b/README.md index a2776cb..5b20036 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ GET /speech?text= [&pitch=<0,99; default 50>] [&speed=<80,450; default 175 wpm>] [&voice=] - [&encoding=] + [&encoding=] ``` Response: diff --git a/server.go b/server.go index a2092e0..d4d17f8 100644 --- a/server.go +++ b/server.go @@ -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 @@ -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 { @@ -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 { diff --git a/server_test.go b/server_test.go new file mode 100644 index 0000000..b1456f5 --- /dev/null +++ b/server_test.go @@ -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") + } +}