diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..86b85b5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,82 @@ +name: nimreleaser + +on: + release: + types: + - published + +env: + APP_NAME: 'voicepeaky' + NIM_VERSION: 'stable' + MAINTAINER: 'solaoi' + +jobs: + build-artifact: + runs-on: ${{ matrix.os.name }} + strategy: + matrix: + os: + - name: ubuntu-latest + platform: linux + cpu: amd64 + - name: macOS-latest + platform: darwin + cpu: amd64 + steps: + - uses: actions/checkout@v1 + - uses: jiro4989/setup-nim-action@v1 + with: + nim-version: ${{ env.NIM_VERSION }} + - run: nimble build -Y -d:release --threads:on + - name: Create artifact + run: | + assets="${{ env.APP_NAME }}_${{ matrix.os.platform }}_${{ matrix.os.cpu }}" + tar czf "$assets.tar.gz" "${{ env.APP_NAME }}" + shell: bash + - uses: actions/upload-artifact@v2 + with: + name: artifact-${{ matrix.os.name }} + path: | + *.tar.gz + + upload-release: + runs-on: ubuntu-latest + needs: build-artifact + strategy: + matrix: + include: + - os: ubuntu-latest + asset_name_suffix: linux_amd64.tar.gz + asset_content_type: application/gzip + - os: macOS-latest + asset_name_suffix: darwin_amd64.tar.gz + asset_content_type: application/gzip + steps: + - uses: actions/download-artifact@v2 + with: + name: artifact-${{ matrix.os }} + + - name: Upload Release Asset + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ${{ env.APP_NAME }}_${{ matrix.asset_name_suffix }} + asset_name: ${{ env.APP_NAME }}_${{ matrix.asset_name_suffix }} + asset_content_type: ${{ matrix.asset_content_type }} + + homebrew: + runs-on: ubuntu-latest + needs: upload-release + steps: + - name: Update Homebrew Formula + uses: izumin5210/action-homebrew-tap@releases/v0 + with: + tap: solaoi/homebrew-tap + token: ${{ secrets.GITHUB_TOKEN }} + tap-token: ${{ secrets.TAP_GITHUB_TOKEN }} + release-branch: main + formula: voicepeaky.rb + commit-message: Brew formula update for voicepeaky version ${{ env.RELEASE_VERSION }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b8493e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +voicepeaky +*.json +!sample.json +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..064e1f1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 solaoi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..005f5eb --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Voicepeaky + +[![license](https://img.shields.io/github/license/solaoi/voicepeaky)](https://github.com/solaoi/voicepeaky/blob/main/LICENSE) +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/solaoi/voicepeaky)](https://github.com/solaoi/voicepeaky/releases) +[![GitHub Sponsors](https://img.shields.io/github/sponsors/solaoi?color=db61a2)](https://github.com/sponsors/solaoi) + +This is a server to use voicepeak as api. + +## Requirements + +- [VOICEPEAK 商用可能 6ナレーターセット](https://www.ah-soft.com/voice/6nare/index.html) +- [VOICEPEAK 商用可能 ナレーター](https://www.ah-soft.com/voice/narrator/index.html) + +## Usage + +### Serve + +```sh +voicepeaky -p 9999 -s +``` + +Option is below. + +| Option | Description | +| --------- | ---------------------------------------- | +| -p,--port | specify the port you want to serve | +| -s,--skip | skip old text when new text is requested | + +### Request + +```sh +curl -X POST -H "Content-Type: application/json" -d '@sample.json' localhost:9999 +``` + +RequestBody (JSON Format) is below. +see a sample [here](https://raw.githubusercontent.com/solaoi/voicepeaky/main/sample.json). + +| Field | Type | Sample | +| ------------- | ----------------------- | --------------------- | +| - (parent) | JSONObject | - | +| narrator | string*1 | "Japanese Male Child" | +| emotion | JSONObject | - | +| emotion/happy | number(0 - 100) | 0 | +| emotion/fun | number(0 - 100) | 0 | +| emotion/angry | number(0 - 100) | 0 | +| emotion/sad | number(0 - 100) | 0 | +| speed | number(50 - 200) | 100 | +| pitch | number(-300 - 300) | 0 | +| text | string | "こんにちは" | + +*1 +| Types of Narrators | Requirements | +| --------------------- | ---------------------------------------------------------------------------------- | +| Japanese Male Child | [VOICEPEAK 商用可能 ナレーター](https://www.ah-soft.com/voice/narrator/index.html) | +| Japanese Female Child | [VOICEPEAK 商用可能 6ナレーターセット](https://www.ah-soft.com/voice/6nare/index.html) | +| Japanese Male 1 | [VOICEPEAK 商用可能 6ナレーターセット](https://www.ah-soft.com/voice/6nare/index.html) | +| Japanese Male 2 | [VOICEPEAK 商用可能 6ナレーターセット](https://www.ah-soft.com/voice/6nare/index.html) | +| Japanese Male 3 | [VOICEPEAK 商用可能 6ナレーターセット](https://www.ah-soft.com/voice/6nare/index.html) | +| Japanese Male 4 | [VOICEPEAK 商用可能 ナレーター](https://www.ah-soft.com/voice/narrator/index.html) | +| Japanese Female 1 | [VOICEPEAK 商用可能 6ナレーターセット](https://www.ah-soft.com/voice/6nare/index.html) | +| Japanese Female 2 | [VOICEPEAK 商用可能 6ナレーターセット](https://www.ah-soft.com/voice/6nare/index.html) | +| Japanese Female 3 | [VOICEPEAK 商用可能 6ナレーターセット](https://www.ah-soft.com/voice/6nare/index.html) | +| Japanese Female 4 | [VOICEPEAK 商用可能 ナレーター](https://www.ah-soft.com/voice/narrator/index.html) | + +## Install + +### 1. Mac + +``` +# Install +brew install solaoi/tap/voicepeaky +# Update +brew upgrade voicepeaky +``` + +### 2. BinaryRelease + +```sh +# Install with wget or curl +## set the latest version on releases. +VERSION=v1.0.0 +## set the OS you use. (macos) +OS=linux +## case you use wget +wget https://github.com/solaoi/voicepeaky/releases/download/$VERSION/voicepeaky${OS}.tar.gz +## case you use curl +curl -LO https://github.com/voicepeaky/broly/releases/download/$VERSION/voicepeaky${OS}.tar.gz +## extract +tar xvf ./voicepeaky${OS}.tar.gz +## move it to a location in your $PATH, such as /usr/local/bin. +mv ./voicepeaky /usr/local/bin/ +``` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..a76fd10 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Supported Versions + +There is only one version track for voicepeaky so all patches will be applied to the latest version only. + +| Version | Supported | +| ------- | ------------------ | +| Latest | :white_check_mark: | + +## Reporting a Vulnerability + +Critical vulnerabilities can be responsibly disclosed to [mail@aota.blog](mailto:mail@aota.blog). +Bugs and low level vulneratbilities can be reported via repository issues. \ No newline at end of file diff --git a/nim.cfg b/nim.cfg new file mode 100644 index 0000000..9d57ecf --- /dev/null +++ b/nim.cfg @@ -0,0 +1 @@ +--threads:on \ No newline at end of file diff --git a/sample.json b/sample.json new file mode 100644 index 0000000..d4446f9 --- /dev/null +++ b/sample.json @@ -0,0 +1,12 @@ +{ + "narrator": "Japanese Female Child", + "emotion": { + "happy": 0, + "fun": 0, + "angry": 0, + "sad": 0 + }, + "speed": 100, + "pitch": 0, + "text": "こんにちは" +} \ No newline at end of file diff --git a/src/speech.nim b/src/speech.nim new file mode 100644 index 0000000..72c3b27 --- /dev/null +++ b/src/speech.nim @@ -0,0 +1,28 @@ +import json,strformat + +type Speech* = ref object + narrator*: string + happy*: int + fun*: int + angry*: int + sad*: int + speed*: int + pitch*: int + text*: string + +type + SpeechInitError* = object of IOError + +proc createSpeech*(json:JsonNode, text:string) :Speech= + result = new Speech + try: + result.narrator = json["narrator"].getStr + result.happy = json["emotion"]["happy"].getInt + result.fun = json["emotion"]["fun"].getInt + result.angry = json["emotion"]["angry"].getInt + result.sad = json["emotion"]["sad"].getInt + result.speed = json["speed"].getInt + result.pitch = json["pitch"].getInt + result.text = text + except: + raise newException(SpeechInitError, fmt"request body is invalid") diff --git a/src/voicepeaky.nim b/src/voicepeaky.nim new file mode 100644 index 0000000..a1713e7 --- /dev/null +++ b/src/voicepeaky.nim @@ -0,0 +1,89 @@ +import httpbeast,os,strutils,parseopt,json,asyncdispatch,options,threadpool,times,algorithm,osproc +import speech + +let workspace = "/tmp/voicepeaky" +var speeches: seq[Speech] +var is_skip: bool + +proc getArgs():tuple[port:int, skip:bool] = + result = (port:8080, skip:false) + var opt = parseopt.initOptParser( os.commandLineParams().join(" ") ) + for kind, key, val in opt.getopt(): + case key + of "port", "p": + case kind + of parseopt.cmdLongOption, parseopt.cmdShortOption: + opt.next() + result.port = opt.key.parseInt() + else: discard + of "skip", "s": + result.skip = true + +proc onRequest(req: Request): Future[void]{.async.} = + var isSend:bool + {.cast(gcsafe).}: + if req.httpMethod == some(HttpPost): + try: + let requestBody = req.body.get.parseJson + + let text = requestBody["text"].getStr + var textArr: seq[Speech] + for line in splitLines(text): + for temp in line.split("。"): + for value in temp.split("、"): + if not value.isEmptyOrWhitespace(): + let speech = createSpeech(requestBody, value) + textArr.add(speech) + if is_skip: + speeches = @[] + speeches = @textArr.reversed & @speeches + + req.send(Http201) + except Exception: + let + headers = "Content-type: application/json; charset=utf-8" + response = %*{"message": "Error occurred."} + req.send(Http400, $response, headers) + finally: + isSend=true + break + if not isSend: + req.send(Http404) + +proc pollingFiles() {.thread.} = + {.cast(gcsafe).}: + while true: + var files: seq[string] + for f in walkDir(workspace): + files.add(f.path) + files.sort(); + for file in files: + let _ = execCmd("afplay " & file) + removeFile(file) + +proc pollingSpeeches() {.thread.} = + {.cast(gcsafe).}: + while true: + if speeches.len != 0: + let speech = speeches.pop() + let _ = execCmd("/Applications/voicepeak.app/Contents/MacOS/voicepeak --say " & speech.text & + " --narrator \"" & speech.narrator & + "\" --emotion happy=" & $speech.happy & + ",fun=" & $speech.fun & + ",angry=" & $speech.fun & + ",sad=" & $speech.fun & + " --speed " & $speech.speed & + " --pitch " & $speech.pitch & + " --out " & workspace & "/" & $now() & ".wav") + +when isMainModule: + if not dirExists(workspace): + createDir(workspace) + + spawn pollingSpeeches() + spawn pollingFiles() + + let (port, skip) = getArgs() + is_skip = skip + let settings = initSettings(Port(port)) + run(onRequest, settings) diff --git a/voicepeaky.nimble b/voicepeaky.nimble new file mode 100644 index 0000000..8fd621c --- /dev/null +++ b/voicepeaky.nimble @@ -0,0 +1,14 @@ +# Package + +version = "1.0.0" +author = "solaoi" +description = "Voicepeak Server" +license = "MIT" +srcDir = "src" +bin = @["voicepeaky"] + + +# Dependencies + +requires "nim >= 1.6.10" +requires "httpbeast >= 0.4.1"