valenceai 0.4.0

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/src/config.js ADDED
@@ -0,0 +1,36 @@
1
+ import dotenv from 'dotenv';
2
+ dotenv.config();
3
+
4
+ /**
5
+ * Configuration object for the Valence SDK
6
+ * Loads settings from environment variables with sensible defaults
7
+ */
8
+ export const config = {
9
+ apiKey: process.env.VALENCE_API_KEY,
10
+ discreteAudioUrl: process.env.VALENCE_DISCRETE_URL || 'https://xc8n2bo4f0.execute-api.us-west-2.amazonaws.com/emotionprediction',
11
+ asyncAudioUrl: process.env.VALENCE_ASYNC_URL || 'https://wsgol61783.execute-api.us-west-2.amazonaws.com/prod',
12
+ logLevel: process.env.VALENCE_LOG_LEVEL || 'info',
13
+ };
14
+
15
+ /**
16
+ * Validates the current configuration
17
+ * @throws {Error} If required configuration is missing or invalid
18
+ */
19
+ export function validateConfig() {
20
+ if (!config.apiKey) {
21
+ throw new Error('VALENCE_API_KEY environment variable is required');
22
+ }
23
+
24
+ if (!config.discreteAudioUrl || !config.discreteAudioUrl.startsWith('https://')) {
25
+ throw new Error('VALENCE_DISCRETE_URL must be a valid HTTPS URL');
26
+ }
27
+
28
+ if (!config.asyncAudioUrl || !config.asyncAudioUrl.startsWith('https://')) {
29
+ throw new Error('VALENCE_ASYNC_URL must be a valid HTTPS URL');
30
+ }
31
+
32
+ const validLogLevels = ['debug', 'info', 'warn', 'error'];
33
+ if (!validLogLevels.includes(config.logLevel)) {
34
+ throw new Error(`VALENCE_LOG_LEVEL must be one of: ${validLogLevels.join(', ')}`);
35
+ }
36
+ }
@@ -0,0 +1,61 @@
1
+ import axios from 'axios';
2
+ import { getHeaders } from './client.js';
3
+ import { config } from './config.js';
4
+ import { log } from './utils/logger.js';
5
+ import fs from 'fs';
6
+ import FormData from 'form-data';
7
+
8
+ /**
9
+ * Predicts emotions for discrete (short) audio files
10
+ * @param {string} filePath - Path to the audio file
11
+ * @param {string} model - Model type ('4emotions' or '7emotions')
12
+ * @returns {Promise<Object>} Emotion prediction results
13
+ * @throws {Error} If file doesn't exist, API key missing, or request fails
14
+ */
15
+ export async function predictDiscreteAudioEmotion(filePath, model = '4emotions') {
16
+ // Validation
17
+ if (!filePath || typeof filePath !== 'string') {
18
+ throw new Error('filePath is required and must be a string');
19
+ }
20
+
21
+ if (!['4emotions', '7emotions'].includes(model)) {
22
+ throw new Error('model must be either "4emotions" or "7emotions"');
23
+ }
24
+
25
+ if (!config.apiKey) {
26
+ throw new Error('VALENCE_API_KEY is required');
27
+ }
28
+
29
+ if (!fs.existsSync(filePath)) {
30
+ throw new Error(`File not found: ${filePath}`);
31
+ }
32
+
33
+ log(`Predicting emotions for ${filePath} using ${model} model`, 'info');
34
+
35
+ try {
36
+ const formData = new FormData();
37
+ formData.append('file', fs.createReadStream(filePath));
38
+ const url = `${config.discreteAudioUrl}?model=${model}`;
39
+
40
+ const response = await axios.post(url, formData, {
41
+ headers: {
42
+ ...getHeaders(),
43
+ ...formData.getHeaders()
44
+ },
45
+ timeout: 30000 // 30 second timeout
46
+ });
47
+
48
+ log('Emotion prediction completed successfully', 'info');
49
+ return response.data;
50
+ } catch (error) {
51
+ log(`Error predicting emotions: ${error.message}`, 'error');
52
+
53
+ if (error.response) {
54
+ throw new Error(`API error (${error.response.status}): ${error.response.data?.message || error.response.statusText}`);
55
+ } else if (error.request) {
56
+ throw new Error('Network error: Unable to reach the API');
57
+ } else {
58
+ throw new Error(`Request error: ${error.message}`);
59
+ }
60
+ }
61
+ }
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { ValenceClient } from './valenceClient.js';
2
+ export { validateConfig } from './config.js';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Simple logger function controlled by VALENCE_LOG_LEVEL environment variable
3
+ * @param {string} message - Message to log
4
+ * @param {string} level - Log level ('debug', 'info', 'warn', 'error')
5
+ */
6
+ export function log(message, level = 'info') {
7
+ const logLevel = process.env.VALENCE_LOG_LEVEL || 'info';
8
+ const levels = { debug: 0, info: 1, warn: 2, error: 3 };
9
+ const currentLevel = levels[logLevel] || 1;
10
+ const messageLevel = levels[level] || 1;
11
+
12
+ if (messageLevel >= currentLevel) {
13
+ const timestamp = new Date().toISOString();
14
+ console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
15
+ }
16
+ }
File without changes
@@ -0,0 +1,256 @@
1
+ import axios from 'axios';
2
+ import fs from 'fs';
3
+ import FormData from 'form-data';
4
+ import { config } from './config.js';
5
+ import { getHeaders } from './client.js';
6
+ import { log } from './utils/logger.js';
7
+
8
+ /**
9
+ * Client for discrete (short) audio processing
10
+ */
11
+ class DiscreteClient {
12
+ constructor() {}
13
+
14
+ /**
15
+ * Get emotions for discrete (short) audio files
16
+ * @param {string} filePath - Path to the audio file
17
+ * @param {string} model - Model type ('4emotions' or '7emotions')
18
+ * @returns {Promise<Object>} Emotion prediction results
19
+ * @throws {Error} If file doesn't exist, API key missing, or request fails
20
+ */
21
+ async emotions(filePath, model = '4emotions') {
22
+ // Validation
23
+ if (!filePath || typeof filePath !== 'string') {
24
+ throw new Error('filePath is required and must be a string');
25
+ }
26
+
27
+ if (!['4emotions', '7emotions'].includes(model)) {
28
+ throw new Error('model must be either "4emotions" or "7emotions"');
29
+ }
30
+
31
+ if (!config.apiKey) {
32
+ throw new Error('VALENCE_API_KEY is required');
33
+ }
34
+
35
+ if (!fs.existsSync(filePath)) {
36
+ throw new Error(`File not found: ${filePath}`);
37
+ }
38
+
39
+ log(`Getting emotions for discrete audio: ${filePath} using ${model} model`, 'info');
40
+
41
+ try {
42
+ const formData = new FormData();
43
+ formData.append('file', fs.createReadStream(filePath));
44
+ const url = `${config.discreteAudioUrl}?model=${model}`;
45
+
46
+ const response = await axios.post(url, formData, {
47
+ headers: {
48
+ ...getHeaders(),
49
+ ...formData.getHeaders()
50
+ },
51
+ timeout: 30000 // 30 second timeout
52
+ });
53
+
54
+ log('Discrete emotion prediction completed successfully', 'info');
55
+ return response.data;
56
+ } catch (error) {
57
+ log(`Error getting discrete emotions: ${error.message}`, 'error');
58
+
59
+ if (error.response) {
60
+ throw new Error(`API error (${error.response.status}): ${error.response.data?.message || error.response.statusText}`);
61
+ } else if (error.request) {
62
+ throw new Error('Network error: Unable to reach the API');
63
+ } else {
64
+ throw new Error(`Request error: ${error.message}`);
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Client for async (long) audio processing
72
+ */
73
+ class AsyncClient {
74
+ constructor(partSize = 5 * 1024 * 1024, maxRetries = 3) {
75
+ this.partSize = partSize;
76
+ this.maxRetries = maxRetries;
77
+ }
78
+
79
+ /**
80
+ * Upload async (long) audio files using multipart upload
81
+ * @param {string} filePath - Path to the audio file
82
+ * @returns {Promise<string>} Request ID for tracking the upload
83
+ * @throws {Error} If file doesn't exist, API key missing, or upload fails
84
+ */
85
+ async upload(filePath) {
86
+ // Validation
87
+ if (!filePath || typeof filePath !== 'string') {
88
+ throw new Error('filePath is required and must be a string');
89
+ }
90
+
91
+ if (!config.apiKey) {
92
+ throw new Error('VALENCE_API_KEY is required');
93
+ }
94
+
95
+ if (!fs.existsSync(filePath)) {
96
+ throw new Error(`File not found: ${filePath}`);
97
+ }
98
+
99
+ if (this.partSize < 1024 * 1024 || this.partSize > 100 * 1024 * 1024) {
100
+ throw new Error('partSize must be between 1MB and 100MB');
101
+ }
102
+
103
+ log(`Starting async audio upload for ${filePath}`, 'info');
104
+
105
+ try {
106
+ const fileSize = fs.statSync(filePath).size;
107
+ const partCount = Math.ceil(fileSize / this.partSize);
108
+ const initiateUrl = `${config.asyncAudioUrl}/upload/initiate`;
109
+
110
+ log(`File size: ${fileSize} bytes, parts: ${partCount}`, 'debug');
111
+
112
+ const { data } = await axios.get(initiateUrl, {
113
+ headers: getHeaders(),
114
+ params: { file_name: filePath.split('/').pop(), part_count: partCount },
115
+ timeout: 30000
116
+ });
117
+
118
+ const fileStream = fs.createReadStream(filePath, { highWaterMark: this.partSize });
119
+ const parts = [];
120
+ let index = 0;
121
+
122
+ for await (const chunk of fileStream) {
123
+ if (!data.presigned_urls || !data.presigned_urls[index]) {
124
+ throw new Error(`Missing presigned URL for part ${index + 1}`);
125
+ }
126
+
127
+ const part = data.presigned_urls[index];
128
+ const partNumber = part.part_number;
129
+ const url = part.url;
130
+
131
+ log(`Uploading part ${index + 1}/${partCount}`, 'debug');
132
+
133
+ const res = await axios.put(url, chunk, {
134
+ headers: { 'Content-Length': chunk.length },
135
+ timeout: 60000 // 1 minute timeout for each part
136
+ });
137
+
138
+ if (!res.headers.etag) {
139
+ throw new Error(`Failed to get ETag for part ${partNumber}`);
140
+ }
141
+
142
+ parts.push({ ETag: res.headers.etag, PartNumber: partNumber });
143
+ index++;
144
+ }
145
+
146
+ log('All parts uploaded, completing multipart upload', 'info');
147
+
148
+ const completeUrl = `${config.asyncAudioUrl}/upload/complete`;
149
+ await axios.post(completeUrl, {
150
+ request_id: data.request_id,
151
+ upload_id: data.upload_id,
152
+ parts: parts.sort((a, b) => a.PartNumber - b.PartNumber)
153
+ }, {
154
+ headers: getHeaders(),
155
+ timeout: 30000
156
+ });
157
+
158
+ log(`Async audio upload completed. Request ID: ${data.request_id}`, 'info');
159
+ return data.request_id;
160
+ } catch (error) {
161
+ log(`Error uploading async audio: ${error.message}`, 'error');
162
+
163
+ if (error.response) {
164
+ throw new Error(`API error (${error.response.status}): ${error.response.data?.message || error.response.statusText}`);
165
+ } else if (error.request) {
166
+ throw new Error('Network error: Unable to reach the API');
167
+ } else {
168
+ throw new Error(`Upload error: ${error.message}`);
169
+ }
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Get emotion prediction results for async audio processing
175
+ * @param {string} requestId - Request ID from upload method
176
+ * @param {number} maxAttempts - Maximum polling attempts (default: 20)
177
+ * @param {number} intervalSeconds - Polling interval in seconds (default: 5)
178
+ * @returns {Promise<Object>} Emotion prediction results
179
+ * @throws {Error} If requestId is invalid or prediction times out
180
+ */
181
+ async emotions(requestId, maxAttempts = 20, intervalSeconds = 5) {
182
+ // Validation
183
+ if (!requestId || typeof requestId !== 'string') {
184
+ throw new Error('requestId is required and must be a string');
185
+ }
186
+
187
+ if (!config.apiKey) {
188
+ throw new Error('VALENCE_API_KEY is required');
189
+ }
190
+
191
+ if (maxAttempts < 1 || maxAttempts > 100) {
192
+ throw new Error('maxAttempts must be between 1 and 100');
193
+ }
194
+
195
+ if (intervalSeconds < 1 || intervalSeconds > 60) {
196
+ throw new Error('intervalSeconds must be between 1 and 60');
197
+ }
198
+
199
+ log(`Starting emotion prediction polling for request ${requestId}`, 'info');
200
+
201
+ const url = `${config.asyncAudioUrl}/prediction`;
202
+ const intervalMs = intervalSeconds * 1000;
203
+
204
+ for (let i = 0; i < maxAttempts; i++) {
205
+ try {
206
+ log(`Polling attempt ${i + 1}/${maxAttempts}`, 'debug');
207
+
208
+ const res = await axios.get(url, {
209
+ headers: getHeaders(),
210
+ params: { request_id: requestId },
211
+ timeout: 15000
212
+ });
213
+
214
+ if (res.status === 200 && res.data) {
215
+ log(`Emotions retrieved for request ID: ${requestId}`, 'info');
216
+ return res.data;
217
+ }
218
+
219
+ if (res.status === 202) {
220
+ log('Prediction still processing, waiting...', 'debug');
221
+ }
222
+
223
+ } catch (error) {
224
+ if (error.response?.status === 404) {
225
+ throw new Error(`Request ID not found: ${requestId}`);
226
+ } else if (error.response?.status >= 400 && error.response?.status < 500) {
227
+ throw new Error(`Client error (${error.response.status}): ${error.response.data?.message || error.response.statusText}`);
228
+ }
229
+
230
+ log(`Polling error (attempt ${i + 1}): ${error.message}`, 'warn');
231
+ }
232
+
233
+ if (i < maxAttempts - 1) {
234
+ await new Promise(resolve => setTimeout(resolve, intervalMs));
235
+ }
236
+ }
237
+
238
+ throw new Error(`Prediction not available after ${maxAttempts} attempts. The request may still be processing.`);
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Main client for Valence API with nested discrete and async clients
244
+ */
245
+ export class ValenceClient {
246
+ /**
247
+ * Initialize the Valence client
248
+ * @param {number} partSize - Size of each part in bytes for async uploads (default: 5MB)
249
+ * @param {number} maxRetries - Maximum retry attempts for async uploads (default: 3)
250
+ */
251
+ constructor(partSize = 5 * 1024 * 1024, maxRetries = 3) {
252
+ // Initialize nested clients
253
+ this.discrete = new DiscreteClient();
254
+ this.asynch = new AsyncClient(partSize, maxRetries);
255
+ }
256
+ }