Skip to content

Conversation

@DeveloperAmrit
Copy link
Contributor

@DeveloperAmrit DeveloperAmrit commented Jan 11, 2026

Closes #196

Changes made

  • Added a speaker icon in debate window, when clicked the application read out loud the opponent's statement
  • Implemented for both vs bot and vs player
  • Used SSML for natural speech with breaks, low pitch/high pitch
  • Automatically stops if user does concede

Video

My.Movie.mp4

Summary by CodeRabbit

New Features

  • Added text-to-speech (TTS) functionality to debate rooms with an enable/disable toggle button
  • Introduced a confirmation modal for conceding debates, replacing the browser prompt
  • Automatic speech playback now reads aloud debate phase changes and opponent messages when TTS is enabled
  • Speech playback stops automatically when TTS is disabled, debate ends, or during turn transitions

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 11, 2026

📝 Walkthrough

Walkthrough

The PR introduces text-to-speech functionality across both debate room interfaces using the Web Speech API, adds a reusable ConfirmationModal component for user confirmations, and replaces direct browser confirms with a modal-based concede flow in both local and online debate rooms.

Changes

Cohort / File(s) Summary
Debate Room Core Logic
frontend/src/Pages/DebateRoom.tsx, frontend/src/Pages/OnlineDebateRoom.tsx
Replaces window.confirm() with modal-based concede confirmation; adds concedeModalOpen state and confirmConcede handler. Introduces TTS toggle state (isTTSEnabled) and lifecycle hooks to speak bot/opponent statements when enabled and cancel speech on debate end. WebSocket integration added for online version to send concede messages.
UI Components
frontend/src/components/ConfirmationModal.tsx
New reusable modal component with isOpen, onClose, onConfirm, title, and description props; renders overlay with Cancel and Confirm buttons.
Utilities
frontend/src/utils/tts.ts
New TTS utility module exporting speak(text, rate, pitch) and cancelSpeech() functions; uses Web Speech API with voice preference logic and SSML awareness without full processing.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Component
    participant Modal
    participant SpeechAPI
    participant WebSocket

    User->>Component: Click Concede
    Component->>Modal: Open Concede Modal
    User->>Modal: Click Confirm
    Modal->>Component: onConfirm triggered
    Component->>SpeechAPI: Cancel any ongoing speech
    Component->>WebSocket: Send concede message (Online only)
    Component->>Component: Update debate state to Finished
    Component->>User: Show confirmation popup & navigate
Loading
sequenceDiagram
    participant Opponent
    participant Component
    participant TTSToggle
    participant SpeechAPI

    Opponent->>Component: New statement received / Phase change
    Component->>TTSToggle: Check if TTS enabled
    
    alt TTS Enabled
        TTSToggle->>SpeechAPI: Call speak(text)
        SpeechAPI->>SpeechAPI: Select voice & synthesize
    else TTS Disabled
        TTSToggle->>SpeechAPI: Call cancelSpeech()
    end
    
    Note over Component: On debate end or TTS disable
    Component->>SpeechAPI: Cancel ongoing speech
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 The rabbit hops with joy so bright,
Speech flows soft, the TTS takes flight,
A modal pops to ask "are you sure?"
Clear confirmation, calm and pure,
Text becomes sound, debates run smooth,
Web Speech API's got the groove! 🎤

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements TTS for both bot and player modes with toggle controls, but the implementation does not use SSML as required by issue #196. Update the TTS implementation to support SSML formatting with breaks and pitch variation as specified in issue #196 requirements.
Out of Scope Changes check ⚠️ Warning The PR includes a concede confirmation modal that is unrelated to the TTS feature requirements in issue #196. Remove concede confirmation modal changes or move them to a separate PR focused on improving the concede flow.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title 'Fix issue 196 text to speech' directly aligns with the PR's main objective: implementing text-to-speech functionality to address issue #196. It clearly summarizes the primary change.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In @frontend/src/Pages/DebateRoom.tsx:
- Around line 270-291: The confirmConcede function currently only logs errors
from concedeDebate and still proceeds to mark the debate ended and navigate;
update confirmConcede so that if await concedeDebate(debateData.debateId,
state.messages) throws you catch the error, do NOT setState to isDebateEnded or
navigate, and instead call setPopup(...) to show a clear error message (include
error.message or serialized error) and keep the modal open; preserve the
cancelSpeech() behavior, ensure the catch block sets a visible user-facing error
via setPopup and optionally restores any temporary UI state rather than silently
failing.
- Around line 299-308: The TTS effect currently speaks any existing Bot messages
on mount and on unrelated re-renders because it only watches state.messages and
isTTSEnabled; fix by adding a ref (e.g., lastSpokenIndexRef) to track the index
or id of the last spoken message and initialize it to state.messages.length on
mount (so hydrated messages aren't spoken), then update the useEffect to only
call speak(lastMsg.text) when isTTSEnabled is true AND the new last message
index > lastSpokenIndexRef.current AND lastMsg.sender === "Bot", afterwards set
lastSpokenIndexRef.current to the new index; also keep the cancelSpeech() branch
when isTTSEnabled becomes false.

In @frontend/src/Pages/OnlineDebateRoom.tsx:
- Around line 1167-1171: Guard the call to speak() so you never pass undefined:
check that data.message is a defined, non-empty string before calling speak
(e.g., if (isTTSEnabledRef.current && typeof data.message === 'string' &&
data.message.trim() !== '') speak(data.message);). Update the block around
isTTSEnabledRef.current and speak() (and related WSMessage handling) to perform
this validation prior to invoking speak(), leaving setDebatePhase(data.phase)
unchanged.
- Around line 816-832: The confirmConcede handler never closes the concede
modal; update the confirmConcede function to call setConcedeModalOpen(false)
after sending the concede message (and before/after updating state as you
prefer) and add setConcedeModalOpen to the hook dependencies; locate the
confirmConcede useCallback in OnlineDebateRoom.tsx and insert
setConcedeModalOpen(false) so the modal actually closes on confirmation.
- Around line 812-814: The component uses setConcedeModalOpen in handleConcede
but never declares concedeModalOpen state; add a useState hook in the component
scope: declare const [concedeModalOpen, setConcedeModalOpen] = useState(false)
(ensure React useState is imported) so the modal's open/onClose handlers have
the corresponding state and setter.

In @frontend/src/utils/tts.ts:
- Around line 36-56: The if-block in tts.ts that checks
text.trim().startsWith('<speak>') contains only lengthy commented deliberation
and should be cleaned up; remove the dead block and replace it with a concise
implementation or comment: either implement a small helper (e.g., formatSSML or
stripSSML) to handle SSML-safe text fallback or simply add a one-line comment
stating SSML is not supported by the Web Speech API and proceed with plain text;
ensure the unique check text.trim().startsWith('<speak>') is kept only if used
by the implemented helper, otherwise remove that check entirely to eliminate
noise.
🧹 Nitpick comments (2)
frontend/src/components/ConfirmationModal.tsx (1)

21-36: Missing accessibility features for modal.

The modal lacks several accessibility features:

  1. No role="dialog" and aria-modal="true" on the dialog container
  2. No focus trapping (focus can escape to elements behind the modal)
  3. No Escape key handler to close the modal
  4. Clicking the backdrop doesn't close the modal (common UX pattern)
♻️ Suggested accessibility improvements
+import { useEffect, useRef } from 'react';
+
 const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
   isOpen,
   onClose,
   onConfirm,
   title,
   description,
 }) => {
+  const dialogRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    if (isOpen) {
+      dialogRef.current?.focus();
+      const handleEscape = (e: KeyboardEvent) => {
+        if (e.key === 'Escape') onClose();
+      };
+      document.addEventListener('keydown', handleEscape);
+      return () => document.removeEventListener('keydown', handleEscape);
+    }
+  }, [isOpen, onClose]);
+
   if (!isOpen) return null;

   return (
-    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
-      <div className="bg-white rounded-lg shadow-lg p-6 w-full max-w-md">
+    <div 
+      className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
+      onClick={onClose}
+    >
+      <div 
+        ref={dialogRef}
+        role="dialog"
+        aria-modal="true"
+        aria-labelledby="modal-title"
+        tabIndex={-1}
+        className="bg-white rounded-lg shadow-lg p-6 w-full max-w-md"
+        onClick={(e) => e.stopPropagation()}
+      >
-        <h2 className="text-xl font-bold mb-4">{title}</h2>
+        <h2 id="modal-title" className="text-xl font-bold mb-4">{title}</h2>
frontend/src/Pages/DebateRoom.tsx (1)

293-297: Missing TTS cleanup on component unmount.

Speech is cancelled when the debate ends, but if the user navigates away during an active debate (e.g., browser back button), ongoing speech will continue playing.

♻️ Add cleanup effect
   useEffect(() => {
     if (state.isDebateEnded) {
       cancelSpeech();
     }
   }, [state.isDebateEnded]);

+  useEffect(() => {
+    return () => {
+      cancelSpeech();
+    };
+  }, []);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb07eaf and 0f3e8a3.

📒 Files selected for processing (4)
  • frontend/src/Pages/DebateRoom.tsx
  • frontend/src/Pages/OnlineDebateRoom.tsx
  • frontend/src/components/ConfirmationModal.tsx
  • frontend/src/utils/tts.ts
🧰 Additional context used
🧬 Code graph analysis (3)
frontend/src/components/ConfirmationModal.tsx (1)
frontend/src/components/ui/button.tsx (1)
  • Button (57-57)
frontend/src/Pages/DebateRoom.tsx (2)
frontend/src/utils/tts.ts (2)
  • cancelSpeech (61-65)
  • speak (1-59)
frontend/src/services/vsbot.ts (1)
  • concedeDebate (103-118)
frontend/src/Pages/OnlineDebateRoom.tsx (1)
frontend/src/utils/tts.ts (2)
  • cancelSpeech (61-65)
  • speak (1-59)
🔇 Additional comments (8)
frontend/src/Pages/DebateRoom.tsx (2)

765-773: LGTM!

The conditional rendering ensures the concede modal is only available during an active debate. Props are correctly wired to the modal callbacks.


812-820: LGTM!

The TTS toggle button provides clear visual feedback with appropriate icons and a descriptive tooltip. The styling changes based on the enabled state.

frontend/src/utils/tts.ts (2)

15-25: Voice list may be empty on first call due to async loading.

In many browsers (especially Chrome), speechSynthesis.getVoices() returns an empty array until voices are loaded asynchronously. The first TTS invocation may not get the preferred voice.

Consider using the voiceschanged event to cache voices, or fall back gracefully:

♻️ Suggested approach
+let cachedVoices: SpeechSynthesisVoice[] = [];
+
+if ('speechSynthesis' in window) {
+  cachedVoices = window.speechSynthesis.getVoices();
+  window.speechSynthesis.onvoiceschanged = () => {
+    cachedVoices = window.speechSynthesis.getVoices();
+  };
+}
+
 export const speak = (text: string, rate: number = 1.0, pitch: number = 1.0) => {
   // ...
-  const voices = window.speechSynthesis.getVoices();
+  const voices = cachedVoices.length > 0 ? cachedVoices : window.speechSynthesis.getVoices();

61-65: LGTM!

The cancelSpeech function is clean and includes a proper feature check before calling the API.

frontend/src/Pages/OnlineDebateRoom.tsx (4)

449-454: LGTM!

Using a ref to sync the TTS enabled state allows callbacks (like WebSocket handlers) to access the current value without stale closures. Cancelling speech when TTS is disabled provides immediate feedback.


2497-2505: LGTM!

The TTS toggle implementation is consistent with DebateRoom.tsx and provides clear visual feedback.


2629-2635: LGTM!

The ConfirmationModal is properly integrated. The Concede button is already conditionally rendered based on debate phase, so the modal visibility is appropriately controlled.


1990-1992: LGTM!

Properly cancels any ongoing speech when the debate phase transitions to Finished.

Comment on lines +270 to 291
const confirmConcede = async () => {
try {
cancelSpeech(); // Stop any ongoing speech
if (debateData.debateId) {
await concedeDebate(debateData.debateId, state.messages);
}

setState(prev => ({ ...prev, isDebateEnded: true }));
setPopup({
show: true,
message: "You have conceded the debate.",
isJudging: false
});

setTimeout(() => {
navigate("/game");
}, 2000);

} catch (error) {
console.error("Error conceding:", error);
}
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

User receives no feedback when concede fails.

If the concedeDebate API call fails, the error is only logged to console. The user is left without feedback, and the modal may have already closed. Consider showing an error message to the user.

🐛 Proposed fix to show error feedback
   const confirmConcede = async () => {
     try {
       cancelSpeech(); // Stop any ongoing speech
       if (debateData.debateId) {
           await concedeDebate(debateData.debateId, state.messages);
       }
       
       setState(prev => ({ ...prev, isDebateEnded: true }));
       setPopup({
           show: true,
           message: "You have conceded the debate.",
           isJudging: false
       });
       
       setTimeout(() => {
           navigate("/game");
       }, 2000);

     } catch (error) {
       console.error("Error conceding:", error);
+      setPopup({
+        show: true,
+        message: "Failed to concede. Please try again.",
+        isJudging: false
+      });
+    } finally {
+      setConcedeModalOpen(false);
     }
   };
🤖 Prompt for AI Agents
In @frontend/src/Pages/DebateRoom.tsx around lines 270 - 291, The confirmConcede
function currently only logs errors from concedeDebate and still proceeds to
mark the debate ended and navigate; update confirmConcede so that if await
concedeDebate(debateData.debateId, state.messages) throws you catch the error,
do NOT setState to isDebateEnded or navigate, and instead call setPopup(...) to
show a clear error message (include error.message or serialized error) and keep
the modal open; preserve the cancelSpeech() behavior, ensure the catch block
sets a visible user-facing error via setPopup and optionally restores any
temporary UI state rather than silently failing.

Comment on lines +299 to +308
useEffect(() => {
if (isTTSEnabled && state.messages.length > 0) {
const lastMsg = state.messages[state.messages.length - 1];
if (lastMsg.sender === "Bot") {
speak(lastMsg.text);
}
} else if (!isTTSEnabled) {
cancelSpeech();
}
}, [state.messages, isTTSEnabled]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

TTS effect may trigger unintended speech on component mount or re-renders.

The effect runs whenever state.messages changes, which includes initial state hydration from localStorage. This could cause unexpected speech on page load if there are existing bot messages. Additionally, the effect checks state.messages.length > 0 but doesn't track which messages have already been spoken, potentially causing repeated speech on unrelated state changes.

🐛 Proposed fix using a ref to track spoken messages
+  const lastSpokenIndexRef = useRef(-1);
+
   useEffect(() => {
-    if (isTTSEnabled && state.messages.length > 0) {
-      const lastMsg = state.messages[state.messages.length - 1];
-      if (lastMsg.sender === "Bot") {
-        speak(lastMsg.text);
+    if (isTTSEnabled && state.messages.length > 0) {
+      const lastIndex = state.messages.length - 1;
+      const lastMsg = state.messages[lastIndex];
+      if (lastMsg.sender === "Bot" && lastIndex > lastSpokenIndexRef.current) {
+        lastSpokenIndexRef.current = lastIndex;
+        speak(lastMsg.text);
       }
     } else if (!isTTSEnabled) {
       cancelSpeech();
+      lastSpokenIndexRef.current = state.messages.length - 1; // Avoid speaking skipped messages when re-enabled
     }
   }, [state.messages, isTTSEnabled]);
🤖 Prompt for AI Agents
In @frontend/src/Pages/DebateRoom.tsx around lines 299 - 308, The TTS effect
currently speaks any existing Bot messages on mount and on unrelated re-renders
because it only watches state.messages and isTTSEnabled; fix by adding a ref
(e.g., lastSpokenIndexRef) to track the index or id of the last spoken message
and initialize it to state.messages.length on mount (so hydrated messages aren't
spoken), then update the useEffect to only call speak(lastMsg.text) when
isTTSEnabled is true AND the new last message index > lastSpokenIndexRef.current
AND lastMsg.sender === "Bot", afterwards set lastSpokenIndexRef.current to the
new index; also keep the cancelSpeech() branch when isTTSEnabled becomes false.

Comment on lines 812 to +814
const handleConcede = useCallback(() => {
if (window.confirm("Are you sure you want to concede? This will count as a loss.")) {
if (wsRef.current) {
wsRef.current.send(JSON.stringify({
type: "concede",
room: roomId,
userId: currentUserId,
username: currentUser?.displayName || "User"
}));
}
setDebatePhase(DebatePhase.Finished);
setPopup({
show: true,
message: "You have conceded the debate.",
isJudging: false,
});
setConcedeModalOpen(true);
}, []);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "concedeModalOpen" frontend/src/Pages/OnlineDebateRoom.tsx

Repository: AOSSIE-Org/DebateAI

Length of output: 101


🏁 Script executed:

rg -n "setConcedeModalOpen|useState.*concedeModalOpen" frontend/src/Pages/OnlineDebateRoom.tsx

Repository: AOSSIE-Org/DebateAI

Length of output: 153


🏁 Script executed:

rg -n "useState" frontend/src/Pages/OnlineDebateRoom.tsx | head -20

Repository: AOSSIE-Org/DebateAI

Length of output: 1395


🏁 Script executed:

cat frontend/src/Pages/OnlineDebateRoom.tsx | grep -n "concedeModalOpen" || echo "No matches found"

Repository: AOSSIE-Org/DebateAI

Length of output: 101


🏁 Script executed:

sed -n '160,250p' frontend/src/Pages/OnlineDebateRoom.tsx

Repository: AOSSIE-Org/DebateAI

Length of output: 3814


🏁 Script executed:

sed -n '250,350p' frontend/src/Pages/OnlineDebateRoom.tsx

Repository: AOSSIE-Org/DebateAI

Length of output: 3098


🏁 Script executed:

rg -n "useState.*Modal" frontend/src/Pages/OnlineDebateRoom.tsx

Repository: AOSSIE-Org/DebateAI

Length of output: 45


🏁 Script executed:

sed -n '810,820p' frontend/src/Pages/OnlineDebateRoom.tsx

Repository: AOSSIE-Org/DebateAI

Length of output: 339


Add missing concedeModalOpen state declaration.

The code uses setConcedeModalOpen at line 813, and concedeModalOpen is referenced at line 2630 (with the modal's onClose handler at line 2631), but the corresponding useState hook is not declared. This will cause a runtime error. Add the state declaration:

const [concedeModalOpen, setConcedeModalOpen] = useState(false);
🤖 Prompt for AI Agents
In @frontend/src/Pages/OnlineDebateRoom.tsx around lines 812 - 814, The
component uses setConcedeModalOpen in handleConcede but never declares
concedeModalOpen state; add a useState hook in the component scope: declare
const [concedeModalOpen, setConcedeModalOpen] = useState(false) (ensure React
useState is imported) so the modal's open/onClose handlers have the
corresponding state and setter.

Comment on lines +816 to 832
const confirmConcede = useCallback(() => {
cancelSpeech(); // Stop any ongoing speech
if (wsRef.current) {
wsRef.current.send(JSON.stringify({
type: "concede",
room: roomId,
userId: currentUserId,
username: currentUser?.displayName || "User"
}));
}
setDebatePhase(DebatePhase.Finished);
setPopup({
show: true,
message: "You have conceded the debate.",
isJudging: false,
});
}, [roomId, currentUserId, currentUser, setDebatePhase, setPopup]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Concede modal is not closed after confirmation.

Unlike the onClose callback which sets concedeModalOpen to false, confirmConcede doesn't close the modal. The modal will remain visible after the user confirms.

🐛 Proposed fix
   const confirmConcede = useCallback(() => {
     cancelSpeech(); // Stop any ongoing speech
     if (wsRef.current) {
       wsRef.current.send(JSON.stringify({
         type: "concede",
         room: roomId,
         userId: currentUserId,
         username: currentUser?.displayName || "User"
       }));
     }
     setDebatePhase(DebatePhase.Finished);
     setPopup({
       show: true,
       message: "You have conceded the debate.",
       isJudging: false,
     });
+    setConcedeModalOpen(false);
   }, [roomId, currentUserId, currentUser, setDebatePhase, setPopup]);
🤖 Prompt for AI Agents
In @frontend/src/Pages/OnlineDebateRoom.tsx around lines 816 - 832, The
confirmConcede handler never closes the concede modal; update the confirmConcede
function to call setConcedeModalOpen(false) after sending the concede message
(and before/after updating state as you prefer) and add setConcedeModalOpen to
the hook dependencies; locate the confirmConcede useCallback in
OnlineDebateRoom.tsx and insert setConcedeModalOpen(false) so the modal actually
closes on confirmation.

Comment on lines +1167 to 1171

if (isTTSEnabledRef.current) {
speak(data.message);
}
setDebatePhase(data.phase);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential undefined value passed to speak().

data.message may be undefined for phaseChange events (the WSMessage interface shows message is optional). Passing undefined to speak() would result in the TTS reading "undefined" aloud.

🐛 Proposed fix
             if (isTTSEnabledRef.current) {
-               speak(data.message);
+               if (data.message) {
+                 speak(data.message);
+               }
             }
🤖 Prompt for AI Agents
In @frontend/src/Pages/OnlineDebateRoom.tsx around lines 1167 - 1171, Guard the
call to speak() so you never pass undefined: check that data.message is a
defined, non-empty string before calling speak (e.g., if
(isTTSEnabledRef.current && typeof data.message === 'string' &&
data.message.trim() !== '') speak(data.message);). Update the block around
isTTSEnabledRef.current and speak() (and related WSMessage handling) to perform
this validation prior to invoking speak(), leaving setDebatePhase(data.phase)
unchanged.

Comment on lines +36 to +56
// If the text starts with <speak>, it's SSML.
if (text.trim().startsWith('<speak>')) {
// Logic to handle SSML if needed, or naive stripping if we suspect it won't work?
// For now, we trust the browser or the specific voice implementation.
// But to be safe against reading tags aloud:
// We will perform a check. Chrome's "native" voices usually don't support SSML.
// We will strip tags for safety unless we are sure. (Actually, for this task, I will strip tags for safety
// because reading tags is worse than flat speech).

// text = text.replace(/<[^>]*>/g, '');
// Wait, if I strip tags, I lose the instructions.
// Let's rely on the prompt "Use SSML for natural speaking" as an instruction for ME to GENERATE SSML?
// No, "read the statement given by openent".
// I will assume the opponent gives plain text. I should WRAP it in SSML if valid?
// No, standard Web Speech API doesn't take SSML.
// I'll stick to plain text with good voice selection for now.

// But wait - "Use SSML for natural speaking" - Maybe I should fake prosody?
// No. I'll just leave it as is. If the user REALLY wants SSML, they might be using an extension or specific browser config.
// But I will create a function that *formats* it as SSML compatible string just in case an API is swapped later.
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Dead code block with extensive comments.

This if-block contains only comments and no executable code. The extensive internal deliberation comments are distracting and should be removed or replaced with a concise explanation of the decision.

♻️ Clean up dead code
-  // If the text starts with <speak>, it's SSML.
-  if (text.trim().startsWith('<speak>')) {
-      // Logic to handle SSML if needed, or naive stripping if we suspect it won't work?
-      // For now, we trust the browser or the specific voice implementation.
-      // But to be safe against reading tags aloud:
-      // We will perform a check. Chrome's "native" voices usually don't support SSML.
-      // We will strip tags for safety unless we are sure. (Actually, for this task, I will strip tags for safety 
-      // because reading tags is worse than flat speech).
-      
-      // text = text.replace(/<[^>]*>/g, '');
-      // Wait, if I strip tags, I lose the instructions.
-      // Let's rely on the prompt "Use SSML for natural speaking" as an instruction for ME to GENERATE SSML?
-      // No, "read the statement given by openent".
-      // I will assume the opponent gives plain text. I should WRAP it in SSML if valid?
-      // No, standard Web Speech API doesn't take SSML.
-      // I'll stick to plain text with good voice selection for now.
-      
-      // But wait - "Use SSML for natural speaking" - Maybe I should fake prosody?
-      // No. I'll just leave it as is. If the user REALLY wants SSML, they might be using an extension or specific browser config.
-      // But I will create a function that *formats* it as SSML compatible string just in case an API is swapped later.
-  }
+  // Note: Web Speech API doesn't support SSML in most browsers.
+  // Natural-sounding speech is achieved via voice selection and rate/pitch params.
📝 Committable suggestion

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

Suggested change
// If the text starts with <speak>, it's SSML.
if (text.trim().startsWith('<speak>')) {
// Logic to handle SSML if needed, or naive stripping if we suspect it won't work?
// For now, we trust the browser or the specific voice implementation.
// But to be safe against reading tags aloud:
// We will perform a check. Chrome's "native" voices usually don't support SSML.
// We will strip tags for safety unless we are sure. (Actually, for this task, I will strip tags for safety
// because reading tags is worse than flat speech).
// text = text.replace(/<[^>]*>/g, '');
// Wait, if I strip tags, I lose the instructions.
// Let's rely on the prompt "Use SSML for natural speaking" as an instruction for ME to GENERATE SSML?
// No, "read the statement given by openent".
// I will assume the opponent gives plain text. I should WRAP it in SSML if valid?
// No, standard Web Speech API doesn't take SSML.
// I'll stick to plain text with good voice selection for now.
// But wait - "Use SSML for natural speaking" - Maybe I should fake prosody?
// No. I'll just leave it as is. If the user REALLY wants SSML, they might be using an extension or specific browser config.
// But I will create a function that *formats* it as SSML compatible string just in case an API is swapped later.
}
// Note: Web Speech API doesn't support SSML in most browsers.
// Natural-sounding speech is achieved via voice selection and rate/pitch params.
🤖 Prompt for AI Agents
In @frontend/src/utils/tts.ts around lines 36 - 56, The if-block in tts.ts that
checks text.trim().startsWith('<speak>') contains only lengthy commented
deliberation and should be cleaned up; remove the dead block and replace it with
a concise implementation or comment: either implement a small helper (e.g.,
formatSSML or stripSSML) to handle SSML-safe text fallback or simply add a
one-line comment stating SSML is not supported by the Web Speech API and proceed
with plain text; ensure the unique check text.trim().startsWith('<speak>') is
kept only if used by the implemented helper, otherwise remove that check
entirely to eliminate noise.

@DeveloperAmrit DeveloperAmrit changed the title Fix 196 text to speech Fix issue 196 text to speech Jan 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feat: Add text to speech so the application reads out loud the opponent's statement

1 participant