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 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) Pulse API for emotion detection. It provides a convenient interface to upload audio files, stream real-time audio, and retrieve detected emotional states.
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 (4-10s)
8
- - **Async audio processing** - Multipart streaming for long files with timeline data
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
- ## Emotion Models
17
+ ## API Overview
23
18
 
24
- The SDK supports two emotion detection models:
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
- - **4emotions** (default): angry, happy, neutral, sad
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 number of emotions, emotional buckets, and language support can be customized. If you are interested in a custom model, please [contact us](https://www.getvalenceai.com/contact).
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
- ## API Overview
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
- | API | Best For | Input | Output | Response Time |
34
- |-----|----------|-------|--------|---------------|
35
- | **Discrete** | Real-time analysis | Short audio (4-10s) | Single emotion prediction | 100-500ms |
36
- | **Async** | Pre-recorded files | Long audio (up to 1GB) | Timeline with emotion changes | Depends on file size |
37
- | **Streaming** | Live audio streams | Audio chunks via WebSocket | Real-time emotion updates | Near real-time |
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
- ## Async API Processing Workflow
81
+ ## Asynch API Processing Workflow
40
82
 
41
- The Async API uses a multi-step process to handle long audio files. Understanding this workflow is crucial for proper implementation:
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 to S3 using presigned URLs
49
- - **Returns a `requestId`** - This is a tracking identifier, NOT a completion signal
50
- - At this point: File is uploaded to S3, but **NOT processed yet**
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
- - Background processor checks for new uploads every 10 seconds
57
- - Downloads audio from S3 when detected
98
+ - Checks for new uploads
99
+ - Downloads audio when a new File is detected
58
100
  - Splits audio into 5-second segments
59
- - Extracts audio features (MFCC) from each segment
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-2 minutes for a 60-minute audio file. The exact time depends on file length and current server load.
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 to S3 (processing not started)
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 to S3 |
83
- | `upload_completed` | Upload finished | File is in S3, waiting for background processor |
84
- | `processing` | Processing active | Server is analyzing audio with ML model |
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
- - **The `requestId` is NOT a completion indicator** - It's just a tracking ID
90
- - **`upload()` completing does NOT mean results are ready** - It only means the file is in S3
91
- - **Background processing takes time** - Plan for 1-2 minutes per hour of audio
92
- - **You can check status anytime** - The `requestId` remains valid for retrieving results
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 (uses VALENCE_API_KEY environment variable)
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', '4emotions');
110
- console.log(`Emotion: ${result.dominant_emotion}`);
145
+ const result = await client.discrete.emotions('short_audio.wav');
146
+ console.log(`Emotion: ${result.main_emotion}`);
111
147
 
112
- // Async API - Long audio with timeline
113
- // Step 1: Upload file to S3 (returns tracking ID, NOT results)
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 timeline and dominant emotion from results
118
- const timeline = await client.asynch.getTimeline(requestId);
119
- const dominant = await client.asynch.getDominantEmotion(requestId);
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('4emotions');
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
- // File upload
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
- // In-memory audio array
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.05,
185
- neutral: 0.05
192
+ angry: 0.08,
193
+ neutral: 0.15
186
194
  },
187
- dominant_emotion: 'happy'
195
+ main_emotion: 'happy'
188
196
  }
189
197
  ```
190
198
 
191
- ### Async API
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 to S3 (multipart upload)
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
- #### Timeline Analysis
252
+ Note: The `all_predictions` field is only included when `comprehensiveOutput: true` is set in the client constructor.
253
253
 
254
- ```javascript
255
- // Get full timeline
256
- const timeline = await client.asynch.getTimeline(requestId);
254
+ #### Helper Methods
257
255
 
258
- // Get emotion at specific time
259
- const emotion = await client.asynch.getEmotionAtTime(requestId, 5.2);
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 dominant emotion across entire audio
262
- const dominant = await client.asynch.getDominantEmotion(requestId);
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('4emotions');
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.05
311
+ neutral: 0.15
311
312
  },
