From 871d605a33212c5dbbb1437d70a63ee0f4a9bf21 Mon Sep 17 00:00:00 2001 From: tegnike Date: Thu, 5 Dec 2024 18:21:44 +0100 Subject: [PATCH 1/5] =?UTF-8?q?=E3=81=AB=E3=81=98=E3=83=9C=E3=82=A4?= =?UTF-8?q?=E3=82=B9=E3=82=92TTS=E3=81=AE=E9=81=B8=E6=8A=9E=E8=82=A2?= =?UTF-8?q?=E3=81=AB=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/settings/language.tsx | 3 +- src/components/settings/voice.tsx | 88 ++++++++++++++++++- src/features/constants/settings.ts | 2 +- src/features/messages/speakCharacter.ts | 8 ++ .../messages/synthesizeVoiceNijivoice.ts | 37 ++++++++ src/features/stores/settings.ts | 12 +++ src/pages/api/get-nijivoice-actors.ts | 29 ++++++ src/pages/api/tts-nijivoice.ts | 50 +++++++++++ 8 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 src/features/messages/synthesizeVoiceNijivoice.ts create mode 100644 src/pages/api/get-nijivoice-actors.ts create mode 100644 src/pages/api/tts-nijivoice.ts diff --git a/src/components/settings/language.tsx b/src/components/settings/language.tsx index 65e3f3a..9f649e8 100644 --- a/src/components/settings/language.tsx +++ b/src/components/settings/language.tsx @@ -23,7 +23,8 @@ const LanguageSetting = () => { const jaVoiceSelected = ss.selectVoice === 'voicevox' || ss.selectVoice === 'koeiromap' || - ss.selectVoice === 'aivis_speech' + ss.selectVoice === 'aivis_speech' || + ss.selectVoice === 'nijivoice' switch (newLanguage) { case 'ja': diff --git a/src/components/settings/voice.tsx b/src/components/settings/voice.tsx index ce620dd..4640c18 100644 --- a/src/components/settings/voice.tsx +++ b/src/components/settings/voice.tsx @@ -1,4 +1,5 @@ import { useTranslation } from 'react-i18next' +import { useState, useEffect } from 'react' import { PRESET_A, @@ -53,8 +54,37 @@ const Voice = () => { const openaiTTSSpeed = settingsStore((s) => s.openaiTTSSpeed) const azureTTSKey = settingsStore((s) => s.azureTTSKey) const azureTTSEndpoint = settingsStore((s) => s.azureTTSEndpoint) + const nijivoiceApiKey = settingsStore((s) => s.nijivoiceApiKey) + const nijivoiceActorId = settingsStore((s) => s.nijivoiceActorId) + const nijivoiceSpeed = settingsStore((s) => s.nijivoiceSpeed) const { t } = useTranslation() + const [nijivoiceSpeakers, setNijivoiceSpeakers] = useState>([]) + + // にじボイスの話者一覧を取得する関数 + const fetchNijivoiceSpeakers = async () => { + try { + const response = await fetch( + `/api/get-nijivoice-actors?apiKey=${nijivoiceApiKey}` + ) + const data = await response.json() + if (data.voiceActors) { + const sortedActors = data.voiceActors.sort( + (a: any, b: any) => a.id - b.id + ) + setNijivoiceSpeakers(sortedActors) + } + } catch (error) { + console.error('Failed to fetch nijivoice speakers:', error) + } + } + + // コンポーネントマウント時またはにじボイス選択時に話者一覧を取得 + useEffect(() => { + if (selectVoice === 'nijivoice') { + fetchNijivoiceSpeakers() + } + }, [selectVoice, nijivoiceApiKey]) return (
@@ -78,7 +108,8 @@ const Voice = () => { - {/* 追加 */} + +
@@ -791,6 +822,61 @@ const Voice = () => { /> ) + } else if (selectVoice === 'nijivoice') { + return ( + <> +
{t('NijiVoiceInfo')}
+
{t('NijiVoiceApiKey')}
+
+ + settingsStore.setState({ + nijivoiceApiKey: e.target.value, + }) + } + /> +
+
{t('SpeakerSelection')}
+
+ +
+
+ {t('Speed')}: {nijivoiceSpeed} +
+ { + settingsStore.setState({ + nijivoiceSpeed: Number(e.target.value), + }) + }} + /> + + ) } })()}
diff --git a/src/features/constants/settings.ts b/src/features/constants/settings.ts index f1b2871..fa8ce4f 100644 --- a/src/features/constants/settings.ts +++ b/src/features/constants/settings.ts @@ -35,11 +35,11 @@ export type AIVoice = | 'voicevox' | 'stylebertvits2' | 'aivis_speech' + | 'nijivoice' | 'gsvitts' | 'elevenlabs' | 'openai' | 'azure' - export type Language = 'en' | 'ja' | 'ko' | 'zh' // ISO 639-1 export const LANGUAGES: Language[] = ['en', 'ja', 'ko', 'zh'] diff --git a/src/features/messages/speakCharacter.ts b/src/features/messages/speakCharacter.ts index 1b78762..d931df0 100644 --- a/src/features/messages/speakCharacter.ts +++ b/src/features/messages/speakCharacter.ts @@ -15,6 +15,7 @@ import { synthesizeVoiceAzureOpenAIApi } from './synthesizeVoiceAzureOpenAI' import toastStore from '@/features/stores/toast' import i18next from 'i18next' import { SpeakQueue } from './speakQueue' +import { synthesizeVoiceNijivoiceApi } from './synthesizeVoiceNijivoice' interface EnglishToJapanese { [key: string]: string @@ -121,6 +122,13 @@ const createSpeakCharacter = () => { ss.openaiTTSVoice, ss.openaiTTSSpeed ) + } else if (ss.selectVoice == 'nijivoice') { + buffer = await synthesizeVoiceNijivoiceApi( + talk, + ss.nijivoiceApiKey, + ss.nijivoiceActorId, + ss.nijivoiceSpeed + ) } } catch (error) { handleTTSError(error, ss.selectVoice) diff --git a/src/features/messages/synthesizeVoiceNijivoice.ts b/src/features/messages/synthesizeVoiceNijivoice.ts new file mode 100644 index 0000000..fc4599a --- /dev/null +++ b/src/features/messages/synthesizeVoiceNijivoice.ts @@ -0,0 +1,37 @@ +import { Talk } from './messages' + +export async function synthesizeVoiceNijivoiceApi( + talk: Talk, + apiKey: string, + voiceActorId: string, + speed: number +) { + try { + const res = await fetch('/api/tts-nijivoice', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + script: talk.message, + speed, + voiceActorId, + apiKey, + }), + }) + + if (!res.ok) { + throw new Error( + `Nijivoice APIからの応答が異常です。ステータスコード: ${res.status}` + ) + } + + return await res.arrayBuffer() + } catch (error) { + if (error instanceof Error) { + throw new Error(`Nijivoiceでエラーが発生しました: ${error.message}`) + } else { + throw new Error('Nijivoiceで不明なエラーが発生しました') + } + } +} diff --git a/src/features/stores/settings.ts b/src/features/stores/settings.ts index 4c698aa..225bf30 100644 --- a/src/features/stores/settings.ts +++ b/src/features/stores/settings.ts @@ -71,6 +71,9 @@ interface ModelProvider { openaiTTSVoice: OpenAITTSVoice openaiTTSModel: OpenAITTSModel openaiTTSSpeed: number + nijivoiceApiKey: string + nijivoiceActorId: string + nijivoiceSpeed: number } interface Integrations { @@ -248,6 +251,12 @@ const settingsStore = create()( slideMode: process.env.NEXT_PUBLIC_SLIDE_MODE === 'true', messageReceiverEnabled: false, clientId: '', + + // NijiVoice settings + nijivoiceApiKey: '', + nijivoiceActorId: process.env.NEXT_PUBLIC_NIJIVOICE_ACTOR_ID || '', + nijivoiceSpeed: + parseFloat(process.env.NEXT_PUBLIC_NIJIVOICE_SPEED || '1.0') || 1.0, }), { name: 'aitube-kit-settings', @@ -316,6 +325,9 @@ const settingsStore = create()( azureTTSKey: state.azureTTSKey, azureTTSEndpoint: state.azureTTSEndpoint, selectedVrmPath: state.selectedVrmPath, + nijivoiceApiKey: state.nijivoiceApiKey, + nijivoiceActorId: state.nijivoiceActorId, + nijivoiceSpeed: state.nijivoiceSpeed, }), } ) diff --git a/src/pages/api/get-nijivoice-actors.ts b/src/pages/api/get-nijivoice-actors.ts new file mode 100644 index 0000000..bcb9284 --- /dev/null +++ b/src/pages/api/get-nijivoice-actors.ts @@ -0,0 +1,29 @@ +import type { NextApiRequest, NextApiResponse } from 'next' + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { apiKey } = req.query + + const nijivoiceApiKey = apiKey || process.env.NIJIVOICE_API_KEY + if (!nijivoiceApiKey) { + return res.status(400).json({ error: 'API key is required' }) + } + + try { + const response = await fetch( + 'https://ai-voice-api-kerb-538219057988.asia-northeast1.run.app/api/platform/v1/voice-actors', + { + headers: { + 'x-api-key': nijivoiceApiKey as string, + }, + } + ) + const data = await response.json() + return res.status(200).json(data) + } catch (error) { + console.error('Failed to fetch voice actors:', error) + return res.status(500).json({ error: 'Failed to fetch voice actors' }) + } +} diff --git a/src/pages/api/tts-nijivoice.ts b/src/pages/api/tts-nijivoice.ts new file mode 100644 index 0000000..c65426d --- /dev/null +++ b/src/pages/api/tts-nijivoice.ts @@ -0,0 +1,50 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import axios from 'axios' + +type Data = { + audio?: ArrayBuffer + error?: string +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { script, speed, voiceActorId, apiKey } = req.body + + const nijivoiceApiKey = apiKey || process.env.NIJIVOICE_API_KEY + if (!nijivoiceApiKey) { + return res.status(400).json({ error: 'API key is required' }) + } + + try { + const response = await axios.post( + `https://ai-voice-api-kerb-538219057988.asia-northeast1.run.app/api/platform/v1/voice-actors/${voiceActorId}/generate-voice`, + { + script, + speed: speed.toString(), + format: 'wav', + }, + { + headers: { + 'Content-Type': 'application/json', + 'x-api-key': nijivoiceApiKey, + }, + timeout: 30000, + } + ) + + const audioUrl = response.data.generatedVoice.url + + const audioResponse = await axios.get(audioUrl, { + responseType: 'stream', + timeout: 30000, + }) + + res.setHeader('Content-Type', 'audio/mpeg') + audioResponse.data.pipe(res) + } catch (error) { + console.error('Error in Nijivoice TTS:', error) + res.status(500).json({ error: 'Internal Server Error' }) + } +} From 9dbe89ae7a88750c42fa600826ca77837d7d4c32 Mon Sep 17 00:00:00 2001 From: tegnike Date: Tue, 10 Dec 2024 16:14:07 +0100 Subject: [PATCH 2/5] =?UTF-8?q?=E8=A3=BD=E5=93=81=E7=89=88=E3=81=AB?= =?UTF-8?q?=E5=90=88=E3=82=8F=E3=81=9B=E3=81=A6=E3=82=A8=E3=83=B3=E3=83=89?= =?UTF-8?q?=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88=E3=81=AA=E3=81=A9=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/api/get-nijivoice-actors.ts | 2 +- src/pages/api/tts-nijivoice.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/api/get-nijivoice-actors.ts b/src/pages/api/get-nijivoice-actors.ts index bcb9284..b2f79e7 100644 --- a/src/pages/api/get-nijivoice-actors.ts +++ b/src/pages/api/get-nijivoice-actors.ts @@ -13,7 +13,7 @@ export default async function handler( try { const response = await fetch( - 'https://ai-voice-api-kerb-538219057988.asia-northeast1.run.app/api/platform/v1/voice-actors', + 'https://api.nijivoice.com/api/platform/v1/voice-actors', { headers: { 'x-api-key': nijivoiceApiKey as string, diff --git a/src/pages/api/tts-nijivoice.ts b/src/pages/api/tts-nijivoice.ts index c65426d..bead945 100644 --- a/src/pages/api/tts-nijivoice.ts +++ b/src/pages/api/tts-nijivoice.ts @@ -19,7 +19,7 @@ export default async function handler( try { const response = await axios.post( - `https://ai-voice-api-kerb-538219057988.asia-northeast1.run.app/api/platform/v1/voice-actors/${voiceActorId}/generate-voice`, + `https://api.nijivoice.com/api/platform/v1/voice-actors/${voiceActorId}/generate-voice`, { script, speed: speed.toString(), @@ -34,7 +34,7 @@ export default async function handler( } ) - const audioUrl = response.data.generatedVoice.url + const audioUrl = response.data.generatedVoice.audioFileUrl const audioResponse = await axios.get(audioUrl, { responseType: 'stream', From 463ac1fb3fb7db56d07dc2dab2180a433c41062a Mon Sep 17 00:00:00 2001 From: tegnike Date: Tue, 10 Dec 2024 16:22:07 +0100 Subject: [PATCH 3/5] =?UTF-8?q?locale=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=AA=E3=81=A9=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 5 +++++ .env.example | 5 +++++ locales/en/translation.json | 5 +++++ locales/ja/translation.json | 5 +++++ locales/ko/translation.json | 5 +++++ locales/zh/translation.json | 5 +++++ src/components/settings/voice.tsx | 6 +++--- 7 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.env b/.env index 68e13bd..41cdc83 100644 --- a/.env +++ b/.env @@ -85,6 +85,11 @@ NEXT_PUBLIC_OPENAI_TTS_SPEED="" AZURE_TTS_KEY="" AZURE_TTS_ENDPOINT="" +# NijiVoice +NIJIVOICE_API_KEY= +NEXT_PUBLIC_NIJIVOICE_ACTOR_ID= +NEXT_PUBLIC_NIJIVOICE_SPEED= + # Youtube NEXT_PUBLIC_YOUTUBE_API_KEY="" NEXT_PUBLIC_YOUTUBE_LIVE_ID="" diff --git a/.env.example b/.env.example index 72c5e97..6da7acb 100644 --- a/.env.example +++ b/.env.example @@ -85,6 +85,11 @@ NEXT_PUBLIC_OPENAI_TTS_SPEED="" AZURE_TTS_KEY="" AZURE_TTS_ENDPOINT="" +# NijiVoice +NIJIVOICE_API_KEY= +NEXT_PUBLIC_NIJIVOICE_ACTOR_ID= +NEXT_PUBLIC_NIJIVOICE_SPEED= + # Youtube NEXT_PUBLIC_YOUTUBE_API_KEY="" NEXT_PUBLIC_YOUTUBE_LIVE_ID="" diff --git a/locales/en/translation.json b/locales/en/translation.json index 3b44497..6e118c6 100644 --- a/locales/en/translation.json +++ b/locales/en/translation.json @@ -64,6 +64,11 @@ "AivisSpeechSpeed": "Speed", "AivisSpeechPitch": "Pitch", "AivisSpeechIntonation": "Intonation", + "UsingNijiVoice": "NijiVoice", + "NijiVoiceInfo": "NijiVoice API is used. It supports only Japanese. API key can be obtained from the URL below.", + "NijiVoiceApiKey": "NijiVoice API Key", + "NijiVoiceActorId": "NijiVoice Actor ID", + "NijiVoiceSpeed": "NijiVoice Speed", "UpdateSpeakerList": "Update Speaker List", "UsingGoogleTTS": "Google TTS", "UsingStyleBertVITS2": "Style-Bert-VITS2", diff --git a/locales/ja/translation.json b/locales/ja/translation.json index f73668b..958c9f1 100644 --- a/locales/ja/translation.json +++ b/locales/ja/translation.json @@ -64,6 +64,11 @@ "AivisSpeechSpeed": "話速", "AivisSpeechPitch": "音高", "AivisSpeechIntonation": "抑揚", + "UsingNijiVoice": "にじボイスを使用する", + "NijiVoiceInfo": "にじボイス APIを使用しています。日本語のみに対応しています。APIキーを下記のURLから取得してください。", + "NijiVoiceApiKey": "にじボイス API キー", + "NijiVoiceActorId": "にじボイス 話者ID", + "NijiVoiceSpeed": "にじボイス 話速", "UpdateSpeakerList": "話者リストを更新", "UsingGoogleTTS": "Google TTSを使用する", "UsingStyleBertVITS2": "Style-Bert-VITS2を使用する", diff --git a/locales/ko/translation.json b/locales/ko/translation.json index 75e1f1e..039a1ac 100644 --- a/locales/ko/translation.json +++ b/locales/ko/translation.json @@ -64,6 +64,11 @@ "AivisSpeechSpeed": "말 속도", "AivisSpeechPitch": "음높이", "AivisSpeechIntonation": "억양", + "UsingNijiVoice": "NijiVoice", + "NijiVoiceInfo": "NijiVoice API를 사용하고 있습니다. 일본어만 지원됩니다. API 키는 아래 URL에서 얻을 수 있습니다.", + "NijiVoiceApiKey": "NijiVoice API 키", + "NijiVoiceActorId": "NijiVoice 화자 ID", + "NijiVoiceSpeed": "NijiVoice 말 속도", "UpdateSpeakerList": "보이스 타입 업데이트", "UsingGoogleTTS": "Google TTS 사용", "UsingStyleBertVITS2": "Style-Bert-VITS2 사용", diff --git a/locales/zh/translation.json b/locales/zh/translation.json index cc5fa4b..8de358f 100644 --- a/locales/zh/translation.json +++ b/locales/zh/translation.json @@ -64,6 +64,11 @@ "AivisSpeechSpeed": "語速", "AivisSpeechPitch": "音調", "AivisSpeechIntonation": "語調", + "UsingNijiVoice": "使用 NijiVoice", + "NijiVoiceInfo": "NijiVoice API 使用中。僅支援日語。API 金鑰可以從下方的 URL 取得。", + "NijiVoiceApiKey": "NijiVoice API 金鑰", + "NijiVoiceActorId": "NijiVoice 話者ID", + "NijiVoiceSpeed": "NijiVoice 話速", "UpdateSpeakerList": "更新語音角色", "UsingGoogleTTS": "使用 Google TTS", "UsingStyleBertVITS2": "使用 Style-Bert-VITS2", diff --git a/src/components/settings/voice.tsx b/src/components/settings/voice.tsx index 5be1197..8d01a99 100644 --- a/src/components/settings/voice.tsx +++ b/src/components/settings/voice.tsx @@ -113,7 +113,7 @@ const Voice = () => {
-
+
{t('VoiceAdjustment')}
{(() => { @@ -840,7 +840,7 @@ const Voice = () => { } />
-
{t('SpeakerSelection')}
+
{t('NijiVoiceActorId')}
- {t('Speed')}: {nijivoiceSpeed} + {t('NijiVoiceSpeed')}: {nijivoiceSpeed}
Date: Wed, 11 Dec 2024 00:14:04 +0100 Subject: [PATCH 4/5] =?UTF-8?q?=E3=81=AB=E3=81=98=E3=83=9C=E3=82=A4?= =?UTF-8?q?=E3=82=B9=E3=81=AE=E6=B3=A8=E9=87=88URL=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/settings/voice.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/settings/voice.tsx b/src/components/settings/voice.tsx index 8d01a99..1897c27 100644 --- a/src/components/settings/voice.tsx +++ b/src/components/settings/voice.tsx @@ -826,6 +826,10 @@ const Voice = () => { return ( <>
{t('NijiVoiceInfo')}
+
{t('NijiVoiceApiKey')}
Date: Wed, 11 Dec 2024 00:18:10 +0100 Subject: [PATCH 5/5] =?UTF-8?q?=E3=83=90=E3=83=BC=E3=82=B8=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/settings/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/settings/index.tsx b/src/components/settings/index.tsx index c3df97b..f67c4e2 100644 --- a/src/components/settings/index.tsx +++ b/src/components/settings/index.tsx @@ -126,7 +126,7 @@ const Main = () => { const Footer = () => { return ( ) }