diff --git a/app/src/components/AudioPlayer/AudioPlayer.tsx b/app/src/components/AudioPlayer/AudioPlayer.tsx
index 48dd9e78..1c398ed8 100644
--- a/app/src/components/AudioPlayer/AudioPlayer.tsx
+++ b/app/src/components/AudioPlayer/AudioPlayer.tsx
@@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import { Pause, Play, Repeat, Volume2, VolumeX, X } from 'lucide-react';
-import { useEffect, useMemo, useRef, useState } from 'react';
+import { useEffect, useId, useMemo, useRef, useState } from 'react';
import WaveSurfer from 'wavesurfer.js';
import { Button } from '@/components/ui/button';
import { Slider } from '@/components/ui/slider';
@@ -12,6 +12,7 @@ import { usePlatform } from '@/platform/PlatformContext';
export function AudioPlayer() {
const platform = usePlatform();
+ const volumeLabelId = useId();
const {
audioUrl,
audioId,
@@ -831,6 +832,13 @@ export function AudioPlayer() {
disabled={isLoading || duration === 0}
className="shrink-0"
title={duration === 0 && !isLoading ? 'Audio not loaded' : ''}
+ aria-label={
+ duration === 0 && !isLoading
+ ? 'Audio not loaded'
+ : isPlaying
+ ? 'Pause'
+ : 'Play'
+ }
>
{isPlaying ? : }
@@ -845,6 +853,8 @@ export function AudioPlayer() {
max={100}
step={0.1}
className="w-full"
+ aria-label="Playback position"
+ aria-valuetext={`${formatAudioDuration(currentTime)} of ${formatAudioDuration(duration)}`}
/>
)}
{isLoading && (
@@ -872,26 +882,33 @@ export function AudioPlayer() {
onClick={toggleLoop}
className={isLooping ? 'text-primary' : ''}
title="Toggle loop"
+ aria-label={isLooping ? 'Stop looping' : 'Loop'}
>
{/* Volume Control */}
-
+
+
+ Volume level, {Math.round(volume * 100)}%
+
@@ -902,6 +919,7 @@ export function AudioPlayer() {
onClick={handleClose}
className="shrink-0"
title="Close player"
+ aria-label="Close player"
>
diff --git a/app/src/components/Generation/FloatingGenerateBox.tsx b/app/src/components/Generation/FloatingGenerateBox.tsx
index a8d556a6..0461e58b 100644
--- a/app/src/components/Generation/FloatingGenerateBox.tsx
+++ b/app/src/components/Generation/FloatingGenerateBox.tsx
@@ -300,6 +300,13 @@ export function FloatingGenerateBox({
disabled={isPending || !selectedProfileId}
className="h-10 w-10 rounded-full bg-accent hover:bg-accent/90 hover:scale-105 text-accent-foreground shadow-lg hover:shadow-accent/50 transition-all duration-200"
size="icon"
+ aria-label={
+ isPending
+ ? 'Generating...'
+ : !selectedProfileId
+ ? 'Select a voice profile first'
+ : 'Generate speech'
+ }
>
{isPending ? (
@@ -336,6 +343,11 @@ export function FloatingGenerateBox({
? 'bg-accent text-accent-foreground border border-accent hover:bg-accent/90'
: 'bg-card border border-border hover:bg-background/50',
)}
+ aria-label={
+ isInstructMode
+ ? 'Fine tune instructions, on'
+ : 'Fine tune instructions'
+ }
>
diff --git a/app/src/components/History/HistoryTable.tsx b/app/src/components/History/HistoryTable.tsx
index e572f69e..74f722b7 100644
--- a/app/src/components/History/HistoryTable.tsx
+++ b/app/src/components/History/HistoryTable.tsx
@@ -253,10 +253,17 @@ export function HistoryTable() {
return (
{
// Don't trigger play if clicking on textarea or if text is selected
const target = e.target as HTMLElement;
@@ -265,6 +272,14 @@ export function HistoryTable() {
}
handlePlay(gen.id, gen.text, gen.profile_id);
}}
+ onKeyDown={(e) => {
+ const target = e.target as HTMLElement;
+ if (target.closest('textarea') || target.closest('button')) return;
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ handlePlay(gen.id, gen.text, gen.profile_id);
+ }
+ }}
>
{/* Waveform icon */}
@@ -293,6 +308,7 @@ export function HistoryTable() {
value={gen.text}
className="flex-1 resize-none text-sm text-muted-foreground select-text"
readOnly
+ aria-label={`Transcript for sample from ${gen.profile_name}, ${formatDuration(gen.duration)}`}
/>
diff --git a/app/src/components/ServerSettings/ConnectionForm.tsx b/app/src/components/ServerSettings/ConnectionForm.tsx
index 9e25a52d..0f56555c 100644
--- a/app/src/components/ServerSettings/ConnectionForm.tsx
+++ b/app/src/components/ServerSettings/ConnectionForm.tsx
@@ -57,7 +57,11 @@ export function ConnectionForm() {
}
return (
-
+
Server Connection
diff --git a/app/src/components/ServerSettings/ModelManagement.tsx b/app/src/components/ServerSettings/ModelManagement.tsx
index 4a5fd439..a8d9e284 100644
--- a/app/src/components/ServerSettings/ModelManagement.tsx
+++ b/app/src/components/ServerSettings/ModelManagement.tsx
@@ -278,9 +278,25 @@ interface ModelItemProps {
function ModelItem({ model, onDownload, onDelete, isDownloading, formatSize }: ModelItemProps) {
// Use server's downloading state OR local state (for immediate feedback before server updates)
const showDownloading = model.downloading || isDownloading;
-
+
+ const statusText = model.loaded
+ ? 'Loaded'
+ : showDownloading
+ ? 'Downloading'
+ : model.downloaded
+ ? 'Downloaded'
+ : 'Not downloaded';
+ const sizeText =
+ model.downloaded && model.size_mb && !showDownloading ? `, ${formatSize(model.size_mb)}` : '';
+ const rowLabel = `${model.display_name}, ${statusText}${sizeText}. Use Tab to reach Download or Delete.`;
+
return (
-
+
{model.display_name}
@@ -314,17 +330,27 @@ function ModelItem({ model, onDownload, onDelete, isDownloading, formatSize }: M
variant="outline"
disabled={model.loaded}
title={model.loaded ? 'Unload model before deleting' : 'Delete model'}
+ aria-label={
+ model.loaded
+ ? 'Unload model before deleting'
+ : `Delete ${model.display_name}`
+ }
>
) : showDownloading ? (
-