Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[rest] Added Voice / TTS API #1017

Merged
merged 3 commits into from
Sep 2, 2019
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions bundles/org.openhab.core.io.rest.voice/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
<name>openHAB Core :: Bundles :: Voice REST Interface</name>

<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.audio</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.voice</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2019 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.smarthome.io.rest.voice.internal;

import org.eclipse.smarthome.core.voice.Voice;

/**
* A DTO that is used on the REST API to provide infos about {@link Voice} to UIs.
*
* @author Laurent Garnier - Initial contribution
*
*/
public class VoiceDTO {
public String id;
public String label;
public String locale;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2019 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.smarthome.io.rest.voice.internal;

import org.eclipse.smarthome.core.voice.Voice;

/**
* Mapper class that maps {@link Voice} instanced to their respective DTOs.
*
* @author Laurent Garnier - Initial contribution
*/
public class VoiceMapper {

/**
* Maps a {@link Voice} to an {@link VoiceDTO}.
*
* @param voice the voice
*
* @return the corresponding DTO
*/
public static VoiceDTO map(Voice voice) {
VoiceDTO dto = new VoiceDTO();
dto.id = voice.getUID();
dto.label = voice.getLabel();
dto.locale = voice.getLocale().toString();
return dto;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;

import org.eclipse.smarthome.core.audio.AudioManager;
import org.eclipse.smarthome.core.audio.AudioSink;
import org.eclipse.smarthome.core.auth.Role;
import org.eclipse.smarthome.core.voice.Voice;
import org.eclipse.smarthome.core.voice.VoiceManager;
import org.eclipse.smarthome.core.voice.text.HumanLanguageInterpreter;
import org.eclipse.smarthome.core.voice.text.InterpretationException;
Expand All @@ -54,6 +57,7 @@
* This class acts as a REST resource for voice features.
*
* @author Kai Kreuzer - Initial contribution and API
* @author Laurent Garnier - add TTS feature to the REST API
*/
@Component
@Path(VoiceResource.PATH_SITEMAPS)
Expand All @@ -67,6 +71,7 @@ public class VoiceResource implements RESTResource {
UriInfo uriInfo;

private VoiceManager voiceManager;
private AudioManager audioManager;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should imho do without that dependency - the VoiceManager offers methods to call without sinkIds and it handles the calls internally. The REST API should behave the same way as the Java API.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, this was necessary just to make a pre-control and avoid returning status 200 when say will fail because the provided audio sink is wrong.
But I can suppress this test and so this dependency to AudoManager.

private LocaleService localeService;

@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
Expand All @@ -78,6 +83,15 @@ public void unsetVoiceManager(VoiceManager voiceManager) {
this.voiceManager = null;
}

@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
public void setAudioManager(AudioManager audioManager) {
this.audioManager = audioManager;
}

public void unsetAudioManager(AudioManager audioManager) {
this.audioManager = null;
}

@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
protected void setLocaleService(LocaleService localeService) {
this.localeService = localeService;
Expand Down Expand Up @@ -106,7 +120,7 @@ public Response getInterpreters(
@GET
@Path("/interpreters/{id: [a-zA-Z_0-9]*}")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Gets a single interpreters.", response = HumanLanguageInterpreterDTO.class)
@ApiOperation(value = "Gets a single interpreter.", response = HumanLanguageInterpreterDTO.class)
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 404, message = "Interpreter not found") })
public Response getInterpreter(
Expand Down Expand Up @@ -169,6 +183,91 @@ public Response interpret(@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @ApiParam(va
}
}

@GET
@Path("/voices")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Get the list of all voices.", response = VoiceDTO.class, responseContainer = "List")
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
public Response getVoices() {
Collection<Voice> voices = voiceManager.getAllVoices();
List<VoiceDTO> dtos = new ArrayList<>(voices.size());
for (Voice voice : voices) {
dtos.add(VoiceMapper.map(voice));
}
return Response.ok(dtos).build();
}

@GET
@Path("/defaultvoice")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Gets the default voice.", response = VoiceDTO.class)
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 404, message = "No default voice was found.") })
public Response getDefaultVoice() {
Voice voice = voiceManager.getDefaultVoice();
if (voice != null) {
VoiceDTO dto = VoiceMapper.map(voice);
return Response.ok(dto).build();
} else {
return JSONResponse.createErrorResponse(Status.NOT_FOUND, "Default voice not found");
}
}

@POST
@Path("/say")
@Consumes(MediaType.TEXT_PLAIN)
@ApiOperation(value = "Speaks a given text with the default voice through the default audio sink.")
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 404, message = "No default voice or no default audio sink was found.") })
public Response say(@ApiParam(value = "text to speak", required = true) String text) {
Voice voice = voiceManager.getDefaultVoice();
AudioSink sink = audioManager.getSink();
if (voice == null) {
return JSONResponse.createErrorResponse(Status.NOT_FOUND, "Default voice not found");
} else if (sink == null) {
return JSONResponse.createErrorResponse(Status.NOT_FOUND, "Default audio sink not found");
} else {
voiceManager.say(text, voice.getUID(), sink.getId());
return Response.ok(null, MediaType.TEXT_PLAIN).build();
}
}

