valenceai 1.0.2 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +196 -204
- package/package.json +2 -7
- package/src/config.js +3 -3
- package/src/errors.js +34 -0
- package/src/index.js +6 -1
- package/src/valenceClient.js +125 -7
- package/tests/asyncAudio.test.js +29 -1
- package/tests/discreteAudio.test.js +59 -4
- package/examples/uploadLong.js +0 -10
- package/examples/uploadShort.js +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.3] - 2025-01-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`getEmotionCounts()` method**: Returns an object of emotion occurrence counts for the entire audio file (e.g., `{happy: 10, sad: 3, angry: 8, neutral: 9}`)
|
|
13
|
+
- **`majorityEmotion()` method**: Alias for `getDominantEmotion()`, returns the most frequently occurring emotion as a string
|
|
14
|
+
|
|
15
|
+
### Technical Improvements
|
|
16
|
+
|
|
17
|
+
- **Refactored `getDominantEmotion()`**: Now uses `getEmotionCounts()` internally to avoid code duplication
|
|
18
|
+
|
|
19
|
+
### Usage Example
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
import { ValenceClient } from 'valenceai';
|
|
23
|
+
|
|
24
|
+
const client = new ValenceClient({ apiKey: 'your_api_key' });
|
|
25
|
+
const requestId = await client.asynch.upload('audio.wav');
|
|
26
|
+
const result = await client.asynch.emotions(requestId);
|
|
27
|
+
|
|
28
|
+
// Get emotion counts
|
|
29
|
+
const counts = await client.asynch.getEmotionCounts(requestId);
|
|
30
|
+
// Returns: { happy: 10, sad: 3, angry: 8, neutral: 9 }
|
|
31
|
+
|
|
32
|
+
// Get majority emotion
|
|
33
|
+
const majority = await client.asynch.majorityEmotion(requestId);
|
|
34
|
+
// Returns: "happy"
|
|
35
|
+
```
|
|
36
|
+
|
|
8
37
|
## [1.0.1] - 2025-12-29
|
|
9
38
|
|
|
10
39
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,67 +1,109 @@
|
|
|
1
1
|
# Valence SDK for Emotion Detection
|
|
2
2
|
|
|
3
|
-
**valenceai** is a Node.js SDK for interacting with the [Valence AI](https://getvalenceai.com)
|
|
3
|
+
**valenceai** is a Node.js SDK for interacting with the [Valence AI](https://getvalenceai.com) API for emotion analysis. It provides a convenient interface to upload audio files, stream real-time audio, and retrieve detected emotional states.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **Discrete audio processing** - Real-time analysis for short audio clips
|
|
8
|
-
- **
|
|
7
|
+
- **Discrete audio processing** - Real-time analysis for short audio clips
|
|
8
|
+
- **Asynch audio processing** - Multipart parallel upload for long audio files with temporal emotion analysis
|
|
9
9
|
- **Streaming API** - Real-time WebSocket streaming for live audio
|
|
10
10
|
- **Rate limiting** - Monitor API usage and limits
|
|
11
|
-
- **Model selection** - Choose between 4emotions and 7emotions models
|
|
12
|
-
- **Timeline analysis** - Get emotion changes over time with timestamps
|
|
13
11
|
- **Environment configuration** - Built-in support for .env files
|
|
14
12
|
- **Enhanced logging** - Configurable log levels with timestamps
|
|
15
|
-
- **Robust error handling** - Comprehensive validation and error recovery
|
|
16
13
|
- **TypeScript ready** - Full JSDoc documentation for all functions
|
|
17
|
-
- **100% tested** - Comprehensive test suite with high coverage
|
|
18
|
-
- **Security focused** - Input validation and secure error handling
|
|
19
14
|
|
|
20
|
-
The emotional classification model used in our APIs is optimized for North American English conversational data.
|
|
15
|
+
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_.
|
|
21
16
|
|
|
22
|
-
##
|
|
17
|
+
## API Overview
|
|
23
18
|
|
|
24
|
-
|
|
19
|
+
| API | Best For | Input | Output |
|
|
20
|
+
|-----|----------|-------|--------|
|
|
21
|
+
| **Discrete** | Real-time analysis | Short audio (4-10s) | Single emotion prediction |
|
|
22
|
+
| **Asynch** | Pre-recorded files | Long audio (up to 1GB) | Timeline with emotion changes |
|
|
23
|
+
| **Streaming** | Live audio streams | Audio chunks via WebSocket | Real-time emotion updates |
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
- **7emotions**: happy, sad, angry, neutral, surprised, disgusted, calm
|
|
25
|
+
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.
|
|
28
26
|
|
|
29
|
-
The
|
|
27
|
+
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.
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
## Audio Input Requirements
|
|
32
|
+
|
|
33
|
+
### Format Specifications
|
|
34
|
+
|
|
35
|
+
- **Format**: WAV only
|
|
36
|
+
- **Recommended sampling rate**: 44.1 kHz (44100 Hz)
|
|
37
|
+
- **Minimum sampling rate**: 8 kHz
|
|
38
|
+
- **Channel**: Mono (single channel)
|
|
39
|
+
|
|
40
|
+
### API-Specific Requirements
|
|
41
|
+
|
|
42
|
+
- **Discrete API**: Minimum 4.5 seconds per file, maximum 15 seconds. 5-10 seconds recommended.
|
|
43
|
+
- **Asynch API**: Minimum 5 seconds, maximum 1 GB
|
|
44
|
+
- **Streaming API**: Real-time audio chunks (Buffer or ArrayBuffer)
|
|
45
|
+
|
|
46
|
+
For inquiries about custom microphone specifications or stereo/multi-channel support, please [contact us](https://www.getvalenceai.com/contact).
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install valenceai
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
### Environment Variables
|
|
57
|
+
|
|
58
|
+
Create a `.env` file in your project root:
|
|
59
|
+
|
|
60
|
+
```env
|
|
61
|
+
VALENCE_API_KEY=your_api_key # Required
|
|
62
|
+
VALENCE_API_BASE_URL=https://api.getvalenceai.com # Optional
|
|
63
|
+
VALENCE_WEBSOCKET_URL=wss://api.getvalenceai.com # Optional
|
|
64
|
+
VALENCE_LOG_LEVEL=info # Optional: debug, info, warn, error
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Client Configuration
|
|
32
68
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
69
|
+
```javascript
|
|
70
|
+
const client = new ValenceClient({
|
|
71
|
+
apiKey: 'your_api_key', // API key (required)
|
|
72
|
+
baseUrl: 'https://custom.api', // Custom API endpoint (optional)
|
|
73
|
+
websocketUrl: 'wss://custom.api', // Custom WebSocket endpoint (optional)
|
|
74
|
+
partSize: 5 * 1024 * 1024, // Upload chunk size (default: 5MB)
|
|
75
|
+
maxRetries: 3, // Max retry attempts (default: 3)
|
|
76
|
+
comprehensiveOutput: false // When false: asynch API returns timestamp, main_emotion, confidence only.
|
|
77
|
+
// When true: also includes all_predictions with all emotion confidences (default: false)
|
|
78
|
+
});
|
|
79
|
+
```
|
|
38
80
|
|
|
39
|
-
##
|
|
81
|
+
## Asynch API Processing Workflow
|
|
40
82
|
|
|
41
|
-
The
|
|
83
|
+
The Asynch API uses a multi-step process to handle long audio files. Understanding this workflow is crucial for proper implementation:
|
|
42
84
|
|
|
43
85
|
### 1. Upload Phase (Client-Side)
|
|
44
86
|
|
|
45
87
|
When you call `client.asynch.upload(filePath)`:
|
|
46
88
|
|
|
47
89
|
- SDK splits your file into parts (5MB chunks by default)
|
|
48
|
-
- Uploads parts
|
|
49
|
-
- **Returns a `requestId`** - This is a tracking identifier,
|
|
50
|
-
- At this point: File is uploaded to
|
|
90
|
+
- Uploads parts in parallel
|
|
91
|
+
- **Returns a `requestId`** - This is a tracking identifier, not a completion signal.
|
|
92
|
+
- At this point: File is uploaded to our server, but _NOT processed yet_.
|
|
51
93
|
|
|
52
94
|
### 2. Background Processing (Server-Side)
|
|
53
95
|
|
|
54
96
|
After upload completes, the server automatically:
|
|
55
97
|
|
|
56
|
-
-
|
|
57
|
-
- Downloads audio
|
|
98
|
+
- Checks for new uploads
|
|
99
|
+
- Downloads audio when a new File is detected
|
|
58
100
|
- Splits audio into 5-second segments
|
|
59
|
-
-
|
|
101
|
+
- Processes audio file
|
|
60
102
|
- Invokes machine learning model for emotion detection
|
|
61
103
|
- Stores results in database
|
|
62
104
|
- Updates status to `completed`
|
|
63
105
|
|
|
64
|
-
**Processing Time**: Typically 1-
|
|
106
|
+
**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.
|
|
65
107
|
|
|
66
108
|
### 3. Results Retrieval (Client-Side)
|
|
67
109
|
|
|
@@ -70,7 +112,7 @@ When you call `client.asynch.emotions(requestId)`:
|
|
|
70
112
|
- Polls the status endpoint at regular intervals
|
|
71
113
|
- Waits for status progression:
|
|
72
114
|
- `initiated` → Upload started
|
|
73
|
-
- `upload_completed` → File uploaded
|
|
115
|
+
- `upload_completed` → File uploaded (processing not started)
|
|
74
116
|
- `processing` → Background processing in progress
|
|
75
117
|
- `completed` → Results ready
|
|
76
118
|
- Returns emotion timeline when status is `completed`
|
|
@@ -79,47 +121,44 @@ When you call `client.asynch.emotions(requestId)`:
|
|
|
79
121
|
|
|
80
122
|
| Status | Meaning | What's Happening |
|
|
81
123
|
|--------|---------|------------------|
|
|
82
|
-
| `initiated` | Upload started | SDK is uploading file parts
|
|
83
|
-
| `upload_completed` | Upload finished | File is
|
|
84
|
-
| `processing` | Processing active | Server is analyzing audio
|
|
124
|
+
| `initiated` | Upload started | SDK is uploading file in parts |
|
|
125
|
+
| `upload_completed` | Upload finished | File is waiting for background processor |
|
|
126
|
+
| `processing` | Processing active | Server is analyzing audio |
|
|
85
127
|
| `completed` | Results ready | Emotion timeline is available |
|
|
86
128
|
|
|
87
129
|
### Important Notes
|
|
88
130
|
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
|
|
94
|
-
## Installation
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
npm install valenceai
|
|
98
|
-
```
|
|
131
|
+
- The `requestId` is NOT a completion indicator. It's a request tracking ID.
|
|
132
|
+
- `upload()` completing does not mean results are ready. It means the file is uploaded.
|
|
133
|
+
- Background processing takes time. Processing time varies based on file length and server load.
|
|
134
|
+
- You can check status anytime. The `requestId` remains valid for retrieving results until databases are cleared (see: [DPA](https://getvalenceai.com/legal/data-processing) for more information on data retention policies).
|
|
99
135
|
|
|
100
136
|
## Quick Start
|
|
101
137
|
|
|
102
138
|
```javascript
|
|
103
139
|
import { ValenceClient } from 'valenceai';
|
|
104
140
|
|
|
105
|
-
// Initialize client
|
|
141
|
+
// Initialize client
|
|
106
142
|
const client = new ValenceClient({ apiKey: 'your_api_key' });
|
|
107
143
|
|
|
108
144
|
// Discrete API - Quick emotion detection
|
|
109
|
-
const result = await client.discrete.emotions('short_audio.wav'
|
|
110
|
-
console.log(`Emotion: ${result.
|
|
145
|
+
const result = await client.discrete.emotions('short_audio.wav');
|
|
146
|
+
console.log(`Emotion: ${result.main_emotion}`);
|
|
111
147
|
|
|
112
|
-
//
|
|
113
|
-
// Step 1: Upload file
|
|
148
|
+
// Asynch API - Long audio with timeline
|
|
149
|
+
// Step 1: Upload file (returns tracking ID)
|
|
114
150
|
const requestId = await client.asynch.upload('long_audio.wav');
|
|
115
151
|
// Step 2: Wait for server processing and get results (polls until complete)
|
|
116
152
|
const emotions = await client.asynch.emotions(requestId, 30, 10000);
|
|
117
|
-
// Step 3: Access
|
|
118
|
-
const
|
|
119
|
-
|
|
153
|
+
// Step 3: Access emotion data from results
|
|
154
|
+
const emotionList = emotions.emotions; // List of emotion predictions with timestamps
|
|
155
|
+
|
|
156
|
+
// Get summary statistics
|
|
157
|
+
const majority = await client.asynch.majorityEmotion(requestId); // Most frequent emotion
|
|
158
|
+
const counts = await client.asynch.emotionCounts(requestId); // { happy: 10, sad: 3, ... }
|
|
120
159
|
|
|
121
160
|
// Streaming API - Real-time audio
|
|
122
|
-
const stream = client.streaming.connect(
|
|
161
|
+
const stream = client.streaming.connect();
|
|
123
162
|
stream.on('prediction', (data) => console.log(data.main_emotion));
|
|
124
163
|
stream.connect();
|
|
125
164
|
stream.sendAudio(audioBuffer);
|
|
@@ -130,31 +169,6 @@ const status = await client.rateLimit.getStatus();
|
|
|
130
169
|
const health = await client.rateLimit.getHealth();
|
|
131
170
|
```
|
|
132
171
|
|
|
133
|
-
## Configuration
|
|
134
|
-
|
|
135
|
-
### Environment Variables
|
|
136
|
-
|
|
137
|
-
Create a `.env` file in your project root:
|
|
138
|
-
|
|
139
|
-
```env
|
|
140
|
-
VALENCE_API_KEY=your_api_key # Required
|
|
141
|
-
VALENCE_API_BASE_URL=https://api.getvalenceai.com # Optional
|
|
142
|
-
VALENCE_WEBSOCKET_URL=wss://api.getvalenceai.com # Optional
|
|
143
|
-
VALENCE_LOG_LEVEL=info # Optional: debug, info, warn, error
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
### Client Configuration
|
|
147
|
-
|
|
148
|
-
```javascript
|
|
149
|
-
const client = new ValenceClient({
|
|
150
|
-
apiKey: 'your_api_key', // API key (required)
|
|
151
|
-
baseUrl: 'https://custom.api', // Custom API endpoint (optional)
|
|
152
|
-
websocketUrl: 'wss://custom.api', // Custom WebSocket endpoint (optional)
|
|
153
|
-
partSize: 5 * 1024 * 1024, // Upload chunk size (default: 5MB)
|
|
154
|
-
maxRetries: 3 // Max retry attempts (default: 3)
|
|
155
|
-
});
|
|
156
|
-
```
|
|
157
|
-
|
|
158
172
|
## API Reference
|
|
159
173
|
|
|
160
174
|
### Discrete API
|
|
@@ -162,17 +176,11 @@ const client = new ValenceClient({
|
|
|
162
176
|
For short audio files requiring immediate emotion detection.
|
|
163
177
|
|
|
164
178
|
```javascript
|
|
165
|
-
//
|
|
166
|
-
const result = await client.discrete.emotions(
|
|
167
|
-
'audio.wav',
|
|
168
|
-
'4emotions' // or '7emotions'
|
|
169
|
-
);
|
|
179
|
+
// Direct file upload
|
|
180
|
+
const result = await client.discrete.emotions('audio.wav');
|
|
170
181
|
|
|
171
|
-
//
|
|
172
|
-
const result = await client.discrete.emotions(
|
|
173
|
-
[0.1, 0.2, 0.3, ...],
|
|
174
|
-
'4emotions'
|
|
175
|
-
);
|
|
182
|
+
// Upload via in-memory audio array
|
|
183
|
+
const result = await client.discrete.emotions([0.17278, 0.23738, 0.37912, ...]);
|
|
176
184
|
```
|
|
177
185
|
|
|
178
186
|
**Response:**
|
|
@@ -181,36 +189,28 @@ const result = await client.discrete.emotions(
|
|
|
181
189
|
emotions: {
|
|
182
190
|
happy: 0.78,
|
|
183
191
|
sad: 0.12,
|
|
184
|
-
angry: 0.
|
|
185
|
-
neutral: 0.
|
|
192
|
+
angry: 0.08,
|
|
193
|
+
neutral: 0.15
|
|
186
194
|
},
|
|
187
|
-
|
|
195
|
+
main_emotion: 'happy'
|
|
188
196
|
}
|
|
189
197
|
```
|
|
190
198
|
|
|
191
|
-
###
|
|
199
|
+
### Asynch API
|
|
192
200
|
|
|
193
201
|
For long audio files with timeline analysis.
|
|
194
202
|
|
|
195
|
-
**Workflow**: The Async API uses a 3-step process:
|
|
196
|
-
|
|
197
|
-
1. **Upload** (`upload()`) - Multipart upload to S3, returns `requestId` (tracking ID)
|
|
198
|
-
2. **Background Processing** (automatic) - Server processes audio in 5-second chunks
|
|
199
|
-
3. **Results Retrieval** (`emotions()`) - Polls status endpoint until processing completes
|
|
200
|
-
|
|
201
|
-
**Processing Time**: Typically 1-2 minutes per hour of audio.
|
|
202
|
-
|
|
203
203
|
**Status Progression**: `initiated` → `upload_completed` → `processing` → `completed`
|
|
204
204
|
|
|
205
205
|
#### Upload Audio
|
|
206
206
|
|
|
207
207
|
```javascript
|
|
208
|
-
// Upload file
|
|
208
|
+
// Upload file (multipart upload, automatically validates file size)
|
|
209
209
|
const requestId = await client.asynch.upload('long_audio.wav');
|
|
210
|
-
// Returns: requestId (tracking ID, NOT completion signal)
|
|
211
|
-
// File is uploaded to S3 but NOT processed yet
|
|
212
210
|
```
|
|
213
211
|
|
|
212
|
+
**Note**: The SDK automatically validates file size against your rate limit policy before upload. If the file exceeds the maximum allowed size, a `FileSizeLimitExceededError` is thrown without attempting the upload. Default maximum is 1GB when no rate limit policy is configured.
|
|
213
|
+
|
|
214
214
|
#### Get Emotion Results
|
|
215
215
|
|
|
216
216
|
```javascript
|
|
@@ -249,17 +249,18 @@ const result = await client.asynch.emotions(
|
|
|
249
249
|
}
|
|
250
250
|
```
|
|
251
251
|
|
|
252
|
-
|
|
252
|
+
Note: The `all_predictions` field is only included when `comprehensiveOutput: true` is set in the client constructor.
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
// Get full timeline
|
|
256
|
-
const timeline = await client.asynch.getTimeline(requestId);
|
|
254
|
+
#### Helper Methods
|
|
257
255
|
|
|
258
|
-
|
|
259
|
-
|
|
256
|
+
```javascript
|
|
257
|
+
// Get the most frequently occurring emotion across the entire file
|
|
258
|
+
const majority = await client.asynch.majorityEmotion(requestId);
|
|
259
|
+
// Returns: "happy"
|
|
260
260
|
|
|
261
|
-
// Get
|
|
262
|
-
const
|
|
261
|
+
// Get emotion occurrence counts for the entire file
|
|
262
|
+
const counts = await client.asynch.emotionCounts(requestId);
|
|
263
|
+
// Returns: { happy: 10, sad: 3, angry: 8, neutral: 9 }
|
|
263
264
|
```
|
|
264
265
|
|
|
265
266
|
### Streaming API
|
|
@@ -268,7 +269,7 @@ For real-time emotion detection on live audio streams.
|
|
|
268
269
|
|
|
269
270
|
```javascript
|
|
270
271
|
// Create streaming connection
|
|
271
|
-
const stream = client.streaming.connect(
|
|
272
|
+
const stream = client.streaming.connect();
|
|
272
273
|
|
|
273
274
|
// Register event handlers
|
|
274
275
|
stream.on('prediction', (data) => {
|
|
@@ -307,12 +308,14 @@ stream.disconnect();
|
|
|
307
308
|
happy: 0.87,
|
|
308
309
|
sad: 0.05,
|
|
309
310
|
angry: 0.03,
|
|
310
|
-
neutral: 0.
|
|
311
|
+
neutral: 0.15
|
|
311
312
|
},
|
|
312
|
-
timestamp:
|
|
313
|
+
timestamp: 1706486400000 // Unix timestamp (UTC) in milliseconds
|
|
313
314
|
}
|
|
314
315
|
```
|
|
315
316
|
|
|
317
|
+
The `timestamp` is a Unix timestamp (UTC) in milliseconds representing when the server generated the prediction.
|
|
318
|
+
|
|
316
319
|
### Rate Limit API
|
|
317
320
|
|
|
318
321
|
Monitor your API usage and limits.
|
|
@@ -322,76 +325,105 @@ Monitor your API usage and limits.
|
|
|
322
325
|
const status = await client.rateLimit.getStatus();
|
|
323
326
|
console.log(status);
|
|
324
327
|
// {
|
|
328
|
+
// policy_name: 'standard_policy',
|
|
325
329
|
// limits: {
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
+
// requests_per_second: 10,
|
|
331
|
+
// requests_per_minute: 100,
|
|
332
|
+
// requests_per_hour: 1000,
|
|
333
|
+
// requests_per_day: 10000,
|
|
334
|
+
// burst_limit: 20,
|
|
335
|
+
// max_audio_size_mb: 50, // Maximum file size in MB
|
|
336
|
+
// max_audio_duration_seconds: 300, // Maximum audio duration
|
|
337
|
+
// max_concurrent_requests: 5
|
|
330
338
|
// },
|
|
331
339
|
// current_usage: {
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
//
|
|
340
|
+
// requests_per_second: 2,
|
|
341
|
+
// rejected_per_second: 0,
|
|
342
|
+
// total_audio_size_bytes_per_second: 1048576,
|
|
343
|
+
// requests_per_minute: 15,
|
|
344
|
+
// rejected_per_minute: 0,
|
|
345
|
+
// total_audio_size_bytes_per_minute: 15728640
|
|
346
|
+
// // ... usage for hour and day
|
|
336
347
|
// }
|
|
337
348
|
// }
|
|
338
349
|
|
|
339
350
|
// Check API health
|
|
340
351
|
const health = await client.rateLimit.getHealth();
|
|
341
352
|
console.log(health);
|
|
342
|
-
// { status: 'healthy', timestamp:
|
|
353
|
+
// { status: 'healthy', timestamp: 1738684800 }
|
|
343
354
|
```
|
|
344
355
|
|
|
345
|
-
|
|
356
|
+
The `reset` and `timestamp` values are Unix timestamps (UTC) in seconds.
|
|
346
357
|
|
|
347
|
-
|
|
358
|
+
## Error Responses
|
|
348
359
|
|
|
349
|
-
|
|
350
|
-
- **Recommended sampling rate**: 44.1 kHz (44100 Hz)
|
|
351
|
-
- **Minimum sampling rate**: 8 kHz
|
|
352
|
-
- **Channel**: Mono (single channel)
|
|
353
|
-
|
|
354
|
-
### API-Specific Requirements
|
|
360
|
+
### Discrete API Errors
|
|
355
361
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
362
|
+
| HTTP Status | Error Code | Description |
|
|
363
|
+
|-------------|------------|-------------|
|
|
364
|
+
| 400 | `AUDIO_TOO_SHORT` | Audio duration below minimum (4.5 seconds). Response includes `min_duration_seconds` and `actual_duration_seconds` |
|
|
365
|
+
| 400 | `AUDIO_TOO_LONG` | Audio duration above maximum (15 seconds). Response includes `max_duration_seconds` and `actual_duration_seconds` |
|
|
366
|
+
| 400 | Bad Request | Invalid request format or parameters |
|
|
367
|
+
| 401 | Unauthorized | Invalid or missing API key |
|
|
368
|
+
| 500 | Server Error | Internal server error |
|
|
359
369
|
|
|
360
|
-
|
|
370
|
+
### Asynch API Errors
|
|
361
371
|
|
|
362
|
-
|
|
372
|
+
| HTTP Status | Error Code | Description |
|
|
373
|
+
|-------------|------------|-------------|
|
|
374
|
+
| 400 | `AUDIO_TOO_SHORT` | Audio duration below minimum (5 seconds) |
|
|
375
|
+
| 400 | `FILE_SIZE_LIMIT_EXCEEDED` | File size exceeds rate limit policy maximum. Raised before upload attempt |
|
|
376
|
+
| 400 | `FILE_TOO_LARGE` | File exceeds maximum upload size (1 GB). Response includes `max_file_size_bytes` and `actual_file_size_bytes` |
|
|
377
|
+
| 400 | Bad Request | Invalid request format or parameters |
|
|
378
|
+
| 401 | Unauthorized | Invalid or missing API key |
|
|
379
|
+
| 404 | Not Found | Request ID not found |
|
|
380
|
+
| 500 | Server Error | Internal server error |
|
|
363
381
|
|
|
364
|
-
|
|
382
|
+
**Asynch Status Values:**
|
|
365
383
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
384
|
+
| Status | Meaning |
|
|
385
|
+
|--------|---------|
|
|
386
|
+
| `initiated` | Upload in progress |
|
|
387
|
+
| `upload_completed` | Upload finished, awaiting processing |
|
|
388
|
+
| `processing` | Server analyzing audio |
|
|
389
|
+
| `completed` | Results ready |
|
|
390
|
+
| `failed` | Processing failed |
|
|
369
391
|
|
|
370
|
-
|
|
371
|
-
npm run example:discrete
|
|
392
|
+
### Streaming API Errors
|
|
372
393
|
|
|
373
|
-
|
|
374
|
-
|
|
394
|
+
| Event | Description |
|
|
395
|
+
|-------|-------------|
|
|
396
|
+
| `error` | Server-side error during streaming |
|
|
397
|
+
| `warning` | Non-fatal warning from server |
|
|
398
|
+
| `connect_error` | WebSocket connection failed |
|
|
399
|
+
| `disconnect` | Connection closed |
|
|
375
400
|
|
|
376
|
-
|
|
377
|
-
npm run example:streaming
|
|
401
|
+
### Rate Limit API Errors
|
|
378
402
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
403
|
+
| HTTP Status | Description |
|
|
404
|
+
|-------------|-------------|
|
|
405
|
+
| 401 | Unauthorized - Invalid API key |
|
|
406
|
+
| 429 | Too Many Requests - Rate limit exceeded |
|
|
407
|
+
| 500 | Server Error |
|
|
384
408
|
|
|
385
409
|
## Error Handling
|
|
386
410
|
|
|
387
411
|
```javascript
|
|
388
|
-
import {
|
|
412
|
+
import {
|
|
413
|
+
ValenceClient,
|
|
414
|
+
AudioTooShortError,
|
|
415
|
+
FileSizeLimitExceededError
|
|
416
|
+
} from 'valenceai';
|
|
389
417
|
|
|
390
418
|
try {
|
|
391
419
|
const client = new ValenceClient({ apiKey: 'your_key' });
|
|
392
420
|
const result = await client.discrete.emotions('audio.wav');
|
|
393
421
|
} catch (error) {
|
|
394
|
-
if (error
|
|
422
|
+
if (error instanceof AudioTooShortError) {
|
|
423
|
+
console.error(`Audio too short: ${error.actualDuration}s (min: ${error.minDuration}s)`);
|
|
424
|
+
} else if (error instanceof FileSizeLimitExceededError) {
|
|
425
|
+
console.error(`File too large: ${error.actualSizeMb.toFixed(2)} MB (max: ${error.maxSizeMb} MB)`);
|
|
426
|
+
} else if (error.message.includes('API key')) {
|
|
395
427
|
console.error('Authentication error:', error.message);
|
|
396
428
|
} else if (error.message.includes('File not found')) {
|
|
397
429
|
console.error('File error:', error.message);
|
|
@@ -403,45 +435,16 @@ try {
|
|
|
403
435
|
}
|
|
404
436
|
```
|
|
405
437
|
|
|
406
|
-
## Development
|
|
407
|
-
|
|
408
|
-
### Testing
|
|
409
|
-
|
|
410
|
-
```bash
|
|
411
|
-
# Run all tests
|
|
412
|
-
npm test
|
|
413
|
-
|
|
414
|
-
# Run tests with coverage
|
|
415
|
-
npm run test:coverage
|
|
416
|
-
|
|
417
|
-
# Watch mode for development
|
|
418
|
-
npm run test:watch
|
|
419
|
-
|
|
420
|
-
# Run specific test file
|
|
421
|
-
npm test -- discrete.test.js
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
### Building and Publishing
|
|
425
|
-
|
|
426
|
-
```bash
|
|
427
|
-
# Validate configuration and run tests
|
|
428
|
-
npm test
|
|
429
|
-
|
|
430
|
-
# Publish to npm
|
|
431
|
-
npm login
|
|
432
|
-
npm publish --access public
|
|
433
|
-
```
|
|
434
|
-
|
|
435
438
|
## Migration from v0.x
|
|
436
439
|
|
|
437
|
-
### Key Changes in v1.0.
|
|
440
|
+
### Key Changes in v1.0.5
|
|
438
441
|
|
|
439
|
-
1. **Environment Variable**: `VALENCE_API_KEY` is now the standard (consistent naming)
|
|
442
|
+
1. **Environment Variable**: `VALENCE_API_KEY` is now the standard (consistent naming across SDKs)
|
|
440
443
|
2. **Unified Client**: Single `ValenceClient` class with nested APIs
|
|
441
444
|
3. **Streaming API**: New WebSocket-based real-time emotion detection
|
|
442
445
|
4. **Rate Limiting**: New API for monitoring usage
|
|
443
|
-
5. **Timeline Data**:
|
|
444
|
-
6. **
|
|
446
|
+
5. **Timeline Data**: Asynch API now returns detailed timestamp information
|
|
447
|
+
6. **Helper Methods**: Asynch API now includes functions for baseline analysis of emotion timeline
|
|
445
448
|
|
|
446
449
|
### Updating Your Code
|
|
447
450
|
|
|
@@ -453,10 +456,10 @@ const result = await predictDiscreteAudioEmotion('file.wav');
|
|
|
453
456
|
// New (v1.0.0)
|
|
454
457
|
import { ValenceClient } from 'valenceai';
|
|
455
458
|
const client = new ValenceClient({ apiKey: 'your_key' });
|
|
456
|
-
const result = await client.discrete.emotions('file.wav'
|
|
459
|
+
const result = await client.discrete.emotions('file.wav');
|
|
457
460
|
|
|
458
461
|
// New streaming capability
|
|
459
|
-
const stream = client.streaming.connect(
|
|
462
|
+
const stream = client.streaming.connect();
|
|
460
463
|
stream.on('prediction', callback);
|
|
461
464
|
await stream.connect();
|
|
462
465
|
```
|
|
@@ -467,7 +470,6 @@ await stream.connect();
|
|
|
467
470
|
- `uploadAsyncAudio()` → `client.asynch.upload()`
|
|
468
471
|
- `getEmotions()` → `client.asynch.emotions()`
|
|
469
472
|
- All methods now require creating a `ValenceClient` instance first
|
|
470
|
-
- Model parameter is now required and explicit
|
|
471
473
|
|
|
472
474
|
See [CHANGELOG.md](./CHANGELOG.md) for complete migration guide.
|
|
473
475
|
|
|
@@ -481,26 +483,16 @@ import { ValenceClient } from 'valenceai';
|
|
|
481
483
|
const client: ValenceClient = new ValenceClient({ apiKey: 'your_key' });
|
|
482
484
|
|
|
483
485
|
// Full type inference and autocomplete
|
|
484
|
-
const result = await client.discrete.emotions('audio.wav'
|
|
485
|
-
// result.
|
|
486
|
+
const result = await client.discrete.emotions('audio.wav');
|
|
487
|
+
// result.main_emotion is typed
|
|
486
488
|
```
|
|
487
489
|
|
|
488
|
-
## Contributing
|
|
489
|
-
|
|
490
|
-
We welcome contributions! Please:
|
|
491
|
-
|
|
492
|
-
1. Fork the repository
|
|
493
|
-
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
|
494
|
-
3. Make your changes with tests
|
|
495
|
-
4. Ensure all tests pass: `npm test`
|
|
496
|
-
5. Submit a pull request
|
|
497
|
-
|
|
498
490
|
## Support
|
|
499
491
|
|
|
500
|
-
- **Documentation**: [API Documentation](https://docs.getvalenceai.com)
|
|
501
|
-
- **
|
|
502
|
-
- **
|
|
492
|
+
- **Additional Documentation**: [API Documentation](https://docs.getvalenceai.com)
|
|
493
|
+
- **Detailed Usage Examples**: [SDK Examples](https://docs.getvalenceai.com/examples)
|
|
494
|
+
- **Contact**: [Valence AI Support](https://www.getvalenceai.com/contact)
|
|
503
495
|
|
|
504
496
|
## License
|
|
505
497
|
|
|
506
|
-
Private License ©
|
|
498
|
+
Private License © 2026 [Valence Vibrations, Inc](https://getvalenceai.com), a Delaware public benefit corporation.
|
package/package.json
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "valenceai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"description": "Node.js SDK for Valence AI Emotion Detection API - Real-time, Async, and Streaming Support",
|
|
7
7
|
"keywords": ["valence", "emotion", "detection", "ai", "audio", "streaming", "websocket", "real-time"],
|
|
8
8
|
"author": "julian.olarte",
|
|
9
|
-
"license": "
|
|
9
|
+
"license": "Private License",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
12
|
"url": "https://github.com/valenceai/sdk"
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"start": "node examples/uploadShort.js",
|
|
16
|
-
"example:discrete": "node examples/uploadShort.js",
|
|
17
|
-
"example:async": "node examples/uploadLong.js",
|
|
18
16
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
19
17
|
"test:coverage": "c8 jest",
|
|
20
18
|
"test:watch": "jest --watch"
|
|
@@ -32,8 +30,5 @@
|
|
|
32
30
|
},
|
|
33
31
|
"engines": {
|
|
34
32
|
"node": ">=14.0.0"
|
|
35
|
-
},
|
|
36
|
-
"directories": {
|
|
37
|
-
"example": "examples"
|
|
38
33
|
}
|
|
39
34
|
}
|
package/src/config.js
CHANGED
|
@@ -2,11 +2,11 @@ import dotenv from 'dotenv';
|
|
|
2
2
|
dotenv.config();
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Default URLs point to
|
|
5
|
+
* Default URLs point to production environment (api.getvalenceai.com)
|
|
6
6
|
* These can be overridden by environment variables or constructor parameters
|
|
7
7
|
*/
|
|
8
|
-
const DEFAULT_BASE_URL = 'https://
|
|
9
|
-
const DEFAULT_WEBSOCKET_URL = 'wss://
|
|
8
|
+
const DEFAULT_BASE_URL = 'https://api.getvalenceai.com';
|
|
9
|
+
const DEFAULT_WEBSOCKET_URL = 'wss://api.getvalenceai.com';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Configuration object for the Valence SDK
|
package/src/errors.js
CHANGED
|
@@ -24,3 +24,37 @@ export class AudioTooShortError extends ValenceSDKError {
|
|
|
24
24
|
this.actualDuration = actualDuration;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Error thrown when the audio file exceeds the maximum allowed duration.
|
|
30
|
+
*/
|
|
31
|
+
export class AudioTooLongError extends ValenceSDKError {
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} message - Error message
|
|
34
|
+
* @param {number|null} maxDuration - Maximum allowed duration in seconds
|
|
35
|
+
* @param {number|null} actualDuration - Actual duration of the provided audio in seconds
|
|
36
|
+
*/
|
|
37
|
+
constructor(message, maxDuration = null, actualDuration = null) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'AudioTooLongError';
|
|
40
|
+
this.maxDuration = maxDuration;
|
|
41
|
+
this.actualDuration = actualDuration;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Error thrown when the audio file size exceeds the rate limit policy maximum.
|
|
47
|
+
*/
|
|
48
|
+
export class FileSizeLimitExceededError extends ValenceSDKError {
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} message - Error message
|
|
51
|
+
* @param {number|null} maxSizeMb - Maximum allowed file size in MB
|
|
52
|
+
* @param {number|null} actualSizeMb - Actual file size in MB
|
|
53
|
+
*/
|
|
54
|
+
constructor(message, maxSizeMb = null, actualSizeMb = null) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = 'FileSizeLimitExceededError';
|
|
57
|
+
this.maxSizeMb = maxSizeMb;
|
|
58
|
+
this.actualSizeMb = actualSizeMb;
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export { ValenceClient } from './valenceClient.js';
|
|
2
2
|
export { validateConfig } from './config.js';
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
ValenceSDKError,
|
|
5
|
+
AudioTooShortError,
|
|
6
|
+
AudioTooLongError,
|
|
7
|
+
FileSizeLimitExceededError
|
|
8
|
+
} from './errors.js';
|
package/src/valenceClient.js
CHANGED
|
@@ -6,7 +6,7 @@ import { getHeaders } from './client.js';
|
|
|
6
6
|
import { log } from './utils/logger.js';
|
|
7
7
|
import { RateLimitAPI } from './rateLimit.js';
|
|
8
8
|
import { StreamingAPI } from './streaming.js';
|
|
9
|
-
import { AudioTooShortError } from './errors.js';
|
|
9
|
+
import { AudioTooShortError, AudioTooLongError, FileSizeLimitExceededError } from './errors.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Client for discrete (short) audio processing
|
|
@@ -23,6 +23,7 @@ class DiscreteClient {
|
|
|
23
23
|
* @param {string} model - Model type ('4emotions' or '7emotions')
|
|
24
24
|
* @returns {Promise<Object>} Emotion prediction results
|
|
25
25
|
* @throws {Error} If validation fails, API key missing, or request fails
|
|
26
|
+
* @throws {FileSizeLimitExceededError} If file size exceeds 6MB limit
|
|
26
27
|
*/
|
|
27
28
|
async emotions(filePath = null, audioArray = null, model = '4emotions') {
|
|
28
29
|
// Validation
|
|
@@ -51,6 +52,17 @@ class DiscreteClient {
|
|
|
51
52
|
throw new Error(`File not found: ${filePath}`);
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
// Validate file size (6MB limit for discrete API)
|
|
56
|
+
const fileSizeBytes = fs.statSync(filePath).size;
|
|
57
|
+
const fileSizeMb = fileSizeBytes / (1024 * 1024);
|
|
58
|
+
const maxSizeMb = 6.0;
|
|
59
|
+
|
|
60
|
+
if (fileSizeMb > maxSizeMb) {
|
|
61
|
+
const errorMsg = `File size (${fileSizeMb.toFixed(2)} MB) exceeds discrete API maximum (${maxSizeMb} MB)`;
|
|
62
|
+
log(errorMsg, 'error');
|
|
63
|
+
throw new FileSizeLimitExceededError(errorMsg, maxSizeMb, fileSizeMb);
|
|
64
|
+
}
|
|
65
|
+
|
|
54
66
|
log(`Getting emotions for discrete audio: ${filePath} using ${model} model`, 'info');
|
|
55
67
|
|
|
56
68
|
const formData = new FormData();
|
|
@@ -87,6 +99,13 @@ class DiscreteClient {
|
|
|
87
99
|
} catch (error) {
|
|
88
100
|
log(`Error getting discrete emotions: ${error.message}`, 'error');
|
|
89
101
|
|
|
102
|
+
// Re-throw custom SDK errors without wrapping
|
|
103
|
+
if (error instanceof FileSizeLimitExceededError ||
|
|
104
|
+
error instanceof AudioTooShortError ||
|
|
105
|
+
error instanceof AudioTooLongError) {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
|
|
90
109
|
if (error.response) {
|
|
91
110
|
// Check for AUDIO_TOO_SHORT error
|
|
92
111
|
if (error.response.status === 400 && error.response.data?.error_code === 'AUDIO_TOO_SHORT') {
|
|
@@ -96,6 +115,14 @@ class DiscreteClient {
|
|
|
96
115
|
error.response.data.actual_duration_seconds
|
|
97
116
|
);
|
|
98
117
|
}
|
|
118
|
+
// Check for AUDIO_TOO_LONG or FILE_TOO_LARGE error
|
|
119
|
+
if (error.response.status === 400 && ['AUDIO_TOO_LONG', 'FILE_TOO_LARGE'].includes(error.response.data?.error_code)) {
|
|
120
|
+
throw new AudioTooLongError(
|
|
121
|
+
error.response.data.error || 'Audio file exceeds maximum allowed duration',
|
|
122
|
+
error.response.data.max_duration_seconds,
|
|
123
|
+
error.response.data.actual_duration_seconds
|
|
124
|
+
);
|
|
125
|
+
}
|
|
99
126
|
throw new Error(`API error (${error.response.status}): ${error.response.data?.message || error.response.statusText}`);
|
|
100
127
|
} else if (error.request) {
|
|
101
128
|
throw new Error('Network error: Unable to reach the API');
|
|
@@ -115,6 +142,61 @@ class AsyncClient {
|
|
|
115
142
|
this.partSize = partSize;
|
|
116
143
|
this.maxRetries = maxRetries;
|
|
117
144
|
this.comprehensiveOutput = comprehensiveOutput;
|
|
145
|
+
this._rateLimitApi = null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Lazy initialization of RateLimitAPI
|
|
150
|
+
* @private
|
|
151
|
+
* @returns {RateLimitAPI} Rate limit API instance
|
|
152
|
+
*/
|
|
153
|
+
_getRateLimitApi() {
|
|
154
|
+
if (!this._rateLimitApi) {
|
|
155
|
+
this._rateLimitApi = new RateLimitAPI(this.config);
|
|
156
|
+
}
|
|
157
|
+
return this._rateLimitApi;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Validate file size against rate limit policy
|
|
162
|
+
* @private
|
|
163
|
+
* @param {string} filePath - Path to the audio file
|
|
164
|
+
* @throws {FileSizeLimitExceededError} If file size exceeds the rate limit policy maximum
|
|
165
|
+
*/
|
|
166
|
+
async _validateFileSize(filePath) {
|
|
167
|
+
const fileSizeBytes = fs.statSync(filePath).size;
|
|
168
|
+
const fileSizeMb = fileSizeBytes / (1024 * 1024);
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
// Get rate limit status to check max_audio_size_mb
|
|
172
|
+
const rateLimitStatus = await this._getRateLimitApi().getStatus();
|
|
173
|
+
const limits = rateLimitStatus.limits || {};
|
|
174
|
+
let maxSizeMb = limits.max_audio_size_mb;
|
|
175
|
+
|
|
176
|
+
// If no policy is set, default to 1GB (1024 MB)
|
|
177
|
+
if (maxSizeMb === undefined || maxSizeMb === null) {
|
|
178
|
+
maxSizeMb = 1024;
|
|
179
|
+
log('No rate limit policy found, using default maximum of 1GB', 'debug');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (fileSizeMb > maxSizeMb) {
|
|
183
|
+
const errorMsg = `File size (${fileSizeMb.toFixed(2)} MB) exceeds rate limit maximum (${maxSizeMb} MB)`;
|
|
184
|
+
log(errorMsg, 'error');
|
|
185
|
+
throw new FileSizeLimitExceededError(errorMsg, maxSizeMb, fileSizeMb);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
log(`File size validation passed: ${fileSizeMb.toFixed(2)} MB / ${maxSizeMb} MB`, 'info');
|
|
189
|
+
|
|
190
|
+
} catch (error) {
|
|
191
|
+
// Re-throw FileSizeLimitExceededError
|
|
192
|
+
if (error instanceof FileSizeLimitExceededError) {
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
// If rate limit check fails, log warning but don't block upload
|
|
196
|
+
// This ensures backward compatibility if rate limit API is unavailable
|
|
197
|
+
log(`Could not validate file size against rate limits: ${error.message}`, 'warn');
|
|
198
|
+
log('Proceeding with upload without rate limit validation', 'info');
|
|
199
|
+
}
|
|
118
200
|
}
|
|
119
201
|
|
|
120
202
|
/**
|
|
@@ -122,6 +204,7 @@ class AsyncClient {
|
|
|
122
204
|
* @param {string} filePath - Path to the audio file
|
|
123
205
|
* @returns {Promise<string>} Request ID for tracking the upload
|
|
124
206
|
* @throws {Error} If file doesn't exist, API key missing, or upload fails
|
|
207
|
+
* @throws {FileSizeLimitExceededError} If file size exceeds rate limit maximum
|
|
125
208
|
*/
|
|
126
209
|
async upload(filePath) {
|
|
127
210
|
// Validation
|
|
@@ -141,6 +224,9 @@ class AsyncClient {
|
|
|
141
224
|
throw new Error('partSize must be between 1MB and 100MB');
|
|
142
225
|
}
|
|
143
226
|
|
|
227
|
+
// Validate file size against rate limit policy before upload
|
|
228
|
+
await this._validateFileSize(filePath);
|
|
229
|
+
|
|
144
230
|
log(`Starting async audio upload for ${filePath}`, 'info');
|
|
145
231
|
|
|
146
232
|
try {
|
|
@@ -210,6 +296,14 @@ class AsyncClient {
|
|
|
210
296
|
error.response.data.actual_duration_seconds
|
|
211
297
|
);
|
|
212
298
|
}
|
|
299
|
+
// Check for AUDIO_TOO_LONG or FILE_TOO_LARGE error
|
|
300
|
+
if (error.response.status === 400 && ['AUDIO_TOO_LONG', 'FILE_TOO_LARGE'].includes(error.response.data?.error_code)) {
|
|
301
|
+
throw new AudioTooLongError(
|
|
302
|
+
error.response.data.error || 'Audio file exceeds maximum allowed duration',
|
|
303
|
+
error.response.data.max_duration_seconds,
|
|
304
|
+
error.response.data.actual_duration_seconds
|
|
305
|
+
);
|
|
306
|
+
}
|
|
213
307
|
throw new Error(`API error (${error.response.status}): ${error.response.data?.message || error.response.statusText}`);
|
|
214
308
|
} else if (error.request) {
|
|
215
309
|
throw new Error('Network error: Unable to reach the API');
|
|
@@ -324,20 +418,44 @@ class AsyncClient {
|
|
|
324
418
|
* @returns {Promise<string|null>} The dominant emotion across the timeline
|
|
325
419
|
*/
|
|
326
420
|
async getDominantEmotion(requestId) {
|
|
421
|
+
const counts = await this.getEmotionCounts(requestId);
|
|
422
|
+
if (!counts || Object.keys(counts).length === 0) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return Object.keys(counts).reduce((a, b) =>
|
|
427
|
+
counts[a] > counts[b] ? a : b
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Alias for getDominantEmotion. Get the most frequently occurring emotion.
|
|
433
|
+
* @param {string} requestId - Request ID from upload method
|
|
434
|
+
* @returns {Promise<string|null>} The dominant emotion across the timeline
|
|
435
|
+
*/
|
|
436
|
+
async majorityEmotion(requestId) {
|
|
437
|
+
return this.getDominantEmotion(requestId);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Get counts of each emotion in the timeline
|
|
442
|
+
* @param {string} requestId - Request ID from upload method
|
|
443
|
+
* @returns {Promise<Object>} Object mapping emotion names to their occurrence counts
|
|
444
|
+
* (e.g., {happy: 10, sad: 3, angry: 8, neutral: 9})
|
|
445
|
+
*/
|
|
446
|
+
async getEmotionCounts(requestId) {
|
|
327
447
|
const timeline = await this.getTimeline(requestId);
|
|
328
448
|
if (!timeline || timeline.length === 0) {
|
|
329
|
-
return
|
|
449
|
+
return {};
|
|
330
450
|
}
|
|
331
451
|
|
|
332
|
-
const
|
|
452
|
+
const counts = {};
|
|
333
453
|
for (const emotionData of timeline) {
|
|
334
454
|
const emotion = emotionData.emotion;
|
|
335
|
-
|
|
455
|
+
counts[emotion] = (counts[emotion] || 0) + 1;
|
|
336
456
|
}
|
|
337
457
|
|
|
338
|
-
return
|
|
339
|
-
emotionCounts[a] > emotionCounts[b] ? a : b
|
|
340
|
-
);
|
|
458
|
+
return counts;
|
|
341
459
|
}
|
|
342
460
|
}
|
|
343
461
|
|
package/tests/asyncAudio.test.js
CHANGED
|
@@ -2,7 +2,7 @@ import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globa
|
|
|
2
2
|
import nock from 'nock';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import { ValenceClient } from '../src/valenceClient.js';
|
|
5
|
-
import { AudioTooShortError } from '../src/errors.js';
|
|
5
|
+
import { AudioTooShortError, AudioTooLongError } from '../src/errors.js';
|
|
6
6
|
|
|
7
7
|
describe('AsyncAudio', () => {
|
|
8
8
|
const originalEnv = process.env;
|
|
@@ -205,6 +205,34 @@ describe('AsyncAudio', () => {
|
|
|
205
205
|
expect(error.message).toContain('too short');
|
|
206
206
|
}
|
|
207
207
|
});
|
|
208
|
+
|
|
209
|
+
test('should throw AudioTooLongError when audio is too long', async () => {
|
|
210
|
+
fsMock.mockReturnValue(true);
|
|
211
|
+
statMock.mockReturnValue({ size: 2000000000 }); // Large file
|
|
212
|
+
|
|
213
|
+
nock('https://test-api.com')
|
|
214
|
+
.post('/v1/asynch/emotion/upload/initiate')
|
|
215
|
+
.query(true)
|
|
216
|
+
.reply(400, {
|
|
217
|
+
error: 'Audio file exceeds maximum duration. Maximum: 3600 seconds, provided: 5000 seconds',
|
|
218
|
+
error_code: 'AUDIO_TOO_LONG',
|
|
219
|
+
max_duration_seconds: 3600,
|
|
220
|
+
actual_duration_seconds: 5000
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const client = new ValenceClient({ apiKey: 'test-api-key' });
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
await client.asynch.upload('long_audio.wav');
|
|
227
|
+
expect.fail('Should have thrown AudioTooLongError');
|
|
228
|
+
} catch (error) {
|
|
229
|
+
expect(error).toBeInstanceOf(AudioTooLongError);
|
|
230
|
+
expect(error.name).toBe('AudioTooLongError');
|
|
231
|
+
expect(error.maxDuration).toBe(3600);
|
|
232
|
+
expect(error.actualDuration).toBe(5000);
|
|
233
|
+
expect(error.message).toContain('exceeds maximum');
|
|
234
|
+
}
|
|
235
|
+
});
|
|
208
236
|
});
|
|
209
237
|
|
|
210
238
|
describe('getEmotions', () => {
|
|
@@ -2,7 +2,7 @@ import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globa
|
|
|
2
2
|
import nock from 'nock';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import { ValenceClient } from '../src/valenceClient.js';
|
|
5
|
-
import { AudioTooShortError } from '../src/errors.js';
|
|
5
|
+
import { AudioTooShortError, FileSizeLimitExceededError } from '../src/errors.js';
|
|
6
6
|
|
|
7
7
|
describe('DiscreteAudio', () => {
|
|
8
8
|
const originalEnv = process.env;
|
|
@@ -11,12 +11,24 @@ describe('DiscreteAudio', () => {
|
|
|
11
11
|
beforeEach(() => {
|
|
12
12
|
process.env = { ...originalEnv };
|
|
13
13
|
process.env.VALENCE_API_KEY = 'test-api-key';
|
|
14
|
+
process.env.VALENCE_API_BASE_URL = 'https://test-api.com';
|
|
14
15
|
process.env.VALENCE_DISCRETE_URL = 'https://test-api.com/predict';
|
|
15
|
-
|
|
16
|
+
|
|
16
17
|
fsMock = jest.spyOn(fs, 'existsSync');
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
// Default mock for createReadStream
|
|
20
|
+
const mockStream = {
|
|
18
21
|
pipe: jest.fn(),
|
|
19
|
-
on: jest.fn()
|
|
22
|
+
on: jest.fn(),
|
|
23
|
+
pause: jest.fn(),
|
|
24
|
+
resume: jest.fn(),
|
|
25
|
+
destroy: jest.fn()
|
|
26
|
+
};
|
|
27
|
+
jest.spyOn(fs, 'createReadStream').mockReturnValue(mockStream);
|
|
28
|
+
|
|
29
|
+
// Default mock for statSync (1MB file)
|
|
30
|
+
jest.spyOn(fs, 'statSync').mockReturnValue({
|
|
31
|
+
size: 1 * 1024 * 1024
|
|
20
32
|
});
|
|
21
33
|
});
|
|
22
34
|
|
|
@@ -205,5 +217,48 @@ describe('DiscreteAudio', () => {
|
|
|
205
217
|
'API error (400): Invalid file format'
|
|
206
218
|
);
|
|
207
219
|
});
|
|
220
|
+
|
|
221
|
+
test('should throw FileSizeLimitExceededError when file exceeds 6MB', async () => {
|
|
222
|
+
fsMock.mockReturnValue(true);
|
|
223
|
+
|
|
224
|
+
// Mock file size to be 7MB (7 * 1024 * 1024 bytes)
|
|
225
|
+
jest.spyOn(fs, 'statSync').mockReturnValue({
|
|
226
|
+
size: 7 * 1024 * 1024
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const client = new ValenceClient();
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
await client.discrete.emotions('large_audio.wav');
|
|
233
|
+
expect.fail('Should have thrown FileSizeLimitExceededError');
|
|
234
|
+
} catch (error) {
|
|
235
|
+
expect(error).toBeInstanceOf(FileSizeLimitExceededError);
|
|
236
|
+
expect(error.name).toBe('FileSizeLimitExceededError');
|
|
237
|
+
expect(error.maxSizeMb).toBe(6.0);
|
|
238
|
+
expect(error.actualSizeMb).toBe(7.0);
|
|
239
|
+
expect(error.message).toContain('exceeds discrete API maximum');
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
test('should throw FileSizeLimitExceededError when file is exactly 6.1MB', async () => {
|
|
245
|
+
fsMock.mockReturnValue(true);
|
|
246
|
+
|
|
247
|
+
// Mock file size to be 6.1MB
|
|
248
|
+
jest.spyOn(fs, 'statSync').mockReturnValue({
|
|
249
|
+
size: 6.1 * 1024 * 1024
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const client = new ValenceClient();
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
await client.discrete.emotions('large_audio.wav');
|
|
256
|
+
expect.fail('Should have thrown FileSizeLimitExceededError');
|
|
257
|
+
} catch (error) {
|
|
258
|
+
expect(error).toBeInstanceOf(FileSizeLimitExceededError);
|
|
259
|
+
expect(error.maxSizeMb).toBe(6.0);
|
|
260
|
+
expect(error.actualSizeMb).toBeCloseTo(6.1, 1);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
208
263
|
});
|
|
209
264
|
});
|
package/examples/uploadLong.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { ValenceClient } from 'valenceai';
|
|
2
|
-
|
|
3
|
-
(async () => {
|
|
4
|
-
const client = new ValenceClient();
|
|
5
|
-
const requestId = await client.asynch.upload('long_audio.wav');
|
|
6
|
-
console.log('Request ID:', requestId);
|
|
7
|
-
|
|
8
|
-
const result = await client.asynch.emotions(requestId);
|
|
9
|
-
console.log('Emotion:', result);
|
|
10
|
-
})();
|