Skip to content

Add VoicemailBox helper for greeting and caller recording #41

Description

@Justinabox

Motivation

The roadmap calls for a voicemail system, and the current voice stack already has most low-level primitives: CallSession.play(), CallSession.record(), WAV format validation, DTMF collection, and an auto-answer example in server.py. What is missing is a small, reusable voicemail helper that turns those primitives into a safe PBX building block: play a greeting, record caller audio, store metadata, and make tests deterministic without hardware.

This should be a one-PR slice focused on local voicemail capture, not a full dashboard or remote mailbox product.

User journey

  1. An operator configures a voicemail directory and greeting WAV.
  2. Their call handler delegates unanswered/after-hours calls to a voicemail box.
  3. Callstack plays the greeting, records up to a bounded duration, optionally stops on DTMF, and hangs up cleanly.
  4. A VoicemailMessage metadata record is returned so the app can show, upload, or notify about the saved message.
  5. File names and logs avoid embedding raw phone numbers.

API / UX sketch

from callstack.voice.voicemail import VoicemailBox

voicemail = VoicemailBox(
    directory="/var/lib/callstack/voicemail",
    greeting="audio/voicemail_greeting.wav",
    max_duration=120.0,
    stop_on_dtmf=True,
)

@modem.on_call
async def handle_call(session):
    message = await voicemail.record(session)
    print(f"Saved voicemail {message.id} at {message.audio_path}")

Optional later integration with pre-answer routing can send only rejected/after-hours calls to voicemail, but this first slice can work with the existing on_call flow.

Technical approach

  1. Add a new callstack/voice/voicemail.py module with:
    • VoicemailBox configuration: directory, greeting path, max duration, stop-on-DTMF, optional goodbye prompt.
    • VoicemailMessage dataclass: id, audio path, caller label/ID, started/ended timestamps, duration seconds, byte size, termination reason.
  2. Use existing CallSession.play() and CallSession.record() instead of duplicating audio transport code.
  3. Generate privacy-safe, collision-resistant filenames, for example timestamp + short random id. Do not include caller numbers in paths.
  4. Store metadata as a small JSON sidecar next to the WAV file for this first PR. A future issue can add SQLite/call-history storage if needed.
  5. Ensure directories are created with normal user permissions and errors are surfaced as explicit exceptions.
  6. Validate prompt WAVs through existing playback validation. Do not resample silently.
  7. When recording completes, optionally play a goodbye prompt, then hang up if the session is still active.
  8. Add unit tests with fake CallSession/AudioPipeline or MockTransport so no real modem/audio hardware is needed.

Affected modules and tests

Likely files:

  • Create: callstack/voice/voicemail.pyVoicemailBox, VoicemailMessage, metadata helpers.
  • Modify: callstack/voice/__init__.py and/or callstack/__init__.py — public exports if consistent with existing API style.
  • Create: tests/test_voicemail.py — greeting playback, recording path generation, JSON metadata, DTMF stop option, hangup behavior, error cases.
  • Optional modify: server.py only if replacing the demo greet-and-hangup with a tiny commented example; keep docs/examples minimal to avoid scope creep.

Existing context:

  • CallSession.record(output_path, max_duration, stop_on_dtmf) already records 8 kHz, 16-bit mono WAV via AudioPipeline.record().
  • AudioPlayer.validate() already rejects incompatible greeting WAV files.
  • server.py currently auto-answers and plays audio/greet.wav, then hangs up.

Hardware / modem caveats

  • SIMCOM-style PCM audio is currently assumed by AudioPipeline; voicemail should not introduce new modem-specific commands.
  • Audio quality and full-duplex behavior vary by modem/audio port. This issue only records what the existing audio pipeline receives.
  • DTMF stop depends on normalized DTMFEvent behavior; if Normalize quoted DTMF URC digits before IVR dispatch #37 is unresolved, tests should use normalized public events rather than raw URCs.
  • Do not require real calls, real SIMs, or phone numbers in tests.

Acceptance criteria

  • VoicemailBox.record(session) plays the configured greeting before recording.
  • Recording writes a WAV file under the configured directory using a privacy-safe filename.
  • A metadata JSON sidecar is written with message id, timestamps, duration, byte size, caller field, and termination reason.
  • Caller numbers are not included in filenames or default logs.
  • max_duration is enforced and stop_on_dtmf=True is passed through to the existing recording path.
  • If configured, a goodbye prompt plays after recording and before hangup.
  • The helper hangs up active sessions after voicemail capture, but does not fail if the caller already disconnected.
  • Tests cover success, invalid/missing greeting, directory creation, metadata contents, privacy-safe paths, and no-hardware operation.

Exact verification gates

git diff --check
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/test_voicemail.py tests/test_audio.py tests/test_call_service.py -q
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q

Non-goals

  • Browser dashboard or HTTP voicemail retrieval.
  • Speech-to-text transcription.
  • Remote storage/upload integrations.
  • SIP/PBX call transfer.
  • Durable SQLite call history beyond the local JSON sidecar.
  • Audio resampling or modem-family audio certification.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions