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

Add Flowery TTS as source #85

Merged
merged 15 commits into from
Aug 5, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,17 @@ AudioPlayerManager playerManager = new DefaultAudioPlayerManager();
playerManager.registerSourceManager(new YandexMusicSourceManager("...");
```

#### Flowery Text-to-Speech

Get list of all voices and languages supported [here](https://api.flowery.pw/v1/tts/voices)

```java
AudioPlayerManager playerManager = new DefaultAudioPlayerManager();

// create a new FloweryTTSSourceManager with a valid voice and register it
playerManager.registerSourceManager(new FloweryTTSSourceManager("..."));
```

---

## Lavalink Usage
Expand Down Expand Up @@ -217,6 +228,12 @@ plugins:
masterDecryptionKey: "your master decryption key" # the master key used for decrypting the deezer tracks. (yes this is not here you need to get it from somewhere else)
yandexmusic:
accessToken: "your access token" # the token used for accessing the yandex music api. See https://github.com/TopiSenpai/LavaSrc#yandex-music
flowery.tts:
voice: "default voice" # (case-sensitive) get default voice from here https://api.flowery.pw/v1/tts/voices
translate: false # whether to translate the text to the native language of voice
silence: 0 # the silence parameter is in milliseconds. Range is 0 to 10000. The default is 0.
speed: 1.0 # the speed parameter is a float between 0.5 and 10. The default is 1.0. (0.5 is half speed, 2.0 is double speed, etc.)
audioFormat: "mp3" # supported formats are: mp3, ogg_opus, ogg_vorbis, aac, wav, and flac. Default format is mp3
```

---
Expand Down Expand Up @@ -257,4 +274,7 @@ plugins:
* https://music.yandex.ru/users/yamusic-bestsongs/playlists/701626
* https://music.yandex.ru/artist/701626

### Flowery TTS
* `ftts://hello%20world`
* `ftts://hello%20world?audio_format=ogg_opus&translate=False&silence=1000&speed=1.0`
---
7 changes: 7 additions & 0 deletions application.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ plugins:
applemusic: false # Enable Apple Music source
deezer: false # Enable Deezer source
yandexmusic: false # Enable Yandex Music source
flowerytts: false # Enable Flowery TTs source
spotify:
clientId: "your client id"
clientSecret: "your client secret"
Expand All @@ -26,6 +27,12 @@ plugins:
masterDecryptionKey: "your master decryption key" # the master key used for decrypting the deezer tracks. (yes this is not here you need to get it from somewhere else)
yandexmusic:
accessToken: "your access token" # the token used for accessing the yandex music api. See https://github.com/TopiSenpai/LavaSrc#yandex-music
flowery.tts:
voice: "default voice" # (case-sensitive) get default voice here https://flowery.pw/docs/flowery/tts-voices-v-1-tts-voices-get
translate: false # whether to translate the text to the native language of voice
silence: 0 # the silence parameter is in milliseconds. Range is 0 to 10000. The default is 0.
speed: 1.0 # the speed parameter is a float between 0.5 and 10. The default is 1.0. (0.5 is half speed, 2.0 is double speed, etc.)
bachtran02 marked this conversation as resolved.
Show resolved Hide resolved
audioFormat: "mp3" # supported formats are: mp3, ogg_opus, ogg_vorbis, aac, wav, and flac. Default format is mp3

server: # REST and WS server
port: 2333
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.github.topi314.lavasrc.flowerytts;

import com.sedmelluq.discord.lavaplayer.container.adts.AdtsAudioTrack;
import com.sedmelluq.discord.lavaplayer.container.flac.FlacAudioTrack;
import com.sedmelluq.discord.lavaplayer.container.ogg.OggAudioTrack;
import com.sedmelluq.discord.lavaplayer.container.mp3.Mp3AudioTrack;
import com.sedmelluq.discord.lavaplayer.container.wav.WavAudioTrack;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager;
import com.sedmelluq.discord.lavaplayer.tools.io.PersistentHttpStream;
import com.sedmelluq.discord.lavaplayer.tools.io.SeekableInputStream;
import com.sedmelluq.discord.lavaplayer.track.BaseAudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo;
import com.sedmelluq.discord.lavaplayer.track.DelegatedAudioTrack;
import com.sedmelluq.discord.lavaplayer.track.playback.LocalAudioTrackExecutor;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URIBuilder;

import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class FloweryTTSAudioTrack extends DelegatedAudioTrack {
private final FloweryTTSSourceManager sourceManager;
private final Map<String, Class<? extends BaseAudioTrack>> audioFormatMap;
public static final String API_BASE = "https://api.flowery.pw/v1/tts";


public FloweryTTSAudioTrack(AudioTrackInfo trackInfo, FloweryTTSSourceManager sourceManager) {
super(trackInfo);
this.sourceManager = sourceManager;

this.audioFormatMap = new HashMap<>();
this.audioFormatMap.put("mp3", Mp3AudioTrack.class);
this.audioFormatMap.put("ogg_opus", OggAudioTrack.class);
this.audioFormatMap.put("ogg_vorbis", OggAudioTrack.class);
this.audioFormatMap.put("wav", WavAudioTrack.class);
this.audioFormatMap.put("flac", FlacAudioTrack.class);
this.audioFormatMap.put("aac", AdtsAudioTrack.class);
}

@Override
public void process(LocalAudioTrackExecutor executor) throws Exception {
try (var httpInterface = this.sourceManager.getHttpInterface()) {

URIBuilder parsed = new URIBuilder(this.trackInfo.identifier);
List<NameValuePair> queryParams = parsed.getQueryParams();
URIBuilder apiUri = new URIBuilder(API_BASE);
String format = null;

apiUri.addParameter("text", this.trackInfo.title);
for (NameValuePair pair : this.sourceManager.getDefaultConfig()){
var value = queryParams.stream()
.filter((p) -> pair.getName().equals(p.getName()) && !"voice".equals(p.getName()))
.map(NameValuePair::getValue)
.findFirst()
.orElse(pair.getValue());
apiUri.addParameter(pair.getName(), value);
format = ("audio_format".equals(pair.getName()))? value : format;
}
try (var stream = new PersistentHttpStream(httpInterface, apiUri.build(), null)) {
var audioTrackClass = this.audioFormatMap.get(format);
if (audioTrackClass == null){
throw new IllegalArgumentException("Invalid audio format");
}
var streamClass = ("aac".equals(format)) ? InputStream.class : SeekableInputStream.class;
processDelegate(audioTrackClass.getConstructor(AudioTrackInfo.class, streamClass).newInstance(this.trackInfo, stream), executor);
}
}
}

@Override
protected AudioTrack makeShallowClone() {
return new FloweryTTSAudioTrack(this.trackInfo, this.sourceManager);
}

@Override
public AudioSourceManager getSourceManager() {
return this.sourceManager;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package com.github.topi314.lavasrc.flowerytts;

import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager;
import com.sedmelluq.discord.lavaplayer.tools.Units;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterfaceManager;
import com.sedmelluq.discord.lavaplayer.track.AudioItem;
import com.sedmelluq.discord.lavaplayer.track.AudioReference;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.HttpClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;

public class FloweryTTSSourceManager implements AudioSourceManager, HttpConfigurable {

public static final String TTS_PREFIX = "ftts://";
private static final Logger log = LoggerFactory.getLogger(FloweryTTSSourceManager.class);
private static final int CHAR_MAX = 2000;
private static final int SILENCE_MIN = 0;
private static final int SILENCE_MAX = 10000;
private static final float SPEED_MIN = 0.5f;
private static final float SPEED_MAX = 10;

private final String voice;
private final HttpInterfaceManager httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager();
private boolean translate = false;
private int silence = 0;
private float speed = 1;
private String audioFormat = "mp3";

public FloweryTTSSourceManager(String voice) {
if (voice == null || voice.isEmpty()) {
throw new IllegalArgumentException("Default voice must be set");
}
this.voice = voice;
}

public void setTranslate(boolean translate) {
this.translate = translate;
}

public void setSilence(int silence) {
this.silence = Math.max(SILENCE_MIN, Math.min(SILENCE_MAX, silence));
}

public void setSpeed(float speed) {
this.speed = Math.max(SPEED_MIN, Math.min(SPEED_MAX, speed));
}

public void setAudioFormat(String audioFormat) {
this.audioFormat = audioFormat;
}

public Map<String, String> getDefaultConfig() {
return Map.of(
"voice", this.voice,
"translate", Boolean.toString(this.translate),
"silence", Integer.toString(this.silence),
"speed", Float.toString(this.speed),
"audio_format", this.audioFormat
);
}

@Override
public String getSourceName() {
return "flowery-tts";
}

@Override
public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) {
if (!reference.identifier.startsWith(TTS_PREFIX)) {
return null;
}

try {
var text = new URI(reference.identifier).getAuthority();
if (text == null) {
return null;
}
if (text.length() > CHAR_MAX) {
throw new IllegalArgumentException("Character limit per request exceeded");
}

return new FloweryTTSAudioTrack(
new AudioTrackInfo(
text,
"flowery-tts",
Units.CONTENT_LENGTH_UNKNOWN,
reference.identifier,
false,
null), this);
} catch (URISyntaxException | IllegalArgumentException e) {
throw new RuntimeException(e);
}
}

@Override
public boolean isTrackEncodable(AudioTrack track) {
return true;
}

@Override
public void encodeTrack(AudioTrack track, DataOutput output) {
// nothing to encode
}

@Override
public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) {
return new FloweryTTSAudioTrack(trackInfo, this);
}

@Override
public void shutdown() {
try {
this.httpInterfaceManager.close();
} catch (IOException e) {
log.error("Failed to close HTTP interface manager", e);
}
}

@Override
public void configureRequests(Function<RequestConfig, RequestConfig> configurator) {
this.httpInterfaceManager.configureRequests(configurator);
}

@Override
public void configureBuilder(Consumer<HttpClientBuilder> configurator) {
this.httpInterfaceManager.configureBuilder(configurator);
}

public HttpInterface getHttpInterface() {
return this.httpInterfaceManager.getInterface();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.github.topi314.lavasrc.plugin;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@ConfigurationProperties(prefix = "plugins.lavasrc.flowery.tts")
@Component
public class FloweryTTSConfig {

private String voice;
private boolean translate;
private int silence;
private float speed;
private String audioFormat;

public String getVoice() {
return this.voice;
}

public void setVoice(String voice) {
this.voice = voice;
}

public boolean getTranslate() {
return this.translate;
}

public void setTranslate(boolean translate) {
this.translate = translate;
}

public int getSilence() {
return this.silence;
}

public void setSilence(int silence) {
this.silence = silence;
}

public float getSpeed() {
return this.speed;
}

public void setSpeed(float speed) {
this.speed = speed;
}

public String getAudioFormat() {
return this.audioFormat;
}

public void setAudioFormat(String audioFormat) {
this.audioFormat = audioFormat;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ public JsonObject modifyAudioPlaylistPluginInfo(@NotNull AudioPlaylist playlist)
public JsonObject modifyAudioTrackPluginInfo(@NotNull AudioTrack track) {
if (track instanceof ExtendedAudioTrack extendedTrack) {
return new JsonObject(Map.of(
"albumName", JsonElementKt.JsonPrimitive(extendedTrack.getAlbumName()),
"albumUrl", JsonElementKt.JsonPrimitive(extendedTrack.getAlbumUrl()),
"artistUrl", JsonElementKt.JsonPrimitive(extendedTrack.getArtistUrl()),
"artistArtworkUrl", JsonElementKt.JsonPrimitive(extendedTrack.getArtistArtworkUrl()),
"previewUrl", JsonElementKt.JsonPrimitive(extendedTrack.getPreviewUrl()),
"isPreview", JsonElementKt.JsonPrimitive(extendedTrack.isPreview())
"albumName", JsonElementKt.JsonPrimitive(extendedTrack.getAlbumName()),
"albumUrl", JsonElementKt.JsonPrimitive(extendedTrack.getAlbumUrl()),
"artistUrl", JsonElementKt.JsonPrimitive(extendedTrack.getArtistUrl()),
"artistArtworkUrl", JsonElementKt.JsonPrimitive(extendedTrack.getArtistArtworkUrl()),
"previewUrl", JsonElementKt.JsonPrimitive(extendedTrack.getPreviewUrl()),
"isPreview", JsonElementKt.JsonPrimitive(extendedTrack.isPreview())
));
}
return null;
Expand Down
Loading