@neosapience/n8n-nodes-typecast 1.0.2 → 1.0.4
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/README.md +7 -8
- package/credentials/TypecastApi.credentials.ts +37 -36
- package/dist/credentials/TypecastApi.credentials.js.map +1 -1
- package/dist/nodes/Typecast/Typecast.node.d.ts +6 -1
- package/dist/nodes/Typecast/Typecast.node.js +139 -19
- package/dist/nodes/Typecast/Typecast.node.js.map +1 -1
- package/dist/nodes/Typecast/resources/speech/index.js +367 -55
- package/dist/nodes/Typecast/resources/speech/index.js.map +1 -1
- package/dist/nodes/Typecast/resources/voice/index.js +152 -48
- package/dist/nodes/Typecast/resources/voice/index.js.map +1 -1
- package/dist/nodes/Typecast/shared/transport.d.ts +2 -2
- package/dist/nodes/Typecast/shared/transport.js +20 -6
- package/dist/nodes/Typecast/shared/transport.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/nodes/Typecast/Typecast.node.ts +358 -199
- package/nodes/Typecast/resources/speech/index.ts +563 -228
- package/nodes/Typecast/resources/voice/index.ts +189 -88
- package/nodes/Typecast/shared/transport.ts +72 -44
- package/package.json +1 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
NodeConnectionTypes,
|
|
3
|
+
type IExecuteFunctions,
|
|
4
|
+
type ILoadOptionsFunctions,
|
|
5
|
+
type INodeExecutionData,
|
|
6
|
+
type INodeListSearchResult,
|
|
7
|
+
type INodeType,
|
|
8
|
+
type INodeTypeDescription,
|
|
9
|
+
type IDataObject,
|
|
8
10
|
} from 'n8n-workflow';
|
|
9
11
|
|
|
10
12
|
import { typecastApiRequest, typecastApiRequestBinary } from './shared/transport';
|
|
@@ -13,197 +15,354 @@ import { voiceDescription } from './resources/voice';
|
|
|
13
15
|
import { speechDescription } from './resources/speech';
|
|
14
16
|
|
|
15
17
|
export class Typecast implements INodeType {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
18
|
+
description: INodeTypeDescription = {
|
|
19
|
+
displayName: 'Typecast',
|
|
20
|
+
name: 'typecast',
|
|
21
|
+
icon: 'file:../../icons/typecast.svg',
|
|
22
|
+
group: ['transform'],
|
|
23
|
+
version: 1,
|
|
24
|
+
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
|
25
|
+
description: 'Interact with Typecast TTS API',
|
|
26
|
+
documentationUrl: 'https://typecast.ai/docs/integrations/n8n',
|
|
27
|
+
usableAsTool: true,
|
|
28
|
+
defaults: {
|
|
29
|
+
name: 'Typecast',
|
|
30
|
+
},
|
|
31
|
+
inputs: [NodeConnectionTypes.Main],
|
|
32
|
+
outputs: [NodeConnectionTypes.Main],
|
|
33
|
+
credentials: [
|
|
34
|
+
{
|
|
35
|
+
name: 'typecastApi',
|
|
36
|
+
required: true,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
properties: [
|
|
40
|
+
{
|
|
41
|
+
displayName: 'Resource',
|
|
42
|
+
name: 'resource',
|
|
43
|
+
type: 'options',
|
|
44
|
+
noDataExpression: true,
|
|
45
|
+
options: [
|
|
46
|
+
{
|
|
47
|
+
name: 'Speech',
|
|
48
|
+
value: 'speech',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'Voice',
|
|
52
|
+
value: 'voice',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
default: 'speech',
|
|
56
|
+
},
|
|
57
|
+
...voiceDescription,
|
|
58
|
+
...speechDescription,
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
methods = {
|
|
63
|
+
listSearch: {
|
|
64
|
+
async searchVoices(
|
|
65
|
+
this: ILoadOptionsFunctions,
|
|
66
|
+
filter?: string,
|
|
67
|
+
): Promise<INodeListSearchResult> {
|
|
68
|
+
const results: INodeListSearchResult = {
|
|
69
|
+
results: [],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Get the selected model to filter voices
|
|
74
|
+
const model = (this.getNodeParameter('model', 0) as string) || 'ssfm-v30';
|
|
75
|
+
const qs: IDataObject = {};
|
|
76
|
+
|
|
77
|
+
// Add model filter to query string
|
|
78
|
+
if (model) {
|
|
79
|
+
qs.model = model;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Fetch voices from v2 API filtered by model
|
|
83
|
+
const response = await typecastApiRequest.call(this, 'GET', '/voices', {}, qs, 'v2');
|
|
84
|
+
|
|
85
|
+
// Process the response - it could be an array directly or wrapped in a result object
|
|
86
|
+
const voices = Array.isArray(response) ? response : response.result || [];
|
|
87
|
+
|
|
88
|
+
for (const voice of voices) {
|
|
89
|
+
const voiceId = voice.voice_id;
|
|
90
|
+
const voiceName = voice.voice_name || voiceId;
|
|
91
|
+
const gender = voice.gender
|
|
92
|
+
? voice.gender.charAt(0).toUpperCase() + voice.gender.slice(1)
|
|
93
|
+
: 'Unknown';
|
|
94
|
+
const age = voice.age
|
|
95
|
+
? voice.age.replace(/_/g, ' ').replace(/\b\w/g, (c: string) => c.toUpperCase())
|
|
96
|
+
: 'Unknown';
|
|
97
|
+
|
|
98
|
+
// Get supported emotions from models array (prefer ssfm-v30)
|
|
99
|
+
let emotions: string[] = [];
|
|
100
|
+
if (voice.models && Array.isArray(voice.models)) {
|
|
101
|
+
// Try to find ssfm-v30 first, then fall back to first model
|
|
102
|
+
const v30Model = voice.models.find((m: IDataObject) => m.version === 'ssfm-v30');
|
|
103
|
+
const modelToUse = v30Model || voice.models[0];
|
|
104
|
+
if (modelToUse && Array.isArray(modelToUse.emotions)) {
|
|
105
|
+
emotions = modelToUse.emotions as string[];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Format emotions for display
|
|
109
|
+
const emotionList = emotions.join(', ');
|
|
110
|
+
const emotionDisplay = emotionList || 'N/A';
|
|
111
|
+
|
|
112
|
+
// Get use cases
|
|
113
|
+
const useCases = voice.use_cases || [];
|
|
114
|
+
const useCaseList = Array.isArray(useCases) ? useCases.join(', ') : '';
|
|
115
|
+
const useCaseDisplay = useCaseList || 'N/A';
|
|
116
|
+
|
|
117
|
+
// Format: Name | Gender | Age | Emotions (all info in name for visibility)
|
|
118
|
+
const displayName = `${voiceName} | ${gender} | ${age} | ${emotionDisplay}`;
|
|
119
|
+
const description = `ID: ${voiceId} | Use Cases: ${useCaseDisplay}`;
|
|
120
|
+
|
|
121
|
+
// Apply filter if provided
|
|
122
|
+
if (filter) {
|
|
123
|
+
const searchLower = filter.toLowerCase();
|
|
124
|
+
const matchesName = voiceName.toLowerCase().includes(searchLower);
|
|
125
|
+
const matchesId = voiceId.toLowerCase().includes(searchLower);
|
|
126
|
+
const matchesGender = gender.toLowerCase().includes(searchLower);
|
|
127
|
+
const matchesAge = age.toLowerCase().includes(searchLower);
|
|
128
|
+
const matchesEmotion = emotions.some((e: string) =>
|
|
129
|
+
e.toLowerCase().includes(searchLower),
|
|
130
|
+
);
|
|
131
|
+
const matchesUseCase = useCases.some((u: string) =>
|
|
132
|
+
u.toLowerCase().includes(searchLower),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
!matchesName &&
|
|
137
|
+
!matchesId &&
|
|
138
|
+
!matchesGender &&
|
|
139
|
+
!matchesAge &&
|
|
140
|
+
!matchesEmotion &&
|
|
141
|
+
!matchesUseCase
|
|
142
|
+
) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
results.results.push({
|
|
148
|
+
name: displayName,
|
|
149
|
+
value: voiceId,
|
|
150
|
+
url: `https://typecast.ai/developers/api/voices/${voiceId}`,
|
|
151
|
+
description,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Sort by name
|
|
156
|
+
results.results.sort((a, b) => a.name.localeCompare(b.name));
|
|
157
|
+
} catch (error) {
|
|
158
|
+
// If API call fails, show helpful message - user can still use "By ID" mode
|
|
159
|
+
const errorMessage = (error as Error).message || 'Unknown error';
|
|
160
|
+
let hint = 'Check your Typecast API credentials';
|
|
161
|
+
|
|
162
|
+
if (errorMessage.includes('401') || errorMessage.includes('Unauthorized')) {
|
|
163
|
+
hint = 'Invalid API key - update your credentials';
|
|
164
|
+
} else if (errorMessage.includes('403') || errorMessage.includes('Forbidden')) {
|
|
165
|
+
hint = 'API key has no permission - check credentials';
|
|
166
|
+
} else if (errorMessage.includes('network') || errorMessage.includes('ENOTFOUND')) {
|
|
167
|
+
hint = 'Network error - check internet connection';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
results.results = [
|
|
171
|
+
{
|
|
172
|
+
name: `⚠️ ${hint}`,
|
|
173
|
+
value: '',
|
|
174
|
+
description: 'Use "By ID" mode to enter Voice ID directly',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: '💡 Switch to "By ID" Mode to Enter Voice ID Manually',
|
|
178
|
+
value: '',
|
|
179
|
+
description: 'Click the dropdown on the left and select "By ID"',
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return results;
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
190
|
+
const items = this.getInputData();
|
|
191
|
+
const returnData: INodeExecutionData[] = [];
|
|
192
|
+
const resource = this.getNodeParameter('resource', 0);
|
|
193
|
+
const operation = this.getNodeParameter('operation', 0);
|
|
194
|
+
|
|
195
|
+
for (let i = 0; i < items.length; i++) {
|
|
196
|
+
try {
|
|
197
|
+
if (resource === 'voice') {
|
|
198
|
+
// ----------------------------------
|
|
199
|
+
// voice:getMany
|
|
200
|
+
// ----------------------------------
|
|
201
|
+
if (operation === 'getMany') {
|
|
202
|
+
const filters = this.getNodeParameter('filters', i, {}) as IDataObject;
|
|
203
|
+
const qs: IDataObject = {};
|
|
204
|
+
|
|
205
|
+
// Add filters to query string
|
|
206
|
+
if (filters.model) {
|
|
207
|
+
qs.model = filters.model;
|
|
208
|
+
}
|
|
209
|
+
if (filters.gender) {
|
|
210
|
+
qs.gender = filters.gender;
|
|
211
|
+
}
|
|
212
|
+
if (filters.age) {
|
|
213
|
+
qs.age = filters.age;
|
|
214
|
+
}
|
|
215
|
+
if (filters.use_cases) {
|
|
216
|
+
qs.use_cases = filters.use_cases;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const response = await typecastApiRequest.call(this, 'GET', '/voices', {}, qs, 'v2');
|
|
220
|
+
returnData.push(
|
|
221
|
+
...this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(response), {
|
|
222
|
+
itemData: { item: i },
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (resource === 'speech') {
|
|
229
|
+
// ----------------------------------
|
|
230
|
+
// speech:textToSpeech
|
|
231
|
+
// ----------------------------------
|
|
232
|
+
if (operation === 'textToSpeech') {
|
|
233
|
+
const voiceId = this.getNodeParameter('voiceId', i, '', {
|
|
234
|
+
extractValue: true,
|
|
235
|
+
}) as string;
|
|
236
|
+
const text = this.getNodeParameter('text', i) as string;
|
|
237
|
+
const model = this.getNodeParameter('model', i) as string;
|
|
238
|
+
const additionalOptions = this.getNodeParameter(
|
|
239
|
+
'additionalOptions',
|
|
240
|
+
i,
|
|
241
|
+
{},
|
|
242
|
+
) as IDataObject;
|
|
243
|
+
|
|
244
|
+
const body: IDataObject = {
|
|
245
|
+
voice_id: voiceId,
|
|
246
|
+
text,
|
|
247
|
+
model,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// Add optional language parameter
|
|
251
|
+
if (additionalOptions.language) {
|
|
252
|
+
body.language = additionalOptions.language;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Build prompt object based on model and emotion type
|
|
256
|
+
const prompt: IDataObject = {};
|
|
257
|
+
|
|
258
|
+
if (model === 'ssfm-v30') {
|
|
259
|
+
// Get emotion type for ssfm-v30
|
|
260
|
+
const emotionType = this.getNodeParameter('emotionType', i, 'preset') as string;
|
|
261
|
+
|
|
262
|
+
if (emotionType === 'smart') {
|
|
263
|
+
// Smart Emotion: AI automatically infers emotion from context
|
|
264
|
+
prompt.emotion_type = 'smart';
|
|
265
|
+
|
|
266
|
+
const previousText = this.getNodeParameter('previousText', i, '') as string;
|
|
267
|
+
const nextText = this.getNodeParameter('nextText', i, '') as string;
|
|
268
|
+
|
|
269
|
+
if (previousText) {
|
|
270
|
+
prompt.previous_text = previousText;
|
|
271
|
+
}
|
|
272
|
+
if (nextText) {
|
|
273
|
+
prompt.next_text = nextText;
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
// Preset Emotion: Manual selection
|
|
277
|
+
prompt.emotion_type = 'preset';
|
|
278
|
+
|
|
279
|
+
const emotionPreset = this.getNodeParameter('emotionPreset', i, 'normal') as string;
|
|
280
|
+
const emotionIntensity = this.getNodeParameter('emotionIntensity', i, 1) as number;
|
|
281
|
+
|
|
282
|
+
prompt.emotion_preset = emotionPreset;
|
|
283
|
+
prompt.emotion_intensity = emotionIntensity;
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
// ssfm-v21: Use legacy prompt format (no emotion_type field)
|
|
287
|
+
// For v21, emotion settings are in additionalOptions
|
|
288
|
+
if (additionalOptions.emotionPresetV21) {
|
|
289
|
+
prompt.emotion_preset = additionalOptions.emotionPresetV21;
|
|
290
|
+
}
|
|
291
|
+
if (additionalOptions.emotionIntensityV21 !== undefined) {
|
|
292
|
+
prompt.emotion_intensity = additionalOptions.emotionIntensityV21;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (Object.keys(prompt).length > 0) {
|
|
297
|
+
body.prompt = prompt;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Add output settings
|
|
301
|
+
const output: IDataObject = {};
|
|
302
|
+
if (additionalOptions.volume !== undefined) {
|
|
303
|
+
output.volume = additionalOptions.volume;
|
|
304
|
+
}
|
|
305
|
+
if (additionalOptions.audioPitch !== undefined) {
|
|
306
|
+
output.audio_pitch = additionalOptions.audioPitch;
|
|
307
|
+
}
|
|
308
|
+
if (additionalOptions.audioTempo !== undefined) {
|
|
309
|
+
output.audio_tempo = additionalOptions.audioTempo;
|
|
310
|
+
}
|
|
311
|
+
if (additionalOptions.audioFormat) {
|
|
312
|
+
output.audio_format = additionalOptions.audioFormat;
|
|
313
|
+
}
|
|
314
|
+
if (Object.keys(output).length > 0) {
|
|
315
|
+
body.output = output;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Add seed if provided
|
|
319
|
+
if (additionalOptions.seed !== undefined) {
|
|
320
|
+
body.seed = additionalOptions.seed;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const binaryProperty = additionalOptions.binaryProperty || 'data';
|
|
324
|
+
const audioFormat = additionalOptions.audioFormat || 'wav';
|
|
325
|
+
const mimeType = audioFormat === 'mp3' ? 'audio/mpeg' : 'audio/wav';
|
|
326
|
+
|
|
327
|
+
const response = await typecastApiRequestBinary.call(
|
|
328
|
+
this,
|
|
329
|
+
'POST',
|
|
330
|
+
'/text-to-speech',
|
|
331
|
+
body,
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const newItem: INodeExecutionData = {
|
|
335
|
+
json: {
|
|
336
|
+
voice_id: voiceId,
|
|
337
|
+
text,
|
|
338
|
+
model,
|
|
339
|
+
},
|
|
340
|
+
binary: {
|
|
341
|
+
[binaryProperty as string]: await this.helpers.prepareBinaryData(
|
|
342
|
+
response,
|
|
343
|
+
`audio.${audioFormat}`,
|
|
344
|
+
mimeType,
|
|
345
|
+
),
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
returnData.push(newItem);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (this.continueOnFail()) {
|
|
354
|
+
returnData.push({
|
|
355
|
+
json: {
|
|
356
|
+
error: (error as Error).message,
|
|
357
|
+
},
|
|
358
|
+
pairedItem: { item: i },
|
|
359
|
+
});
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return [returnData];
|
|
367
|
+
}
|
|
209
368
|
}
|