@pipedream/sonix 0.0.1 → 1.0.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/README.md +11 -0
- package/actions/create-new-translation/create-new-translation.mjs +37 -0
- package/actions/get-text-transcript/get-text-transcript.mjs +23 -0
- package/actions/upload-media/upload-media.mjs +90 -0
- package/common/constants.mjs +154 -0
- package/common/utils.mjs +6 -0
- package/package.json +5 -1
- package/sonix.app.mjs +123 -5
- package/sources/media-upload-complete/media-upload-complete.mjs +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Overview
|
|
2
|
+
|
|
3
|
+
The Sonix API enables automated transcription of audio and video files into text, offering functions like uploading media, managing files, and retrieving transcripts. Leveraging Pipedream’s capabilities, you can integrate the Sonix API with various services to streamline media processing workflows, making transcription tasks more efficient. By automating interactions with Sonix, you can trigger actions based on the transcription status, analyze content, and connect transcribed text with other apps for further processing or analysis.
|
|
4
|
+
|
|
5
|
+
# Example Use Cases
|
|
6
|
+
|
|
7
|
+
- **Transcription Triggered from Cloud Storage Upload**: When a new audio file is uploaded to cloud storage (e.g., Google Drive), Pipedream detects the event and triggers a workflow that uploads the file to Sonix for transcription. Once the transcript is ready, it can be saved back to Google Drive or sent via email.
|
|
8
|
+
|
|
9
|
+
- **Media Management with Slack Notifications**: Use Pipedream to monitor your Sonix account for new transcripts. When a transcript is completed, send a notification with details and a download link to a designated Slack channel. This keeps the team updated on transcription progress in real-time without manual checks.
|
|
10
|
+
|
|
11
|
+
- **Content Analysis and Keyword Extraction**: After a transcript is generated, a workflow can pass the text to a natural language processing (NLP) service, like the Google Cloud Language API, to extract keywords and analyze sentiment. The results could then be sent to a Google Sheet for tracking trends over time.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { LANGUAGE_OPTIONS } from "../../common/constants.mjs";
|
|
2
|
+
import sonix from "../../sonix.app.mjs";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
key: "sonix-create-new-translation",
|
|
6
|
+
name: "Create New Translation",
|
|
7
|
+
description: "Creates a new translation for a selected media file. [See the documentation](https://sonix.ai/docs/api#create_translation)",
|
|
8
|
+
version: "0.0.1",
|
|
9
|
+
type: "action",
|
|
10
|
+
props: {
|
|
11
|
+
sonix,
|
|
12
|
+
mediaId: {
|
|
13
|
+
propDefinition: [
|
|
14
|
+
sonix,
|
|
15
|
+
"mediaId",
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
language: {
|
|
19
|
+
type: "string",
|
|
20
|
+
label: "Language",
|
|
21
|
+
description: "Language code for the translation",
|
|
22
|
+
options: LANGUAGE_OPTIONS,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
async run({ $ }) {
|
|
26
|
+
const response = await this.sonix.createTranslation({
|
|
27
|
+
$,
|
|
28
|
+
mediaId: this.mediaId,
|
|
29
|
+
data: {
|
|
30
|
+
language: this.language,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
$.export("$summary", `Successfully created translation for media ID: ${this.mediaId}`);
|
|
35
|
+
return response;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import sonix from "../../sonix.app.mjs";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
key: "sonix-get-text-transcript",
|
|
5
|
+
name: "Get Text Transcript",
|
|
6
|
+
description: "Gets the text transcript of a selected media file. [See the documentation](https://sonix.ai/docs/api#get_transcript)",
|
|
7
|
+
version: "0.0.1",
|
|
8
|
+
type: "action",
|
|
9
|
+
props: {
|
|
10
|
+
sonix,
|
|
11
|
+
mediaId: {
|
|
12
|
+
propDefinition: [
|
|
13
|
+
sonix,
|
|
14
|
+
"mediaId",
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
async run({ $ }) {
|
|
19
|
+
const response = await this.sonix.getTextTranscript(this.mediaId);
|
|
20
|
+
$.export("$summary", `Successfully fetched transcript for media ID: ${this.mediaId}`);
|
|
21
|
+
return response;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { getFileStreamAndMetadata } from "@pipedream/platform";
|
|
2
|
+
import FormData from "form-data";
|
|
3
|
+
import { LANGUAGE_OPTIONS } from "../../common/constants.mjs";
|
|
4
|
+
import sonix from "../../sonix.app.mjs";
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
key: "sonix-upload-media",
|
|
8
|
+
name: "Upload Media",
|
|
9
|
+
description: "Submits new media for processing. [See the documentation](https://sonix.ai/docs/api#new_media)",
|
|
10
|
+
version: "1.0.0",
|
|
11
|
+
type: "action",
|
|
12
|
+
props: {
|
|
13
|
+
sonix,
|
|
14
|
+
file: {
|
|
15
|
+
type: "string",
|
|
16
|
+
label: "File Path or URL",
|
|
17
|
+
description: "The audio or video file to upload (max 100MB). Provide either a file URL or a path to a file in the `/tmp` directory (for example, `/tmp/myFile.txt`)",
|
|
18
|
+
},
|
|
19
|
+
language: {
|
|
20
|
+
type: "string",
|
|
21
|
+
label: "Language",
|
|
22
|
+
description: "Language code for the transcription.",
|
|
23
|
+
options: LANGUAGE_OPTIONS,
|
|
24
|
+
},
|
|
25
|
+
folderId: {
|
|
26
|
+
propDefinition: [
|
|
27
|
+
sonix,
|
|
28
|
+
"folderId",
|
|
29
|
+
],
|
|
30
|
+
optional: true,
|
|
31
|
+
},
|
|
32
|
+
name: {
|
|
33
|
+
type: "string",
|
|
34
|
+
label: "Name",
|
|
35
|
+
description: "Name of the file in Sonix. If no name is provided, we will default to the filename.",
|
|
36
|
+
optional: true,
|
|
37
|
+
},
|
|
38
|
+
transcriptText: {
|
|
39
|
+
type: "boolean",
|
|
40
|
+
label: "Transcript Text",
|
|
41
|
+
description: "Existing transcript - if present, will align the transcript rather than transcribing.",
|
|
42
|
+
optional: true,
|
|
43
|
+
},
|
|
44
|
+
keywords: {
|
|
45
|
+
type: "string[]",
|
|
46
|
+
label: "Keywords",
|
|
47
|
+
description: "Comma separated list of words or phrases to use as hints to the transcription engine. If this is provided, then the account level keywords will not be used.",
|
|
48
|
+
optional: true,
|
|
49
|
+
},
|
|
50
|
+
customData: {
|
|
51
|
+
type: "object",
|
|
52
|
+
label: "Custom Data",
|
|
53
|
+
description: "Set of key-value pairs that you can attach to the media. This can be useful for storing additional information about file.",
|
|
54
|
+
optional: true,
|
|
55
|
+
},
|
|
56
|
+
callbackUrl: {
|
|
57
|
+
type: "string",
|
|
58
|
+
label: "Callback URL",
|
|
59
|
+
description: "URL for Sonix to make a POST request notifying of a change in transcript status (either failed or completed). The POST will include the media status JSON.",
|
|
60
|
+
optional: true,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
async run({ $ }) {
|
|
64
|
+
const formData = new FormData();
|
|
65
|
+
|
|
66
|
+
const {
|
|
67
|
+
stream, metadata,
|
|
68
|
+
} = await getFileStreamAndMetadata(this.file);
|
|
69
|
+
formData.append("file", stream, {
|
|
70
|
+
contentType: metadata.contentType,
|
|
71
|
+
knownLength: metadata.size,
|
|
72
|
+
filename: metadata.name,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
this.language && formData.append("language", this.language);
|
|
76
|
+
this.name && formData.append("name", this.name);
|
|
77
|
+
this.transcriptText && formData.append("transcript_text", `${this.transcriptText}`);
|
|
78
|
+
this.folderId && formData.append("folder_id", this.folderId);
|
|
79
|
+
this.keywords && formData.append("keywords", this.keywords.toString());
|
|
80
|
+
this.customData && formData.append("custom_data", JSON.stringify(this.customData));
|
|
81
|
+
this.callbackUrl && formData.append("callback_url", this.callbackUrl);
|
|
82
|
+
|
|
83
|
+
const response = await this.sonix.submitNewMedia({
|
|
84
|
+
data: formData,
|
|
85
|
+
headers: formData.getHeaders(),
|
|
86
|
+
});
|
|
87
|
+
$.export("$summary", `Successfully uploaded media with ID: ${response.id}`);
|
|
88
|
+
return response;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export const LANGUAGE_OPTIONS = [
|
|
2
|
+
{
|
|
3
|
+
label: "English",
|
|
4
|
+
value: "en",
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
label: "French",
|
|
8
|
+
value: "fr",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
label: "German",
|
|
12
|
+
value: "de",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
label: "Spanish",
|
|
16
|
+
value: "es",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
label: "Arabic",
|
|
20
|
+
value: "ar",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
label: "Armenian",
|
|
24
|
+
value: "hy-AM",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
label: "Bulgarian",
|
|
28
|
+
value: "bg",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
label: "Catalan",
|
|
32
|
+
value: "ca",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: "Croatian",
|
|
36
|
+
value: "hr",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: "Chinese (Cantonese)",
|
|
40
|
+
value: "yue-Hant-HK",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: "Chinese (Mandarin)",
|
|
44
|
+
value: "cmn-Hans-CN",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
label: "Czech",
|
|
48
|
+
value: "cs",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
label: "Danish",
|
|
52
|
+
value: "da",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
label: "Dutch",
|
|
56
|
+
value: "nl",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
label: "Finnish",
|
|
60
|
+
value: "fi",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
label: "Greek",
|
|
64
|
+
value: "el",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
label: "Hebrew",
|
|
68
|
+
value: "he-IL",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
label: "Hindi",
|
|
72
|
+
value: "hi",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: "Hungarian",
|
|
76
|
+
value: "hu",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
label: "Indonesian",
|
|
80
|
+
value: "id-ID",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
label: "Italian",
|
|
84
|
+
value: "it",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
label: "Japanese",
|
|
88
|
+
value: "ja",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
label: "Korean",
|
|
92
|
+
value: "ko",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: "Latvian",
|
|
96
|
+
value: "lv",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
label: "Lithuanian",
|
|
100
|
+
value: "lt",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
label: "Malay",
|
|
104
|
+
value: "ms-MY",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
label: "Norwegian",
|
|
108
|
+
value: "nb-NO",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
label: "Polish",
|
|
112
|
+
value: "pl",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
label: "Portuguese",
|
|
116
|
+
value: "pt",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
label: "Romanian",
|
|
120
|
+
value: "ro",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
label: "Russian",
|
|
124
|
+
value: "ru",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
label: "Slovak",
|
|
128
|
+
value: "sk",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
label: "Slovenian",
|
|
132
|
+
value: "sl",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
label: "Swedish",
|
|
136
|
+
value: "sv",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
label: "Thai",
|
|
140
|
+
value: "th-TH",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
label: "Turkish",
|
|
144
|
+
value: "tr-TR",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
label: "Ukrainian",
|
|
148
|
+
value: "uk",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
label: "Vietnamese",
|
|
152
|
+
value: "vi-VN",
|
|
153
|
+
},
|
|
154
|
+
];
|
package/common/utils.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipedream/sonix",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Pipedream Sonix Components",
|
|
5
5
|
"main": "sonix.app.mjs",
|
|
6
6
|
"keywords": [
|
|
@@ -11,5 +11,9 @@
|
|
|
11
11
|
"author": "Pipedream <support@pipedream.com> (https://pipedream.com/)",
|
|
12
12
|
"publishConfig": {
|
|
13
13
|
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@pipedream/platform": "^3.1.0",
|
|
17
|
+
"form-data": "^4.0.0"
|
|
14
18
|
}
|
|
15
19
|
}
|
package/sonix.app.mjs
CHANGED
|
@@ -1,11 +1,129 @@
|
|
|
1
|
+
import { axios } from "@pipedream/platform";
|
|
2
|
+
|
|
1
3
|
export default {
|
|
2
4
|
type: "app",
|
|
3
5
|
app: "sonix",
|
|
4
|
-
propDefinitions: {
|
|
6
|
+
propDefinitions: {
|
|
7
|
+
folderId: {
|
|
8
|
+
type: "string",
|
|
9
|
+
label: "Folder ID",
|
|
10
|
+
description: "ID of folder to place the media in",
|
|
11
|
+
async options() {
|
|
12
|
+
const { folders } = await this.listFolders();
|
|
13
|
+
|
|
14
|
+
return folders.map(({
|
|
15
|
+
id: value, name, email,
|
|
16
|
+
}) => ({
|
|
17
|
+
label: name || email,
|
|
18
|
+
value,
|
|
19
|
+
}));
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
mediaId: {
|
|
23
|
+
type: "string",
|
|
24
|
+
label: "Media ID",
|
|
25
|
+
description: "ID of the media file",
|
|
26
|
+
async options({ page }) {
|
|
27
|
+
const { media } = await this.listMedia({
|
|
28
|
+
params: {
|
|
29
|
+
page: page + 1,
|
|
30
|
+
status: "completed",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return media.map(({
|
|
35
|
+
name: label, id: value,
|
|
36
|
+
}) => ({
|
|
37
|
+
label,
|
|
38
|
+
value,
|
|
39
|
+
}));
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
5
43
|
methods: {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
44
|
+
_getApiKey() {
|
|
45
|
+
return this.$auth.api_key;
|
|
46
|
+
},
|
|
47
|
+
_baseUrl() {
|
|
48
|
+
return "https://api.sonix.ai/v1";
|
|
49
|
+
},
|
|
50
|
+
_getHeaders(headers) {
|
|
51
|
+
return {
|
|
52
|
+
...headers,
|
|
53
|
+
Authorization: `Bearer ${this._getApiKey()}`,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
_makeRequest({
|
|
57
|
+
$ = this, path, headers, ...otherOpts
|
|
58
|
+
}) {
|
|
59
|
+
const config = {
|
|
60
|
+
...otherOpts,
|
|
61
|
+
url: this._baseUrl() + path,
|
|
62
|
+
headers: this._getHeaders(headers),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return axios($, config);
|
|
66
|
+
},
|
|
67
|
+
listFolders() {
|
|
68
|
+
return this._makeRequest({
|
|
69
|
+
path: "/folders",
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
listMedia(opts = {}) {
|
|
73
|
+
return this._makeRequest({
|
|
74
|
+
...opts,
|
|
75
|
+
path: "/media",
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
submitNewMedia(opts = {}) {
|
|
79
|
+
return this._makeRequest({
|
|
80
|
+
...opts,
|
|
81
|
+
method: "POST",
|
|
82
|
+
path: "/media",
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
getTextTranscript(mediaId) {
|
|
86
|
+
return this._makeRequest({
|
|
87
|
+
path: `/media/${mediaId}/transcript`,
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
createTranslation({
|
|
91
|
+
mediaId, ...opts
|
|
92
|
+
}) {
|
|
93
|
+
return this._makeRequest({
|
|
94
|
+
...opts,
|
|
95
|
+
method: "POST",
|
|
96
|
+
path: `/media/${mediaId}/translations`,
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
async *paginate({
|
|
100
|
+
fn, params = {}, maxResults = null,
|
|
101
|
+
}) {
|
|
102
|
+
let hasMore = false;
|
|
103
|
+
let count = 0;
|
|
104
|
+
let page = 0;
|
|
105
|
+
|
|
106
|
+
do {
|
|
107
|
+
params.page = ++page;
|
|
108
|
+
const {
|
|
109
|
+
media,
|
|
110
|
+
page: currentPage,
|
|
111
|
+
total_pages: lastPage,
|
|
112
|
+
} = await fn({
|
|
113
|
+
params,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
for (const d of media) {
|
|
117
|
+
yield d;
|
|
118
|
+
|
|
119
|
+
if (maxResults && ++count === maxResults) {
|
|
120
|
+
return count;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
hasMore = currentPage != lastPage;
|
|
125
|
+
|
|
126
|
+
} while (hasMore);
|
|
9
127
|
},
|
|
10
128
|
},
|
|
11
|
-
};
|
|
129
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform";
|
|
2
|
+
import sonix from "../../sonix.app.mjs";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
key: "sonix-media-upload-complete",
|
|
6
|
+
name: "New Media Upload Complete",
|
|
7
|
+
description: "Emit new event any time the media status of an item changes to completed.",
|
|
8
|
+
version: "0.0.1",
|
|
9
|
+
type: "source",
|
|
10
|
+
dedupe: "unique",
|
|
11
|
+
props: {
|
|
12
|
+
sonix,
|
|
13
|
+
db: "$.service.db",
|
|
14
|
+
timer: {
|
|
15
|
+
type: "$.interface.timer",
|
|
16
|
+
default: {
|
|
17
|
+
intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
methods: {
|
|
22
|
+
_getLastDate() {
|
|
23
|
+
return this.db.get("lastDate") ?? 0;
|
|
24
|
+
},
|
|
25
|
+
_setLastDate(status) {
|
|
26
|
+
this.db.set("lastDate", status);
|
|
27
|
+
},
|
|
28
|
+
async startEvent(maxResults = 0) {
|
|
29
|
+
const lastDate = this._getLastDate();
|
|
30
|
+
const items = this.sonix.paginate({
|
|
31
|
+
fn: this.sonix.listMedia,
|
|
32
|
+
maxResults,
|
|
33
|
+
params: {
|
|
34
|
+
status: "completed",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let responseArray = [];
|
|
39
|
+
|
|
40
|
+
for await (const item of items) {
|
|
41
|
+
if (new Date(item.created_at) <= new Date(lastDate)) break;
|
|
42
|
+
responseArray.push(item);
|
|
43
|
+
}
|
|
44
|
+
if (responseArray.length) this._setLastDate(responseArray[0].created_at);
|
|
45
|
+
|
|
46
|
+
for (const item of responseArray.reverse()) {
|
|
47
|
+
this.$emit(
|
|
48
|
+
item,
|
|
49
|
+
{
|
|
50
|
+
id: item.id,
|
|
51
|
+
summary: `Media upload completed: ${item.name}`,
|
|
52
|
+
ts: Date.parse(item.created_at),
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
hooks: {
|
|
59
|
+
async deploy() {
|
|
60
|
+
await this.startEvent(25);
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
async run() {
|
|
64
|
+
await this.startEvent();
|
|
65
|
+
},
|
|
66
|
+
};
|