mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-26 17:13:54 +08:00
Merge branch 'main' into update-tts-voice-data
This commit is contained in:
@@ -130,7 +130,7 @@ export default {
|
||||
'chatHistory.loading': 'Loading...',
|
||||
'chatHistory.noMoreData': 'No more data',
|
||||
'chatHistory.noChatRecords': 'No chat records',
|
||||
'chatRecordsDescription': 'Conversation records with agents will be displayed here',
|
||||
'chatHistory.chatRecordsDescription': 'Conversation records with agents will be displayed here',
|
||||
// Chat History Detail Page
|
||||
'chatHistory.pageTitle': 'Chat Detail',
|
||||
'chatHistory.assistantName': 'Intelligent Assistant',
|
||||
|
||||
@@ -8,6 +8,7 @@ import en from './en'
|
||||
import zh_TW from './zh_TW'
|
||||
import de from './de'
|
||||
import vi from './vi'
|
||||
import pt_BR from './pt_BR'
|
||||
|
||||
// 语言包映射
|
||||
const messages = {
|
||||
@@ -16,6 +17,7 @@ const messages = {
|
||||
zh_TW: zh_TW,
|
||||
de,
|
||||
vi,
|
||||
pt_BR: pt_BR,
|
||||
}
|
||||
|
||||
// 当前使用的语言
|
||||
@@ -72,5 +74,6 @@ export function getSupportedLanguages(): { code: Language, name: string }[] {
|
||||
{ code: 'zh_TW', name: '繁體中文' },
|
||||
{ code: 'de', name: 'Deutsch' },
|
||||
{ code: 'vi', name: 'Tiếng Việt' },
|
||||
{ code: 'pt_BR', name: 'Português (Brasil)' },
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
// Pacote de idioma Português Brasileiro
|
||||
export default {
|
||||
// TabBar
|
||||
'tabBar.home': 'Início',
|
||||
'tabBar.deviceConfig': 'Config. de Rede',
|
||||
'tabBar.settings': 'Sistema',
|
||||
// Settings page title
|
||||
'settings.title': 'Configurações',
|
||||
// Login page
|
||||
'login.pageTitle': 'Entrar',
|
||||
'login.navigationTitle': 'Entrar',
|
||||
'login.fetchConfigError': 'Falha ao buscar configuração:',
|
||||
'login.selectLanguage': 'Selecionar Idioma',
|
||||
'login.selectLanguageTip': '中文',
|
||||
'login.welcomeBack': 'Bem-vindo de Volta',
|
||||
'login.pleaseLogin': 'Por favor, entre na sua conta',
|
||||
'login.enterUsername': 'Por favor, insira o nome de usuário',
|
||||
'login.enterPassword': 'Por favor, insira a senha',
|
||||
'login.enterCaptcha': 'Por favor, insira o código de verificação',
|
||||
'login.loginButton': 'Entrar',
|
||||
'login.loggingIn': 'Entrando...',
|
||||
'login.noAccount': 'Cadastrar',
|
||||
'login.enterPhone': 'Por favor, insira o número de telefone',
|
||||
'login.selectCountry': 'Selecionar País/Região',
|
||||
'login.confirm': 'Confirmar',
|
||||
'login.serverSetting': 'Configurações do Servidor',
|
||||
'login.requiredUsername': 'O nome de usuário não pode estar vazio',
|
||||
'login.requiredPassword': 'A senha não pode estar vazia',
|
||||
'login.requiredCaptcha': 'O código de verificação não pode estar vazio',
|
||||
'login.requiredMobile': 'Por favor, insira um número de telefone válido',
|
||||
'login.captchaError': 'Erro no código de verificação gráfico',
|
||||
'login.forgotPassword': 'Esqueceu a Senha',
|
||||
|
||||
// Register page
|
||||
'register.pageTitle': 'Cadastro',
|
||||
'register.createAccount': 'Criar Conta',
|
||||
'register.enterUsername': 'Por favor, insira o nome de usuário',
|
||||
'register.enterPassword': 'Por favor, insira a senha',
|
||||
'register.confirmPassword': 'Por favor, confirme a senha',
|
||||
'register.enterPhone': 'Por favor, insira o número de telefone',
|
||||
'register.enterCode': 'Por favor, insira o código de verificação',
|
||||
'register.getCode': 'Obter Código',
|
||||
'register.agreeTerms': 'Li e concordo com os',
|
||||
'register.terms': 'Termos de Uso',
|
||||
'register.privacy': 'Política de Privacidade',
|
||||
'register.registerButton': 'Cadastrar',
|
||||
'register.registering': 'Cadastrando...',
|
||||
'register.haveAccount': 'Já tem uma conta?',
|
||||
'register.loginNow': 'Entrar Agora',
|
||||
'register.selectCountry': 'Selecionar País/Região',
|
||||
'register.confirm': 'Confirmar',
|
||||
'register.captchaSendSuccess': 'Código de verificação enviado com sucesso',
|
||||
|
||||
// Home page
|
||||
'home.pageTitle': 'Início',
|
||||
'home.createAgent': 'Criar Agente',
|
||||
'home.agentName': 'Agente',
|
||||
'home.modelInfo': 'Info do Modelo',
|
||||
'home.lastActive': 'Última Atividade',
|
||||
'home.greeting': 'Olá Jarvis',
|
||||
'home.subtitle': 'Vamos ter',
|
||||
'home.wonderfulDay': 'um dia maravilhoso!',
|
||||
'home.emptyState': 'Nenhum agente disponível',
|
||||
'home.deviceManagement': 'Gerenciamento de Dispositivos',
|
||||
'home.lastConversation': 'Última Conversa:',
|
||||
'home.delete': 'Excluir',
|
||||
'home.createFirstAgent': 'Clique no botão + no canto inferior direito para criar seu primeiro agente',
|
||||
'home.dialogTitle': 'Criar Agente',
|
||||
'home.inputPlaceholder': 'ex: Assistente de Atendimento, Assistente de Voz, Perguntas e Respostas',
|
||||
'home.createError': 'Por favor, insira o nome do agente',
|
||||
'home.createNow': 'Criar Agora',
|
||||
'home.justNow': 'Agora mesmo',
|
||||
'home.minutesAgo': 'minutos atrás',
|
||||
'home.hoursAgo': 'horas atrás',
|
||||
'home.daysAgo': 'dias atrás',
|
||||
|
||||
// Agent page
|
||||
'agent.pageTitle': 'Agente',
|
||||
'agent.roleConfig': 'Configuração de Papel',
|
||||
'agent.deviceManagement': 'Gerenciamento de Dispositivos',
|
||||
'agent.chatHistory': 'Histórico de Conversas',
|
||||
'agent.voiceprintManagement': 'Gerenciamento de Impressão Vocal',
|
||||
'agent.editTitle': 'Editar Agente',
|
||||
'agent.toolsTitle': 'Editar Funcionalidades',
|
||||
'agent.voiceActivityDetection': 'Detecção de Atividade de Voz',
|
||||
'agent.speechRecognition': 'Reconhecimento de Fala',
|
||||
'agent.largeLanguageModel': 'Modelo de Linguagem Grande',
|
||||
'agent.save': 'Salvar',
|
||||
'agent.cancel': 'Cancelar',
|
||||
// Agent Edit Page
|
||||
'agent.basicInfo': 'Informações Básicas',
|
||||
'agent.agentName': 'Nome do Agente',
|
||||
'agent.inputAgentName': 'Por favor, insira o nome do agente',
|
||||
'agent.roleMode': 'Modo de Papel',
|
||||
'agent.roleDescription': 'Descrição do Papel',
|
||||
'agent.inputRoleDescription': 'Por favor, insira a descrição do papel',
|
||||
'agent.modelConfig': 'Configuração do Modelo',
|
||||
'agent.vad': 'Detecção de Atividade de Voz',
|
||||
'agent.asr': 'Reconhecimento de Fala',
|
||||
'agent.llm': 'Modelo de Linguagem Grande',
|
||||
'agent.vllm': 'Modelo de Linguagem Visual',
|
||||
'agent.intent': 'Reconhecimento de Intenção',
|
||||
'agent.memory': 'Memória',
|
||||
'agent.voiceSettings': 'Configurações de Voz',
|
||||
'agent.tts': 'Texto para Fala',
|
||||
'agent.voiceprint': 'Voz do Agente',
|
||||
'agent.plugins': 'Plugins',
|
||||
'agent.editFunctions': 'Editar Funções',
|
||||
'agent.historyMemory': 'Memória de Histórico',
|
||||
'agent.memoryContent': 'Conteúdo da memória',
|
||||
'agent.saving': 'Salvando...',
|
||||
'agent.saveSuccess': 'Salvo com sucesso',
|
||||
'agent.saveFail': 'Falha ao salvar',
|
||||
'agent.loadFail': 'Falha ao carregar',
|
||||
'agent.pleaseInputAgentName': 'Por favor, insira o nome do agente',
|
||||
'agent.pleaseInputRoleDescription': 'Por favor, insira a descrição do papel',
|
||||
'agent.pleaseSelect': 'Por favor, selecione',
|
||||
|
||||
// Chat History Page
|
||||
'chatHistory.getChatSessions': 'Obter lista de sessões de conversa',
|
||||
'chatHistory.noSelectedAgent': 'Nenhum agente selecionado',
|
||||
'chatHistory.getChatSessionsFailed': 'Falha ao obter lista de sessões de conversa:',
|
||||
'chatHistory.unknownTime': 'Hora desconhecida',
|
||||
'chatHistory.justNow': 'Agora mesmo',
|
||||
'chatHistory.minutesAgo': '{minutes} minutos atrás',
|
||||
'chatHistory.hoursAgo': '{hours} horas atrás',
|
||||
'chatHistory.daysAgo': '{days} dias atrás',
|
||||
'chatHistory.conversationRecord': 'Registro de Conversas',
|
||||
'chatHistory.totalChats': '{count} conversas no total',
|
||||
'chatHistory.loading': 'Carregando...',
|
||||
'chatHistory.noMoreData': 'Não há mais dados',
|
||||
'chatHistory.noChatRecords': 'Nenhum registro de conversa',
|
||||
'chatHistory.chatRecordsDescription': 'Os registros de conversas com agentes serão exibidos aqui',
|
||||
// Chat History Detail Page
|
||||
'chatHistory.pageTitle': 'Detalhe da Conversa',
|
||||
'chatHistory.assistantName': 'Assistente Inteligente',
|
||||
'chatHistory.userName': 'Usuário',
|
||||
'chatHistory.aiAssistantName': 'Assistente IA',
|
||||
'chatHistory.loadFailed': 'Falha ao carregar histórico de conversas',
|
||||
'chatHistory.parameterError': 'Erro de parâmetro da página',
|
||||
'chatHistory.invalidAudioId': 'ID de áudio inválido',
|
||||
'chatHistory.audioPlayFailed': 'Falha na reprodução de áudio',
|
||||
'chatHistory.playAudioFailed': 'Falha ao reproduzir áudio',
|
||||
|
||||
// Device management page
|
||||
'device.pageTitle': 'Gerenciamento de Dispositivos',
|
||||
'device.noDevices': 'Nenhum dispositivo disponível',
|
||||
'device.macAddress': 'Endereço MAC',
|
||||
'device.firmwareVersion': 'Versão do Firmware',
|
||||
'device.lastConnected': 'Última Conversa',
|
||||
'device.otaUpdate': 'Atualização OTA',
|
||||
'device.unbind': 'Desvincular',
|
||||
'device.confirmUnbind': 'Confirmar',
|
||||
'device.bindDevice': 'Vincular Novo Dispositivo',
|
||||
'device.deviceType': 'Tipo de Dispositivo',
|
||||
'device.loading': 'Carregando...',
|
||||
'device.neverConnected': 'Nunca conectado',
|
||||
'device.justNow': 'Agora mesmo',
|
||||
'device.minutesAgo': '{minutes} minutos atrás',
|
||||
'device.hoursAgo': '{hours} horas atrás',
|
||||
'device.daysAgo': '{days} dias atrás',
|
||||
'device.otaAutoUpdateEnabled': 'Atualização automática OTA ativada',
|
||||
'device.otaAutoUpdateDisabled': 'Atualização automática OTA desativada',
|
||||
'device.operationFailed': 'Operação falhou, por favor tente novamente',
|
||||
'device.deviceUnbound': 'Dispositivo desvinculado',
|
||||
'device.unbindFailed': 'Falha ao desvincular, por favor tente novamente',
|
||||
'device.unbindDevice': 'Desvincular Dispositivo',
|
||||
'device.confirmUnbindDevice': 'Tem certeza que deseja desvincular o dispositivo "{macAddress}"?',
|
||||
'device.cancel': 'Cancelar',
|
||||
'device.noDevice': 'Nenhum Dispositivo',
|
||||
'device.pleaseSelectAgent': 'Por favor, selecione um agente primeiro',
|
||||
'device.deviceBindSuccess': 'Dispositivo vinculado com sucesso!',
|
||||
'device.bindFailed': 'Falha ao vincular, por favor verifique se o código de verificação está correto',
|
||||
'device.enterDeviceCode': 'Por favor, insira o código de verificação do dispositivo',
|
||||
'device.bindNow': 'Vincular Agora',
|
||||
'device.lastConnection': 'Última Conexão',
|
||||
'device.clickToBindFirstDevice': 'Clique no botão + no canto inferior direito para vincular seu primeiro dispositivo',
|
||||
|
||||
// Common
|
||||
'common.success': 'Sucesso',
|
||||
'common.fail': 'Falhou',
|
||||
'common.loading': 'Carregando...',
|
||||
'common.confirm': 'Confirmar',
|
||||
'common.cancel': 'Cancelar',
|
||||
'common.delete': 'Excluir',
|
||||
'common.edit': 'Editar',
|
||||
'common.add': 'Adicionar',
|
||||
'common.pleaseSelect': 'Por favor, selecione',
|
||||
'common.unknownError': 'Erro desconhecido',
|
||||
'common.networkError': 'Erro de rede',
|
||||
|
||||
// Retrieve Password page
|
||||
'retrievePassword.title': 'Redefinir Senha',
|
||||
'retrievePassword.subtitle': 'Recupere a senha da sua conta pelo número de celular',
|
||||
'retrievePassword.mobileRequired': 'Por favor, insira o número de celular',
|
||||
'retrievePassword.inputCorrectMobile': 'Por favor, insira um número de celular válido',
|
||||
'retrievePassword.captchaRequired': 'Por favor, insira o código de verificação gráfico',
|
||||
'retrievePassword.mobileCaptchaRequired': 'Por favor, insira o código de verificação por SMS',
|
||||
'retrievePassword.newPasswordRequired': 'Por favor, insira a nova senha',
|
||||
'retrievePassword.confirmNewPasswordRequired': 'Por favor, confirme a nova senha',
|
||||
'retrievePassword.passwordsNotMatch': 'As senhas não coincidem',
|
||||
'retrievePassword.mobilePlaceholder': 'Por favor, insira o número de celular',
|
||||
'retrievePassword.captchaPlaceholder': 'Por favor, insira o código de verificação gráfico',
|
||||
'retrievePassword.mobileCaptchaPlaceholder': 'Por favor, insira o código de verificação por SMS',
|
||||
'retrievePassword.newPasswordPlaceholder': 'Por favor, insira a nova senha',
|
||||
'retrievePassword.confirmNewPasswordPlaceholder': 'Por favor, confirme a nova senha',
|
||||
'retrievePassword.getMobileCaptcha': 'Obter Código',
|
||||
'retrievePassword.captchaSendSuccess': 'Código de verificação enviado com sucesso',
|
||||
'retrievePassword.passwordUpdateSuccess': 'Senha redefinida com sucesso',
|
||||
'retrievePassword.resetButton': 'Redefinir Senha',
|
||||
'retrievePassword.goToLogin': 'Voltar ao Login',
|
||||
|
||||
// SM2 encryption related error messages
|
||||
'sm2.publicKeyNotConfigured': 'Chave pública SM2 não configurada, por favor entre em contato com o administrador',
|
||||
'sm2.encryptionFailed': 'Falha na criptografia da senha',
|
||||
'sm2.keyGenerationFailed': 'Falha na geração do par de chaves',
|
||||
'sm2.invalidPublicKey': 'Formato de chave pública inválido',
|
||||
'sm2.encryptionError': 'Ocorreu um erro durante a criptografia',
|
||||
'sm2.publicKeyRetry': 'Tentando obter a chave pública novamente...',
|
||||
'sm2.publicKeyRetryFailed': 'Falha na nova tentativa de obter a chave pública',
|
||||
|
||||
// Voiceprint page
|
||||
'voiceprint.noSelectedAgent': 'Nenhum agente selecionado',
|
||||
'voiceprint.pleaseSelectAgent': 'Por favor, selecione um agente primeiro',
|
||||
'voiceprint.fetchHistoryFailed': 'Falha ao buscar histórico de conversas',
|
||||
'voiceprint.clickToSelectVector': 'Clique para selecionar vetor de impressão vocal',
|
||||
'voiceprint.pleaseInputName': 'Por favor, insira o nome',
|
||||
'voiceprint.pleaseSelectVector': 'Por favor, selecione o vetor de impressão vocal',
|
||||
'voiceprint.addSuccess': 'Adicionado com sucesso',
|
||||
'voiceprint.addFailed': 'Falha ao adicionar locutor',
|
||||
'voiceprint.editSuccess': 'Editado com sucesso',
|
||||
'voiceprint.editFailed': 'Falha ao editar locutor',
|
||||
'voiceprint.deleteConfirmMsg': 'Tem certeza que deseja excluir este locutor?',
|
||||
'voiceprint.deleteConfirmTitle': 'Confirmar Exclusão',
|
||||
'voiceprint.deleteSuccess': 'Excluído com sucesso',
|
||||
'voiceprint.loading': 'Carregando...',
|
||||
'voiceprint.delete': 'Excluir',
|
||||
'voiceprint.emptyTitle': 'Nenhum dado de impressão vocal',
|
||||
'voiceprint.emptyDesc': 'Clique no botão + no canto inferior direito para adicionar seu primeiro locutor',
|
||||
'voiceprint.addSpeaker': 'Adicionar Locutor',
|
||||
'voiceprint.voiceVector': 'Vetor de Impressão Vocal',
|
||||
'voiceprint.name': 'Nome',
|
||||
'voiceprint.description': 'Descrição',
|
||||
'voiceprint.pleaseInputDescription': 'Por favor, insira a descrição',
|
||||
'voiceprint.cancel': 'Cancelar',
|
||||
'voiceprint.save': 'Salvar',
|
||||
'voiceprint.editSpeaker': 'Editar Locutor',
|
||||
'voiceprint.selectVector': 'Selecionar Vetor de Impressão Vocal',
|
||||
'voiceprint.voiceprintInterfaceNotConfigured': 'Interface de impressão vocal não configurada',
|
||||
|
||||
// Settings page
|
||||
'settings.pageTitle': 'Configurações',
|
||||
'settings.navigationTitle': 'Configurações',
|
||||
'settings.networkSettings': 'Configurações de Rede',
|
||||
'settings.serverApiUrl': 'URL da API do Servidor',
|
||||
'settings.validServerUrl': 'Por favor, insira um endereço de servidor válido (começando com http ou https e terminando com /xiaozhi)',
|
||||
'settings.saveSettings': 'Salvar Configurações',
|
||||
'settings.resetDefault': 'Restaurar Padrão',
|
||||
'settings.restartApp': 'Reiniciar App',
|
||||
'settings.restartNow': 'Reiniciar Agora',
|
||||
'settings.restartLater': 'Depois',
|
||||
// About us
|
||||
'settings.aboutApp': 'Sobre o Console XiaoZhi',
|
||||
'settings.aboutContent': 'Console XiaoZhi\n\nUm aplicativo móvel de gerenciamento multiplataforma construído com Vue.js 3 + uni-app, oferecendo gerenciamento de dispositivos, configuração de agentes e outras funcionalidades para o hardware inteligente xiaozhi ESP32.\n\n© 2025 xiaozhi-esp32-server {version}',
|
||||
'settings.restartSuccess': 'Salvo, você pode reiniciar o app manualmente depois',
|
||||
'settings.serverUrlSavedAndCacheCleared': 'URL do servidor salva e cache limpo',
|
||||
'settings.resetToDefaultAndCacheCleared': 'Restaurado ao padrão e cache limpo',
|
||||
'settings.resetSuccess': 'Restauração bem-sucedida',
|
||||
'settings.enterServerUrl': 'Por favor, insira a URL do servidor',
|
||||
'settings.clearCacheFailed': 'Falha ao limpar o cache',
|
||||
'settings.cacheManagement': 'Gerenciamento de Cache',
|
||||
'settings.totalCacheSize': 'Tamanho Total do Cache',
|
||||
'settings.appDataSize': 'Tamanho Total dos Dados do App',
|
||||
'settings.cacheClear': 'Limpar Cache',
|
||||
'settings.clearAllCache': 'Limpar todos os dados de cache',
|
||||
'settings.clearCache': 'Limpar Cache',
|
||||
'settings.modifyWillClearCache': 'Modificações irão limpar o cache',
|
||||
'settings.appInfo': 'Informações do App',
|
||||
'settings.aboutUs': 'Sobre Nós',
|
||||
'settings.appVersion': 'Versão do App e Info da Equipe',
|
||||
'settings.confirmClear': 'Confirmar Limpeza',
|
||||
'settings.confirmClearMessage': 'Tem certeza que deseja limpar todo o cache? Isso excluirá todos os dados, incluindo o status de login, e exigirá novo login.',
|
||||
'settings.cacheCleared': 'Cache limpo com sucesso, redirecionando para a página de login',
|
||||
'settings.languageSettings': 'Configurações de Idioma',
|
||||
'settings.language': 'Idioma',
|
||||
'settings.selectLanguage': 'Selecionar Idioma',
|
||||
'settings.languageChanged': 'Idioma alterado com sucesso',
|
||||
|
||||
// Messages
|
||||
'message.loginSuccess': 'Login realizado com sucesso!',
|
||||
'message.loginFail': 'Falha no login',
|
||||
'message.registerSuccess': 'Cadastro realizado com sucesso',
|
||||
'message.registerFail': 'Falha no cadastro',
|
||||
'message.saveSuccess': 'Salvo com sucesso',
|
||||
'message.saveFail': 'Falha ao salvar',
|
||||
'message.deleteSuccess': 'Excluído com sucesso',
|
||||
'message.deleteFail': 'Falha ao excluir',
|
||||
'message.bindSuccess': 'Vinculação realizada com sucesso',
|
||||
'message.bindFail': 'Falha na vinculação',
|
||||
'message.unbindSuccess': 'Desvinculação realizada com sucesso',
|
||||
'message.unbindFail': 'Falha na desvinculação',
|
||||
'message.networkError': 'Erro de rede, por favor verifique sua conexão',
|
||||
'message.serverError': 'Erro no servidor, por favor tente novamente mais tarde',
|
||||
'message.invalidAddress': 'Endereço inválido, por favor verifique se o servidor está iniciado ou se a conexão de rede está normal',
|
||||
'message.languageChanged': 'Idioma alterado',
|
||||
'message.passwordError': 'Conta ou senha incorretos',
|
||||
'message.phoneRegistered': 'Este número de telefone já está cadastrado',
|
||||
|
||||
// Agent Tools Page
|
||||
'agent.tools.pageTitle': 'Ferramentas do Agente',
|
||||
'agent.tools.unselected': 'Não selecionado',
|
||||
'agent.tools.selected': 'Selecionado',
|
||||
'agent.tools.noMorePlugins': 'Não há mais plugins',
|
||||
'agent.tools.pleaseSelectPlugin': 'Por favor, selecione a função do plugin',
|
||||
'agent.tools.builtInPlugins': 'Plugins Integrados',
|
||||
'agent.tools.mcpAccessPoint': 'Ponto de Acesso MCP',
|
||||
'agent.tools.copy': 'Copiar',
|
||||
'agent.tools.noTools': 'Nenhuma ferramenta disponível',
|
||||
'agent.tools.parameterConfig': 'Configuração de Parâmetros',
|
||||
'agent.tools.noParamsNeeded': 'Nenhum parâmetro necessário',
|
||||
'agent.tools.pleaseInput': 'Por favor, insira',
|
||||
'agent.tools.inputOneItemPerLine': 'Insira um item por linha',
|
||||
'agent.tools.pleaseInputValidJson': 'Por favor, insira um formato JSON válido',
|
||||
'agent.tools.enableFunction': 'Ativar Função',
|
||||
'agent.tools.toggleFunction': 'Ativar ou desativar esta função',
|
||||
'agent.tools.jsonFormatError': 'Erro no formato JSON',
|
||||
'agent.tools.noMcpAddressToCopy': 'Nenhum endereço MCP para copiar',
|
||||
'agent.tools.mcpAddressCopied': 'Endereço MCP copiado para a área de transferência',
|
||||
'agent.tools.copyFailed': 'Falha ao copiar, por favor tente novamente',
|
||||
'agent.tools.defaultValue': 'Valor padrão',
|
||||
'agent.tools.notSelected': 'Não selecionado',
|
||||
'agent.tools.clickToConfigure': 'Clique para configurar',
|
||||
'agent.tools.mcpEndpoint': 'Endpoint MCP',
|
||||
'agent.tools.eachLineOneItem': 'Insira um item por linha',
|
||||
|
||||
// Device Config page
|
||||
'deviceConfig.pageTitle': 'Configuração do Dispositivo',
|
||||
'deviceConfig.wifiConfig': 'Configuração WiFi',
|
||||
'deviceConfig.ultrasonicConfig': 'Configuração Ultrassônica',
|
||||
'deviceConfig.selectConfigMethod': 'Selecionar Método de Configuração',
|
||||
'deviceConfig.networkConfig': 'Configuração de Rede',
|
||||
'deviceConfig.selectedNetwork': 'Rede Selecionada',
|
||||
'deviceConfig.signal': 'Sinal',
|
||||
'deviceConfig.openNetwork': 'Rede Aberta',
|
||||
'deviceConfig.encryptedNetwork': 'Rede Criptografada',
|
||||
'deviceConfig.password': 'Senha',
|
||||
'deviceConfig.pleaseEnterPassword': 'Por favor, insira a senha do WiFi',
|
||||
'deviceConfig.startConfig': 'Iniciar Configuração',
|
||||
'deviceConfig.connectToXiaozhiHotspot': 'Por favor, conecte-se ao hotspot xiaozhi primeiro',
|
||||
'deviceConfig.detecting': 'Detectando...',
|
||||
'deviceConfig.reDetect': 'Detectar Novamente',
|
||||
'deviceConfig.alreadyConnected': 'Conectado ao hotspot xiaozhi',
|
||||
'deviceConfig.refreshStatus': 'Atualizar Status',
|
||||
'deviceConfig.wifiNetworks': 'Redes WiFi',
|
||||
'deviceConfig.selectWifiNetwork': 'Selecionar Rede WiFi',
|
||||
'deviceConfig.refreshScan': 'Atualizar Busca',
|
||||
'deviceConfig.noWifiNetworks': 'Nenhuma rede WiFi disponível',
|
||||
'deviceConfig.clickToRefreshScan': 'Por favor, clique em Atualizar Busca',
|
||||
'deviceConfig.signalStrong': 'Sinal Forte',
|
||||
'deviceConfig.signalGood': 'Sinal Bom',
|
||||
'deviceConfig.signalFair': 'Sinal Regular',
|
||||
'deviceConfig.signalWeak': 'Sinal Fraco',
|
||||
'deviceConfig.channel': 'Canal',
|
||||
'deviceConfig.about': 'aproximadamente',
|
||||
'deviceConfig.seconds': 'segundos',
|
||||
'deviceConfig.generating': 'Gerando...',
|
||||
'deviceConfig.playing': 'Reproduzindo...',
|
||||
'deviceConfig.generateAndPlaySoundWave': 'Gerar e Reproduzir Onda Sonora',
|
||||
'deviceConfig.playSoundWave': 'Reproduzir Onda Sonora',
|
||||
'deviceConfig.stopPlaying': 'Parar Reprodução',
|
||||
'deviceConfig.autoLoopPlaySoundWave': 'Reproduzir Onda Sonora em Loop Automático',
|
||||
'deviceConfig.configAudioFile': 'Arquivo de Áudio de Configuração',
|
||||
'deviceConfig.duration': 'Duração',
|
||||
'deviceConfig.ultrasonicConfigInstructions': 'Instruções de Configuração Ultrassônica',
|
||||
'deviceConfig.ensureWifiNetworkSelectedAndPasswordEntered': 'Certifique-se de que a rede WiFi foi selecionada e a senha foi inserida',
|
||||
'deviceConfig.clickGenerateAndPlaySoundWave': 'Clique em Gerar e Reproduzir Onda Sonora, o sistema codificará as informações de configuração em áudio',
|
||||
'deviceConfig.bringPhoneCloseToXiaozhiDevice': 'Aproxime o celular do dispositivo xiaozhi (distância de 1-2 metros)',
|
||||
'deviceConfig.duringAudioPlaybackXiaozhiWillReceive': 'Durante a reprodução do áudio, o xiaozhi receberá e decodificará as informações de configuração',
|
||||
'deviceConfig.afterConfigSuccessDeviceWillConnect': 'Após a configuração bem-sucedida, o dispositivo se conectará automaticamente à rede WiFi',
|
||||
'deviceConfig.usesAfskModulation': 'Utiliza tecnologia de modulação AFSK, transmitindo dados pelas frequências de 1800Hz e 1500Hz',
|
||||
'deviceConfig.ensureModeratePhoneVolume': 'Por favor, certifique-se de que o volume do celular está moderado para evitar interferência de ruídos ambientais',
|
||||
'deviceConfig.generatingUltrasonicConfigAudio': 'Gerando áudio de configuração ultrassônica',
|
||||
'deviceConfig.configData': 'Dados de configuração',
|
||||
'deviceConfig.dataBytesLength': 'Comprimento dos bytes de dados',
|
||||
'deviceConfig.bitStreamLength': 'Comprimento do fluxo de bits',
|
||||
'deviceConfig.base64Length': 'Comprimento Base64',
|
||||
'deviceConfig.audioFileTooLarge': 'Arquivo de áudio muito grande, por favor reduza o tamanho do SSID ou da senha',
|
||||
'deviceConfig.audioGenerationSuccess': 'Áudio gerado com sucesso',
|
||||
'deviceConfig.samplePoints': 'Pontos de amostra',
|
||||
'deviceConfig.soundWaveGenerationSuccess': 'Onda sonora gerada com sucesso',
|
||||
'deviceConfig.audioGenerationFailed': 'Falha na geração do áudio',
|
||||
'deviceConfig.soundWaveGenerationFailed': 'Falha na geração da onda sonora',
|
||||
'deviceConfig.pleaseGenerateAudioFirst': 'Por favor, gere o áudio primeiro',
|
||||
'deviceConfig.startPlayingUltrasonicConfigAudio': 'Iniciando reprodução do áudio de configuração ultrassônica',
|
||||
'deviceConfig.ultrasonicAudioStartedPlaying': 'Reprodução do áudio ultrassônico iniciada',
|
||||
'deviceConfig.startPlayingConfigSoundWave': 'Reprodução da onda sonora de configuração iniciada',
|
||||
'deviceConfig.ultrasonicAudioPlaybackEnded': 'Reprodução do áudio ultrassônico finalizada',
|
||||
'deviceConfig.audioPlaybackFailed': 'Falha na reprodução do áudio',
|
||||
'deviceConfig.audioResourceBusy': 'Recurso de áudio ocupado, por favor tente novamente mais tarde',
|
||||
'deviceConfig.audioFormatNotSupported': 'Formato de áudio não suportado, pode ser um problema de URI de dados',
|
||||
'deviceConfig.audioFileError': 'Erro no arquivo de áudio',
|
||||
'deviceConfig.cleaningUpAudioContext': 'Limpando contexto de áudio',
|
||||
'deviceConfig.cleaningUpAudioContextFailed': 'Falha ao limpar contexto de áudio',
|
||||
'deviceConfig.stoppedPlayingUltrasonicAudio': 'Reprodução do áudio ultrassônico interrompida',
|
||||
'deviceConfig.stoppedPlaying': 'Reprodução interrompida',
|
||||
'deviceConfig.configMethod': 'Método de Configuração',
|
||||
'deviceConfig.enterWifiPassword': 'Por favor, insira a senha do WiFi',
|
||||
'deviceConfig.xiaozhi': 'xiaozhi',
|
||||
'deviceConfig.connectXiaozhiHotspot': 'Por favor, conecte-se ao hotspot xiaozhi',
|
||||
'deviceConfig.wifiScanResponse': 'Resposta da busca WiFi',
|
||||
'deviceConfig.scanSuccess': 'Busca bem-sucedida',
|
||||
'deviceConfig.networks': 'redes',
|
||||
'deviceConfig.wifiScanFailed': 'Falha na busca WiFi',
|
||||
'deviceConfig.scanFailedCheckConnection': 'Falha na busca, por favor verifique a conexão',
|
||||
'deviceConfig.checking': 'Verificando',
|
||||
'deviceConfig.reCheck': 'Verificar Novamente',
|
||||
'deviceConfig.connectedXiaozhiHotspot': 'Conectado ao hotspot xiaozhi',
|
||||
'deviceConfig.wifiNetwork': 'Rede WiFi',
|
||||
'deviceConfig.scanning': 'Buscando',
|
||||
'deviceConfig.cancel': 'Cancelar',
|
||||
'deviceConfig.clickRefreshScan': 'Por favor, clique em Atualizar Busca',
|
||||
'deviceConfig.esp32ConnectionCheckFailed': 'Falha na verificação de conexão ESP32',
|
||||
'deviceConfig.startWifiConfig': 'Iniciando configuração WiFi',
|
||||
'deviceConfig.configSuccess': 'Configuração bem-sucedida',
|
||||
'deviceConfig.deviceWillConnectTo': 'O dispositivo se conectará a',
|
||||
'deviceConfig.deviceWillRestart': 'O dispositivo será reiniciado',
|
||||
'deviceConfig.pleaseDisconnectXiaozhiHotspot': 'Por favor, desconecte-se do hotspot xiaozhi',
|
||||
'deviceConfig.configFailed': 'Falha na configuração',
|
||||
'deviceConfig.wifiConfigFailed': 'Falha na configuração WiFi',
|
||||
'deviceConfig.pleaseCheckNetworkConnection': 'Por favor, verifique a conexão de rede',
|
||||
'deviceConfig.startWifiConfigButton': 'Iniciar Configuração',
|
||||
'deviceConfig.wifiConfigInstructions': 'Instruções de Configuração WiFi',
|
||||
'deviceConfig.phoneConnectXiaozhiHotspot': 'Conecte o celular ao hotspot xiaozhi',
|
||||
'deviceConfig.selectTargetWifiNetwork': 'Selecione a rede WiFi de destino',
|
||||
'deviceConfig.enterWifiPasswordIfNeeded': 'Insira a senha do WiFi se necessário',
|
||||
'deviceConfig.clickStartConfigAndWait': 'Clique em Iniciar Configuração e aguarde',
|
||||
'deviceConfig.afterConfigSuccessDeviceWillRestart': 'Após a configuração bem-sucedida, o dispositivo reiniciará automaticamente',
|
||||
'deviceConfig.audioPlaybackError': 'Erro na reprodução de áudio',
|
||||
'deviceConfig.playbackFailed': 'Falha na reprodução',
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
// 支持的语言类型
|
||||
export type Language = 'zh_CN' | 'en' | 'zh_TW' | 'de' | 'vi'
|
||||
export type Language = 'zh_CN' | 'en' | 'zh_TW' | 'de' | 'vi' | 'pt_BR'
|
||||
|
||||
export interface LangStore {
|
||||
currentLang: Language
|
||||
|
||||
@@ -254,6 +254,8 @@ export default {
|
||||
return this.$t("language.de");
|
||||
case "vi":
|
||||
return this.$t("language.vi");
|
||||
case "pt_BR":
|
||||
return this.$t("language.ptBR");
|
||||
default:
|
||||
return this.$t("language.zhCN");
|
||||
}
|
||||
@@ -272,6 +274,8 @@ export default {
|
||||
return require("@/assets/xiaozhi-ai_de.png");
|
||||
case "vi":
|
||||
return require("@/assets/xiaozhi-ai_vi.png");
|
||||
case "pt_BR":
|
||||
return require("@/assets/xiaozhi-ai_en.png");
|
||||
default:
|
||||
return require("@/assets/xiaozhi-ai.png");
|
||||
}
|
||||
@@ -303,6 +307,10 @@ export default {
|
||||
label: this.$t("language.vi"),
|
||||
value: "vi",
|
||||
},
|
||||
{
|
||||
label: this.$t("language.ptBR"),
|
||||
value: "pt_BR",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -663,6 +663,7 @@ export default {
|
||||
'language.en': 'English',
|
||||
'language.de': 'Deutsch',
|
||||
'language.vi': 'Tiếng Việt',
|
||||
'language.ptBR': 'Português (Brasil)',
|
||||
|
||||
// Home page text
|
||||
'home.addAgent': 'Agent hinzufügen',
|
||||
|
||||
@@ -663,6 +663,7 @@ export default {
|
||||
'language.en': 'English',
|
||||
'language.de': 'Deutsch',
|
||||
'language.vi': 'Tiếng Việt',
|
||||
'language.ptBR': 'Português (Brasil)',
|
||||
|
||||
// Home page text
|
||||
'home.addAgent': 'Add Agent',
|
||||
|
||||
@@ -5,6 +5,7 @@ import zhTW from './zh_TW';
|
||||
import en from './en';
|
||||
import de from './de';
|
||||
import vi from './vi';
|
||||
import ptBR from './pt_BR';
|
||||
|
||||
Vue.use(VueI18n);
|
||||
|
||||
@@ -27,6 +28,9 @@ const getDefaultLanguage = () => {
|
||||
if (browserLang.indexOf('vi') === 0) {
|
||||
return 'vi';
|
||||
}
|
||||
if (browserLang === 'pt-BR' || browserLang === 'pt') {
|
||||
return 'pt_BR';
|
||||
}
|
||||
return 'en';
|
||||
};
|
||||
|
||||
@@ -38,7 +42,8 @@ const i18n = new VueI18n({
|
||||
'zh_TW': zhTW,
|
||||
'en': en,
|
||||
'de': de,
|
||||
'vi': vi
|
||||
'vi': vi,
|
||||
'pt_BR': ptBR
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -663,6 +663,7 @@ export default {
|
||||
'language.en': 'English',
|
||||
'language.de': 'Deutsch',
|
||||
'language.vi': 'Tiếng Việt',
|
||||
'language.ptBR': 'Português (Brasil)',
|
||||
|
||||
// Home page text
|
||||
'home.addAgent': 'Thêm tác nhân',
|
||||
|
||||
@@ -663,6 +663,7 @@ export default {
|
||||
'language.en': 'English',
|
||||
'language.de': 'Deutsch',
|
||||
'language.vi': 'Tiếng Việt',
|
||||
'language.ptBR': 'Português (Brasil)',
|
||||
|
||||
// 首页文本
|
||||
'home.addAgent': '添加智能体',
|
||||
|
||||
@@ -663,6 +663,7 @@ export default {
|
||||
'language.en': 'English',
|
||||
'language.de': 'Deutsch',
|
||||
'language.vi': 'Tiếng Việt',
|
||||
'language.ptBR': 'Português (Brasil)',
|
||||
|
||||
// 首頁文本
|
||||
'home.addAgent': '添加智能體',
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
<el-dropdown-item @click.native="changeLanguage('vi')">
|
||||
{{ $t("language.vi") }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="changeLanguage('pt_BR')">
|
||||
{{ $t("language.ptBR") }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
@@ -188,6 +191,8 @@ export default {
|
||||
return this.$t("language.de");
|
||||
case "vi":
|
||||
return this.$t("language.vi");
|
||||
case "pt_BR":
|
||||
return this.$t("language.ptBR");
|
||||
default:
|
||||
return this.$t("language.zhCN");
|
||||
}
|
||||
|
||||
@@ -137,6 +137,8 @@ export default {
|
||||
return require("@/assets/xiaozhi-ai_de.png");
|
||||
case "vi":
|
||||
return require("@/assets/xiaozhi-ai_vi.png");
|
||||
case "pt_BR":
|
||||
return require("@/assets/xiaozhi-ai_en.png");
|
||||
default:
|
||||
return require("@/assets/xiaozhi-ai.png");
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(tree:*)",
|
||||
"Bash(find:*)"
|
||||
"Bash(find:*)",
|
||||
"Bash(python3:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -1118,6 +1118,14 @@ class ConnectionHandler:
|
||||
async def close(self, ws=None):
|
||||
"""资源清理方法"""
|
||||
try:
|
||||
# 清理 VAD 连接资源
|
||||
if (
|
||||
hasattr(self, "vad")
|
||||
and self.vad
|
||||
and hasattr(self.vad, "release_conn_resources")
|
||||
):
|
||||
self.vad.release_conn_resources(self)
|
||||
|
||||
# 清理音频缓冲区
|
||||
if hasattr(self, "audio_buffer"):
|
||||
self.audio_buffer.clear()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import time
|
||||
import os
|
||||
import numpy as np
|
||||
import torch
|
||||
import opuslib_next
|
||||
import onnxruntime
|
||||
from config.logger import setup_logging
|
||||
from core.providers.vad.base import VADProviderBase
|
||||
|
||||
@@ -12,16 +13,17 @@ logger = setup_logging()
|
||||
class VADProvider(VADProviderBase):
|
||||
def __init__(self, config):
|
||||
logger.bind(tag=TAG).info("SileroVAD", config)
|
||||
self.model, _ = torch.hub.load(
|
||||
repo_or_dir=config["model_dir"],
|
||||
source="local",
|
||||
model="silero_vad",
|
||||
force_reload=False,
|
||||
|
||||
model_path = os.path.join(
|
||||
config["model_dir"], "src", "silero_vad", "data", "silero_vad.onnx"
|
||||
)
|
||||
opts = onnxruntime.SessionOptions()
|
||||
opts.inter_op_num_threads = 1
|
||||
opts.intra_op_num_threads = 1
|
||||
self.session = onnxruntime.InferenceSession(
|
||||
model_path, providers=["CPUExecutionProvider"], sess_options=opts
|
||||
)
|
||||
|
||||
self.decoder = opuslib_next.Decoder(16000, 1)
|
||||
|
||||
# 处理空字符串的情况
|
||||
threshold = config.get("threshold", "0.5")
|
||||
threshold_low = config.get("threshold_low", "0.2")
|
||||
min_silence_duration_ms = config.get("min_silence_duration_ms", "1000")
|
||||
@@ -33,40 +35,58 @@ class VADProvider(VADProviderBase):
|
||||
int(min_silence_duration_ms) if min_silence_duration_ms else 1000
|
||||
)
|
||||
|
||||
# 至少要多少帧才算有语音
|
||||
self.frame_window_threshold = 3
|
||||
|
||||
def __del__(self):
|
||||
if hasattr(self, 'decoder') and self.decoder is not None:
|
||||
try:
|
||||
del self.decoder
|
||||
except Exception:
|
||||
pass
|
||||
def _init_connection_state(self, conn):
|
||||
"""为连接初始化独立的 VAD 状态"""
|
||||
if not hasattr(conn, "_vad_opus_decoder"):
|
||||
conn._vad_opus_decoder = opuslib_next.Decoder(16000, 1)
|
||||
if not hasattr(conn, "_vad_state"):
|
||||
conn._vad_state = np.zeros((2, 1, 128), dtype=np.float32)
|
||||
if not hasattr(conn, "_vad_context"):
|
||||
conn._vad_context = np.zeros((1, 64), dtype=np.float32)
|
||||
|
||||
def release_conn_resources(self, conn):
|
||||
"""释放连接的 VAD 资源(连接关闭时调用)"""
|
||||
for attr in ("_vad_opus_decoder", "_vad_state", "_vad_context"):
|
||||
if hasattr(conn, attr):
|
||||
try:
|
||||
delattr(conn, attr)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def is_vad(self, conn, opus_packet):
|
||||
# 手动模式:直接返回True,不进行实时VAD检测,所有音频都缓存
|
||||
if conn.client_listen_mode == "manual":
|
||||
return True
|
||||
|
||||
try:
|
||||
pcm_frame = self.decoder.decode(opus_packet, 960)
|
||||
conn.client_audio_buffer.extend(pcm_frame) # 将新数据加入缓冲区
|
||||
|
||||
# 处理缓冲区中的完整帧(每次处理512采样点)
|
||||
try:
|
||||
self._init_connection_state(conn)
|
||||
|
||||
pcm_frame = conn._vad_opus_decoder.decode(opus_packet, 960)
|
||||
conn.client_audio_buffer.extend(pcm_frame)
|
||||
|
||||
client_have_voice = False
|
||||
while len(conn.client_audio_buffer) >= 512 * 2:
|
||||
# 提取前512个采样点(1024字节)
|
||||
chunk = conn.client_audio_buffer[: 512 * 2]
|
||||
conn.client_audio_buffer = conn.client_audio_buffer[512 * 2 :]
|
||||
|
||||
# 转换为模型需要的张量格式
|
||||
audio_int16 = np.frombuffer(chunk, dtype=np.int16)
|
||||
audio_float32 = audio_int16.astype(np.float32) / 32768.0
|
||||
audio_tensor = torch.from_numpy(audio_float32)
|
||||
audio_input = np.concatenate(
|
||||
[conn._vad_context, audio_float32.reshape(1, -1)], axis=1
|
||||
).astype(np.float32)
|
||||
|
||||
# 检测语音活动
|
||||
with torch.no_grad():
|
||||
speech_prob = self.model(audio_tensor, 16000).item()
|
||||
ort_inputs = {
|
||||
"input": audio_input,
|
||||
"state": conn._vad_state,
|
||||
"sr": np.array(16000, dtype=np.int64),
|
||||
}
|
||||
out, state = self.session.run(None, ort_inputs)
|
||||
|
||||
conn._vad_state = state
|
||||
conn._vad_context = audio_input[:, -64:]
|
||||
speech_prob = out.item()
|
||||
|
||||
# 双阈值判断
|
||||
if speech_prob >= self.vad_threshold:
|
||||
|
||||
@@ -1201,6 +1201,113 @@ body {
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.chat-stream::-webkit-scrollbar,
|
||||
.modal-body::-webkit-scrollbar,
|
||||
.mcp-tools-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.chat-stream::-webkit-scrollbar-track,
|
||||
.modal-body::-webkit-scrollbar-track,
|
||||
.mcp-tools-list::-webkit-scrollbar-track {
|
||||
background: #2f3136;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chat-stream::-webkit-scrollbar-thumb,
|
||||
.modal-body::-webkit-scrollbar-thumb,
|
||||
.mcp-tools-list::-webkit-scrollbar-thumb {
|
||||
background: #40444b;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chat-stream::-webkit-scrollbar-thumb:hover,
|
||||
.modal-body::-webkit-scrollbar-thumb:hover,
|
||||
.mcp-tools-list::-webkit-scrollbar-thumb:hover {
|
||||
background: #4f545c;
|
||||
}
|
||||
|
||||
/* ==================== 摄像头显示区域样式 ==================== */
|
||||
.camera-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
width: 180px;
|
||||
height: 240px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.camera-container.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.camera-container.dragging {
|
||||
cursor: grabbing;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
|
||||
border-color: rgba(88, 101, 242, 0.5);
|
||||
}
|
||||
|
||||
@keyframes flipWithPosition {
|
||||
0% {
|
||||
transform: var(--original-transform) rotateY(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: var(--original-transform) rotateY(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.camera-container.flip {
|
||||
animation: flipWithPosition 0.5s ease-in-out 1 forwards;
|
||||
}
|
||||
|
||||
#cameraVideo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background: #1a1a1a;
|
||||
pointer-events: none;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.camera-switch-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.camera-switch-container {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.camera-switch {
|
||||
display: none;
|
||||
height: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.camera-switch .btn-icon {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.camera-switch.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ==================== 响应式设计 ==================== */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
@@ -1264,6 +1371,19 @@ body {
|
||||
left: 10px;
|
||||
bottom: 80px;
|
||||
}
|
||||
|
||||
.chat-ipt input {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.camera-container {
|
||||
width: 130px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
@@ -1343,68 +1463,4 @@ body {
|
||||
width: 250px;
|
||||
height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.chat-stream::-webkit-scrollbar,
|
||||
.modal-body::-webkit-scrollbar,
|
||||
.mcp-tools-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.chat-stream::-webkit-scrollbar-track,
|
||||
.modal-body::-webkit-scrollbar-track,
|
||||
.mcp-tools-list::-webkit-scrollbar-track {
|
||||
background: #2f3136;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chat-stream::-webkit-scrollbar-thumb,
|
||||
.modal-body::-webkit-scrollbar-thumb,
|
||||
.mcp-tools-list::-webkit-scrollbar-thumb {
|
||||
background: #40444b;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chat-stream::-webkit-scrollbar-thumb:hover,
|
||||
.modal-body::-webkit-scrollbar-thumb:hover,
|
||||
.mcp-tools-list::-webkit-scrollbar-thumb:hover {
|
||||
background: #4f545c;
|
||||
}
|
||||
|
||||
/* ==================== 摄像头显示区域样式 ==================== */
|
||||
.camera-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
width: 240px;
|
||||
height: 180px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.camera-container.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.camera-container.dragging {
|
||||
cursor: grabbing;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
|
||||
border-color: rgba(88, 101, 242, 0.5);
|
||||
}
|
||||
|
||||
#cameraVideo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background: #1a1a1a;
|
||||
pointer-events: none;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
@@ -25,6 +25,7 @@ class App {
|
||||
this.audioPlayer = null;
|
||||
this.live2dManager = null;
|
||||
this.cameraStream = null;
|
||||
this.currentFacingMode = 'user';
|
||||
}
|
||||
|
||||
// 初始化应用
|
||||
@@ -127,6 +128,9 @@ class App {
|
||||
async initCamera() {
|
||||
const cameraContainer = document.getElementById('cameraContainer');
|
||||
const cameraVideo = document.getElementById('cameraVideo');
|
||||
const cameraSwitch = document.getElementById('cameraSwitch');
|
||||
const cameraSwitchMask = document.getElementById('cameraSwitchMask');
|
||||
const dialBtn = document.getElementById('dialBtn');
|
||||
|
||||
if (!cameraContainer || !cameraVideo) {
|
||||
log('摄像头元素未找到,跳过初始化', 'warning');
|
||||
@@ -188,11 +192,24 @@ class App {
|
||||
}
|
||||
log('正在请求摄像头权限...', 'info');
|
||||
this.cameraStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { width: 320, height: 240, facingMode: 'user' },
|
||||
video: { width: 320, height: 240, facingMode: this.currentFacingMode },
|
||||
audio: false
|
||||
});
|
||||
cameraVideo.srcObject = this.cameraStream;
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
const videoDevices = devices.filter(device => device.kind === 'videoinput');
|
||||
if (videoDevices.length > 1) {
|
||||
if (cameraSwitch) cameraSwitch.classList.add('active');
|
||||
}
|
||||
cameraContainer.classList.add('active');
|
||||
|
||||
// 切换时挂断情况
|
||||
const hasActive = dialBtn.classList.contains('dial-active');
|
||||
if (!hasActive) {
|
||||
cameraContainer.classList.remove('active');
|
||||
cameraSwitch.classList.remove('active');
|
||||
window.stopCamera();
|
||||
}
|
||||
log('摄像头已启动', 'success');
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -217,6 +234,32 @@ class App {
|
||||
}
|
||||
};
|
||||
|
||||
window.switchCamera = async() => {
|
||||
if (window.switchCameraTimer) return;
|
||||
if (this.cameraStream) {
|
||||
const currentTransform = window.getComputedStyle(cameraContainer).transform;
|
||||
const originalTransform = currentTransform === 'none' ? 'translate(0px, 0px)' : currentTransform;
|
||||
cameraContainer.style.setProperty('--original-transform', originalTransform);
|
||||
cameraContainer.classList.add('flip');
|
||||
if (cameraSwitchMask) cameraSwitchMask.style.opacity = 0;
|
||||
this.currentFacingMode = this.currentFacingMode === 'user' ? 'environment' : 'user';
|
||||
window.stopCamera();
|
||||
window.startCamera();
|
||||
|
||||
window.switchCameraTimer = setTimeout(() => {
|
||||
if (this.currentFacingMode === 'user') {
|
||||
cameraVideo.style.transform = 'scaleX(-1)';
|
||||
} else {
|
||||
cameraVideo.style.transform = 'scaleX(1)';
|
||||
}
|
||||
window.switchCameraTimer = null;
|
||||
cameraContainer.classList.remove('flip');
|
||||
cameraContainer.style.removeProperty('--original-transform');
|
||||
if (cameraSwitchMask) cameraSwitchMask.style.opacity = 1;
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
window.takePhoto = (question = '描述一下看到的物品') => {
|
||||
return new Promise(async (resolve) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
|
||||
@@ -98,6 +98,18 @@ class UIController {
|
||||
});
|
||||
}
|
||||
|
||||
// Camera switch button
|
||||
const cameraSwitch = document.getElementById('cameraSwitch');
|
||||
const cameraSwitchMask = document.getElementById('cameraSwitchMask');
|
||||
if (cameraSwitchMask) {
|
||||
cameraSwitchMask.addEventListener('click', () => {
|
||||
const isCameraActive = cameraSwitch.classList.contains('active');
|
||||
if (isCameraActive) {
|
||||
window.switchCamera();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Dial button
|
||||
const dialBtn = document.getElementById('dialBtn');
|
||||
if (dialBtn) {
|
||||
@@ -115,6 +127,7 @@ class UIController {
|
||||
if (isConnected) {
|
||||
wsHandler.disconnect();
|
||||
this.updateDialButton(false);
|
||||
if (cameraSwitch) cameraSwitch.classList.remove('active');
|
||||
this.addChatMessage('Disconnected, see you next time~😊', false);
|
||||
} else {
|
||||
// Check if OTA URL is filled
|
||||
@@ -153,6 +166,7 @@ class UIController {
|
||||
if (isActive) {
|
||||
// 关闭摄像头
|
||||
if (typeof window.stopCamera === 'function') {
|
||||
if (cameraSwitch) cameraSwitch.classList.remove('active');
|
||||
window.stopCamera();
|
||||
}
|
||||
cameraContainer.classList.remove('active');
|
||||
|
||||
@@ -44,9 +44,18 @@
|
||||
|
||||
<!-- 主容器 -->
|
||||
<div class="container">
|
||||
|
||||
<!-- 摄像头显示区域(可拖动) -->
|
||||
<div class="camera-container" id="cameraContainer">
|
||||
<video id="cameraVideo" autoplay playsinline muted></video>
|
||||
<div class="camera-switch-mask" id="cameraSwitchMask">
|
||||
<div class="camera-switch-container">
|
||||
<!-- 摄像头切换按钮 -->
|
||||
<div class="camera-switch" id="cameraSwitch">
|
||||
<svg class="btn-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7319" width="32" height="32"><path d="M722.1 530.3c0-44.9-13.8-86.5-37.2-120.9-0.3-0.6-0.6-1.2-0.9-1.7-2.9-4.2-6.1-8.1-9.2-12-0.4-0.5-0.7-1-1.1-1.5-21.4-26.3-48.3-46.7-78.7-59.9-0.8-0.4-1.7-0.8-2.5-1.2-4.9-2-9.8-3.8-14.9-5.5-1.8-0.6-3.6-1.3-5.4-1.8-4.4-1.3-8.8-2.4-13.3-3.5-2.5-0.6-4.9-1.2-7.4-1.7-1.2-0.2-2.4-0.6-3.6-0.8-3.3-0.6-6.7-0.8-10-1.3-2.3-0.3-4.6-0.7-6.9-0.9-5.6-0.5-11.2-0.8-16.7-0.9-1 0-2-0.1-3-0.1-0.2 0-0.3 0.1-0.5 0.1-42.8 0-84.6 13.1-120.4 38.6-11.5 8.1-14.2 24.1-6.2 35.7 8 11.6 23.8 14.4 35.2 6.3 27.7-19.7 60-29.6 93.1-29.3 4.8 0 9.5 0.3 14.1 0.7 1.4 0.2 2.8 0.4 4.3 0.5 3.8 0.5 7.6 1 11.3 1.8 1.6 0.3 3.3 0.8 4.9 1.1 3.7 0.8 7.3 1.7 10.9 2.8 1.1 0.4 2.2 0.8 3.4 1.2 4.1 1.3 8.1 2.8 12 4.5 0.4 0.2 0.8 0.4 1.2 0.5 23.6 10.3 44.3 26.1 60.4 45.9 0.1 0.1 0.2 0.3 0.3 0.4 22.7 28 36.4 63.9 36.4 102.9h-42.2L697 632.9l67.5-102.6h-42.4z m-119 133c-27.9 19.8-60.5 29.7-93.9 29.3-4.4-0.1-8.8-0.3-13.1-0.7-1.8-0.2-3.5-0.5-5.3-0.7-3.4-0.5-6.8-0.9-10.1-1.6-2-0.4-4-0.9-6.1-1.4-3.3-0.8-6.5-1.5-9.7-2.5-1.5-0.5-3-1-4.6-1.5-3.7-1.2-7.3-2.5-10.8-4-0.8-0.3-1.6-0.7-2.4-1.1-4.1-1.9-8.2-3.8-12.2-6l-0.6-0.3c-13.4-7.5-25.7-16.8-36.4-27.7-0.2-0.2-0.3-0.4-0.5-0.6-3.3-3.4-6.5-6.9-9.5-10.7-0.6-0.8-1.3-1.6-1.9-2.5-21.9-27.8-35.1-63-35.1-101.2h42.2l-67.5-102.6-67.5 102.6h42.2c0 45 13.9 86.7 37.3 121.1 0.3 0.5 0.5 1 0.8 1.5 2.4 3.5 5.1 6.8 7.7 10.1 1 1.3 1.9 2.6 2.9 3.8 3.9 4.7 7.9 9.2 12.1 13.5 0.4 0.4 0.8 0.9 1.2 1.3 14.1 14.3 30.1 26.4 47.4 36 0.5 0.3 0.9 0.6 1.4 0.8 5 2.7 10.1 5.2 15.3 7.5l3.9 1.8c4.4 1.9 9 3.5 13.6 5.1 2.2 0.7 4.4 1.5 6.6 2.2 4 1.2 8.1 2.2 12.3 3.2 2.8 0.7 5.5 1.4 8.3 1.9 1.2 0.3 2.3 0.6 3.4 0.8 3.9 0.7 7.8 1.1 11.8 1.6l4.2 0.6c7 0.7 14.1 1.2 21.1 1.2 42.9 0 84.7-13.3 120.6-38.8 11.4-8.2 14.2-24.1 6.2-35.8-8-11.5-23.8-14.3-35.3-6.2z m271.3-421.6H763c-26.5 0-82.9-119.7-112.5-119.7H370.6c-29.6 0-85.9 119.7-112.4 119.7H147.5c-46.3 0-83.9 40.2-83.9 89.7v478.8c0 49.6 37.6 89.8 83.9 89.8h726.9c46.3 0 83.9-40.1 83.9-89.8V331.4c0-49.5-37.6-89.7-83.9-89.7z m28 538.6c0 33.1-25 59.8-55.9 59.8h-671c-30.9 0-55.9-26.8-55.9-59.8V361.4c0-33 25-59.8 55.9-59.8h111.2c25.8 0 82-119.7 111.6-119.7h223.2c29.5 0 85.6 119.7 111.5 119.7h113.5c30.9 0 55.9 26.8 55.9 59.8v418.9z" p-id="7320" fill="#ffffff"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 连接状态指示器 - 页面顶部中部 -->
|
||||
|
||||
Reference in New Issue
Block a user