Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions src/features/messages/speakCharacter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@ import { synthesizeVoiceVoicevoxApi } from './synthesizeVoiceVoicevox'
import { synthesizeVoiceGSVIApi } from './synthesizeVoiceGSVI'
import toastStore from '@/features/stores/toast'
import i18next from 'i18next'
import { SpeakQueue } from './speakQueue'

interface EnglishToJapanese {
[key: string]: string
}

const typedEnglishToJapanese = englishToJapanese as EnglishToJapanese

const speakQueue = new SpeakQueue()

const createSpeakCharacter = () => {
let lastTime = 0
let prevFetchPromise: Promise<unknown> = Promise.resolve()
let prevSpeakPromise: Promise<unknown> = Promise.resolve()

return (
screenplay: Screenplay,
Expand All @@ -32,7 +34,6 @@ const createSpeakCharacter = () => {
onStart?.()

if (ss.changeEnglishToJapanese && ss.selectLanguage === 'ja') {
// 英単語を日本語で読み上げる
screenplay.talk.message = convertEnglishToJapaneseReading(
screenplay.talk.message
)
Expand Down Expand Up @@ -106,17 +107,17 @@ const createSpeakCharacter = () => {
})

prevFetchPromise = fetchPromise
prevSpeakPromise = Promise.all([fetchPromise, prevSpeakPromise]).then(
([audioBuffer]) => {
if (!audioBuffer) {
return
}
const hs = homeStore.getState()
return hs.viewer.model?.speak(audioBuffer, screenplay, isNeedDecode)
}
)
prevSpeakPromise.then(() => {
onComplete?.()

// キューを使用した処理に変更
fetchPromise.then((audioBuffer) => {
if (!audioBuffer) return

speakQueue.addTask({
audioBuffer,
screenplay,
isNeedDecode,
onComplete,
})
})
}
}
Expand Down
69 changes: 69 additions & 0 deletions src/features/messages/speakQueue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Screenplay } from './messages'
import homeStore from '@/features/stores/home'

type SpeakTask = {
audioBuffer: ArrayBuffer
screenplay: Screenplay
isNeedDecode: boolean
onComplete?: () => void
}

export class SpeakQueue {
private static readonly QUEUE_CHECK_DELAY = 1500
private queue: SpeakTask[] = []
private isProcessing = false

async addTask(task: SpeakTask) {
this.queue.push(task)
await this.processQueue()
}
Comment on lines +16 to +19
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

addTaskメソッドにエラーハンドリングの追加を推奨します

タスク追加時の例外処理を実装することで、より堅牢な実装になります。

 async addTask(task: SpeakTask) {
+  try {
     this.queue.push(task)
     await this.processQueue()
+  } catch (error) {
+    console.error('タスクの追加中にエラーが発生しました:', error)
+    throw error
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async addTask(task: SpeakTask) {
this.queue.push(task)
await this.processQueue()
}
async addTask(task: SpeakTask) {
try {
this.queue.push(task)
await this.processQueue()
} catch (error) {
console.error('タスクの追加中にエラーが発生しました:', error)
throw error
}
}


private async processQueue() {
if (this.isProcessing) return
this.isProcessing = true
const hs = homeStore.getState()

while (this.queue.length > 0) {
const task = this.queue.shift()
if (task) {
try {
const { audioBuffer, screenplay, isNeedDecode, onComplete } = task
await hs.viewer.model?.speak(audioBuffer, screenplay, isNeedDecode)
onComplete?.()
} catch (error) {
console.error(
'An error occurred while processing the speech synthesis task:',
error
)
if (error instanceof Error) {
console.error('Error details:', error.message)
}
}
}
}

this.isProcessing = false
this.scheduleNeutralExpression()
}

private async scheduleNeutralExpression() {
const initialLength = this.queue.length
await new Promise((resolve) =>
setTimeout(resolve, SpeakQueue.QUEUE_CHECK_DELAY)
)

if (this.shouldResetToNeutral(initialLength)) {
const hs = homeStore.getState()
console.log('play neutral')
await hs.viewer.model?.playEmotion('neutral')
}
}

private shouldResetToNeutral(initialLength: number): boolean {
return initialLength === 0 && this.queue.length === 0 && !this.isProcessing
}

clearQueue() {
this.queue = []
}
}
14 changes: 13 additions & 1 deletion src/features/vrmViewer/model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as THREE from 'three'
import { VRM, VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm'
import {
VRM,
VRMExpressionPresetName,
VRMLoaderPlugin,
VRMUtils,
} from '@pixiv/three-vrm'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { VRMAnimation } from '../../lib/VRMAnimation/VRMAnimation'
import { VRMLookAtSmootherLoaderPlugin } from '@/lib/VRMLookAtSmootherLoaderPlugin/VRMLookAtSmootherLoaderPlugin'
Expand Down Expand Up @@ -86,6 +91,13 @@ export class Model {
})
}

/**
* 感情表現を再生する
*/
public async playEmotion(preset: VRMExpressionPresetName) {
this.emoteController?.playEmotion(preset)
}
Comment on lines +94 to +99
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

安全性とドキュメンテーションの改善が必要です

以下の改善点を提案させていただきます:

  1. emoteControllerが未定義の場合のエラーハンドリング
  2. 利用可能な感情プリセットのドキュメント追加
  3. 戻り値の型定義の追加

以下の実装を提案します:

  /**
   * 感情表現を再生する
+  * @param preset - 感情表現のプリセット
+  * - 'neutral' - 通常表情
+  * - 'happy' - 笑顔
+  * - 'angry' - 怒り
+  * - 'sad' - 悲しみ
+  * - etc...
+  * @throws Error emoteControllerが初期化されていない場合
+  * @returns Promise<void>
   */
- public async playEmotion(preset: VRMExpressionPresetName) {
+ public async playEmotion(preset: VRMExpressionPresetName): Promise<void> {
+   if (!this.emoteController) {
+     throw new Error('EmoteController is not initialized. Please load VRM first.');
+   }
-   this.emoteController?.playEmotion(preset)
+   this.emoteController.playEmotion(preset)
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 感情表現を再生する
*/
public async playEmotion(preset: VRMExpressionPresetName) {
this.emoteController?.playEmotion(preset)
}
/**
* 感情表現を再生する
* @param preset - 感情表現のプリセット
* - 'neutral' - 通常表情
* - 'happy' - 笑顔
* - 'angry' - 怒り
* - 'sad' - 悲しみ
* - etc...
* @throws Error emoteControllerが初期化されていない場合
* @returns Promise<void>
*/
public async playEmotion(preset: VRMExpressionPresetName): Promise<void> {
if (!this.emoteController) {
throw new Error('EmoteController is not initialized. Please load VRM first.');
}
this.emoteController.playEmotion(preset)
}


public update(delta: number): void {
if (this._lipSync) {
const { volume } = this._lipSync.update()
Expand Down