diff --git a/application/api/user/attachments/routes.py b/application/api/user/attachments/routes.py index 87af6a7e..cd67f1ba 100644 --- a/application/api/user/attachments/routes.py +++ b/application/api/user/attachments/routes.py @@ -130,11 +130,15 @@ class TextToSpeech(Resource): @api.expect(tts_model) @api.doc(description="Synthesize audio speech from text") def post(self): + from application.utils import clean_text_for_tts + data = request.get_json() text = data["text"] + cleaned_text = clean_text_for_tts(text) + try: tts_instance = TTSCreator.create_tts(settings.TTS_PROVIDER) - audio_base64, detected_language = tts_instance.text_to_speech(text) + audio_base64, detected_language = tts_instance.text_to_speech(cleaned_text) return make_response( jsonify( { diff --git a/application/utils.py b/application/utils.py index 528cbac5..432fca5f 100644 --- a/application/utils.py +++ b/application/utils.py @@ -187,3 +187,44 @@ def generate_image_url(image_path): else: base_url = getattr(settings, "API_URL", "http://localhost:7091") return f"{base_url}/api/images/{image_path}" + + +def clean_text_for_tts(text: str) -> str: + """ + clean text for Text-to-Speech processing. + """ + # Handle code blocks and links + text = re.sub(r'```mermaid[\s\S]*?```', ' flowchart, ', text) ## ```mermaid...``` + text = re.sub(r'```[\s\S]*?```', ' code block, ', text) ## ```code``` + text = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', text) ## [text](url) + text = re.sub(r'!\[([^\]]*)\]\([^\)]+\)', '', text) ## ![alt](url) + + # Remove markdown formatting + text = re.sub(r'`([^`]+)`', r'\1', text) ## `code` + text = re.sub(r'\{([^}]*)\}', r' \1 ', text) ## {text} + text = re.sub(r'[{}]', ' ', text) ## unmatched {} + text = re.sub(r'\[([^\]]+)\]', r' \1 ', text) ## [text] + text = re.sub(r'[\[\]]', ' ', text) ## unmatched [] + text = re.sub(r'(\*\*|__)(.*?)\1', r'\2', text) ## **bold** __bold__ + text = re.sub(r'(\*|_)(.*?)\1', r'\2', text) ## *italic* _italic_ + text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE) ## # headers + text = re.sub(r'^>\s+', '', text, flags=re.MULTILINE) ## > blockquotes + text = re.sub(r'^[\s]*[-\*\+]\s+', '', text, flags=re.MULTILINE) ## - * + lists + text = re.sub(r'^[\s]*\d+\.\s+', '', text, flags=re.MULTILINE) ## 1. numbered lists + text = re.sub(r'^[\*\-_]{3,}\s*$', '', text, flags=re.MULTILINE) ## --- *** ___ rules + text = re.sub(r'<[^>]*>', '', text) ## tags + + #Remove non-ASCII (emojis, special Unicode) + text = re.sub(r'[^\x20-\x7E\n\r\t]', '', text) + + #Replace special sequences + text = re.sub(r'-->', ', ', text) ## --> + text = re.sub(r'<--', ', ', text) ## <-- + text = re.sub(r'=>', ', ', text) ## => + text = re.sub(r'::', ' ', text) ## :: + + #Normalize whitespace + text = re.sub(r'\s+', ' ', text) + text = text.strip() + + return text diff --git a/frontend/src/Navigation.tsx b/frontend/src/Navigation.tsx index b8c714bc..439267c3 100644 --- a/frontend/src/Navigation.tsx +++ b/frontend/src/Navigation.tsx @@ -411,7 +411,9 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { {recentAgents?.length > 0 ? (
-

Agents

+

+ {t('navigation.agents')} +

diff --git a/frontend/src/PageNotFound.tsx b/frontend/src/PageNotFound.tsx index 2129d0ee..36c106a3 100644 --- a/frontend/src/PageNotFound.tsx +++ b/frontend/src/PageNotFound.tsx @@ -1,13 +1,16 @@ import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; export default function PageNotFound() { + const { t } = useTranslation(); + return (

-

404

-

The page you are looking for does not exist.

+

{t('pageNotFound.title')}

+

{t('pageNotFound.message')}

diff --git a/frontend/src/agents/AgentLogs.tsx b/frontend/src/agents/AgentLogs.tsx index 979a701e..62478329 100644 --- a/frontend/src/agents/AgentLogs.tsx +++ b/frontend/src/agents/AgentLogs.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { useNavigate, useParams } from 'react-router-dom'; @@ -11,6 +12,7 @@ import Logs from '../settings/Logs'; import { Agent } from './types'; export default function AgentLogs() { + const { t } = useTranslation(); const navigate = useNavigate(); const { agentId } = useParams(); const token = useSelector(selectToken); @@ -45,12 +47,12 @@ export default function AgentLogs() { left-arrow

- Back to all agents + {t('agents.backToAll')}

- Agent Logs + {t('agents.logs.title')}

@@ -59,9 +61,10 @@ export default function AgentLogs() {

{agent.name}

{agent.last_used_at - ? 'Last used at ' + + ? t('agents.logs.lastUsedAt') + + ' ' + new Date(agent.last_used_at).toLocaleString() - : 'No usage history'} + : t('agents.logs.noUsageHistory')}

)} @@ -79,7 +82,9 @@ export default function AgentLogs() {
) : ( - agent && + agent && ( + + ) )}
); diff --git a/frontend/src/agents/AgentPreview.tsx b/frontend/src/agents/AgentPreview.tsx index a242e52a..cc8f04dc 100644 --- a/frontend/src/agents/AgentPreview.tsx +++ b/frontend/src/agents/AgentPreview.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import MessageInput from '../components/MessageInput'; @@ -17,6 +18,7 @@ import { selectSelectedAgent } from '../preferences/preferenceSlice'; import { AppDispatch } from '../store'; export default function AgentPreview() { + const { t } = useTranslation(); const dispatch = useDispatch(); const queries = useSelector(selectPreviewQueries); @@ -130,8 +132,7 @@ export default function AgentPreview() { />

- This is a preview of the agent. You can publish it to start using it - in conversations. + {t('agents.preview.testMessage')}

diff --git a/frontend/src/agents/AgentsList.tsx b/frontend/src/agents/AgentsList.tsx index 04957443..bdccb0f1 100644 --- a/frontend/src/agents/AgentsList.tsx +++ b/frontend/src/agents/AgentsList.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; @@ -17,6 +18,7 @@ import { agentSectionsConfig } from './agents.config'; import { Agent } from './types'; export default function AgentsList() { + const { t } = useTranslation(); const dispatch = useDispatch(); const token = useSelector(selectToken); const selectedAgent = useSelector(selectSelectedAgent); @@ -33,11 +35,10 @@ export default function AgentsList() { return (

- Agents + {t('agents.title')}

- Discover and create custom versions of DocsGPT that combine - instructions, extra knowledge, and any combination of skills + {t('agents.description')}

{agentSectionsConfig.map((sectionConfig) => ( @@ -51,6 +52,7 @@ function AgentSection({ }: { config: (typeof agentSectionsConfig)[number]; }) { + const { t } = useTranslation(); const navigate = useNavigate(); const dispatch = useDispatch(); const token = useSelector(selectToken); @@ -85,16 +87,18 @@ function AgentSection({

- {config.title} + {t(`agents.sections.${config.id}.title`)}

-

{config.description}

+

+ {t(`agents.sections.${config.id}.description`)} +

{config.showNewAgentButton && ( )}
@@ -117,13 +121,13 @@ function AgentSection({
) : (
-

{config.emptyStateDescription}

+

{t(`agents.sections.${config.id}.emptyState`)}

{config.showNewAgentButton && ( )}
diff --git a/frontend/src/agents/NewAgent.tsx b/frontend/src/agents/NewAgent.tsx index 1eb36c22..c15f4eb6 100644 --- a/frontend/src/agents/NewAgent.tsx +++ b/frontend/src/agents/NewAgent.tsx @@ -1,5 +1,6 @@ import isEqual from 'lodash/isEqual'; import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate, useParams } from 'react-router-dom'; @@ -30,6 +31,7 @@ const embeddingsName = 'huggingface_sentence-transformers/all-mpnet-base-v2'; export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { + const { t } = useTranslation(); const navigate = useNavigate(); const dispatch = useDispatch(); const { agentId } = useParams(); @@ -87,8 +89,8 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { const modeConfig = { new: { - heading: 'New Agent', - buttonText: 'Publish', + heading: t('agents.form.headings.new'), + buttonText: t('agents.form.buttons.publish'), showDelete: false, showSaveDraft: true, showLogs: false, @@ -96,8 +98,8 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { trackChanges: false, }, edit: { - heading: 'Edit Agent', - buttonText: 'Save', + heading: t('agents.form.headings.edit'), + buttonText: t('agents.form.buttons.save'), showDelete: true, showSaveDraft: false, showLogs: true, @@ -105,8 +107,8 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { trackChanges: true, }, draft: { - heading: 'New Agent (Draft)', - buttonText: 'Publish', + heading: t('agents.form.headings.draft'), + buttonText: t('agents.form.buttons.publish'), showDelete: true, showSaveDraft: true, showLogs: false, @@ -116,8 +118,8 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { }; const chunks = ['0', '2', '4', '6', '8', '10']; const agentTypes = [ - { label: 'Classic', value: 'classic' }, - { label: 'ReAct', value: 'react' }, + { label: t('agents.form.agentTypes.classic'), value: 'classic' }, + { label: t('agents.form.agentTypes.react'), value: 'react' }, ]; const isPublishable = () => { @@ -543,7 +545,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { left-arrow

- Back to all agents + {t('agents.backToAll')}

@@ -555,7 +557,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { className="text-purple-30 dark:text-light-gray mr-4 rounded-3xl py-2 text-sm font-medium dark:bg-transparent" onClick={handleCancel} > - Cancel + {t('agents.form.buttons.cancel')} {modeConfig[effectiveMode].showDelete && agent.id && ( )} {modeConfig[effectiveMode].showSaveDraft && ( @@ -578,7 +580,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { {draftLoading ? ( ) : ( - 'Save Draft' + t('agents.form.buttons.saveDraft') )} @@ -589,7 +591,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { onClick={() => navigate(`/agents/logs/${agent.id}`)} > - Logs + {t('agents.form.buttons.logs')} )} {modeConfig[effectiveMode].showAccessDetails && ( @@ -597,7 +599,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { className="hover:bg-violets-are-blue border-violets-are-blue text-violets-are-blue hover:bg-violets-are-blue rounded-3xl border border-solid px-5 py-2 text-sm font-medium transition-colors hover:text-white" onClick={() => setAgentDetails('ACTIVE')} > - Access Details + {t('agents.form.buttons.accessDetails')} )}