312
- timestamp: 1234567890
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
- // second: { limit: 10, remaining: 8, reset: 1234567890 },
327
- // minute: { limit: 100, remaining: 95, reset: 1234567890 },
328
- // hour: { limit: 1000, remaining: 950, reset: 1234567890 },
329
- // day: { limit: 10000, remaining: 9500, reset: 1234567890 }
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
- // second: 2,
333
- // minute: 5,
334
- // hour: 50,
335
- // day: 500
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: 1234567890 }
353
+ // { status: 'healthy', timestamp: 1738684800 }
343
354
  ```
344
355
 
345
- ## Audio Input Requirements
356
+ The `reset` and `timestamp` values are Unix timestamps (UTC) in seconds.
346
357
 
347
- ### Format Specifications
358
+ ## Error Responses
348
359
 
349
- - **Format**: WAV (mono)
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
- - **Discrete API**: 4-10 seconds per file
357
- - **Async API**: Minimum 5 seconds, maximum 1 GB
358
- - **Streaming API**: Real-time audio chunks (Buffer or ArrayBuffer)
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
- For custom microphone specifications or stereo/multi-channel support, please [contact us](https://www.getvalenceai.com/contact).
370
+ ### Asynch API Errors
361
371
 
362
- ## Examples
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
- Example scripts are available in the [`examples/`](./examples) directory:
382
+ **Asynch Status Values:**
365
383
 
366
- ```bash
367
- # Install dependencies
368
- npm install
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
- # Run discrete audio example
371
- npm run example:discrete
392
+ ### Streaming API Errors
372
393
 
373
- # Run async audio example
374
- npm run example:asynch
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
- # Run streaming example
377
- npm run example:streaming
401
+ ### Rate Limit API Errors
378
402
 
379
- # Or run directly
380
- node examples/uploadShort.js
381
- node examples/uploadLong.js
382
- node examples/streamingAudio.js
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 { ValenceClient } from 'valenceai';
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.message.includes('API key')) {
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.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**: Async API now returns detailed timestamp information
444
- 6. **Model Selection**: Explicit model parameter for 4emotions or 7emotions
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', '4emotions');
459
+ const result = await client.discrete.emotions('file.wav');
457
460
 
458
461
  // New streaming capability
459
- const stream = client.streaming.connect('4emotions');
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', '4emotions');
485
- // result.dominant_emotion is typed
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
- - **Issues**: [GitHub Issues](https://github.com/valencevibrations/valence-sdk-js/issues)
502
- - **Questions**: [Valence AI Support](https://www.getvalenceai.com/contact)
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 © 2025 [Valence Vibrations, Inc](https://getvalenceai.com), a Delaware public benefit corporation.
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.2",
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": "ISC",
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 staging environment (demo.getvalenceai.com)
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://demo.getvalenceai.com';
9
- const DEFAULT_WEBSOCKET_URL = 'wss://demo.getvalenceai.com';
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 { ValenceSDKError, AudioTooShortError } from './errors.js';
3
+ export {
4
+ ValenceSDKError,
5
+ AudioTooShortError,
6
+ AudioTooLongError,
7
+ FileSizeLimitExceededError
8
+ } from './errors.js';
@@ -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 null;
449
+ return {};
330
450
  }
331
451
 
332
- const emotionCounts = {};
452
+ const counts = {};
333
453
  for (const emotionData of timeline) {
334
454
  const emotion = emotionData.emotion;
335
- emotionCounts[emotion] = (emotionCounts[emotion] || 0) + 1;
455
+ counts[emotion] = (counts[emotion] || 0) + 1;
336
456
  }
337
457
 
338
- return Object.keys(emotionCounts).reduce((a, b) =>
339
- emotionCounts[a] > emotionCounts[b] ? a : b
340
- );
458
+ return counts;
341
459
  }
342
460
  }
343
461
 
@@ -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
- jest.spyOn(fs, 'createReadStream').mockReturnValue({
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
  });
@@ -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
- })();
@@ -1,7 +0,0 @@
1
- import { ValenceClient } from 'valenceai';
2
-
3
- (async () => {
4
- const client = new ValenceClient();
5
- const result = await client.discrete.emotions('sample.wav', '7emotions');
6
- console.log(result);
7
- })();