fix: lang overflows, sst (#2314)

This commit is contained in:
Alex authored and GitHub committed 2026-03-18 14:50:29 +00:00
1 parent 5894e47189
commit 5ab43fd12c
5 files changed
+194 -15

No files matched your search

+1 -1
View File
@@ -159,7 +159,7 @@ class Settings(BaseSettings):
STT_PROVIDER: str = "openai" # openai or faster_whisper
OPENAI_STT_MODEL: str = "gpt-4o-mini-transcribe"
STT_LANGUAGE: Optional[str] = None
STT_MAX_FILE_SIZE_MB: int = 25
STT_MAX_FILE_SIZE_MB: int = 50
STT_ENABLE_TIMESTAMPS: bool = False
STT_ENABLE_DIARIZATION: bool = False
+2 -3
View File
@@ -111,7 +111,7 @@ For an end-to-end walkthrough, see the [Speech and Audio Guide](/Guides/speech-a
| `STT_PROVIDER` | Speech-to-text backend provider. | `openai`, `faster_whisper` |
| `OPENAI_STT_MODEL` | OpenAI transcription model used when `STT_PROVIDER=openai`. | `gpt-4o-mini-transcribe` |
| `STT_LANGUAGE` | Optional language hint passed to the provider. Leave unset for auto-detection when supported. | `en`, `es`, unset |
| `STT_MAX_FILE_SIZE_MB` | Maximum file size accepted by the synchronous `/api/stt` endpoint. | `25` |
| `STT_MAX_FILE_SIZE_MB` | Maximum file size accepted by the synchronous `/api/stt` endpoint. | `50` |
| `STT_ENABLE_TIMESTAMPS` | Include timestamp segments in the normalized transcript response and stored parser metadata. | `true`, `false` |
| `STT_ENABLE_DIARIZATION` | Reserved provider option for speaker diarization. Some providers may ignore it. | `true`, `false` |
@@ -122,7 +122,7 @@ STT_PROVIDER=openai
OPENAI_API_KEY=YOUR_OPENAI_API_KEY
OPENAI_STT_MODEL=gpt-4o-mini-transcribe
STT_LANGUAGE=
STT_MAX_FILE_SIZE_MB=25
STT_MAX_FILE_SIZE_MB=50
STT_ENABLE_TIMESTAMPS=false
STT_ENABLE_DIARIZATION=false
```
@@ -224,4 +224,3 @@ These are just the basic settings to get you started. The `settings.py` file con
- And many more!
For a complete list of available settings and their descriptions, refer to the `settings.py` file in `application/core`. Remember to restart your Docker containers after making changes to your `.env` file or `settings.py` for the changes to take effect.
+4 -4
View File
@@ -711,7 +711,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
{modeConfig[effectiveMode].showSaveDraft && (
<button
disabled={isJsonSchemaInvalid()}
className={`border-violets-are-blue text-violets-are-blue hover:bg-violets-are-blue w-28 rounded-3xl border border-solid py-2 text-sm font-medium transition-colors hover:text-white ${
className={`border-violets-are-blue text-violets-are-blue hover:bg-violets-are-blue flex min-w-28 items-center justify-center rounded-3xl border border-solid px-5 py-2 text-sm font-medium whitespace-nowrap transition-colors hover:text-white ${
isJsonSchemaInvalid() ? 'cursor-not-allowed opacity-30' : ''
}`}
onClick={handleSaveDraft}
@@ -744,7 +744,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
)}
<button
disabled={!isPublishable() || !hasChanges}
className={`${!isPublishable() || !hasChanges ? 'cursor-not-allowed opacity-30' : ''} bg-purple-30 hover:bg-violets-are-blue flex w-28 items-center justify-center rounded-3xl py-2 text-sm font-medium text-white`}
className={`${!isPublishable() || !hasChanges ? 'cursor-not-allowed opacity-30' : ''} bg-purple-30 hover:bg-violets-are-blue flex min-w-28 items-center justify-center rounded-3xl px-5 py-2 text-sm font-medium whitespace-nowrap text-white`}
onClick={handlePublish}
>
<span className="flex items-center justify-center transition-all duration-200">
@@ -933,7 +933,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
/>
</div>
<button
className="border-violets-are-blue text-violets-are-blue hover:bg-violets-are-blue w-20 shrink-0 basis-full rounded-3xl border-2 border-solid px-5 py-[11px] text-sm transition-colors hover:text-white sm:basis-auto"
className="border-violets-are-blue text-violets-are-blue hover:bg-violets-are-blue min-w-20 shrink-0 basis-full rounded-3xl border-2 border-solid px-5 py-[11px] text-sm whitespace-nowrap transition-colors hover:text-white sm:basis-auto"
onClick={() => setAddPromptModal('ACTIVE')}
>
{t('agents.form.buttons.add')}
@@ -1405,4 +1405,4 @@ function AddPromptModal({
handleAddPrompt={handleAddPrompt}
/>
);
}
}
+179 -7
View File
@@ -34,6 +34,7 @@ import SourcesPopup from './SourcesPopup';
import ToolsPopup from './ToolsPopup';
import { handleAbort } from '../conversation/conversationSlice';
import {
AUDIO_FILE_ACCEPT_ATTR,
FILE_UPLOAD_ACCEPT,
FILE_UPLOAD_ACCEPT_ATTR,
} from '../constants/fileUpload';
@@ -54,6 +55,24 @@ type AudioContextWindow = Window &
webkitAudioContext?: typeof AudioContext;
};
type LegacyNavigator = Navigator & {
getUserMedia?: (
constraints: MediaStreamConstraints,
successCallback: (stream: MediaStream) => void,
errorCallback: (error: DOMException) => void,
) => void;
webkitGetUserMedia?: (
constraints: MediaStreamConstraints,
successCallback: (stream: MediaStream) => void,
errorCallback: (error: DOMException) => void,
) => void;
mozGetUserMedia?: (
constraints: MediaStreamConstraints,
successCallback: (stream: MediaStream) => void,
errorCallback: (error: DOMException) => void,
) => void;
};
type LiveAudioSnapshot = {
blob: Blob;
chunkIndex: number;
@@ -69,6 +88,90 @@ const getAudioContextConstructor = (): typeof AudioContext | null => {
return audioWindow.AudioContext || audioWindow.webkitAudioContext || null;
};
const getLegacyGetUserMedia = () => {
if (typeof navigator === 'undefined') {
return null;
}
const legacyNavigator = navigator as LegacyNavigator;
return (
legacyNavigator.getUserMedia ||
legacyNavigator.webkitGetUserMedia ||
legacyNavigator.mozGetUserMedia ||
null
);
};
const getVoiceInputSupportError = (): string | null => {
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
return 'Voice input is unavailable right now.';
}
if (!window.isSecureContext) {
return 'Voice input requires a secure connection (HTTPS or localhost).';
}
if (!navigator.mediaDevices?.getUserMedia && !getLegacyGetUserMedia()) {
return 'Voice input is not available in this browser.';
}
if (!getAudioContextConstructor()) {
return 'Voice input requires Web Audio support in this browser.';
}
return null;
};
const getUserMediaStream = (
constraints: MediaStreamConstraints,
): Promise<MediaStream> => {
if (navigator.mediaDevices?.getUserMedia) {
return navigator.mediaDevices.getUserMedia(constraints);
}
const legacyGetUserMedia = getLegacyGetUserMedia();
if (!legacyGetUserMedia) {
return Promise.reject(
new Error('Voice input is not available in this browser.'),
);
}
return new Promise((resolve, reject) => {
legacyGetUserMedia.call(navigator, constraints, resolve, reject);
});
};
const getVoiceInputErrorMessage = (error: unknown): string => {
if (typeof window !== 'undefined' && !window.isSecureContext) {
return 'Voice input requires a secure connection (HTTPS or localhost).';
}
if (error instanceof DOMException) {
switch (error.name) {
case 'NotAllowedError':
case 'PermissionDeniedError':
case 'SecurityError':
return 'Microphone access was blocked. Allow microphone permission and try again.';
case 'NotFoundError':
case 'DevicesNotFoundError':
return 'No microphone was found on this device.';
case 'NotReadableError':
case 'TrackStartError':
return 'The microphone is unavailable or already in use.';
case 'AbortError':
return 'Microphone access was interrupted before recording started.';
default:
break;
}
}
if (error instanceof Error && error.message) {
return error.message;
}
return 'Microphone access was denied.';
};
const downsampleFloat32Buffer = (
source: Float32Array,
inputSampleRate: number,
@@ -197,6 +300,7 @@ export default function MessageInput({
const { t } = useTranslation();
const [value, setValue] = useState('');
const inputRef = useRef<HTMLTextAreaElement>(null);
const voiceFileInputRef = useRef<HTMLInputElement>(null);
const sourceButtonRef = useRef<HTMLButtonElement>(null);
const toolButtonRef = useRef<HTMLButtonElement>(null);
const [isSourcesPopupOpen, setIsSourcesPopupOpen] = useState(false);
@@ -808,6 +912,48 @@ export default function MessageInput({
}, 0);
};
const promptVoiceFileFallback = (message: string) => {
setRecordingState('idle');
setVoiceError(`${message} Choose or record an audio file instead.`);
setTimeout(() => {
voiceFileInputRef.current?.click();
}, 0);
};
const transcribeUploadedAudioFile = async (file: File) => {
try {
setVoiceError(null);
setRecordingState('transcribing');
voiceBaseValueRef.current = value;
liveTranscriptRef.current = '';
const response = await userService.transcribeAudio(file, token);
const data = await response.json();
if (!response.ok || !data?.success) {
throw new Error(data?.message || 'Failed to transcribe audio.');
}
if (typeof data.text !== 'string' || !data.text.trim()) {
throw new Error('No transcript was returned for this audio file.');
}
applyLiveTranscript(data.text);
setRecordingState('idle');
if (autoFocus) {
setTimeout(() => {
inputRef.current?.focus();
}, 0);
}
} catch (error) {
console.error('Uploaded audio transcription failed', error);
setRecordingState('error');
setVoiceError(
error instanceof Error ? error.message : 'Failed to transcribe audio.',
);
}
};
const trimLivePcmBuffer = () => {
const maxBufferedSamples =
LIVE_CAPTURE_SAMPLE_RATE * LIVE_CAPTURE_MAX_BUFFER_SECONDS;
@@ -1024,24 +1170,29 @@ export default function MessageInput({
return;
}
if (!navigator.mediaDevices?.getUserMedia) {
setRecordingState('error');
setVoiceError('Voice input is not supported in this browser.');
const voiceInputSupportError = getVoiceInputSupportError();
if (voiceInputSupportError) {
promptVoiceFileFallback(voiceInputSupportError);
return;
}
const AudioContextConstructor = getAudioContextConstructor();
if (!AudioContextConstructor) {
setRecordingState('error');
setVoiceError('Voice input is not supported in this browser.');
setVoiceError('Voice input requires Web Audio support in this browser.');
return;
}
let stream: MediaStream | null = null;
try {
setVoiceError(null);
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream = await getUserMediaStream({ audio: true });
} catch (error) {
promptVoiceFileFallback(getVoiceInputErrorMessage(error));
return;
}
try {
const liveStartResponse = await userService.startLiveTranscription(token);
const liveStartData = await liveStartResponse.json();
if (!liveStartResponse.ok || !liveStartData?.success) {
@@ -1121,7 +1272,7 @@ export default function MessageInput({
setRecordingState('recording');
} catch (error) {
console.error('Microphone access failed', error);
console.error('Live voice transcription failed', error);
stream?.getTracks().forEach((track) => track.stop());
stopAudioProcessing();
await cleanupLiveSession();
@@ -1130,7 +1281,7 @@ export default function MessageInput({
setVoiceError(
error instanceof Error
? error.message
: 'Microphone access was denied.',
: 'Failed to start live transcription.',
);
}
};
@@ -1186,6 +1337,19 @@ export default function MessageInput({
}
};
const handleVoiceFileAttachment = (
e: React.ChangeEvent<HTMLInputElement>,
) => {
const file = e.target.files?.[0];
e.target.value = '';
if (!file) {
return;
}
void transcribeUploadedAudioFile(file);
};
const handlePostDocumentSelect = (_docs: Doc[] | null) => {
// SourcesPopup updates Redux selection directly; this preserves the prop contract.
void _docs;
@@ -1265,6 +1429,14 @@ export default function MessageInput({
<div {...getRootProps()} className="flex w-full flex-col">
{/* react-dropzone input (for drag/drop) */}
<input {...getInputProps()} />
<input
ref={voiceFileInputRef}
type="file"
className="hidden"
accept={AUDIO_FILE_ACCEPT_ATTR}
capture="user"
onChange={handleVoiceFileAttachment}
/>
<div className="border-dark-gray bg-lotion dark:border-grey relative flex w-full flex-col rounded-[23px] border dark:bg-transparent">
<div className="flex flex-wrap gap-1.5 px-2 py-2 sm:gap-2 sm:px-3">
+8
View File
@@ -55,6 +55,14 @@ export const FILE_UPLOAD_ACCEPT_ATTR = [
'.webm',
].join(',');
export const AUDIO_FILE_ACCEPT_ATTR = [
'.wav',
'.mp3',
'.m4a',
'.ogg',
'.webm',
].join(',');
export const SOURCE_FILE_TREE_ACCEPT_ATTR = [
'.rst',
'.md',