-
Notifications
You must be signed in to change notification settings - Fork 136
Fix issue 196 text to speech #197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Fix issue 196 text to speech #197
Conversation
📝 WalkthroughWalkthroughThe 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
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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this 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:
- No
role="dialog"andaria-modal="true"on the dialog container- No focus trapping (focus can escape to elements behind the modal)
- No Escape key handler to close the modal
- 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
📒 Files selected for processing (4)
frontend/src/Pages/DebateRoom.tsxfrontend/src/Pages/OnlineDebateRoom.tsxfrontend/src/components/ConfirmationModal.tsxfrontend/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
voiceschangedevent 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
cancelSpeechfunction 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.
| 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); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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); | ||
| }, []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -n "concedeModalOpen" frontend/src/Pages/OnlineDebateRoom.tsxRepository: AOSSIE-Org/DebateAI
Length of output: 101
🏁 Script executed:
rg -n "setConcedeModalOpen|useState.*concedeModalOpen" frontend/src/Pages/OnlineDebateRoom.tsxRepository: AOSSIE-Org/DebateAI
Length of output: 153
🏁 Script executed:
rg -n "useState" frontend/src/Pages/OnlineDebateRoom.tsx | head -20Repository: 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.tsxRepository: AOSSIE-Org/DebateAI
Length of output: 3814
🏁 Script executed:
sed -n '250,350p' frontend/src/Pages/OnlineDebateRoom.tsxRepository: AOSSIE-Org/DebateAI
Length of output: 3098
🏁 Script executed:
rg -n "useState.*Modal" frontend/src/Pages/OnlineDebateRoom.tsxRepository: AOSSIE-Org/DebateAI
Length of output: 45
🏁 Script executed:
sed -n '810,820p' frontend/src/Pages/OnlineDebateRoom.tsxRepository: 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.
| 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]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
|
|
||
| if (isTTSEnabledRef.current) { | ||
| speak(data.message); | ||
| } | ||
| setDebatePhase(data.phase); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| // 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. | ||
| } |
There was a problem hiding this comment.
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.
| // 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.
Closes #196
Changes made
Video
My.Movie.mp4
Summary by CodeRabbit
New Features
✏️ Tip: You can customize this high-level summary in your review settings.