Skip to content

Commit fe19464

Browse files
committed
feat: added WebAPI and RPC services
1 parent 17236fb commit fe19464

File tree

4 files changed

+221
-66
lines changed

4 files changed

+221
-66
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* FXGL - JavaFX Game Library. The MIT License (MIT).
3+
* Copyright (c) AlmasB (almaslvl@gmail.com).
4+
* See LICENSE for details.
5+
*/
6+
7+
package com.almasb.fxgl.intelligence
8+
9+
import com.almasb.fxgl.core.concurrent.Async
10+
import com.almasb.fxgl.logging.Logger
11+
import com.almasb.fxgl.net.ws.LocalWebSocketServer
12+
import com.almasb.fxgl.net.ws.RPCService
13+
import javafx.beans.property.ReadOnlyBooleanProperty
14+
import javafx.beans.property.ReadOnlyBooleanWrapper
15+
import org.openqa.selenium.WebDriver
16+
import org.openqa.selenium.chrome.ChromeDriver
17+
import org.openqa.selenium.chrome.ChromeOptions
18+
19+
/**
20+
* Provides access to JS-driven implementation.
21+
*
22+
* @author Almas Baim (https://github.com/AlmasB)
23+
*/
24+
abstract class WebAPIService(server: LocalWebSocketServer, private val apiURL: String) : RPCService(server) {
25+
26+
private val log = Logger.get(WebAPIService::class.java)
27+
28+
private val readyProp = ReadOnlyBooleanWrapper(false)
29+
30+
var isReady: Boolean
31+
get() = readyProp.value
32+
protected set(value) { readyProp.value = value }
33+
34+
fun readyProperty(): ReadOnlyBooleanProperty {
35+
return readyProp.readOnlyProperty
36+
}
37+
38+
private var webDriver: WebDriver? = null
39+
40+
/**
41+
* Starts this service in a background thread.
42+
* Can be called after stop() to restart the service.
43+
* If the service has already started, then calls stop() and restarts it.
44+
*/
45+
fun start() {
46+
Async.startAsync {
47+
try {
48+
if (webDriver != null) {
49+
stop()
50+
}
51+
52+
val options = ChromeOptions()
53+
options.addArguments("--headless=new")
54+
options.addArguments("--use-fake-ui-for-media-stream")
55+
56+
webDriver = ChromeDriver(options)
57+
webDriver!!.get(apiURL)
58+
59+
onWebDriverLoaded(webDriver!!)
60+
} catch (e: Exception) {
61+
log.warning("Failed to start Chrome web driver. Ensure Chrome is installed in default location")
62+
log.warning("Error data", e)
63+
}
64+
}
65+
}
66+
67+
/**
68+
* Stops this service.
69+
* No-op if it has not started via start() before.
70+
*/
71+
fun stop() {
72+
try {
73+
if (webDriver != null) {
74+
webDriver!!.quit()
75+
webDriver = null
76+
}
77+
} catch (e: Exception) {
78+
log.warning("Failed to quit web driver", e)
79+
}
80+
}
81+
82+
/**
83+
* Called after the web driver has loaded the page.
84+
*/
85+
protected open fun onWebDriverLoaded(webDriver: WebDriver) { }
86+
}

fxgl-intelligence/src/main/kotlin/com/almasb/fxgl/intelligence/tts/TextToSpeechService.kt

Lines changed: 27 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,85 +6,58 @@
66

77
package com.almasb.fxgl.intelligence.tts
88

9-
import com.almasb.fxgl.core.EngineService
109
import com.almasb.fxgl.core.concurrent.Async
1110
import com.almasb.fxgl.intelligence.WebAPI
11+
import com.almasb.fxgl.intelligence.WebAPIService
1212
import com.almasb.fxgl.logging.Logger
1313
import com.almasb.fxgl.net.ws.LocalWebSocketServer
14-
import com.almasb.fxgl.speechrecog.SpeechRecognitionService
1514
import org.openqa.selenium.By
1615
import org.openqa.selenium.WebDriver
17-
import org.openqa.selenium.chrome.ChromeDriver
18-
import org.openqa.selenium.chrome.ChromeOptions
1916

