Skip to content

Commit 6b037ae

Browse files
authored
Merge pull request #17 from yuu1111/update-coeiro
feat: COEIROINK v2 API対応
2 parents e7798fd + 854e50f commit 6b037ae

15 files changed

+699
-4
lines changed

build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ subprojects {
3838
version = rootProject.version
3939
}
4040

41+
tasks.withType<JavaCompile>().configureEach {
42+
options.encoding = "UTF-8"
43+
}
44+
4145
java {
4246
toolchain {
4347
languageVersion.set(JavaLanguageVersion.of(17))

core/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ tasks.getByName<Test>("test") {
4141
useJUnitPlatform()
4242
}
4343

44+
tasks.withType<JavaCompile>().configureEach {
45+
options.encoding = "UTF-8"
46+
}
47+
4448
publishing {
4549
publications {
4650
create<MavenPublication>("maven") {

core/src/main/java/dev/felnull/itts/core/voice/VoiceManager.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import dev.felnull.itts.core.savedata.SaveDataManager;
55
import dev.felnull.itts.core.savedata.legacy.LegacySaveDataLayer;
66
import dev.felnull.itts.core.savedata.legacy.LegacyServerUserData;
7+
import dev.felnull.itts.core.voice.coeiroink.CoeiroinkManager;
78
import dev.felnull.itts.core.voice.voicetext.VoiceTextManager;
89
import dev.felnull.itts.core.voice.voicevox.VoicevoxManager;
910
import org.jetbrains.annotations.NotNull;
@@ -37,8 +38,8 @@ public class VoiceManager implements ITTSBaseManager {
3738
/**
3839
* COEIROINKの管理
3940
*/
40-
private final VoicevoxManager coeiroinkManager =
41-
new VoicevoxManager("coeiroink", () ->
41+
private final CoeiroinkManager coeiroinkManager =
42+
new CoeiroinkManager("coeiroink", () ->
4243
getConfigManager().getConfig().getCoeirolnkConfig().getApiUrls(), () -> getConfigManager().getConfig().getCoeirolnkConfig());
4344

4445
/**
@@ -81,7 +82,7 @@ public VoicevoxManager getVoicevoxManager() {
8182
return voicevoxManager;
8283
}
8384

84-
public VoicevoxManager getCoeiroinkManager() {
85+
public CoeiroinkManager getCoeiroinkManager() {
8586
return coeiroinkManager;
8687
}
8788

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package dev.felnull.itts.core.voice.coeiroink;
2+
3+
import java.net.MalformedURLException;
4+
import java.net.URI;
5+
import java.net.URL;
6+
7+
/**
8+
* CoeiroinkのエンジンURL
9+
* COEIROINKは全てのエンドポイントが/v1/プレフィックスを使用する
10+
*
11+
* @param url ベースURL
12+
* @author MORIMORI0317
13+
*/
14+
public record CIURL(String url) {
15+
16+
/**
17+
* APIバージョン
18+
*/
19+
private static final String API_VERSION = "v1";
20+
21+
/**
22+
* URLを作成
23+
*
24+
* @param path パス
25+
* @return パスを含めたURL
26+
* @throws MalformedURLException URL生成例外
27+
*/
28+
public URL createURL(String path) throws MalformedURLException {
29+
if (path.startsWith("/")) {
30+
throw new IllegalArgumentException("Do not start with /");
31+
}
32+
33+
String baseUrl = url.endsWith("/") ? url : url + "/";
34+
return new URL(baseUrl + path);
35+
}
36+
37+
/**
38+
* v1 APIのURIを作成
39+
*
40+
* @param path パス (例: "speakers", "synthesis")
41+
* @return /v1/パスを含めたURI
42+
*/
43+
public URI createURI(String path) {
44+
if (path.startsWith("/")) {
45+
throw new IllegalArgumentException("Do not start with /");
46+
}
47+
48+
String baseUrl = url.endsWith("/") ? url : url + "/";
49+
return URI.create(baseUrl + API_VERSION + "/" + path);
50+
}
51+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package dev.felnull.itts.core.voice.coeiroink;
2+
3+
import com.google.common.collect.ImmutableList;
4+
import dev.felnull.itts.core.ITTSRuntimeUse;
5+
import dev.felnull.itts.core.ImmortalityTimer;
6+
import org.apache.commons.lang3.tuple.Pair;
7+
8+
import java.io.IOException;
9+
import java.util.*;
10+
import java.util.concurrent.CompletableFuture;
11+
import java.util.concurrent.ConcurrentHashMap;
12+
import java.util.concurrent.ExecutionException;
13+
import java.util.concurrent.atomic.AtomicInteger;
14+
import java.util.function.Supplier;
15+
16+
/**
17+
* Coeiroinkエンジンの使用バランスを調整
18+
*
19+
* @author MORIMORI0317
20+
*/
21+
public class CoeiroinkBalancer implements ITTSRuntimeUse {
22+
23+
/**
24+
* Coeiroinkマネージャー
25+
*/
26+
private final CoeiroinkManager manager;
27+
28+
/**
29+
* エンジンのURL
30+
*/
31+
private final Supplier<List<String>> enginUrls;
32+
33+
/**
34+
* 確認用ロック
35+
*/
36+
private final Object checkLock = new Object();
37+
38+
/**
39+
* 使用カウント
40+
*/
41+
private final Map<CIURL, AtomicInteger> useCounter = new ConcurrentHashMap<>();
42+
43+
/**
44+
* 使用可能なURL
45+
*/
46+
private List<CIURL> availableUrls;
47+
48+
/**
49+
* 使用可能な話者
50+
*/
51+
private List<CoeiroinkSpeaker> availableSpeakers;
52+
53+
/**
54+
* コンストラクタ
55+
*
56+
* @param manager Coeiroinkマネージャー
57+
* @param enginUrls エンジンのURL
58+
*/
59+
public CoeiroinkBalancer(CoeiroinkManager manager, Supplier<List<String>> enginUrls) {
60+
this.manager = manager;
61+
this.enginUrls = enginUrls;
62+
}
63+
64+
/**
65+
* CoeiroInkの使用カウンタを取得
66+
* このメソッドは、指定されたCIURLに対する使用回数をカウントするためのAtomicIntegerを返す
67+
* 初めて指定されたURLの場合、新しいAtomicIntegerが作成され、既存のURLの場合、既存のカウンタが返される
68+
*
69+
* @param ciurl CoeiroInkのURLを表すCIURLオブジェクト
70+
* @return 指定されたURLの使用回数をカウントするAtomicInteger
71+
*/
72+
private AtomicInteger getUseCounter(CIURL ciurl) {
73+
return useCounter.computeIfAbsent(ciurl, k -> new AtomicInteger());
74+
}
75+
76+
/**
77+
* 全ての話者を取得
78+
*
79+
* @return 話者のリスト
80+
*/
81+
protected List<CoeiroinkSpeaker> getAvailableSpeakers() {
82+
synchronized (checkLock) {
83+
return Objects.requireNonNullElseGet(availableSpeakers, ImmutableList::of);
84+
}
85+
}
86+
87+
/**
88+
* 初期化
89+
*
90+
* @return 初期化を行う
91+
*/
92+
public CompletableFuture<?> init() {
93+
return CompletableFuture.runAsync(this::check, getAsyncExecutor());
94+
}
95+
96+
private void check() {
97+
synchronized (checkLock) {
98+
Pair<List<CIURL>, List<CoeiroinkSpeaker>> cr = checkAndGet();
99+
availableUrls = cr.getLeft();
100+
availableSpeakers = cr.getRight();
101+
}
102+
103+
getImmortalityTimer().schedule(new ImmortalityTimer.ImmortalityTimerTask() {
104+
@Override
105+
public void run() {
106+
CompletableFuture.runAsync(() -> check(), getAsyncExecutor());
107+
}
108+
}, manager.getConfig().getCheckTime());
109+
}
110+
111+
/**
112+
* CoeiroinkエンジンのURLの可用性をチェックし、スピーカー情報を取得する
113+
*
114+
* @return ペアオブジェクトで、第一要素に可用なCIURLのリスト、第二要素にCoeiroinkSpeakerのリストを含む
115+
* スピーカー情報は最初の成功したリクエストから取得されたもの
116+
* @throws RuntimeException IOエラーまたは中断が発生した場合にスローされる
117+
*/
118+
private Pair<List<CIURL>, List<CoeiroinkSpeaker>> checkAndGet() {
119+
List<Pair<CIURL, CompletableFuture<List<CoeiroinkSpeaker>>>> urls = enginUrls.get().stream()
120+
.map(CIURL::new)
121+
.map(n -> Pair.of(n, CompletableFuture.supplyAsync(() -> {
122+
try {
123+
return manager.requestSpeakers(n);
124+
} catch (IOException | InterruptedException e) {
125+
throw new RuntimeException(e);
126+
}
127+
}, getAsyncExecutor())))
128+
.toList();
129+
130+
List<CIURL> rurls = new ArrayList<>();
131+
List<CoeiroinkSpeaker> rspeakers = null;
132+
133+
for (Pair<CIURL, CompletableFuture<List<CoeiroinkSpeaker>>> ret : urls) {
134+
CIURL vu = ret.getLeft();
135+
CompletableFuture<List<CoeiroinkSpeaker>> cf = ret.getRight();
136+
137+
try {
138+
List<CoeiroinkSpeaker> r = cf.get();
139+
if (rspeakers == null) {
140+
rspeakers = r;
141+
}
142+
143+
rurls.add(vu);
144+
145+
if (availableUrls == null || !availableUrls.contains(vu)) {
146+
getITTSLogger().info("Available {} URL: {}", manager.getName(), vu.url());
147+
}
148+
149+
} catch (InterruptedException | ExecutionException e) {
150+
if (availableUrls == null || availableUrls.contains(vu)) {
151+
getITTSLogger().warn("Unavailable {} URL: {}", manager.getName(), vu.url());
152+
}
153+
}
154+
155+
}
156+
157+
return Pair.of(rurls, rspeakers);
158+
}
159+
160+
/**
161+
* エンジンが利用可能かどうかをチェックする
162+
*
163+
* @return エンジンが利用可能であればtrue、それ以外はfalse
164+
* @see #enginUrls
165+
*/
166+
public boolean isAvailable() {
167+
return enginUrls != null && !enginUrls.get().isEmpty();
168+
}
169+
170+
/**
171+
* URLの使用インターフェイスを取得
172+
*
173+
* @return URLの使用インターフェイス
174+
*/
175+
protected CoeiroinkUseURL getUseURL() {
176+
if (availableUrls == null || availableUrls.isEmpty()) {
177+
throw new RuntimeException("No URL available.");
178+
}
179+
180+
CIURL ciurl = availableUrls.stream()
181+
.min(Comparator.comparingInt(r -> getUseCounter(r).get()))
182+
.get();
183+
184+
getUseCounter(ciurl).incrementAndGet();
185+
return new CoeiroinkUseURL() {
186+
@Override
187+
public CIURL getCIURL() {
188+
return ciurl;
189+
}
190+
191+
@Override
192+
public void close() {
193+
getUseCounter(ciurl).decrementAndGet();
194+
}
195+
};
196+
}
197+
}

0 commit comments

Comments
 (0)