# Valence SDK for Emotion Detection

**valenceai** is a Python client library for interacting with the [Valence AI](https://getvalenceai.com) APIs for emotion detection. It provides a convenient interface to upload audio files, stream real-time audio, and retrieve detected emotional states.

## Features

- **Discrete audio processing** - Real-time analysis for short audio clips
- **Asynch audio processing** - Multipart parallel upload for long audio files with temporal emotion analysis
- **Streaming API** - Real-time WebSocket streaming for live audio
- **Rate limiting** - Monitor API usage and limits
- **Environment configuration** - Built-in support for environment variables
- **Enhanced logging** - Configurable log levels

The emotional classification model used in our APIs is optimized for North American English conversational data. The included model detects four emotions: angry, happy, neutral, and sad. _New models coming soon_.

## API Overview

| API | Best For | Input | Output |
|-----|----------|-------|--------|
| **Discrete** | Real-time analysis | Short audio (4.5-10s) | Single emotion prediction |
| **Asynch** | Pre-recorded files | Long audio (up to 1GB) | Timeline with emotion changes |
| **Streaming** | Live audio streams | Audio chunks via WebSocket | Real-time emotion updates |

The **DiscreteAPI** is built for real-time analysis of emotions in audio data. Small snippets of audio are sent to the API to receive feedback in real-time of what emotions are detected based on tone of voice. This API operates on an approximate per-sentence basis, and audio must be cut to the appropriate size.

The **AsynchAPI** is built for emotion analysis of pre-recorded audio files. Files of any length, up to 1 GB in size, can be sent to the API to receive a timeline of emotions throughout the file. 

The **StreamingAPI** is built for real-time audio analysis via WebSocket connections. The audio stream is analyzed in real-time and emotions are returned in reference to 5-second chunks of streamed audio.

## Audio Input Requirements

### Format Specifications

- **Format**: WAV only
- **Recommended sampling rate**: 44.1 kHz (44100 Hz)
- **Minimum sampling rate**: 8 kHz
- **Channel**: Mono (single channel)

### API-Specific Requirements

- **Discrete API**: Minimum 4.5 seconds per file, maximum 15 seconds. 5-10 seconds recommended.
- **Asynch API**: Minimum 5 seconds, maximum 1 GB
- **Streaming API**: Real-time audio chunks (PCM bytes)

For inquiries about custom microphone specifications or stereo/multi-channel support, please [contact us](https://www.getvalenceai.com/contact).

## Installation

```bash
pip install valenceai
```

## Configuration

You can configure the SDK using environment variables or by passing parameters directly:

### Environment Variables

```bash
export VALENCE_API_KEY="api_key_here"
export VALENCE_API_BASE_URL="https://api.getvalenceai.com" # Optional
export VALENCE_WEBSOCKET_URL="wss://api.getvalenceai.com" # Optional
export VALENCE_LOG_LEVEL="INFO"  # Optional: INFO, DEBUG, WARNING, ERROR
```

### Client Configuration

```python
client = ValenceClient(
    api_key="your_api_key",           # API key (required)
    base_url="https://custom.api",    # Custom API endpoint (optional)
    websocket_url="wss://custom.api", # Custom WebSocket endpoint (optional)
    part_size=5*1024*1024,            # Upload chunk size (default: 5MB)
    show_progress=True,               # Show upload progress (default: True)
    max_threads=3,                    # Concurrent upload threads (default: 3)
    comprehensive_output=False        # When False: asynch API returns timestamp, main_emotion, confidence only. When True: also includes all_predictions with all emotion confidences (default: False)
)
```

## Asynch API Processing Workflow

The Asynch API uses a multi-step process to handle long audio files. Understanding this workflow is crucial for proper implementation:

### 1. Upload Phase (Client-Side)

When you call `client.asynch.upload(file_path)`:

- SDK splits your file into parts (5MB chunks by default)
- Uploads parts in parallel
- **Returns a `request_id`** - This is a tracking identifier, not a completion signal.
- At this point: File is uploaded to our server, but _NOT processed yet_.

### 2. Background Processing (Server-Side)

After upload completes, the server automatically:

- Checks for new uploads
- Downloads audio when a new File is detected
- Splits audio into 5-second segments
- Processes audio file
- Invokes machine learning model for emotion detection
- Stores results in database
- Updates status to `completed`

**Processing Time**: Varies based on file length and server load. Typically 1-5 seconds per minute of audio. Upload time varies based on your network speed.

### 3. Results Retrieval (Client-Side)

When you call `client.asynch.emotions(request_id)`:

- Polls the status endpoint at regular intervals
- Waits for status progression:
  - `initiated` → Upload started
  - `upload_completed` → File uploaded (processing not started)
  - `processing` → Background processing in progress
  - `completed` → Results ready
- Returns emotion timeline when status is `completed`

### Status Values

| Status | Meaning | What's Happening |
|--------|---------|------------------|
| `initiated` | Upload started | SDK is uploading file in parts |
| `upload_completed` | Upload finished | File is waiting for background processor |
| `processing` | Processing active | Server is analyzing audio |
| `completed` | Results ready | Emotion timeline is available |

### Important Notes

- The `request_id` is NOT a completion indicator. It's a request tracking ID.
- `upload()` completing does not mean results are ready. It means the file is uploaded.
- Background processing takes time. Processing time varies based on file length and server load.
- You can check status anytime. The `request_id` remains valid for retrieving results until databases are cleared (see: [DPA](https://getvalenceai.com/legal/data-processing) for more information on data retention policies).

## Quick Start

```python
from valenceai import ValenceClient

# Initialize client (uses VALENCE_API_KEY environment variable)
client = ValenceClient(api_key="your_api_key", comprehensive_output=True)

# Discrete API - Quick emotion detection
result = client.discrete.emotions(file_path="short_audio.wav")
print(f"Emotion: {result['main_emotion']}")

# Asynch API - Long audio with timeline
# Step 1: Upload file (returns tracking ID)
request_id = client.asynch.upload("long_audio.wav")
# Step 2: Wait for server processing and get results (polls until complete)
result = client.asynch.emotions(request_id, max_attempts=30, interval_seconds=10)
# Step 3: Access emotion data from results
emotions = result['emotions']  # List of emotion predictions with timestamps

# Get summary statistics
majority = client.asynch.majority_emotion(request_id)  # Most frequent emotion
counts = client.asynch.emotion_counts(request_id)  # {"happy": 10, "sad": 3, ...}

# Streaming API - Real-time audio
stream = client.streaming.connect()
stream.on_prediction(lambda data: print(data['main_emotion']))
stream.connect()
stream.send_audio(audio_bytes)
stream.disconnect()

# Rate Limit API - Monitor usage
status = client.rate_limit.get_status()
health = client.rate_limit.get_health()
```

## API Reference

### Discrete API

For short audio files requiring immediate emotion detection.

```python
# Direct file upload
result = client.discrete.emotions(
    file_path="audio.wav",
)

# Upload via in-memory audio array
result = client.discrete.emotions(
    audio_array=[0.17278, 0.23738, 0.37912, ...],
)
```

**Response:**
```python
{
    "emotions": {
        "happy": 0.78,
        "sad": 0.12,
        "angry": 0.08,
        "neutral": 0.15
    },
    "main_emotion": "happy"
}
```

### Asynch API

For long audio files with timeline analysis.

**Status Progression**: `initiated` → `upload_completed` → `processing` → `completed`

#### Upload Audio

```python
# Upload file
request_id = client.asynch.upload(file_path="long_audio.wav")
```

**Note**: The SDK automatically validates file size before upload. If the file exceeds the maximum allowed size of 1GB, a `FileSizeLimitExceededError` is raised without attempting the upload.

#### Get Emotion Results

```python
# Poll for results until processing completes
result = client.asynch.emotions(
    request_id="abc-123abc-123abc-123abc-123abc-123",
    max_attempts=20,        # Max polling attempts (default: 20, range: 1-100)
    interval_seconds=5      # Polling interval (default: 5, range: 1-60)
)
# This method waits for server processing to complete
# Returns when status is 'completed'
```

**Response:**
```python
{
    "emotions": [
        {
            "timestamp": 0.5,
            "start_time": 0.0,
            "end_time": 1.0,
            "emotion": "happy",
            "confidence": 0.9,
            "all_predictions": {"happy": 0.9, "sad": 0.1, ...}
        },
        {
            "timestamp": 1.5,
            "start_time": 1.0,
            "end_time": 2.0,
            "emotion": "neutral",
            "confidence": 0.85,
            "all_predictions": {"neutral": 0.85, "happy": 0.15, ...}
        }
    ],
    "status": "completed"
}
```

Note: The `all_predictions` field is only included when `comprehensive_output=True` is set in the client constructor.

#### Helper Methods

```python
# Get the most frequently occurring emotion across the entire file
majority = client.asynch.majority_emotion(request_id)
# Returns: "happy"

# Get emotion occurrence counts for the entire file
counts = client.asynch.emotion_counts(request_id)
# Returns: {"happy": 10, "sad": 3, "angry": 8, "neutral": 9}
```

### Streaming API

For real-time emotion detection on live audio streams.

```python
# Create streaming connection
stream = client.streaming.connect()

# Register callbacks
stream.on_prediction(lambda data: print(f"Emotion: {data['main_emotion']}"))
stream.on_error(lambda error: print(f"Error: {error}"))
stream.on_connected(lambda info: print(f"Connected: {info['session_id']}"))

# Connect to WebSocket
stream.connect()

# Send audio chunks (PCM bytes)
stream.send_audio(audio_chunk_bytes)

# Check connection status
if stream.connected:
    print("Streaming active")

# Wait for events (blocking)
stream.wait()

# Disconnect
stream.disconnect()
```

**Prediction Event:**
```python
{
    "main_emotion": "happy",
    "confidence": 0.87,
    "all_predictions": {
        "happy": 0.87,
        "sad": 0.05,
        "angry": 0.03,
        "neutral": 0.15
    },
    "timestamp": 1706486400000  # Unix timestamp (UTC) in milliseconds
}
```

The `timestamp` is a Unix timestamp (UTC) in milliseconds representing when the server generated the prediction.

### Rate Limit API

Monitor your API usage and limits.

```python
# Get rate limit status
status = client.rate_limit.get_status()
print(status)
# {
#     "policy_name": "standard_policy",
#     "limits": {
#         "requests_per_second": 10,
#         "requests_per_minute": 100,
#         "requests_per_hour": 1000,
#         "requests_per_day": 10000,
#         "burst_limit": 20,
#         "max_audio_size_mb": 50,           # Maximum file size in MB
#         "max_audio_duration_seconds": 300, # Maximum audio duration
#         "max_concurrent_requests": 5
#     },
#     "current_usage": {
#         "requests_per_second": 2,
#         "rejected_per_second": 0,
#         "total_audio_size_bytes_per_second": 1048576,
#         "requests_per_minute": 15,
#         "rejected_per_minute": 0,
#         "total_audio_size_bytes_per_minute": 15728640
#         # ... usage for hour and day
#     }
# }

# Check API health
health = client.rate_limit.get_health()
print(health)
# {"status": "healthy", "timestamp": 1738684800}
```

The `reset` and `timestamp` values are Unix timestamps (UTC) in seconds.

## Error Responses

### Discrete API Errors

| HTTP Status | Error Code | Description |
|-------------|------------|-------------|
| 400 | `AUDIO_TOO_SHORT` | Audio duration below minimum (4.5 seconds). Response includes `min_duration_seconds` and `actual_duration_seconds` |
| 400 | `AUDIO_TOO_LONG` | Audio duration above maximum (15 seconds). Response includes `max_duration_seconds` and `actual_duration_seconds` |
| 400 | Bad Request | Invalid request format or parameters |
| 401 | Unauthorized | Invalid or missing API key |
| 500 | Server Error | Internal server error |

### Asynch API Errors

| HTTP Status | Error Code | Description |
|-------------|------------|-------------|
| 400 | `AUDIO_TOO_SHORT` | Audio duration below minimum (5 seconds) |
| 400 | `FILE_SIZE_LIMIT_EXCEEDED` | File size exceeds rate limit policy maximum. Raised before upload attempt |
| 400 | `FILE_TOO_LARGE` | File exceeds maximum upload size (1 GB). Response includes `max_file_size_bytes` and `actual_file_size_bytes` |
| 400 | Bad Request | Invalid request format or parameters |
| 401 | Unauthorized | Invalid or missing API key |
| 404 | Not Found | Request ID not found |
| 500 | Server Error | Internal server error |

**Asynch Status Values:**

| Status | Meaning |
|--------|---------|
| `initiated` | Upload in progress |
| `upload_completed` | Upload finished, awaiting processing |
| `processing` | Server analyzing audio |
| `completed` | Results ready |
| `failed` | Processing failed |

### Streaming API Errors

| Event | Description |
|-------|-------------|
| `error` | Server-side error during streaming |
| `warning` | Non-fatal warning from server |
| `connect_error` | WebSocket connection failed |
| `disconnect` | Connection closed |

### Rate Limit API Errors

| HTTP Status | Description |
|-------------|-------------|
| 401 | Unauthorized - Invalid API key |
| 429 | Too Many Requests - Rate limit exceeded |
| 500 | Server Error |

### SDK Exception Classes

```python
from valenceai import (
    ValenceSDKException,
    AudioTooShortError,
    FileSizeLimitExceededError,
    UploadError,
    PredictionError
)

try:
    result = client.discrete.emotions(file_path="audio.wav")
except AudioTooShortError as e:
    print(f"Audio too short: {e.actual_duration}s (min: {e.min_duration}s)")
except FileSizeLimitExceededError as e:
    print(f"File too large: {e.actual_size_mb:.2f} MB (max: {e.max_size_mb} MB)")
except UploadError as e:
    print(f"Upload failed: {e}")
except PredictionError as e:
    print(f"Prediction failed: {e}")
except ValenceSDKException as e:
    print(f"SDK error: {e}")
```

## Additional Examples

Additional usage examples can be found in the [Valence docs](https://docs.getvalenceai.com/examples).

## Migration from v0.x

### Key Changes in v1.0.5

1. **Environment Variable**: `VALENCE_API_KEY` is now the standard (consistent naming across SDKs)
2. **Streaming API**: New WebSocket-based real-time emotion detection
3. **Rate Limiting**: New API for monitoring usage
4. **Timeline Data**: Asynch API now returns detailed timestamp information
5. **Helper Methods**: Asynch API now includes functions for baseline analysis of emotion timeline

### Updating Your Code

```python
# Old (v0.x)
client = ValenceClient()
result = client.discrete.emotions("file.wav")

# New (v1.0.0) - mostly compatible, with new features
client = ValenceClient(api_key="your_key")
result = client.discrete.emotions(
    file_path="file.wav",
)

# New streaming capability
stream = client.streaming.connect()
stream.on_prediction(callback)
stream.connect()
```

## Support

- **Additional Documentation**: [API Documentation](https://docs.getvalenceai.com)
- **Detailed Usage Examples**: [SDK Examples](https://docs.getvalenceai.com/examples)
- **Contact**: [Valence AI Support](https://www.getvalenceai.com/contact)

## License

Private License © 2026 [Valence Vibrations, Inc](https://getvalenceai.com), a Delaware public benefit corporation.