diff --git a/README.md b/README.md new file mode 100644 index 0000000..45dd8cf --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# SapiServer + +This is SapiServer repository. SapiServer is Windows SAPI simple web API server. + +## API + +### Get Voice Index + +Get available voice index on server machine. + +`GET /sapi/voices` + +#### Response Example + +``` +HTTP/1.1 200 OK +``` + +```json +[ + { + "index":0, + "name":"Microsoft David Desktop", + "gender":"Male", + "language":"409", + "vendor":"Microsoft", + "age":"Adult", + "description":"Microsoft David Desktop - English (United States)" + }, + { + "index":1, + "name":"Microsoft Hazel Desktop", + "gender":"Female", + "language":"809", + "vendor":"Microsoft", + "age":"Adult", + "description":"Microsoft Hazel Desktop - English (Great Britain)" + }, + { + "index":2, + "name":"Microsoft Zira Desktop", + "gender":"Female", + "language":"409", + "vendor":"Microsoft", + "age":"Adult", + "description":"Microsoft Zira Desktop - English (United States)" + }, + { + "index":3, + "name":"Microsoft Haruka Desktop", + "gender":"Female", + "language":"411", + "vendor":"Microsoft", + "age":"Adult", + "description":"Microsoft Haruka Desktop - Japanese" + } +] +``` + + +### Get Speech Data + +Create speech wave data + +`POST /sapi/create` + + +#### Required Parameters + +| Name | Type | Description | Example | +| ------- | ------- | ------- | ------- | +| **message** | *string* | Speech sentence or xml | `"This is a pen."` | +| **voice_index** | *string* | Speech voice index | `"0"` | + +#### Optional Parameters + +| Name | Type | Description | Example | +| ------- | ------- | ------- | ------- | +| **sapi_id** | *string* | unique id | `"abcd12344"` | + + +#### Response Example + +``` +HTTP/1.1 200 OK +``` + +``` + +``` + + +## Install on server + +Require go installation. + +```ps1 +> Set-ExecutionPolicy Unrestricted +``` + +```ps1 +> ./register_service.ps1 +``` + +```ps1 +> Server-Start SapiServer +``` + +and check SapiServer port :9081 on Windows Firewall. diff --git a/app/create_sapi.js b/app/create_sapi.js new file mode 100644 index 0000000..2f88b65 --- /dev/null +++ b/app/create_sapi.js @@ -0,0 +1,35 @@ +var storage_path = WScript.Arguments(0); +var voice_index = WScript.Arguments(1); +if (storage_path == undefined || voice_index == undefined) { + WScript.Quit(1); +} + +WScript.Echo(storage_path); + +var tts = WScript.CreateObject("SAPI.SpVoice"); +var stream = WScript.CreateObject("SAPI.SpFileStream"); + +stream.open(storage_path + ".wav", 3); + +tts.AudioOutputStream = stream; +tts.Voice = tts.GetVoices().Item(voice_index); + +var sr = new ActiveXObject("ADODB.Stream"); +sr.Type = 2 // text mode +sr.charset = "utf-8"; +sr.Open(); +sr.LoadFromFile(storage_path + ".txt"); + +var message = sr.ReadText(-1); // all line +sr.Close(); +fs = null; + +if (message.indexOf(" + + + + SAPI Test + + + + +

SAPI Server Play Test

+ +

+ +

+ +

+ +

+ + + +

+ +

+ +

+ +

