diff --git a/locales/bg-BG/role.json b/locales/bg-BG/role.json index fc653952..ed415c4d 100644 --- a/locales/bg-BG/role.json +++ b/locales/bg-BG/role.json @@ -114,6 +114,7 @@ "head": "Глава", "leg": "Крак" }, + "customEnable": "Активиране на персонализиран допир", "editAction": "Редактиране на отговорно действие", "expression": { "angry": "Ядосан", diff --git a/locales/bg-BG/welcome.json b/locales/bg-BG/welcome.json index 5ef04f96..091379c2 100644 --- a/locales/bg-BG/welcome.json +++ b/locales/bg-BG/welcome.json @@ -7,6 +7,11 @@ } }, "greet": "Здравейте, аз съм {{name}}, с какво мога да ви помогна?", - "loadingTitle": "Приложението се инициализира, моля изчакайте...", - "waiting": "Подготвям целия си свят за теб" + "loading": { + "model": "Зареждане на модела...", + "motions": "Предварително зареждане на файловете с движения...", + "voices": "Генериране на файловете с глас...", + "waiting": "Подготвям целия си свят за теб..." + }, + "loadingTitle": "Приложението се инициализира, моля изчакайте..." } diff --git a/locales/de-DE/role.json b/locales/de-DE/role.json index 5dbcb83f..6fc6f688 100644 --- a/locales/de-DE/role.json +++ b/locales/de-DE/role.json @@ -114,6 +114,7 @@ "head": "Kopf", "leg": "Bein" }, + "customEnable": "Benutzerdefiniertes Touch aktivieren", "editAction": "Bearbeiten der Reaktionsaktion", "expression": { "angry": "Wütend", diff --git a/locales/de-DE/welcome.json b/locales/de-DE/welcome.json index fe51f630..799a91ea 100644 --- a/locales/de-DE/welcome.json +++ b/locales/de-DE/welcome.json @@ -7,6 +7,11 @@ } }, "greet": "Hallo, ich bin {{name}}. Wie kann ich Ihnen helfen?", - "loadingTitle": "Anwendung wird initialisiert, bitte warten...", - "waiting": "Ich bereite gerade meine ganze Welt für dich vor" + "loading": { + "model": "Modell wird geladen...", + "motions": "Lade die Bewegungsdateien vor...", + "voices": "Sprachdateien werden generiert...", + "waiting": "Ich bereite gerade meine ganze Welt für dich vor..." + }, + "loadingTitle": "Anwendung wird initialisiert, bitte warten..." } diff --git a/locales/en-US/role.json b/locales/en-US/role.json index 67553d06..88805697 100644 --- a/locales/en-US/role.json +++ b/locales/en-US/role.json @@ -114,6 +114,7 @@ "head": "Head", "leg": "Leg" }, + "customEnable": "Enable Custom Touch", "editAction": "Edit Response Action", "expression": { "angry": "Angry", diff --git a/locales/en-US/welcome.json b/locales/en-US/welcome.json index 523e91a1..d60fbf6f 100644 --- a/locales/en-US/welcome.json +++ b/locales/en-US/welcome.json @@ -7,6 +7,11 @@ } }, "greet": "Hello, I am {{name}}. How can I assist you today?", - "loadingTitle": "Initializing application, please wait...", - "waiting": "I am preparing my whole world for you" + "loading": { + "model": "Loading model...", + "motions": "Preloading motion files...", + "voices": "Generating voice files...", + "waiting": "Preparing my entire world for you..." + }, + "loadingTitle": "Initializing application, please wait..." } diff --git a/locales/es-ES/role.json b/locales/es-ES/role.json index 639769a0..44578ac0 100644 --- a/locales/es-ES/role.json +++ b/locales/es-ES/role.json @@ -114,6 +114,7 @@ "head": "Cabeza", "leg": "Pierna" }, + "customEnable": "Habilitar toque personalizado", "editAction": "Editar acción de respuesta", "expression": { "angry": "Enojado", diff --git a/locales/es-ES/welcome.json b/locales/es-ES/welcome.json index 01873e51..4d48be75 100644 --- a/locales/es-ES/welcome.json +++ b/locales/es-ES/welcome.json @@ -7,6 +7,11 @@ } }, "greet": "Hola, soy {{name}}, ¿en qué puedo ayudarte?", - "loadingTitle": "Inicializando la aplicación, por favor espere...", - "waiting": "Estoy preparando todo mi mundo para ti" + "loading": { + "model": "Cargando el modelo...", + "motions": "Cargando archivos de animación...", + "voices": "Generando archivos de voz...", + "waiting": "Preparando todo mi mundo para ti..." + }, + "loadingTitle": "Inicializando la aplicación, por favor espere..." } diff --git a/locales/fr-FR/role.json b/locales/fr-FR/role.json index 7e3d1c4a..e018b002 100644 --- a/locales/fr-FR/role.json +++ b/locales/fr-FR/role.json @@ -114,6 +114,7 @@ "head": "Tête", "leg": "Jambe" }, + "customEnable": "Activer le toucher personnalisé", "editAction": "Modifier l'action de réponse", "expression": { "angry": "En colère", diff --git a/locales/fr-FR/welcome.json b/locales/fr-FR/welcome.json index a02535fb..69d4e282 100644 --- a/locales/fr-FR/welcome.json +++ b/locales/fr-FR/welcome.json @@ -7,6 +7,11 @@ } }, "greet": "Bonjour, je suis {{name}}, comment puis-je vous aider ?", - "loadingTitle": "Initialisation de l'application, veuillez patienter...", - "waiting": "Je prépare tout mon monde pour toi" + "loading": { + "model": "Chargement du modèle...", + "motions": "Préchargement des fichiers d'animation...", + "voices": "Génération des fichiers audio...", + "waiting": "Préparation de mon monde entier pour vous..." + }, + "loadingTitle": "Initialisation de l'application, veuillez patienter..." } diff --git a/locales/it-IT/role.json b/locales/it-IT/role.json index ed442b8a..3648025c 100644 --- a/locales/it-IT/role.json +++ b/locales/it-IT/role.json @@ -114,6 +114,7 @@ "head": "Testa", "leg": "Gamba" }, + "customEnable": "Abilita tocco personalizzato", "editAction": "Modifica azione di risposta", "expression": { "angry": "Arrabbiato", diff --git a/locales/it-IT/welcome.json b/locales/it-IT/welcome.json index e4d514b6..99da7827 100644 --- a/locales/it-IT/welcome.json +++ b/locales/it-IT/welcome.json @@ -7,6 +7,11 @@ } }, "greet": "Ciao, sono {{name}}, come posso aiutarti?", - "loadingTitle": "Inizializzazione dell'applicazione, attendere prego...", - "waiting": "Sto preparando il mio intero mondo per te" + "loading": { + "model": "Caricamento del modello in corso...", + "motions": "Caricamento dei file di animazione...", + "voices": "Generazione dei file audio in corso...", + "waiting": "Stiamo preparando il mio intero mondo per te..." + }, + "loadingTitle": "Inizializzazione dell'applicazione, attendere prego..." } diff --git a/locales/ja-JP/role.json b/locales/ja-JP/role.json index 0d40f322..4cd723c2 100644 --- a/locales/ja-JP/role.json +++ b/locales/ja-JP/role.json @@ -114,6 +114,7 @@ "head": "頭部", "leg": "脚" }, + "customEnable": "カスタムタッチを有効にする", "editAction": "応答アクションを編集", "expression": { "angry": "怒っている", diff --git a/locales/ja-JP/welcome.json b/locales/ja-JP/welcome.json index ac1a8e0f..0148063c 100644 --- a/locales/ja-JP/welcome.json +++ b/locales/ja-JP/welcome.json @@ -7,6 +7,11 @@ } }, "greet": "こんにちは、私は{{name}}です。何かお手伝いできることはありますか?", - "loadingTitle": "アプリケーションの初期化中です。しばらくお待ちください...", - "waiting": "私の全世界をあなたのために準備しています" + "loading": { + "model": "モデルを読み込んでいます...", + "motions": "モーションファイルをプリロードしています...", + "voices": "音声ファイルを生成しています...", + "waiting": "私の全世界を準備しています..." + }, + "loadingTitle": "アプリケーションの初期化中です。しばらくお待ちください..." } diff --git a/locales/ko-KR/role.json b/locales/ko-KR/role.json index ec24bee3..faf09ca4 100644 --- a/locales/ko-KR/role.json +++ b/locales/ko-KR/role.json @@ -114,6 +114,7 @@ "head": "머리", "leg": "다리" }, + "customEnable": "사용자 정의 터치 활성화", "editAction": "응답 동작 편집", "expression": { "angry": "화남", diff --git a/locales/ko-KR/welcome.json b/locales/ko-KR/welcome.json index 089195f4..06873807 100644 --- a/locales/ko-KR/welcome.json +++ b/locales/ko-KR/welcome.json @@ -7,6 +7,11 @@ } }, "greet": "안녕하세요, 저는 {{name}}입니다. 무엇을 도와드릴까요?", - "loadingTitle": "애플리케이션 초기화 중입니다. 잠시만 기다려 주세요...", - "waiting": "내 모든 세상을 당신을 위해 준비하고 있습니다" + "loading": { + "model": "모델을 로드하는 중...", + "motions": "모션 파일을 미리 로드하는 중...", + "voices": "음성 파일을 생성하는 중...", + "waiting": "내 세상을 준비하고 있습니다..." + }, + "loadingTitle": "애플리케이션 초기화 중입니다. 잠시만 기다려 주세요..." } diff --git a/locales/nl-NL/role.json b/locales/nl-NL/role.json index 4d073699..9bc17416 100644 --- a/locales/nl-NL/role.json +++ b/locales/nl-NL/role.json @@ -114,6 +114,7 @@ "head": "Hoofd", "leg": "Been" }, + "customEnable": "Schakel aangepaste aanraking in", "editAction": "Bewerk responsactie", "expression": { "angry": "Boos", diff --git a/locales/nl-NL/welcome.json b/locales/nl-NL/welcome.json index e37983d7..e45a7f2b 100644 --- a/locales/nl-NL/welcome.json +++ b/locales/nl-NL/welcome.json @@ -7,6 +7,11 @@ } }, "greet": "Hallo, ik ben {{name}}, hoe kan ik je helpen?", - "loadingTitle": "Applicatie wordt geïnitialiseerd, even geduld...", - "waiting": "Ik ben mijn hele wereld voor je aan het voorbereiden" + "loading": { + "model": "Model aan het laden...", + "motions": "Actiebestanden aan het voorladen...", + "voices": "Stembestanden aan het genereren...", + "waiting": "We zijn bezig met het voorbereiden van mijn hele wereld..." + }, + "loadingTitle": "Applicatie wordt geïnitialiseerd, even geduld..." } diff --git a/locales/pl-PL/role.json b/locales/pl-PL/role.json index c8340f2d..59defd6c 100644 --- a/locales/pl-PL/role.json +++ b/locales/pl-PL/role.json @@ -114,6 +114,7 @@ "head": "Głowa", "leg": "Noga" }, + "customEnable": "Włącz niestandardowy dotyk", "editAction": "Edytuj akcję odpowiedzi", "expression": { "angry": "Zły", diff --git a/locales/pl-PL/welcome.json b/locales/pl-PL/welcome.json index bbe22ff6..ff6f98c9 100644 --- a/locales/pl-PL/welcome.json +++ b/locales/pl-PL/welcome.json @@ -7,6 +7,11 @@ } }, "greet": "Cześć, jestem {{name}}, w czym mogę Ci pomóc?", - "loadingTitle": "Aplikacja jest w trakcie inicjalizacji, proszę czekać...", - "waiting": "Przygotowuję dla Ciebie cały mój świat" + "loading": { + "model": "Ładowanie modelu...", + "motions": "Wstępne ładowanie plików z ruchami...", + "voices": "Generowanie plików dźwiękowych...", + "waiting": "Przygotowuję dla Ciebie cały mój świat..." + }, + "loadingTitle": "Aplikacja jest w trakcie inicjalizacji, proszę czekać..." } diff --git a/locales/pt-BR/role.json b/locales/pt-BR/role.json index 3dbb3d17..161828e0 100644 --- a/locales/pt-BR/role.json +++ b/locales/pt-BR/role.json @@ -114,6 +114,7 @@ "head": "Cabeça", "leg": "Perna" }, + "customEnable": "Ativar toque personalizado", "editAction": "Editar ação de resposta", "expression": { "angry": "Bravo", diff --git a/locales/pt-BR/welcome.json b/locales/pt-BR/welcome.json index 5e848267..1f787c3b 100644 --- a/locales/pt-BR/welcome.json +++ b/locales/pt-BR/welcome.json @@ -7,6 +7,11 @@ } }, "greet": "Olá, eu sou {{name}}, como posso ajudá-lo?", - "loadingTitle": "Aplicação em inicialização, por favor aguarde...", - "waiting": "Estou preparando todo o meu mundo para você" + "loading": { + "model": "Carregando o modelo...", + "motions": "Pré-carregando arquivos de animação...", + "voices": "Gerando arquivos de voz...", + "waiting": "Preparando todo o meu mundo para você..." + }, + "loadingTitle": "Aplicação em inicialização, por favor aguarde..." } diff --git a/locales/ru-RU/role.json b/locales/ru-RU/role.json index 88e0d532..a32f1fd2 100644 --- a/locales/ru-RU/role.json +++ b/locales/ru-RU/role.json @@ -114,6 +114,7 @@ "head": "Голова", "leg": "Нога" }, + "customEnable": "Включить пользовательский сенсорный ввод", "editAction": "Редактировать действие ответа", "expression": { "angry": "Сердитый", diff --git a/locales/ru-RU/welcome.json b/locales/ru-RU/welcome.json index 00bb6a25..b66fed3a 100644 --- a/locales/ru-RU/welcome.json +++ b/locales/ru-RU/welcome.json @@ -7,6 +7,11 @@ } }, "greet": "Здравствуйте, я {{name}}. Чем могу вам помочь?", - "loadingTitle": "Инициализация приложения, пожалуйста, подождите...", - "waiting": "Я готовлю для тебя весь свой мир" + "loading": { + "model": "Загрузка модели...", + "motions": "Предварительная загрузка файлов анимации...", + "voices": "Генерация файлов звука...", + "waiting": "Мы готовим для вас весь мой мир..." + }, + "loadingTitle": "Инициализация приложения, пожалуйста, подождите..." } diff --git a/locales/tr-TR/role.json b/locales/tr-TR/role.json index d69ad387..589a4cf6 100644 --- a/locales/tr-TR/role.json +++ b/locales/tr-TR/role.json @@ -114,6 +114,7 @@ "head": "Baş", "leg": "Bacak" }, + "customEnable": "Özel dokunmayı etkinleştir", "editAction": "Yanıt eylemini düzenle", "expression": { "angry": "Kızgın", diff --git a/locales/tr-TR/welcome.json b/locales/tr-TR/welcome.json index b45c9c13..1a630c53 100644 --- a/locales/tr-TR/welcome.json +++ b/locales/tr-TR/welcome.json @@ -7,6 +7,11 @@ } }, "greet": "Merhaba, ben {{name}}. Size nasıl yardımcı olabilirim?", - "loadingTitle": "Uygulama başlatılıyor, lütfen bekleyin...", - "waiting": "Senin için bütün dünyamı hazırlıyorum" + "loading": { + "model": "Model yükleniyor...", + "motions": "Hareket dosyaları önceden yükleniyor...", + "voices": "Ses dosyaları oluşturuluyor...", + "waiting": "Dünyamı senin için hazırlıyorum..." + }, + "loadingTitle": "Uygulama başlatılıyor, lütfen bekleyin..." } diff --git a/locales/vi-VN/role.json b/locales/vi-VN/role.json index ad8e245f..e87fe5cc 100644 --- a/locales/vi-VN/role.json +++ b/locales/vi-VN/role.json @@ -114,6 +114,7 @@ "head": "Đầu", "leg": "Chân" }, + "customEnable": "Bật tùy chỉnh chạm", "editAction": "Chỉnh sửa hành động phản hồi", "expression": { "angry": "Giận dữ", diff --git a/locales/vi-VN/welcome.json b/locales/vi-VN/welcome.json index 817e2e0b..2cb62145 100644 --- a/locales/vi-VN/welcome.json +++ b/locales/vi-VN/welcome.json @@ -7,6 +7,11 @@ } }, "greet": "Xin chào, tôi là {{name}}, có gì tôi có thể giúp bạn không?", - "loadingTitle": "Đang khởi tạo ứng dụng, vui lòng chờ...", - "waiting": "Đang chuẩn bị cả thế giới của tôi cho bạn" + "loading": { + "model": "Đang tải mô hình...", + "motions": "Đang tải các tệp hành động...", + "voices": "Đang tạo tệp âm thanh...", + "waiting": "Đang chuẩn bị toàn bộ thế giới của tôi cho bạn..." + }, + "loadingTitle": "Đang khởi tạo ứng dụng, vui lòng chờ..." } diff --git a/locales/zh-CN/chat.json b/locales/zh-CN/chat.json index b82027ec..7bc66742 100644 --- a/locales/zh-CN/chat.json +++ b/locales/zh-CN/chat.json @@ -78,7 +78,7 @@ "screenShot": "拍照", "grid": "网格", "axes": "坐标轴", - "downloading": "模型下载中,请稍后..." + "downloading": "正在为你准备我的整个世界..." }, "tts": { "record": "语音识别(需科学上网)", diff --git a/locales/zh-CN/role.json b/locales/zh-CN/role.json index 1d0ca63f..c6e569c1 100644 --- a/locales/zh-CN/role.json +++ b/locales/zh-CN/role.json @@ -58,7 +58,7 @@ "temperatureLabel": "随机性", "temperatureDescription": "值越大,回复越随机", "topPLabel": "核采样", - "topPDescription": "与随机性类型,但不要和随机性一起更改", + "topPDescription": "与随机性类似,但不要和随机性一起更改", "presencePenaltyLabel": "话题新鲜度", "presencePenaltyDescription": "值越大,越有可能拓展到新话题", "frequencyPenaltyLabel": "频率惩罚度", @@ -115,6 +115,7 @@ "touchActionList": "触摸{{touchArea}}时的反应列表", "touchArea": "触摸区域", "noTouchActions": "暂无自定义响应动作,您可以通过点击 '+' 按钮添加", + "customEnable": "启用自定义触摸", "area": { "head": "头部", "arm": "手臂", diff --git a/locales/zh-CN/welcome.json b/locales/zh-CN/welcome.json index d3627a6a..1e9b658d 100644 --- a/locales/zh-CN/welcome.json +++ b/locales/zh-CN/welcome.json @@ -7,6 +7,11 @@ } }, "greet": "你好,我是{{name}},有什么可以帮助你的吗?", - "loadingTitle": "应用初始化中,请稍后...", - "waiting": "正在为你准备我的整个世界" + "loading": { + "waiting": "正在为你准备我的整个世界...", + "motions": "预加载动作文件...", + "model": "加载模型中...", + "voices": "生成语音文件中..." + }, + "loadingTitle": "应用初始化中,请稍后..." } diff --git a/locales/zh-TW/role.json b/locales/zh-TW/role.json index dd673acc..71c2bf9d 100644 --- a/locales/zh-TW/role.json +++ b/locales/zh-TW/role.json @@ -114,6 +114,7 @@ "head": "頭部", "leg": "腿部" }, + "customEnable": "啟用自訂觸控", "editAction": "編輯回應動作", "expression": { "angry": "生氣", diff --git a/locales/zh-TW/welcome.json b/locales/zh-TW/welcome.json index b0296315..8c66e49e 100644 --- a/locales/zh-TW/welcome.json +++ b/locales/zh-TW/welcome.json @@ -7,6 +7,11 @@ } }, "greet": "你好,我是{{name}},有什麼可以幫助你的嗎?", - "loadingTitle": "應用程式初始化中,請稍候...", - "waiting": "正在為你準備我的整個世界" + "loading": { + "model": "加載模型中...", + "motions": "預加載動作檔案...", + "voices": "生成語音檔案中...", + "waiting": "正在為你準備我的整個世界..." + }, + "loadingTitle": "應用程式初始化中,請稍候..." } diff --git a/src/app/role/RoleEdit/Touch/ActionList/index.tsx b/src/app/role/RoleEdit/Touch/ActionList/index.tsx index 34ebb02c..7b81b037 100644 --- a/src/app/role/RoleEdit/Touch/ActionList/index.tsx +++ b/src/app/role/RoleEdit/Touch/ActionList/index.tsx @@ -44,12 +44,10 @@ const AreaList = (props: AreaListProps) => { const { currentTouchArea, style, className, areaOptions = [] } = props; const [currentAgentTouch] = useAgentStore((s) => [agentSelectors.currentAgentTouch(s)]); const { t } = useTranslation('role'); - const items = currentAgentTouch + const items = get(currentAgentTouch, currentTouchArea) ? (get(currentAgentTouch, currentTouchArea) as TouchAction[]) : []; - console.log('items', items); - const touchArea = areaOptions.find((item) => item.value === currentTouchArea)?.label; return ( diff --git a/src/components/ScreenLoading/index.tsx b/src/components/ScreenLoading/index.tsx new file mode 100644 index 00000000..550308f3 --- /dev/null +++ b/src/components/ScreenLoading/index.tsx @@ -0,0 +1,39 @@ +import classNames from 'classnames'; +import React, { memo, useRef } from 'react'; +import { Center, Flexbox } from 'react-layout-kit'; + +import { useStyles } from './style'; + +interface ScreenLoadingProps { + className?: string; + description?: React.ReactNode; + style?: React.CSSProperties; + title: React.ReactNode; +} + +const ScreenLoading = (props: ScreenLoadingProps) => { + const { title, className, style, description } = props; + const { styles } = useStyles(); + const loadingScreenRef = useRef(null); + + return ( + +
+
+
+
+
+ {title} +
+ {description &&
{description}
} +
+
+ ); +}; + +export default memo(ScreenLoading); diff --git a/src/components/ScreenLoading/style.ts b/src/components/ScreenLoading/style.ts new file mode 100644 index 00000000..e8095f44 --- /dev/null +++ b/src/components/ScreenLoading/style.ts @@ -0,0 +1,89 @@ +import { createStyles } from 'antd-style'; + +export const useStyles = createStyles(({ css }) => ({ + container: css` + position: relative; + width: 100%; + height: 100%; + background-color: #000; + + #loading-screen { + width: 150px; + height: 150px; + opacity: 1; + transition: 1s opacity; + } + + #loading-screen.fade-out { + opacity: 0; + } + + #loading-screen.fade-in { + opacity: 1; + } + + #loader { + position: relative; + top: 50%; + left: 50%; + + display: block; + + width: 150px; + height: 150px; + margin: -75px 0 0 -75px; + + border: 3px solid transparent; + border-top-color: #9370db; + border-radius: 50%; + + animation: spin 2s linear infinite; + } + + #loader::before { + content: ''; + + position: absolute; + inset: 5px; + + border: 3px solid transparent; + border-top-color: #ba55d3; + border-radius: 50%; + + animation: spin 3s linear infinite; + } + + #loader::after { + content: ''; + + position: absolute; + inset: 15px; + + border: 3px solid transparent; + border-top-color: #f0f; + border-radius: 50%; + + animation: spin 1.5s linear infinite; + } + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } + } + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } + } + `, +})); diff --git a/src/features/AgentViewer/index.tsx b/src/features/AgentViewer/index.tsx index 4c14b125..8af6768b 100644 --- a/src/features/AgentViewer/index.tsx +++ b/src/features/AgentViewer/index.tsx @@ -1,10 +1,9 @@ import { VRMExpressionPresetName } from '@pixiv/three-vrm'; -import { Progress } from 'antd'; import classNames from 'classnames'; -import React, { memo, useCallback, useRef } from 'react'; +import React, { memo, useCallback, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import PageLoading from '@/components/PageLoading'; +import ScreenLoading from '@/components/ScreenLoading'; import { useLoadModel } from '@/hooks/useLoadModel'; import { MotionPresetName } from '@/libs/emoteController/motionPresetMap'; import { MotionFileType } from '@/libs/emoteController/type'; @@ -12,10 +11,13 @@ import { speakCharacter } from '@/libs/messages/speakCharacter'; import { agentSelectors, useAgentStore } from '@/store/agent'; import { useGlobalStore } from '@/store/global'; import { TouchAreaEnum } from '@/types/touch'; +import { preloadVoice } from '@/utils/voice'; import ToolBar from './ToolBar'; import { useStyles } from './style'; +// 假设我们有这个工具函数 + interface Props { /** * agent id @@ -37,9 +39,13 @@ function AgentViewer(props: Props) { const playingRef = useRef(false); const ref = useRef(null); const viewer = useGlobalStore((s) => s.viewer); - const { t } = useTranslation('chat'); + const { t } = useTranslation('welcome'); - const { downloading, percent, fetchModelUrl } = useLoadModel(); + const { fetchModelUrl, percent: modelPercent } = useLoadModel(); + const [loading, setLoading] = useState(false); + const [loadingStep, setLoadingStep] = useState(0); + const [voiceLoadingProgress, setVoiceLoadingProgress] = useState(0); + const [motionLoadingProgress, setMotionLoadingProgress] = useState(0); const agent = useAgentStore((s) => s.getAgentById(agentId)); const getAgentTouchActionsByIdAndArea = useAgentStore((s) => agentSelectors.getAgentTouchActionsByIdAndArea(s), @@ -76,49 +82,99 @@ function AgentViewer(props: Props) { } }; + const preloadAgentResources = async () => { + setLoading(true); + setLoadingStep(1); + try { + // 加载步骤一: 加载模型 + const modelUrl = await fetchModelUrl(agent!.agentId, agent!.meta.model!); + if (!modelUrl) return; + await viewer.loadVrm(modelUrl); + + // 如果不是交互模式,加载到这里就结束了 + if (!interactive) return; + + setLoadingStep(2); + // 加载步骤二: 预加载动作,目前预加载的动作都是通用的 + if (viewer?.model) { + await viewer.model.preloadAllMotions((loaded, total) => { + setMotionLoadingProgress((loaded / total) * 100); + }); + } + + setLoadingStep(3); + // 加载步骤三:加载语音 + let voiceCount = 0; + let totalVoices = 0; + + // 计算总语音数量 + if (agent?.greeting) { + totalVoices++; + } + const touchAreas = Object.values(TouchAreaEnum); + for (const area of touchAreas) { + const touchActions = getAgentTouchActionsByIdAndArea(agentId, area); + if (touchActions) { + totalVoices += touchActions.length; + } + } + + // 预加载语音,根据角色的语音配置不同,需要每次重新判断预加载 + // 这个是角色招呼语音 + if (agent?.greeting) { + await preloadVoice({ + ...agent.tts, + message: agent.greeting, + }); + voiceCount++; + setVoiceLoadingProgress((voiceCount / totalVoices) * 100); + } + // 这个是角色的触摸动画语音 + for (const area of touchAreas) { + const touchActions = getAgentTouchActionsByIdAndArea(agentId, area); + if (touchActions) { + for (const action of touchActions) { + await preloadVoice({ + ...agent!.tts, + message: action.text, + }); + voiceCount++; + setVoiceLoadingProgress((voiceCount / totalVoices) * 100); + } + } + } + } finally { + setLoading(false); + setLoadingStep(0); + setVoiceLoadingProgress(0); + setMotionLoadingProgress(0); + } + }; + const canvasRef = useCallback( (canvas: HTMLCanvasElement) => { if (canvas) { viewer.setup(canvas, handleTouchArea); - - // 这里根据 agentId 获取 agent 配置. - fetchModelUrl(agent!.agentId, agent!.meta.model!).then(async (modelUrl) => { - if (modelUrl) { - // add loading dom - const agentViewer = document.querySelector('#agent-viewer')!; - const loadingScreen = document.createElement('div'); - loadingScreen.setAttribute('id', 'loading-screen'); - const loader = document.createElement('div'); - loader.setAttribute('id', 'loader'); - loadingScreen.append(loader); - agentViewer.append(loadingScreen); - - // load vrm - await viewer.loadVrm(modelUrl); - // remove loading dom - loadingScreen.classList.add('fade-out'); - loadingScreen.addEventListener('transitionend', (event) => { - (event.target as HTMLDivElement)!.remove(); - }); - - if (interactive) { - // load motion - speakCharacter( - { - expression: VRMExpressionPresetName.Happy, - tts: { - ...agent?.tts, - message: agent?.greeting, - }, - motion: MotionPresetName.FemaleGreeting, + preloadAgentResources().then(() => { + if (interactive) { + playingRef.current = true; + // load motion + speakCharacter( + { + expression: VRMExpressionPresetName.Happy, + tts: { + ...agent?.tts, + message: agent?.greeting, }, - viewer, - () => {}, - () => { - viewer.model?.loadIdleAnimation(); - }, - ); - } + motion: MotionPresetName.FemaleGreeting, + }, + viewer, + () => {}, + () => { + viewer.model?.loadIdleAnimation(); + playingRef.current = false; + }, + ); } }); @@ -185,11 +241,19 @@ function AgentViewer(props: Props) { style={{ height, width, ...style }} > - {downloading ? ( - } + {loading ? ( + ) : null} diff --git a/src/features/AgentViewer/style.ts b/src/features/AgentViewer/style.ts index 883d6b2a..d82f24ef 100644 --- a/src/features/AgentViewer/style.ts +++ b/src/features/AgentViewer/style.ts @@ -1,94 +1,15 @@ import { createStyles } from 'antd-style'; +import { rgba } from 'polished'; export const useStyles = createStyles(({ css, token }) => ({ viewer: css` + cursor: pointer; + position: relative; + width: 100%; height: 100%; min-height: 0; - - #loading-screen { - position: absolute; - top: 0; - left: 0; - - width: 100%; - height: 100%; - - opacity: 1; - background-color: #000; - - transition: 1s opacity; - } - - #loading-screen.fade-out { - opacity: 0; - } - - #loading-screen.fade-in { - opacity: 0; - } - - #loader { - position: relative; - top: 50%; - left: 50%; - - display: block; - - width: 150px; - height: 150px; - margin: -75px 0 0 -75px; - - border: 3px solid transparent; - border-top-color: #9370db; - border-radius: 50%; - animation: spin 2s linear infinite; - } - - #loader::before { - content: ''; - - position: absolute; - inset: 5px; - - border: 3px solid transparent; - border-top-color: #ba55d3; - border-radius: 50%; - animation: spin 3s linear infinite; - } - - #loader::after { - content: ''; - - position: absolute; - inset: 15px; - - border: 3px solid transparent; - border-top-color: #f0f; - border-radius: 50%; - animation: spin 1.5s linear infinite; - } - - @keyframes spin { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } - } - - @keyframes spin { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } - } `, toolbar: css` position: absolute; @@ -100,6 +21,14 @@ export const useStyles = createStyles(({ css, token }) => ({ position: absolute; top: 0; left: 0; + + width: 100%; + max-width: 100%; + height: 100%; + max-height: 100%; + + background-color: ${rgba(token.colorBgContainer, 0.8)}; + backdrop-filter: saturate(180%) blur(2px); `, canvas: css` display: block; diff --git a/src/libs/emoteController/emoteController.ts b/src/libs/emoteController/emoteController.ts index e0866edb..abf5c2bb 100644 --- a/src/libs/emoteController/emoteController.ts +++ b/src/libs/emoteController/emoteController.ts @@ -18,6 +18,15 @@ export class EmoteController { this._motionController = new MotionController(vrm); } + public async preloadMotion(motion: MotionPresetName) { + const { type, url } = this._motionController.getMotionInfo(motion); + await this._motionController.preloadMotionUrl(type, url); + } + + public async preloadMotionUrl(fileType: MotionFileType, url: string) { + await this._motionController.preloadMotionUrl(fileType, url); + } + public playEmotion(preset: VRMExpressionPresetName) { this._expressionController.playEmotion(preset); } diff --git a/src/libs/emoteController/motionController.ts b/src/libs/emoteController/motionController.ts index ea9b7e2c..c07b8952 100644 --- a/src/libs/emoteController/motionController.ts +++ b/src/libs/emoteController/motionController.ts @@ -5,10 +5,19 @@ import { MotionPresetName, motionPresetMap } from './motionPresetMap'; import { MotionFileType } from './type'; export class MotionController { - private _motionManager: MotionManager; // 假设有一个动作管理器 + private motionManager: MotionManager; constructor(vrm: VRM) { - this._motionManager = new MotionManager(vrm); + this.motionManager = new MotionManager(vrm); + } + + public async preloadMotion(motion: MotionPresetName) { + const { type, url } = this.getMotionInfo(motion); + await this.motionManager.preloadMotion(type, url); + } + + public async preloadMotionUrl(fileType: MotionFileType, url: string) { + await this.motionManager.preloadMotion(fileType, url); } /** @@ -16,27 +25,23 @@ export class MotionController { * @param motion */ public playMotion(motion: MotionPresetName) { - this._motionManager.disposeCurrentMotion(); // 停止当前动作 - - // 这里将motion转换为url - const preset = motionPresetMap[motion]; - - if (preset) { - this._motionManager.loadMotionUrl(preset.type, preset.url, true); // 播放新动作 - } + const { type, url } = this.getMotionInfo(motion); + this.motionManager.loadMotionUrl(type, url); } public playMotionUrl(fileType: MotionFileType, url: string, loop: boolean = true) { - this._motionManager.disposeCurrentMotion(); // 停止当前动作 + this.motionManager.loadMotionUrl(fileType, url, loop); + } - this._motionManager.loadMotionUrl(fileType, url, loop); // 播放新动作 + public getMotionInfo(motion: MotionPresetName) { + return motionPresetMap[motion]; } public stopMotion() { - this._motionManager.disposeCurrentMotion(); + this.motionManager.disposeCurrentMotion(); } public update(delta: number) { - this._motionManager.update(delta); + this.motionManager.update(delta); } } diff --git a/src/libs/emoteController/motionManager.ts b/src/libs/emoteController/motionManager.ts index 4b573e01..fb0f85c1 100644 --- a/src/libs/emoteController/motionManager.ts +++ b/src/libs/emoteController/motionManager.ts @@ -13,8 +13,11 @@ export class MotionManager { private vrm: VRM; private mixer: AnimationMixer; private currentAction?: AnimationAction; + private nextAction?: AnimationAction; + private mixDuration: number = 0.5; // 混合持续时间 private currentClip?: AnimationClip; private ikHandler: VRMIKHandler; + private preloadedMotions = new Map(); constructor(vrm: VRM) { this.vrm = vrm; @@ -24,6 +27,15 @@ export class MotionManager { this.currentClip = undefined; } + public async preloadMotion(fileType: MotionFileType, url: string): Promise { + if (!this.preloadedMotions.has(url)) { + const clip = await this.loadMotionClip(fileType, url); + if (clip) { + this.preloadedMotions.set(url, clip); + } + } + } + public async loadMotionUrl( fileType: MotionFileType, url: string, @@ -33,32 +45,54 @@ export class MotionManager { let clip: AnimationClip | undefined; - switch (fileType) { - case 'vmd': - clip = await this.loadVMD(url); - break; - case 'fbx': - clip = await this.loadFBX(url); - break; - case 'vrma': - clip = await this.loadVRMA(url); - break; - default: - throw new Error('不支持的文件格式'); + if (this.preloadedMotions.has(url)) { + clip = this.preloadedMotions.get(url); + } else { + clip = await this.loadMotionClip(fileType, url); } if (!clip) { return; } - const action = this.mixer.clipAction(clip); - if (!loop) action.setLoop(LoopOnce, 1); - action.play(); + const nextAction = this.mixer.clipAction(clip); + if (!loop) nextAction.setLoop(LoopOnce, 1); + + if (this.currentAction) { + // 设置下一个动作 + this.nextAction = nextAction; + // 开始混合 + this.currentAction.crossFadeTo(this.nextAction, this.mixDuration, true); + setTimeout(() => { + this.currentAction?.stop(); + this.currentAction = this.nextAction; + this.nextAction = undefined; + }, this.mixDuration * 1000); + } else { + // 如果是第一个动作,直接播放 + this.currentAction = nextAction; + this.currentAction.play(); + } - this.currentAction = action; this.currentClip = clip; } + private async loadMotionClip( + fileType: MotionFileType, + url: string, + ): Promise { + switch (fileType) { + case 'vmd': + return await this.loadVMD(url); + case 'fbx': + return await this.loadFBX(url); + case 'vrma': + return await this.loadVRMA(url); + default: + throw new Error('不支持的文件格式'); + } + } + private async loadVMD(url: string): Promise { return await loadVMDAnimation(url, this.vrm); } @@ -89,6 +123,7 @@ export class MotionManager { public update(delta: number): void { this.mixer.update(delta); + this.vrm.update(delta); this.ikHandler.update(); } } diff --git a/src/libs/emoteController/motionPresetMap.ts b/src/libs/emoteController/motionPresetMap.ts index c4c952ed..55dffe24 100644 --- a/src/libs/emoteController/motionPresetMap.ts +++ b/src/libs/emoteController/motionPresetMap.ts @@ -4,6 +4,7 @@ export enum MotionPresetName { FemaleHappy = 'femalehappy', //开心 FemaleAngry = 'femaleangry', //生气 FemaleGreeting = 'femalegreeting', //招呼 + Idle = 'idle', //空闲 } export const motionPresetMap: Record< @@ -14,6 +15,11 @@ export const motionPresetMap: Record< name: string; } > = { + idle: { + type: MotionFileType.VRMA, + name: 'Idle', + url: './idle_loop.vrma', + }, femalehappy: { url: 'https://r2.vidol.chat/animations/c9c98a38-b96c-11e4-a802-0aaa78deedf9.fbx', type: MotionFileType.FBX, diff --git a/src/libs/messages/speakCharacter.ts b/src/libs/messages/speakCharacter.ts index 5adcd133..8ae6c1e6 100644 --- a/src/libs/messages/speakCharacter.ts +++ b/src/libs/messages/speakCharacter.ts @@ -1,7 +1,7 @@ import { Viewer } from '@/libs/vrmViewer/viewer'; -import { getMotionBlobUrl } from '@/services/motion'; import { speechApi } from '@/services/tts'; import { Screenplay } from '@/types/touch'; +import { getPreloadedVoice } from '@/utils/voice'; import { wait } from '@/utils/wait'; const createSpeakCharacter = () => { @@ -21,7 +21,9 @@ const createSpeakCharacter = () => { await wait(1000 - (now - lastTime)); } - const [buffer] = await Promise.all([speechApi(screenplay.tts).catch(() => null)]); + const buffer = + (await getPreloadedVoice(screenplay.tts)) || + (await speechApi(screenplay.tts).catch(() => null)); lastTime = Date.now(); return buffer; }); @@ -32,7 +34,6 @@ const createSpeakCharacter = () => { if (!audioBuffer) { return; } - console.log('screenplay', screenplay); return viewer.model?.speak(audioBuffer, screenplay); }); prevSpeakPromise.then(() => { diff --git a/src/libs/vrmViewer/model.ts b/src/libs/vrmViewer/model.ts index 418c6e20..c0436769 100644 --- a/src/libs/vrmViewer/model.ts +++ b/src/libs/vrmViewer/model.ts @@ -7,6 +7,7 @@ import { EmoteController } from '@/libs/emoteController/emoteController'; import { LipSync } from '@/libs/lipSync/lipSync'; import { Screenplay } from '@/types/touch'; +import { MotionPresetName } from '../emoteController/motionPresetMap'; import { MotionFileType } from '../emoteController/type'; /** @@ -62,7 +63,7 @@ export class Model { public async loadIdleAnimation() { this.emoteController?.playEmotion(VRMExpressionPresetName.Neutral); - this.emoteController?.playMotionUrl(MotionFileType.VRMA, './idle_loop.vrma', true); + this.emoteController?.playMotion(MotionPresetName.Idle); } /** @@ -71,8 +72,11 @@ export class Model { * @param screenplay */ public async speak(buffer: ArrayBuffer, screenplay: Screenplay) { + // 播放人物表情 this.emoteController?.playEmotion(screenplay.expression); + // 播放人物动作 if (screenplay.motion) this.emoteController?.playMotion(screenplay.motion); + // 唇形同步 await new Promise((resolve) => { this._lipSync?.playFromArrayBuffer(buffer, () => { resolve(true); @@ -93,9 +97,25 @@ export class Model { const { volume } = this._lipSync.update(); this.emoteController?.lipSync('aa', volume); } - // vrm 先更新 - this.vrm?.update(delta); - // 后更新表情动作 + // 更新表情动作 this.emoteController?.update(delta); } + + public async preloadMotion(motion: MotionPresetName) { + await this.emoteController?.preloadMotion(motion); + } + public async preloadAllMotions(onLoad?: (loaded: number, total: number) => void) { + const motions = Object.values(MotionPresetName); + let loaded = 0; + const total = motions.length; + for (const motion of motions) { + await this.preloadMotion(motion); + loaded++; + onLoad?.(loaded, total); + } + } + + public async preloadMotionUrl(fileType: MotionFileType, url: string) { + await this.emoteController?.preloadMotionUrl(fileType, url); + } } diff --git a/src/libs/vrmViewer/viewer.ts b/src/libs/vrmViewer/viewer.ts index 545e4fb2..3fd037b9 100644 --- a/src/libs/vrmViewer/viewer.ts +++ b/src/libs/vrmViewer/viewer.ts @@ -27,7 +27,6 @@ export class Viewer { private _mouse: THREE.Vector2; private _canvas?: HTMLCanvasElement; private _boundHandleClick: (event: MouseEvent) => void; - private _boundHandleMouseMove: (event: MouseEvent) => void; private _onBodyTouch?: (area: TouchAreaEnum) => void; constructor() { @@ -60,8 +59,6 @@ export class Viewer { // 在构造函数中绑定 handleClick 方法 this._boundHandleClick = this.handleClick.bind(this); - // 在构造函数中绑定 handleMouseMove 方法 - this._boundHandleMouseMove = this.handleMouseMove.bind(this); } /** @@ -131,7 +128,6 @@ export class Viewer { // 重新设置事件监听器 if (this._canvas) { this._canvas.addEventListener('click', this._boundHandleClick, false); - this._canvas.addEventListener('mousemove', this._boundHandleMouseMove, false); } } @@ -184,7 +180,6 @@ export class Viewer { // 使用存储的绑定函数添加事件监听器 this._canvas.addEventListener('click', this._boundHandleClick, false); - this._canvas.addEventListener('mousemove', this._boundHandleMouseMove, false); this.isReady = true; this.update(); @@ -194,7 +189,6 @@ export class Viewer { // 使用存储的绑定函数移除事件监听器 if (this._canvas) { this._canvas.removeEventListener('click', this._boundHandleClick, false); - this._canvas.removeEventListener('mousemove', this._boundHandleMouseMove, false); } // 卸载模型 diff --git a/src/locales/default/chat.ts b/src/locales/default/chat.ts index 93b636f0..698f80f9 100644 --- a/src/locales/default/chat.ts +++ b/src/locales/default/chat.ts @@ -73,7 +73,7 @@ export default { screenShot: '拍照', grid: '网格', axes: '坐标轴', - downloading: '模型下载中,请稍后...', + downloading: '正在为你准备我的整个世界...', }, animation: { animationList: '动作列表', diff --git a/src/locales/default/welcome.ts b/src/locales/default/welcome.ts index dcae5e1a..3a87f0d3 100644 --- a/src/locales/default/welcome.ts +++ b/src/locales/default/welcome.ts @@ -1,6 +1,6 @@ export default { loadingTitle: '应用初始化中,请稍后...', - waiting: '正在为你准备我的整个世界', + agent: { hello: '你好呀', meta: { @@ -8,5 +8,11 @@ export default { description: '这是一个自定义角色', }, }, + loading: { + waiting: '正在为你准备我的整个世界...', + motions: '预加载动作文件...', + model: '加载模型中...', + voices: '生成语音文件中...', + }, greet: '你好,我是{{name}},有什么可以帮助你的吗?', }; diff --git a/src/store/agent/index.ts b/src/store/agent/index.ts index ff790aff..c6ee5f46 100644 --- a/src/store/agent/index.ts +++ b/src/store/agent/index.ts @@ -145,8 +145,8 @@ const createAgentStore: StateCreator systemRole: '', greeting: t('agent.hello', { ns: 'welcome' }), meta: { - name: t('agent.meta.name'), - description: t('agent.meta.description'), + name: t('agent.meta.name', { ns: 'welcome' }), + description: t('agent.meta.description', { ns: 'welcome' }), avatar: DEFAULT_AGENT_AVATAR_URL, cover: '', category: CategoryEnum.ANIME, diff --git a/src/utils/voice.ts b/src/utils/voice.ts new file mode 100644 index 00000000..ac21b2a8 --- /dev/null +++ b/src/utils/voice.ts @@ -0,0 +1,29 @@ +import { speechApi } from '@/services/tts'; +import storage from '@/utils/storage'; + +const VOICE_CACHE_PREFIX = 'voice_cache_'; + +export const preloadVoice = async (ttsParams: any) => { + const cacheKey = VOICE_CACHE_PREFIX + JSON.stringify(ttsParams); + let cachedVoice = await storage.getItem(cacheKey); + + if (!cachedVoice) { + const voiceBuffer = await speechApi(ttsParams); + const blob = new Blob([voiceBuffer], { type: 'audio/mp3' }); + await storage.setItem(cacheKey, blob); + cachedVoice = blob; + } + + return cachedVoice; +}; + +export const getPreloadedVoice = async (ttsParams: any): Promise => { + const cacheKey = VOICE_CACHE_PREFIX + JSON.stringify(ttsParams); + const cachedBlob = await storage.getItem(cacheKey); + + if (cachedBlob) { + return await cachedBlob.arrayBuffer(); + } + + return null; +};