{"record":{"id":"4a1361cffdc1eb47","repo":"jamiepine/voicebox","slug":"failed-to-save-audio-to-path-e","errorCode":null,"errorMessage":"Failed to save audio to {path}: {e}","messagePattern":"Failed to save audio to (.+?): (.+?)","errorType":"exception","errorClass":"OSError","httpStatus":null,"severity":"error","filePath":"backend/utils/audio.py","lineNumber":110,"sourceCode":"        # Ensure parent directory exists\n        Path(path).parent.mkdir(parents=True, exist_ok=True)\n\n        # Write to temporary file first (explicit format since .tmp\n        # extension is not recognised by soundfile)\n        sf.write(temp_path, audio, sample_rate, format='WAV')\n\n        # Atomic rename to final path\n        os.replace(temp_path, path)\n\n    except Exception as e:\n        # Clean up temp file on failure\n        try:\n            if Path(temp_path).exists():\n                Path(temp_path).unlink()\n        except Exception:\n            pass  # Best effort cleanup\n\n        raise OSError(f\"Failed to save audio to {path}: {e}\") from e\n\n\ndef has_tts_runaway(\n    audio: np.ndarray,\n    sample_rate: int = 24000,\n    frame_ms: int = 20,\n    silence_threshold_db: float = -40.0,\n    max_internal_silence_ms: int = 2000,\n) -> bool:\n    \"\"\"Detect speech followed by a long silence and then more output.\n\n    This shape is a reliable signal that a TTS model missed EOS and resumed\n    with hallucinated speech or codec noise. Leading and trailing silence do\n    not count because they are not bounded by non-silent audio.\n    \"\"\"\n    frame_len = int(sample_rate * frame_ms / 1000)\n    if frame_len == 0 or len(audio) < frame_len:\n        return False","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/utils/audio.py#L92-L128","documentation":"Raised by save_audio() in backend/utils/audio.py when any exception occurs during the atomic write (mkdir, sf.write, or os.replace). The temp file is best-effort cleaned up and the original exception is chained. save_audio uses an atomic temp-then-rename strategy, so this typically indicates a filesystem-level failure rather than a partial/corrupt final file.","triggerScenarios":"Destination disk full; parent directory not writable (permissions); path on a read-only mount; soundfile/libsndfile cannot encode the array (dtype/shape mismatch); sample_rate invalid; path length / illegal characters causing OS errors during rename.","commonSituations":"Volume out of space during long synthesis runs; Docker container running as a UID without write permission to the mounted profiles dir; numpy array passed as int instead of float32 that libsndfile rejects; path on a network mount that dropped.","solutions":["Free disk space on the destination volume and retry.","Check write permissions on Path(path).parent for the running process UID.","Ensure the audio array is a float32 numpy array and sample_rate is a positive int before calling save_audio.","Verify the storage mount is healthy (not read-only / disconnected).","If the underlying error is a libsndfile encoding issue, convert the array dtype: audio = audio.astype(np.float32)."],"exampleFix":"// before: OSError because array is int16 and libsndfile rejects it\nsave_audio(audio, path, 24000)\n// after: normalize dtype and confirm disk space\nimport numpy as np\naudio = np.asarray(audio, dtype=np.float32)\nsave_audio(audio, path, 24000)","handlingStrategy":"try-catch","validationCode":"from pathlib import Path\nimport numpy as np\n\ndef can_save_audio(path: str, audio) -> bool:\n    parent = Path(path).parent\n    try:\n        parent.mkdir(parents=True, exist_ok=True)\n    except OSError:\n        return False\n    if not os.access(parent, os.W_OK):\n        return False\n    return isinstance(audio, np.ndarray) and audio.dtype == np.float32","typeGuard":"import numpy as np\n\ndef is_writable_audio_array(audio) -> bool:\n    return isinstance(audio, np.ndarray) and audio.dtype == np.float32","tryCatchPattern":"try:\n    save_audio(audio, path, sample_rate)\nexcept OSError as e:\n    logger.error('save_audio failed for %s: %s', path, e)\n    raise HTTPException(507, f'Could not persist audio: {e}')","preventionTips":["Run the process under a UID with write permission to the storage volume.","Monitor free disk space and alert before exhaustion during long synthesis runs.","Normalize audio arrays to float32 before calling save_audio.","Validate Path(path).parent is writable at startup."],"tags":["audio","filesystem","save","voicebox"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}