Skip to content
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
2 changes: 2 additions & 0 deletions core/src/main/java/dev/felnull/itts/core/tts/TTSManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ public void sayGuildMemberText(@NotNull Guild guild, @NotNull MessageChannel mes

VoiceType vt = getVoiceManager().getVoiceType(guildId, userId);
if (vt == null) {
getITTSLogger().warn("No available voice type for guild {} user {}", guildId, userId);
return;
}

Expand Down Expand Up @@ -345,6 +346,7 @@ public void onVCEvent(@NotNull Guild guild, @NotNull Member member, @Nullable Au

VoiceType vt = getVoiceManager().getVoiceType(guildId, userId);
if (vt == null) {
getITTSLogger().warn("No available voice type for guild {} user {}", guildId, userId);
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,14 @@ public CompletableFuture<?> init() {
}

private void check() {
List<CIURL> previousUrls;
synchronized (checkLock) {
previousUrls = availableUrls;
}

Pair<List<CIURL>, List<CoeiroinkSpeaker>> cr = checkAndGet(previousUrls);

synchronized (checkLock) {
Pair<List<CIURL>, List<CoeiroinkSpeaker>> cr = checkAndGet();
availableUrls = cr.getLeft();
availableSpeakers = cr.getRight();
}
Expand All @@ -112,10 +118,10 @@ public void run() {
* CoeiroinkエンジンのURLの可用性をチェックし、スピーカー情報を取得する
*
* @return ペアオブジェクトで、第一要素に可用なCIURLのリスト、第二要素にCoeiroinkSpeakerのリストを含む
* スピーカー情報は最初の成功したリクエストから取得されたもの
* スピーカー情報は成功した全リクエストから重複を除いて取得されたもの
* @throws RuntimeException IOエラーまたは中断が発生した場合にスローされる
*/
private Pair<List<CIURL>, List<CoeiroinkSpeaker>> checkAndGet() {
private Pair<List<CIURL>, List<CoeiroinkSpeaker>> checkAndGet(List<CIURL> previousUrls) {
List<Pair<CIURL, CompletableFuture<List<CoeiroinkSpeaker>>>> urls = enginUrls.get().stream()
.map(CIURL::new)
.map(n -> Pair.of(n, CompletableFuture.supplyAsync(() -> {
Expand All @@ -128,33 +134,33 @@ private Pair<List<CIURL>, List<CoeiroinkSpeaker>> checkAndGet() {
.toList();

List<CIURL> rurls = new ArrayList<>();
List<CoeiroinkSpeaker> rspeakers = null;
Map<String, CoeiroinkSpeaker> speakerByUuid = new LinkedHashMap<>();

for (Pair<CIURL, CompletableFuture<List<CoeiroinkSpeaker>>> ret : urls) {
CIURL vu = ret.getLeft();
CompletableFuture<List<CoeiroinkSpeaker>> cf = ret.getRight();

try {
List<CoeiroinkSpeaker> r = cf.get();
if (rspeakers == null) {
rspeakers = r;
for (CoeiroinkSpeaker speaker : r) {
speakerByUuid.putIfAbsent(speaker.speakerUuid().toString(), speaker);
}

rurls.add(vu);

if (availableUrls == null || !availableUrls.contains(vu)) {
if (previousUrls == null || !previousUrls.contains(vu)) {
getITTSLogger().info("Available {} URL: {}", manager.getName(), vu.url());
}

} catch (InterruptedException | ExecutionException e) {
if (availableUrls == null || availableUrls.contains(vu)) {
if (previousUrls == null || previousUrls.contains(vu)) {
getITTSLogger().warn("Unavailable {} URL: {}", manager.getName(), vu.url());
}
}

}

return Pair.of(rurls, rspeakers);
return Pair.of(rurls, List.copyOf(speakerByUuid.values()));
}

/**
Expand All @@ -164,7 +170,9 @@ private Pair<List<CIURL>, List<CoeiroinkSpeaker>> checkAndGet() {
* @see #enginUrls
*/
public boolean isAvailable() {
return enginUrls != null && !enginUrls.get().isEmpty();
synchronized (checkLock) {
return availableUrls != null && !availableUrls.isEmpty();
}
}

/**
Expand All @@ -173,13 +181,16 @@ public boolean isAvailable() {
* @return URLの使用インターフェイス
*/
protected CoeiroinkUseURL getUseURL() {
if (availableUrls == null || availableUrls.isEmpty()) {
throw new RuntimeException("No URL available.");
}
CIURL ciurl;
synchronized (checkLock) {
if (availableUrls == null || availableUrls.isEmpty()) {
throw new RuntimeException("No URL available.");
}

CIURL ciurl = availableUrls.stream()
.min(Comparator.comparingInt(r -> getUseCounter(r).get()))
.get();
ciurl = availableUrls.stream()
.min(Comparator.comparingInt(r -> getUseCounter(r).get()))
.get();
}

getUseCounter(ciurl).incrementAndGet();
return new CoeiroinkUseURL() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ public boolean isAvailable() {

public List<VoiceType> getAvailableVoiceTypes() {
return balancer.getAvailableSpeakers().stream()
.map(r -> (VoiceType) new CoeiroinkVoiceType(r, this))
.flatMap(speaker -> speaker.styles().stream()
.map(style -> (VoiceType) new CoeiroinkVoiceType(speaker, style, this, speaker.styles().indexOf(style) == 0)))
.toList();
}

Expand Down Expand Up @@ -137,7 +138,15 @@ protected List<CoeiroinkSpeaker> requestSpeakers(CIURL ciurl) throws IOException
ImmutableList.Builder<CoeiroinkSpeaker> speakerBuilder = new ImmutableList.Builder<>();

for (JsonElement je : ja) {
speakerBuilder.add(CoeiroinkSpeaker.of(je.getAsJsonObject()));
try {
if (!je.isJsonObject()) {
throw new IllegalArgumentException("Speaker entry is not object");
}

speakerBuilder.add(CoeiroinkSpeaker.of(je.getAsJsonObject()));
} catch (RuntimeException ex) {
ITTSRuntime.getInstance().getLogger().warn("Invalid {} speaker entry was skipped", name, ex);
}
}

return speakerBuilder.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,24 @@ public class CoeiroinkVoice extends CachedVoice {
*/
private final CoeiroinkSpeaker speaker;

/**
* 話者スタイル
*/
private final CoeiroinkStyle style;

/**
* コンストラクタ
*
* @param voiceType 声タイプ
* @param manager マネージャー
* @param speaker 話者
* @param style 話者スタイル
*/
protected CoeiroinkVoice(VoiceType voiceType, CoeiroinkManager manager, CoeiroinkSpeaker speaker) {
protected CoeiroinkVoice(VoiceType voiceType, CoeiroinkManager manager, CoeiroinkSpeaker speaker, CoeiroinkStyle style) {
super(voiceType);
this.manager = manager;
this.speaker = speaker;
this.style = style;
}

@Override
Expand All @@ -41,15 +48,11 @@ protected InputStream openVoiceStream(String text) throws IOException, Interrupt
throw new IllegalArgumentException("Text cannot be null or empty");
}

if (speaker.styles().isEmpty()) {
throw new IllegalStateException("Speaker has no styles available");
}

return this.manager.openVoiceStream(text, speaker.styles().get(0).styleId(), speaker.speakerUuid().toString());
return this.manager.openVoiceStream(text, style.styleId(), speaker.speakerUuid().toString());
}

@Override
protected String createHashCodeChars() {
return this.speaker.speakerUuid().toString();
return this.speaker.speakerUuid() + ":" + this.style.styleId();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ public class CoeiroinkVoiceType implements VoiceType {
*/
private final CoeiroinkSpeaker coeiroinkSpeaker;

/**
* 話者スタイル
*/
private final CoeiroinkStyle coeiroinkStyle;

/**
* 互換性のため従来IDを使うかどうか
*/
private final boolean legacyId;

/**
* マネージャー
*/
Expand All @@ -25,21 +35,26 @@ public class CoeiroinkVoiceType implements VoiceType {
* コンストラクタ
*
* @param coeiroinkSpeaker 話者
* @param coeiroinkStyle 話者スタイル
* @param coeiroinkManager マネージャー
* @param legacyId 従来IDを使うかどうか
*/
public CoeiroinkVoiceType(CoeiroinkSpeaker coeiroinkSpeaker, CoeiroinkManager coeiroinkManager) {
public CoeiroinkVoiceType(CoeiroinkSpeaker coeiroinkSpeaker, CoeiroinkStyle coeiroinkStyle, CoeiroinkManager coeiroinkManager, boolean legacyId) {
this.coeiroinkSpeaker = coeiroinkSpeaker;
this.coeiroinkStyle = coeiroinkStyle;
this.manager = coeiroinkManager;
this.legacyId = legacyId;
}

@Override
public String getName() {
return this.coeiroinkSpeaker.speakerName();
return this.legacyId ? this.coeiroinkSpeaker.speakerName() : this.coeiroinkSpeaker.speakerName() + " " + this.coeiroinkStyle.styleName();
}

@Override
public String getId() {
return manager.getName() + "-" + this.coeiroinkSpeaker.speakerUuid().toString();
String baseId = manager.getName() + "-" + this.coeiroinkSpeaker.speakerUuid();
return this.legacyId ? baseId : baseId + "-" + this.coeiroinkStyle.styleId();
}

@Override
Expand All @@ -54,7 +69,7 @@ public VoiceCategory getCategory() {

@Override
public Voice createVoice(long guildId, long userId) {
return new CoeiroinkVoice(this, manager, coeiroinkSpeaker);
return new CoeiroinkVoice(this, manager, coeiroinkSpeaker, coeiroinkStyle);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.felnull.itts.core.voice.voicetext;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import dev.felnull.fnjl.util.FNStringUtil;
Expand Down Expand Up @@ -60,8 +61,14 @@ private String getApiKey() {
return getConfigManager().getConfig().getVoiceTextConfig().getApiKey();
}

/**
* VoiceTextが利用可能かどうかを取得
*
* @return 利用可能な場合はtrue
*/
public boolean isAvailable() {
return getConfigManager().getConfig().getVoiceTextConfig().isEnable();
String apiKey = getApiKey();
return getConfigManager().getConfig().getVoiceTextConfig().isEnable() && apiKey != null && !apiKey.isBlank();
}

/**
Expand Down Expand Up @@ -103,16 +110,37 @@ public InputStream openVoiceStream(@NotNull VoiceTextSpeaker speaker, @NotNull S
return res.body();
}

if ("application/json".equals(content.get())) {
try (InputStream stream = new BufferedInputStream(res.body()); Reader reader = new InputStreamReader(stream)) {
if (content.get().startsWith("application/json")) {
try (InputStream stream = new BufferedInputStream(res.body()); Reader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) {
JsonObject jo = GSON.fromJson(reader, JsonObject.class);
JsonObject ejo = jo.getAsJsonObject("error");
throw new IOException("VoiceText error (" + ejo.get("message").getAsString() + "): " + code);
throw new IOException("VoiceText error (" + getErrorMessage(jo) + "): " + code);
} catch (JsonSyntaxException ignored) {
// Json解析エラーの場合は無視
}
}

throw new IOException("Not audio data: " + code);
}

private String getErrorMessage(JsonObject response) {
if (response == null) {
return "invalid JSON response";
}

JsonElement errorElement = response.get("error");
if (errorElement != null && errorElement.isJsonObject()) {
JsonObject error = errorElement.getAsJsonObject();
JsonElement messageElement = error.get("message");
if (messageElement != null && messageElement.isJsonPrimitive()) {
return messageElement.getAsString();
}
}

JsonElement messageElement = response.get("message");
if (messageElement != null && messageElement.isJsonPrimitive()) {
return messageElement.getAsString();
}

return "unknown error";
}
}
Loading
Loading