-
+
{t('VoiceAdjustment')}
{(() => {
@@ -791,6 +822,65 @@ const Voice = () => {
/>
>
)
+ } else if (selectVoice === 'nijivoice') {
+ return (
+ <>
+
{t('NijiVoiceInfo')}
+
+
{t('NijiVoiceApiKey')}
+
+
+ settingsStore.setState({
+ nijivoiceApiKey: e.target.value,
+ })
+ }
+ />
+
+
{t('NijiVoiceActorId')}
+
+
+
+
+ {t('NijiVoiceSpeed')}: {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 69b6b13..57e969c 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 {
@@ -251,6 +254,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',
@@ -320,6 +329,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..b2f79e7
--- /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://api.nijivoice.com/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..bead945
--- /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://api.nijivoice.com/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.audioFileUrl
+
+ 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' })
+ }
+}