From 2103e97015bd0dbcec3c0e3483404c3d800c36e5 Mon Sep 17 00:00:00 2001 From: yuu1111 Date: Sat, 4 Jul 2026 00:00:00 +0900 Subject: [PATCH] Refine voice backend balancing and style handling --- .../dev/felnull/itts/core/tts/TTSManager.java | 2 + .../voice/coeiroink/CoeiroinkBalancer.java | 43 +++++++++----- .../voice/coeiroink/CoeiroinkManager.java | 13 +++- .../core/voice/coeiroink/CoeiroinkVoice.java | 17 +++--- .../voice/coeiroink/CoeiroinkVoiceType.java | 23 ++++++-- .../voice/voicetext/VoiceTextManager.java | 41 +++++++++++-- .../core/voice/voicevox/VoicevoxBalancer.java | 47 ++++++++++----- .../core/voice/voicevox/VoicevoxManager.java | 59 ++++++++++--------- .../core/voice/voicevox/VoicevoxVoice.java | 13 +++- .../voice/voicevox/VoicevoxVoiceType.java | 23 ++++++-- 10 files changed, 198 insertions(+), 83 deletions(-) diff --git a/core/src/main/java/dev/felnull/itts/core/tts/TTSManager.java b/core/src/main/java/dev/felnull/itts/core/tts/TTSManager.java index 0228610..4c30b3a 100644 --- a/core/src/main/java/dev/felnull/itts/core/tts/TTSManager.java +++ b/core/src/main/java/dev/felnull/itts/core/tts/TTSManager.java @@ -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; } @@ -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; } diff --git a/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkBalancer.java b/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkBalancer.java index 15c9d0b..8505152 100644 --- a/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkBalancer.java +++ b/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkBalancer.java @@ -94,8 +94,14 @@ public CompletableFuture init() { } private void check() { + List previousUrls; + synchronized (checkLock) { + previousUrls = availableUrls; + } + + Pair, List> cr = checkAndGet(previousUrls); + synchronized (checkLock) { - Pair, List> cr = checkAndGet(); availableUrls = cr.getLeft(); availableSpeakers = cr.getRight(); } @@ -112,10 +118,10 @@ public void run() { * CoeiroinkエンジンのURLの可用性をチェックし、スピーカー情報を取得する * * @return ペアオブジェクトで、第一要素に可用なCIURLのリスト、第二要素にCoeiroinkSpeakerのリストを含む - * スピーカー情報は最初の成功したリクエストから取得されたもの + * スピーカー情報は成功した全リクエストから重複を除いて取得されたもの * @throws RuntimeException IOエラーまたは中断が発生した場合にスローされる */ - private Pair, List> checkAndGet() { + private Pair, List> checkAndGet(List previousUrls) { List>>> urls = enginUrls.get().stream() .map(CIURL::new) .map(n -> Pair.of(n, CompletableFuture.supplyAsync(() -> { @@ -128,7 +134,7 @@ private Pair, List> checkAndGet() { .toList(); List rurls = new ArrayList<>(); - List rspeakers = null; + Map speakerByUuid = new LinkedHashMap<>(); for (Pair>> ret : urls) { CIURL vu = ret.getLeft(); @@ -136,25 +142,25 @@ private Pair, List> checkAndGet() { try { List 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())); } /** @@ -164,7 +170,9 @@ private Pair, List> checkAndGet() { * @see #enginUrls */ public boolean isAvailable() { - return enginUrls != null && !enginUrls.get().isEmpty(); + synchronized (checkLock) { + return availableUrls != null && !availableUrls.isEmpty(); + } } /** @@ -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() { diff --git a/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkManager.java b/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkManager.java index a1e233d..394a6bd 100644 --- a/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkManager.java +++ b/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkManager.java @@ -96,7 +96,8 @@ public boolean isAvailable() { public List 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(); } @@ -137,7 +138,15 @@ protected List requestSpeakers(CIURL ciurl) throws IOException ImmutableList.Builder 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(); diff --git a/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkVoice.java b/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkVoice.java index 10e9162..c18f915 100644 --- a/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkVoice.java +++ b/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkVoice.java @@ -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 @@ -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(); } } diff --git a/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkVoiceType.java b/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkVoiceType.java index 841afbc..6ac75de 100644 --- a/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkVoiceType.java +++ b/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkVoiceType.java @@ -16,6 +16,16 @@ public class CoeiroinkVoiceType implements VoiceType { */ private final CoeiroinkSpeaker coeiroinkSpeaker; + /** + * 話者スタイル + */ + private final CoeiroinkStyle coeiroinkStyle; + + /** + * 互換性のため従来IDを使うかどうか + */ + private final boolean legacyId; + /** * マネージャー */ @@ -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 @@ -54,6 +69,6 @@ public VoiceCategory getCategory() { @Override public Voice createVoice(long guildId, long userId) { - return new CoeiroinkVoice(this, manager, coeiroinkSpeaker); + return new CoeiroinkVoice(this, manager, coeiroinkSpeaker, coeiroinkStyle); } } diff --git a/core/src/main/java/dev/felnull/itts/core/voice/voicetext/VoiceTextManager.java b/core/src/main/java/dev/felnull/itts/core/voice/voicetext/VoiceTextManager.java index e08ee8f..fac2e23 100644 --- a/core/src/main/java/dev/felnull/itts/core/voice/voicetext/VoiceTextManager.java +++ b/core/src/main/java/dev/felnull/itts/core/voice/voicetext/VoiceTextManager.java @@ -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; @@ -15,6 +16,8 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -58,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(); } /** @@ -79,6 +88,7 @@ public InputStream openVoiceStream(@NotNull VoiceTextSpeaker speaker, @NotNull S HttpRequest request = HttpRequest.newBuilder(URI.create(API_URL)) .header("Authorization", basic) .header("Content-Type", "application/x-www-form-urlencoded; charset=utf-8") + .timeout(Duration.of(10, ChronoUnit.SECONDS)) .POST(HttpRequest.BodyPublishers.ofString(String.format("text=%s&speaker=%s", text, speaker.getId()))) .build(); HttpResponse res = hc.send(request, HttpResponse.BodyHandlers.ofInputStream()); @@ -94,11 +104,10 @@ 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解析エラーの場合は無視 } @@ -106,4 +115,26 @@ public InputStream openVoiceStream(@NotNull VoiceTextSpeaker speaker, @NotNull S 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"; + } } diff --git a/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxBalancer.java b/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxBalancer.java index da5692f..fd8e032 100644 --- a/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxBalancer.java +++ b/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxBalancer.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -93,8 +94,14 @@ public CompletableFuture init() { } private void check() { + List previousUrls; + synchronized (checkLock) { + previousUrls = availableUrls; + } + + Pair, List> cr = checkAndGet(previousUrls); + synchronized (checkLock) { - Pair, List> cr = checkAndGet(); availableUrls = cr.getLeft(); availableSpeakers = cr.getRight(); } @@ -107,7 +114,7 @@ public void run() { }, manager.getConfig().getCheckTime()); } - private Pair, List> checkAndGet() { + private Pair, List> checkAndGet(List previousUrls) { List>>> urls = enginUrls.get().stream() .map(VVURL::new) .map(n -> Pair.of(n, CompletableFuture.supplyAsync(() -> { @@ -120,7 +127,7 @@ private Pair, List> checkAndGet() { .toList(); List rurls = new ArrayList<>(); - List rspeakers = null; + Map speakerByUuid = new LinkedHashMap<>(); for (Pair>> ret : urls) { VVURL vu = ret.getLeft(); @@ -129,29 +136,36 @@ private Pair, List> checkAndGet() { try { List r = cf.get(); - if (rspeakers == null) { - rspeakers = r; + for (VoicevoxSpeaker speaker : r) { + speakerByUuid.putIfAbsent(speaker.uuid().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())); } + /** + * 利用可能なURLがあるかどうかを取得 + * + * @return 利用可能なURLがある場合はtrue + */ public boolean isAvailable() { - return enginUrls != null && !enginUrls.get().isEmpty(); + synchronized (checkLock) { + return availableUrls != null && !availableUrls.isEmpty(); + } } /** @@ -160,13 +174,16 @@ public boolean isAvailable() { * @return URLの使用インターフェイス */ protected VoicevoxUseURL getUseURL() { - if (availableUrls == null || availableUrls.isEmpty()) { - throw new RuntimeException("No URL available."); - } + VVURL vvurl; + synchronized (checkLock) { + if (availableUrls == null || availableUrls.isEmpty()) { + throw new RuntimeException("No URL available."); + } - VVURL vvurl = availableUrls.stream() - .min(Comparator.comparingInt(r -> getUseCounter(r).get())) - .get(); + vvurl = availableUrls.stream() + .min(Comparator.comparingInt(r -> getUseCounter(r).get())) + .get(); + } getUseCounter(vvurl).incrementAndGet(); return new VoicevoxUseURL() { diff --git a/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxManager.java b/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxManager.java index b607cd7..9749fc9 100644 --- a/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxManager.java +++ b/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxManager.java @@ -97,7 +97,8 @@ public boolean isAvailable() { public List getAvailableVoiceTypes() { return balancer.getAvailableSpeakers().stream() - .map(r -> (VoiceType) new VoicevoxVoiceType(r, this)) + .flatMap(speaker -> speaker.styles().stream() + .map(style -> (VoiceType) new VoicevoxVoiceType(speaker, style, this, speaker.styles().indexOf(style) == 0))) .toList(); } @@ -138,41 +139,45 @@ protected List requestSpeakers(VVURL vvurl) throws IOException, ImmutableList.Builder speakerBuilder = new ImmutableList.Builder<>(); for (JsonElement je : ja) { - speakerBuilder.add(VoicevoxSpeaker.of(je.getAsJsonObject())); + try { + if (!je.isJsonObject()) { + throw new IllegalArgumentException("Speaker entry is not object"); + } + + speakerBuilder.add(VoicevoxSpeaker.of(je.getAsJsonObject())); + } catch (RuntimeException ex) { + ITTSRuntime.getInstance().getLogger().warn("Invalid {} speaker entry was skipped", name, ex); + } } return speakerBuilder.build(); } - private JsonObject getQuery(String text, int speakerId) { + private JsonObject getQuery(VVURL vvurl, String text, int speakerId) throws IOException, InterruptedException { text = URLEncoder.encode(text, StandardCharsets.UTF_8); - try (var urlUse = balancer.getUseURL()) { - HttpClient hc = ITTSRuntime.getInstance().getNetworkManager().getHttpClient(); - HttpRequest req = HttpRequest.newBuilder(urlUse.getVVURL().createURI(String.format("audio_query?text=%s&speaker=%d", text, speakerId))) - .POST(HttpRequest.BodyPublishers.noBody()) - .timeout(Duration.of(10, ChronoUnit.SECONDS)) - .build(); - HttpResponse rep = hc.send(req, HttpResponse.BodyHandlers.ofInputStream()); - - int statusCode = rep.statusCode(); - if (statusCode != 200) { - throw new IOException(name + " audio_query API error: HTTP " + statusCode); - } + HttpClient hc = ITTSRuntime.getInstance().getNetworkManager().getHttpClient(); + HttpRequest req = HttpRequest.newBuilder(vvurl.createURI(String.format("audio_query?text=%s&speaker=%d", text, speakerId))) + .POST(HttpRequest.BodyPublishers.noBody()) + .timeout(Duration.of(10, ChronoUnit.SECONDS)) + .build(); + HttpResponse rep = hc.send(req, HttpResponse.BodyHandlers.ofInputStream()); - JsonObject result; - try (InputStream stream = new BufferedInputStream(rep.body()); Reader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { - result = GSON.fromJson(reader, JsonObject.class); - } + int statusCode = rep.statusCode(); + if (statusCode != 200) { + throw new IOException(name + " audio_query API error: HTTP " + statusCode); + } - if (result == null) { - throw new IOException(name + " audio_query API returned invalid JSON response"); - } + JsonObject result; + try (InputStream stream = new BufferedInputStream(rep.body()); Reader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { + result = GSON.fromJson(reader, JsonObject.class); + } - return result; - } catch (Exception e) { - throw new RuntimeException(e); + if (result == null) { + throw new IOException(name + " audio_query API returned invalid JSON response"); } + + return result; } /** @@ -185,8 +190,8 @@ private JsonObject getQuery(String text, int speakerId) { * @throws InterruptedException 割り込み例外 */ protected InputStream openVoiceStream(String text, int speakerId) throws IOException, InterruptedException { - JsonObject qry = getQuery(text, speakerId); - try (var urlUse = balancer.getUseURL()) { + try (VoicevoxUseURL urlUse = balancer.getUseURL()) { + JsonObject qry = getQuery(urlUse.getVVURL(), text, speakerId); HttpClient hc = ITTSRuntime.getInstance().getNetworkManager().getHttpClient(); HttpRequest request = HttpRequest.newBuilder(urlUse.getVVURL().createURI(String.format("synthesis?speaker=%d", speakerId))) .timeout(Duration.of(10, ChronoUnit.SECONDS)) diff --git a/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxVoice.java b/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxVoice.java index 3febd1b..e31f5f6 100644 --- a/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxVoice.java +++ b/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxVoice.java @@ -22,26 +22,33 @@ public class VoicevoxVoice extends CachedVoice { */ private final VoicevoxSpeaker speaker; + /** + * 話者スタイル + */ + private final VoicevoxStyle style; + /** * コンストラクタ * * @param voiceType 声タイプ * @param manager マネージャー * @param speaker 話者 + * @param style 話者スタイル */ - protected VoicevoxVoice(VoiceType voiceType, VoicevoxManager manager, VoicevoxSpeaker speaker) { + protected VoicevoxVoice(VoiceType voiceType, VoicevoxManager manager, VoicevoxSpeaker speaker, VoicevoxStyle style) { super(voiceType); this.manager = manager; this.speaker = speaker; + this.style = style; } @Override protected InputStream openVoiceStream(String text) throws IOException, InterruptedException { - return this.manager.openVoiceStream(text, speaker.styles().get(0).id()); + return this.manager.openVoiceStream(text, style.id()); } @Override protected String createHashCodeChars() { - return this.speaker.uuid().toString(); + return this.speaker.uuid() + ":" + this.style.id(); } } diff --git a/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxVoiceType.java b/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxVoiceType.java index 8e2f718..fbf6242 100644 --- a/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxVoiceType.java +++ b/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxVoiceType.java @@ -16,6 +16,16 @@ public class VoicevoxVoiceType implements VoiceType { */ private final VoicevoxSpeaker voicevoxSpeaker; + /** + * 話者スタイル + */ + private final VoicevoxStyle voicevoxStyle; + + /** + * 互換性のため従来IDを使うかどうか + */ + private final boolean legacyId; + /** * マネージャー */ @@ -25,21 +35,26 @@ public class VoicevoxVoiceType implements VoiceType { * コンストラクタ * * @param voicevoxSpeaker 話者 + * @param voicevoxStyle 話者スタイル * @param voicevoxManager マネージャー + * @param legacyId 従来IDを使うかどうか */ - public VoicevoxVoiceType(VoicevoxSpeaker voicevoxSpeaker, VoicevoxManager voicevoxManager) { + public VoicevoxVoiceType(VoicevoxSpeaker voicevoxSpeaker, VoicevoxStyle voicevoxStyle, VoicevoxManager voicevoxManager, boolean legacyId) { this.voicevoxSpeaker = voicevoxSpeaker; + this.voicevoxStyle = voicevoxStyle; this.manager = voicevoxManager; + this.legacyId = legacyId; } @Override public String getName() { - return this.voicevoxSpeaker.name(); + return this.legacyId ? this.voicevoxSpeaker.name() : this.voicevoxSpeaker.name() + " " + this.voicevoxStyle.name(); } @Override public String getId() { - return manager.getName() + "-" + this.voicevoxSpeaker.uuid().toString(); + String baseId = manager.getName() + "-" + this.voicevoxSpeaker.uuid(); + return this.legacyId ? baseId : baseId + "-" + this.voicevoxStyle.id(); } @Override @@ -54,6 +69,6 @@ public VoiceCategory getCategory() { @Override public Voice createVoice(long guildId, long userId) { - return new VoicevoxVoice(this, manager, voicevoxSpeaker); + return new VoicevoxVoice(this, manager, voicevoxSpeaker, voicevoxStyle); } }