2017
/**
21-
* TODO: remove duplicate code
22-
*
2318
* @author Almas Baim (https://github.com/AlmasB)
2419
*/
25-
class TextToSpeechService : EngineService() {
20+
class TextToSpeechService : WebAPIService(
21+
LocalWebSocketServer("TTSServer", WebAPI.TEXT_TO_SPEECH_PORT),
22+
WebAPI.TEXT_TO_SPEECH_API
23+
) {
2624

2725
private val log = Logger.get(TextToSpeechService::class.java)
28-
private val server = LocalWebSocketServer("TTSServer", WebAPI.TEXT_TO_SPEECH_PORT)
2926

30-
private var webDriver: WebDriver? = null
27+
private val synthVoices = arrayListOf<Voice>()
28+
var selectedVoice: Voice = Voice("NULL")
3129

32-
override fun onInit() {
33-
server.start()
34-
}
35-
36-
/**
37-
* Starts this service in a background thread.
38-
* Can be called after stop() to restart the service.
39-
* If the service has already started, then calls stop() and restarts it.
40-
*/
41-
fun start() {
42-
Async.startAsync {
43-
try {
44-
if (webDriver != null) {
45-
stop()
46-
}
30+
val voices: List<Voice>
31+
get() = synthVoices.toList()
4732

48-
val options = ChromeOptions()
49-
options.addArguments("--headless=new")
50-
options.addArguments("--use-fake-ui-for-media-stream")
51-
52-
webDriver = ChromeDriver(options)
53-
webDriver!!.get(WebAPI.TEXT_TO_SPEECH_API)
33+
override fun onWebDriverLoaded(webDriver: WebDriver) {
34+
// force it to play, so the audio output is initialized
35+
webDriver.findElement(By.id("play")).click()
36+
}
5437

55-
// TODO: update web-api impl
56-
// force it to play, so the audio output is initialized
57-
webDriver!!.findElement(By.id("play")).click()
38+
private fun initVoices(voiceNames: List<String>) {
39+
synthVoices += voiceNames.map { Voice(it) }
5840

59-
// we are ready to use the web api service
60-
} catch (e: Exception) {
61-
log.warning("Failed to start Chrome web driver. Ensure Chrome is installed in default location")
62-
log.warning("Error data", e)
63-
}
41+
if (synthVoices.isNotEmpty()) {
42+
selectedVoice = synthVoices[0]
6443
}
65-
}
6644

67-
/**
68-
* Stops this service.
69-
* No-op if it has not started via start() before.
70-
*/
71-
fun stop() {
72-
try {
73-
if (webDriver != null) {
74-
webDriver!!.quit()
75-
webDriver = null
76-
}
77-
} catch (e: Exception) {
78-
log.warning("Failed to quit web driver", e)
45+
Async.startAsyncFX {
46+
isReady = true
7947
}
8048
}
8149

8250
fun speak(text: String) {
83-
server.send(text)
51+
if (!isReady || synthVoices.isEmpty())
52+
return
53+
54+
rpcRun("speak", selectedVoice.name, text)
8455
}
8556

8657
override fun onExit() {
8758
stop()
88-
server.stop()
59+
super.onExit()
8960
}
90-
}
61+
}
62+
63+
data class Voice internal constructor(val name: String)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* FXGL - JavaFX Game Library. The MIT License (MIT).
3+
* Copyright (c) AlmasB (almaslvl@gmail.com).
4+
* See LICENSE for details.
5+
*/
6+
7+
package com.almasb.fxgl.net.ws
8+
9+
import com.almasb.fxgl.core.EngineService
10+
import com.almasb.fxgl.core.reflect.ReflectionFunctionCaller
11+
import com.almasb.fxgl.logging.Logger
12+
13+
private const val SEPARATOR = "*,,*"
14+
private const val FUNCTION_CALL_TAG = "F_CALL:"
15+
private const val FUNCTION_RETURN_TAG = "F_RETURN:"
16+
17+
/**
18+
* Allows a remote application (possibly written in a different language)
19+
* to issue function calls to and accept function calls from subclasses of this service.
20+
*
21+
* @author Almas Baim (https://github.com/AlmasB)
22+
*/
23+
abstract class RPCService(
24+
25+
/**
26+
* The server to which clients connect.
27+
* Maintenance responsibility of the server object lies with this RPC service.
28+
*/
29+
protected val server: LocalWebSocketServer
30+
) : EngineService() {
31+
32+
private val log = Logger.get(RPCService::class.java)
33+
34+
private val rfc = ReflectionFunctionCaller()
35+
36+
init {
37+
server.addMessageHandler { message ->
38+
if (message.startsWith(FUNCTION_CALL_TAG)) {
39+
val funcName = message.substringAfter(FUNCTION_CALL_TAG).substringBefore(SEPARATOR)
40+
val args = message.substringAfter(SEPARATOR)
41+
.split(SEPARATOR)
42+
.filter { it.isNotEmpty() }
43+
44+
rfc.call(funcName, args)
45+
}
46+
47+
if (message.startsWith(FUNCTION_RETURN_TAG)) {
48+
// TODO:
49+
}
50+
}
51+
}
52+
53+
override fun onInit() {
54+
rfc.addFunctionCallTarget(this)
55+
log.debug("Added ${javaClass.simpleName} methods: ${rfc.methods.map { it.name }}")
56+
57+
server.start()
58+
}
59+
60+
fun rpcRun(funcName: String, vararg args: String) {
61+
rpcRun(funcName, args.toList())
62+
}
63+
64+
fun rpcRun(funcName: String, args: List<String>) {
65+
var argsString = ""
66+
67+
args.forEach { argsString += it + SEPARATOR }
68+
69+
if (argsString.isNotEmpty()) {
70+
argsString = argsString.removeSuffix(SEPARATOR)
71+
}
72+
73+
server.send("$FUNCTION_CALL_TAG$funcName$SEPARATOR$argsString")
74+
}
75+
76+
private fun rpcReturn() {
77+
// TODO:
78+
}
79+
80+
override fun onExit() {
81+
server.stop()
82+
}
83+
}

fxgl-samples/src/main/java/sandbox/net/TTSSample.java

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
import com.almasb.fxgl.app.GameApplication;
1010
import com.almasb.fxgl.app.GameSettings;
1111
import com.almasb.fxgl.intelligence.tts.TextToSpeechService;
12-
import com.almasb.fxgl.speechrecog.SpeechRecognitionService;
1312
import com.almasb.fxgl.ui.FontType;
13+
import javafx.collections.FXCollections;
1414
import javafx.scene.control.Button;
1515
import javafx.scene.control.TextArea;
16+
import javafx.scene.text.Font;
1617

1718
import static com.almasb.fxgl.dsl.FXGL.*;
1819

@@ -27,38 +28,50 @@ public class TTSSample extends GameApplication {
2728

2829
@Override
2930
protected void initSettings(GameSettings settings) {
30-
settings.addEngineService(SpeechRecognitionService.class);
31+
settings.setWidth(1280);
32+
settings.setHeight(720);
3133
settings.addEngineService(TextToSpeechService.class);
3234
}
3335

3436
@Override
3537
protected void initGame() {
36-
getService(SpeechRecognitionService.class).addInputHandler(input -> {
37-
if (input.isEmpty() || input.trim().isEmpty())
38-
return;
39-
40-
getService(TextToSpeechService.class).speak(input);
41-
});
42-
43-
//getService(SpeechRecognitionService.class).start();
4438
getService(TextToSpeechService.class).start();
4539
}
4640

4741
@Override
4842
protected void initUI() {
4943
output = new TextArea();
5044
output.setWrapText(true);
51-
output.setPrefSize(getAppWidth(), getAppHeight() - 200);
45+
output.setPrefSize(getAppWidth() - 200, getAppHeight() - 200);
5246
output.setFont(getUIFactoryService().newFont(FontType.MONO, 18));
5347

5448
addUINode(output);
5549

5650
var btn = new Button("Speak");
51+
btn.setFont(Font.font(16.0));
5752
btn.setOnAction(e -> {
5853
getService(TextToSpeechService.class).speak(output.getText());
5954
});
6055

61-
addUINode(btn, 50, getAppHeight() - 150);
56+
addUINode(btn, 50, getAppHeight() - 100);
57+
58+
getService(TextToSpeechService.class).readyProperty().addListener((o, old, isReady) -> {
59+
if (isReady) {
60+
System.out.println("TTS service is ready");
61+
62+
var cb = getUIFactoryService().newChoiceBox(FXCollections.observableArrayList(getService(TextToSpeechService.class).getVoices()));
63+
cb.setPrefWidth(400);
64+
cb.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
65+
getService(TextToSpeechService.class).setSelectedVoice(newValue);
66+
});
67+
68+
if (!cb.getItems().isEmpty()) {
69+
cb.getSelectionModel().selectFirst();
70+
}
71+
72+
addUINode(cb, 50, getAppHeight() - 150);
73+
}
74+
});
6275
}
6376

6477
public static void main(String[] args) {

0 commit comments

Comments
 (0)