+ + + + diff --git a/app/sapi_voices.js b/app/sapi_voices.js new file mode 100644 index 0000000..16f1dc7 --- /dev/null +++ b/app/sapi_voices.js @@ -0,0 +1,19 @@ +var tts = WScript.CreateObject("SAPI.SpVoice"); +voices = tts.GetVoices(); + +WScript.StdOut.Write("["); +for(var i = 0; i < voices.Count; i++){ + if(i > 0){ + WScript.StdOut.Write(","); + } + voice = voices.Item(i); + WScript.StdOut.Write("{\"index\":" + i + ", "); + WScript.StdOut.Write("\"name\":\"" + voice.GetAttribute("name") + "\", "); + WScript.StdOut.Write("\"gender\":\"" + voice.GetAttribute("gender") + "\", "); + WScript.StdOut.Write("\"language\":\"" + voice.GetAttribute("language") + "\", "); + WScript.StdOut.Write("\"vendor\":\"" + voice.GetAttribute("vendor") + "\", "); + WScript.StdOut.Write("\"age\":\"" + voice.GetAttribute("age") + "\", "); + WScript.StdOut.Write("\"description\":\"" + voice.GetDescription() + "\"} "); + +} +WScript.StdOut.Write("]"); diff --git a/app/server.exe b/app/server.exe new file mode 100644 index 0000000..8a45389 --- /dev/null +++ b/app/server.exe Binary files differ diff --git a/app/server.go b/app/server.go new file mode 100644 index 0000000..7ba4fda --- /dev/null +++ b/app/server.go @@ -0,0 +1,153 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os/exec" + "os" + "io/ioutil" + "runtime" + "path" + + "crypto/md5" + "encoding/hex" +) + +func currentSourcePath() string { + _, filename, _, _ := runtime.Caller(1) + return path.Dir(filename) +} + +func storagePath() string { + return currentSourcePath() + "\\..\\audio_data"; +} + +func storageFilePath(sapiId string) string { + return currentSourcePath() + "\\..\\audio_data\\" + sapiId +} + +func getVoicesInfo() string { + cmd := exec.Command("CScript", "//nologo", currentSourcePath() + "\\sapi_voices.js") + output, _ := cmd.Output() + return string(output) +} + +func md5Hash(text string) string { + hasher := md5.New() + hasher.Write([]byte(text)) + return hex.EncodeToString(hasher.Sum(nil)) +} + +func createWaveWithSapi(message string, voiceIndex string, sapiId string) bool { + // create message txt + err := ioutil.WriteFile(storageFilePath(sapiId) + ".txt", []byte(message), os.ModePerm) + if err != nil { + // err handle + log.Print("file output error, is there permission?") + return false + } + + // create wave + cmd := exec.Command("CScript", "//nologo", currentSourcePath() +"\\create_sapi.js", storageFilePath(sapiId), voiceIndex); + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Run() + return true +} + +func isExist(filename string) bool { + _, err := os.Stat(filename) + return err == nil +} + +func sapiHandle(w http.ResponseWriter, r *http.Request) { + message := r.FormValue("message") + voiceIndex := r.FormValue("voice_index") + + if message == "" || voiceIndex == "" { + w.WriteHeader(401) + fmt.Fprintf(w, "{\"error\":\"required params missing\"}") + return + } + + sapiId := md5Hash(voiceIndex + message) + + if !isExist(storageFilePath(sapiId) + ".wav") { + success := createWaveWithSapi(message, voiceIndex, sapiId) + + if !success { + w.WriteHeader(500) + fmt.Fprintf(w, "{\"error\":\"unable to create wave file\"}") + return + } + } + http.ServeFile(w, r, storageFilePath(sapiId) + ".wav") +} + +func voicesHandle(w http.ResponseWriter, r *http.Request) { + data := getVoicesInfo() + + fmt.Fprintf(w, data) +} + +func indexHandle(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, currentSourcePath() + "\\index.html") +} + +func initialize() { + fmt.Println("start init") + currentVoices := getVoicesInfo() + + // load prevoius voices + fmt.Println("load previous voices") + fi, fierr := os.Open("voices.json") + defer fi.Close() + previousVoices := "" + if fierr != nil { + fmt.Println("previous voices not detected") + }else{ + fmt.Println("previous voices loaded") + buf := make([]byte, 1024) + fi.Read(buf) + previousVoices = string(buf) + } + + // compare voices + if previousVoices != currentVoices { + fmt.Println("current voices chagned, clear caches") + // clear caches if voices updated + err := os.RemoveAll(storagePath()) + if err != nil { + panic(err) + } + + err = os.Mkdir(storagePath(), 077) + if err != nil { + panic(err) + } + } + + // update latest voices + fmt.Println("update voices") + fo, err := os.Create("voices.json") + if err != nil { + panic(err) + } + defer fo.Close() + + fo.WriteString(getVoicesInfo()) +} + + +func main(){ + initialize() + + http.HandleFunc("/sapi/create", sapiHandle) + http.HandleFunc("/sapi/voices", voicesHandle) + http.HandleFunc("/", indexHandle) + + if err := http.ListenAndServe(":9081", nil); err != nil { + log.Fatal("ListenAndServe ", err) + } +} diff --git a/app/voices.json b/app/voices.json new file mode 100644 index 0000000..c06b5c0 --- /dev/null +++ b/app/voices.json @@ -0,0 +1 @@ +[{"index":0, "name":"Microsoft Haruka Desktop", "gender":"Female", "language":"411", "vendor":"Microsoft", "age":"Adult", "description":"Microsoft Haruka Desktop - Japanese"} ,{"index":1, "name":"Microsoft Zira Desktop", "gender":"Female", "language":"409", "vendor":"Microsoft", "age":"Adult", "description":"Microsoft Zira Desktop - English (United States)"} ] \ No newline at end of file diff --git a/register_service.ps1 b/register_service.ps1 new file mode 100644 index 0000000..1eb0964 --- /dev/null +++ b/register_service.ps1 @@ -0,0 +1,13 @@ +go build -o app\server.exe app\server.go + +$CommandStr = +@" +New-Service -Name SapiServer -BinaryPathName 'D:\Tools\srvany-ng.exe' +New-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\SapiServer\Parameters' +Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\SapiServer\Parameters' -Name Application "$($PSScriptRoot)\app\server.exe" +Write-Host '`Start-Service SapiServer` to start service' +Write-Host '`sc.exe delete SapiServer` to delete service' +"@ +$Command = [Scriptblock]::Create($CommandStr) + +Start-Process PowerShell -ArgumentList "-NoExit -Command & { $Command }" -Verb RunAs