Skip to content

Commit

Permalink
Merge pull request #259 from tegnike/develop
Browse files Browse the repository at this point in the history
本番リリース
  • Loading branch information
tegnike authored Dec 10, 2024
2 parents 409ef16 + efbbf75 commit 53b265b
Show file tree
Hide file tree
Showing 15 changed files with 262 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -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=""
Expand Down
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=""
Expand Down
5 changes: 5 additions & 0 deletions locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions locales/ja/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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を使用する",
Expand Down
5 changes: 5 additions & 0 deletions locales/ko/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 사용",
Expand Down
5 changes: 5 additions & 0 deletions locales/zh/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/components/settings/based.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ const Based = () => {
const jaVoiceSelected =
ss.selectVoice === 'voicevox' ||
ss.selectVoice === 'koeiromap' ||
ss.selectVoice === 'aivis_speech'
ss.selectVoice === 'aivis_speech' ||
ss.selectVoice === 'nijivoice'

switch (newLanguage) {
case 'ja':
Expand Down
2 changes: 1 addition & 1 deletion src/components/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ const Main = () => {
const Footer = () => {
return (
<footer className="absolute py-4 bg-[#413D43] text-center text-white font-Montserrat bottom-0 w-full">
powered by ChatVRM from Pixiv. version 2.15.0
powered by ChatVRM from Pixiv. version 2.16.0
</footer>
)
}
94 changes: 92 additions & 2 deletions src/components/settings/voice.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next'
import { useState, useEffect } from 'react'

import {
PRESET_A,
Expand Down Expand Up @@ -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<Array<any>>([])

// にじボイスの話者一覧を取得する関数
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 (
<div className="">
Expand All @@ -78,11 +108,12 @@ const Voice = () => {
<option value="gsvitts">{t('UsingGSVITTS')}</option>
<option value="elevenlabs">{t('UsingElevenLabs')}</option>
<option value="openai">{t('UsingOpenAITTS')}</option>
<option value="azure">{t('UsingAzureTTS')}</option> {/* 追加 */}
<option value="azure">{t('UsingAzureTTS')}</option>
<option value="nijivoice">{t('UsingNijiVoice')}</option>
</select>
</div>
<div className="mt-40">
<div className="m-16 typography-20 font-bold">
<div className="mb-16 typography-20 font-bold">
{t('VoiceAdjustment')}
</div>
{(() => {
Expand Down Expand Up @@ -791,6 +822,65 @@ const Voice = () => {
/>
</>
)
} else if (selectVoice === 'nijivoice') {
return (
<>
<div>{t('NijiVoiceInfo')}</div>
<Link
url="https://app.nijivoice.com/"
label="https://app.nijivoice.com/"
/>
<div className="mt-16 font-bold">{t('NijiVoiceApiKey')}</div>
<div className="mt-8">
<input
className="text-ellipsis px-16 py-8 w-col-span-4 bg-surface1 hover:bg-surface1-hover rounded-8"
type="text"
placeholder="..."
value={nijivoiceApiKey}
onChange={(e) =>
settingsStore.setState({
nijivoiceApiKey: e.target.value,
})
}
/>
</div>
<div className="mt-16 font-bold">{t('NijiVoiceActorId')}</div>
<div className="mt-8">
<select
value={nijivoiceActorId}
onChange={(e) => {
settingsStore.setState({
nijivoiceActorId: e.target.value,
})
}}
className="px-16 py-8 bg-surface1 hover:bg-surface1-hover rounded-8"
>
<option value="">{t('Select')}</option>
{nijivoiceSpeakers.map((actor) => (
<option key={actor.id} value={actor.id}>
{actor.name}
</option>
))}
</select>
</div>
<div className="mt-16 font-bold">
{t('NijiVoiceSpeed')}: {nijivoiceSpeed}
</div>
<input
type="range"
min={0.4}
max={3.0}
step={0.1}
value={nijivoiceSpeed}
className="mt-8 mb-16 input-range"
onChange={(e) => {
settingsStore.setState({
nijivoiceSpeed: Number(e.target.value),
})
}}
/>
</>
)
}
})()}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/features/constants/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
8 changes: 8 additions & 0 deletions src/features/messages/speakCharacter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
37 changes: 37 additions & 0 deletions src/features/messages/synthesizeVoiceNijivoice.ts
Original file line number Diff line number Diff line change
@@ -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で不明なエラーが発生しました')
}
}
}
12 changes: 12 additions & 0 deletions src/features/stores/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ interface ModelProvider {
openaiTTSVoice: OpenAITTSVoice
openaiTTSModel: OpenAITTSModel
openaiTTSSpeed: number
nijivoiceApiKey: string
nijivoiceActorId: string
nijivoiceSpeed: number
}

interface Integrations {
Expand Down Expand Up @@ -251,6 +254,12 @@ const settingsStore = create<SettingsState>()(
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',
Expand Down Expand Up @@ -320,6 +329,9 @@ const settingsStore = create<SettingsState>()(
azureTTSKey: state.azureTTSKey,
azureTTSEndpoint: state.azureTTSEndpoint,
selectedVrmPath: state.selectedVrmPath,
nijivoiceApiKey: state.nijivoiceApiKey,
nijivoiceActorId: state.nijivoiceActorId,
nijivoiceSpeed: state.nijivoiceSpeed,
}),
}
)
Expand Down
29 changes: 29 additions & 0 deletions src/pages/api/get-nijivoice-actors.ts
Original file line number Diff line number Diff line change
@@ -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' })
}
}
Loading

0 comments on commit 53b265b

Please sign in to comment.