{"record":{"id":"83f276fe7ac918e3","repo":"mudler/LocalAI","slug":"voicecreate-audio-durationerror","errorCode":null,"errorMessage":"voiceCreate.audio.durationError","messagePattern":"voiceCreate\\.audio\\.durationError","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"core/http/react-ui/src/pages/VoiceProfileCreate.jsx","lineNumber":100,"sourceCode":"    quality: !!audio && durationRecommended,\n    name: !!name.trim(),\n    transcript: !!transcript.trim(),\n    consent,\n  }), [audio, durationValid, durationRecommended, name, transcript, consent])\n\n  const handleAudio = async (sample) => {\n    if (!sample) {\n      setAudio(null)\n      setAudioError('')\n      return\n    }\n    setAudioProcessing(true)\n    setAudioError('')\n    try {\n      const normalized = await normalizeAudioSample(sample)\n      if (normalized.duration < 1 || normalized.duration > 120) {\n        if (normalized.objectUrl) URL.revokeObjectURL(normalized.dataUrl)\n        throw new Error(t('voiceCreate.audio.durationError'))\n      }\n      setAudio(normalized)\n    } catch (err) {\n      setAudio(null)\n      setAudioError(err.message || t('voiceCreate.audio.decodeError'))\n    } finally {\n      setAudioProcessing(false)\n    }\n  }\n\n  const submit = async (event) => {\n    event.preventDefault()\n    if (!formReady) return\n    setSubmitting(true)\n    try {\n      const formData = new FormData()\n      formData.append('name', name.trim())\n      formData.append('description', description.trim())","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/mudler/LocalAI/blob/44413a9d06bf5bc52ce088ba8ca74e5a2e8bee26/core/http/react-ui/src/pages/VoiceProfileCreate.jsx#L82-L118","documentation":"Thrown by handleAudio() when the normalized sample's duration falls outside [1, 120] seconds. The message is the i18n key 'voiceCreate.audio.durationError' itself (t() lookup), so the user sees the localized string while the error string stored in audioError is the key. The objectUrl created during normalization is revoked before throwing to avoid leaking the blob.","triggerScenarios":"Selecting a recording shorter than 1 s (mic blip, immediate stop) or longer than 120 s (reading a long passage); also a decode that reports a bogus near-zero duration for a corrupt file.","commonSituations":"User taps record and stops instantly; user uploads a full song or lengthy narration instead of a short sample; truncated/corrupt upload where decodeAudioData yields a tiny duration.","solutions":["Record a sample between 1 and 120 seconds (a few sentences is typical)","If uploading, trim the file to that window first","If the duration looks wrong for a valid file, re-encode to a standard format (WAV/MP3) and retry"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Check duration bounds before accepting the sample\nfunction durationWithinBounds(d) {\n  return Number.isFinite(d) && d >= 1 && d <= 120\n}","typeGuard":null,"tryCatchPattern":"// Errors here are expected user-input corrections: render inline, never as toasts\ntry {\n  const normalized = await normalizeAudioSample(sample)\n  if (!durationWithinBounds(normalized.duration)) {\n    if (normalized.objectUrl) URL.revokeObjectURL(normalized.dataUrl)\n    throw new Error(t('voiceCreate.audio.durationError'))\n  }\n  setAudio(normalized)\n} catch (err) {\n  setAudio(null)\n  setAudioError(err.message || t('voiceCreate.audio.decodeError'))\n}","preventionTips":["Show the target duration (1–120 s) in the recorder UI before the user records","Validate duration immediately after decode so feedback is instant","Note the error string stored is the i18n key — keep t() lookups in render paths, not in stored state, if you log it"],"tags":["validation","audio","duration","i18n"],"backgroundTag":null,"analyzedSha":"44413a9d06bf5bc52ce088ba8ca74e5a2e8bee26","analyzedAt":"2026-08-15T10:13:50.291Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}