@POST
@Path("/say/{sinkid: [a-zA-Z_:0-9]*}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sinkId is not a member of an entity "say", so it should imho rather be a QueryParam instead of a PathParam here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense.
I think I do it like that because the interpret API just above was using this pattern.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but for the interpreters call, it is selecting an entity by id from the list of interpreters, so it is a different situation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the difference.
Here I want to select a voice amongst a list of voices, and a sink amongst a list of audio sinks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your uri is say/sinkid and not sinks/sinkid or voices/voiceid. Hence "sinkid" is not an entity of the "say" entities. See the difference?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean now.

@Consumes(MediaType.TEXT_PLAIN)
@ApiOperation(value = "Speaks a given text with the default voice through the given audio sink.")
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 404, message = "No default voice was found or audio sink does not exist.") })
public Response say(@ApiParam(value = "text to speak", required = true) String text,
@PathParam("sinkid") @ApiParam(value = "audio sink id", required = true) String sinkId) {
Voice voice = voiceManager.getDefaultVoice();
if (voice == null) {
return JSONResponse.createErrorResponse(Status.NOT_FOUND, "Default voice not found");
} else if (audioManager.getSink(sinkId) == null) {
return JSONResponse.createErrorResponse(Status.NOT_FOUND, "Undefined audio sink");
} else {
voiceManager.say(text, voice.getUID(), sinkId);
return Response.ok(null, MediaType.TEXT_PLAIN).build();
}
}

@POST
@Path("/say/{sinkid: [a-zA-Z_:0-9]*}/{voiceid: [a-zA-Z_:0-9]*}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: Make sinkid and voiceid to QueryParams (this will actually allow you to combine the three methods into one as two params can both be optional).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to have QueryParam in a POST command ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would think/hope so.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a doubt. I will check if we have such case in all our REST APIs (POST).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing a WEB search, it looks like this is possible.

@Consumes(MediaType.TEXT_PLAIN)
@ApiOperation(value = "Speaks a given text with a given voice through the given audio sink.")
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 404, message = "Audio sink does not exist.") })
public Response say(@ApiParam(value = "text to speak", required = true) String text,
@PathParam("sinkid") @ApiParam(value = "audio sink id", required = true) String sinkId,
@PathParam("voiceid") @ApiParam(value = "voice id", required = true) String voiceId) {
if (audioManager.getSink(sinkId) == null) {
return JSONResponse.createErrorResponse(Status.NOT_FOUND, "Undefined audio sink");
} else {
voiceManager.say(text, voiceId, sinkId);
return Response.ok(null, MediaType.TEXT_PLAIN).build();
}
}

@Override
public boolean isSatisfied() {
return voiceManager != null && localeService != null;
Expand Down