@larc-iu/plaid-client 0.2.0-alpha.2 → 0.2.0-alpha.20

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/roles.js CHANGED
@@ -25,6 +25,32 @@ export const PLAID_NAMESPACE = 'plaid';
25
25
  export const ROLE_KEY = 'role';
26
26
 
27
27
  /** The fixed role inventory. */
28
+ /**
29
+ * Layer config key naming the metadata keys a token born of a SPLIT inherits
30
+ * from the token it came from. Plaid honors it without knowing what the keys
31
+ * mean; see the manual's `Metadata Preserved Across a Split`. An app declares
32
+ * it so that a split in ANY app, including one that has never heard of these
33
+ * keys, does not silently drop them.
34
+ */
35
+ export const PRESERVE_ON_SPLIT_KEY = 'preserveOnSplit';
36
+
37
+ /**
38
+ * Layer config key (true) asking Plaid to take a text edit plainly on the
39
+ * layer: an edit inside a token, or touching its edge with no whitespace
40
+ * between, grows or shrinks it, a typed space never splits it, a deleted one
41
+ * never joins two, and only a token whose whole text is deleted goes. See the
42
+ * manual's `Changing a text's body`.
43
+ */
44
+ export const PLAIN_EDITS_KEY = 'plainEdits';
45
+
46
+ /**
47
+ * Layer config key (true), beside PLAIN_EDITS_KEY: a space typed inside a
48
+ * token of the text's words splits it, the token going on the half sharing
49
+ * more letters with it. Declared on a word layer another app shares, it holds
50
+ * for the whole text.
51
+ */
52
+ export const SPLIT_ON_SPACE_KEY = 'splitOnSpace';
53
+
28
54
  export const ROLES = Object.freeze({
29
55
  BASELINE: 'baseline',
30
56
  SENTENCE: 'sentence',
@@ -21,11 +21,16 @@
21
21
  * parameters: [ // ordered; rendered into a form
22
22
  * { key, label, type, description?, default?, required?,
23
23
  * options?: [{value, label}], // enum / multiselect
24
- * min?, max?, step?, // number
25
- * placeholder?, multiline? } // string
24
+ * min?, max?, step?, slider?, // number (slider: drag, don't type)
25
+ * placeholder?, multiline?, // string
26
+ * scope? } // field: 'Sentence' | 'Word' | 'Morpheme'
26
27
  * ]
27
28
  * }
28
29
  *
30
+ * A `field` parameter names one of the project's annotation fields at its
31
+ * `scope`. A UI that knows the project offers those fields to choose from; to
32
+ * everything here, and to a UI that does not, it is a string.
33
+ *
29
34
  * A parameter's `key` is a string VALUE, so it passes over the wire verbatim;
30
35
  * the UI sends `{ [param.key]: value }` in the request data. Declare each `key`
31
36
  * in your service's own convention (Python: snake_case; JS: camelCase) and read
@@ -43,13 +48,48 @@
43
48
  * without an explicit `overwrite` parameter. See the manual, "Provenance".
44
49
  */
45
50
  export const TASKS = Object.freeze({
46
- TOKENIZE: 'tokenize',
47
- PARSE: 'parse',
48
- TRANSCRIBE: 'transcribe',
51
+ TOKENIZE: "tokenize",
52
+ PARSE: "parse",
53
+ TRANSCRIBE: "transcribe",
49
54
  /** Create vocab links for unlinked tokens. */
50
- LINK_VOCAB: 'link-vocab',
55
+ LINK_VOCAB: "link-vocab",
51
56
  /** Propose an interlinear analysis (morpheme segmentation + glosses) for words. */
52
- ANALYZE: 'analyze',
57
+ ANALYZE: "analyze",
58
+ /**
59
+ * Draft a meaning representation graph over the sentences of a document,
60
+ * for correction by hand. The request names a `scope` ('document' or
61
+ * 'sentence'), a `sentence` number when the scope is one sentence, and
62
+ * `overwrite`. See plaid-umr's drafting services.
63
+ *
64
+ * Apart from `analyze` on purpose. A task is the CONTRACT a spot offers a
65
+ * service against, and these two share almost none of it: an analyze
66
+ * request names the three token layers plus the language, metalanguage and
67
+ * orthography, and writes morphemes and glosses. Both claimed `analyze` for
68
+ * a while, so every app offering an analyze spot offered the other app's
69
+ * services, which could not answer the request they would have been given.
70
+ */
71
+ DRAFT_GRAPH: "draft-graph",
72
+ /** Propose a free translation for each sentence. */
73
+ TRANSLATE: "translate",
74
+ /**
75
+ * Propose time-aligned speech regions for a recording.
76
+ *
77
+ * The odd one out: a detect-speech service RETURNS its regions and writes
78
+ * nothing, because a time-aligned segment is a stretch of the baseline and
79
+ * cannot exist without text. The response carries
80
+ * `{segments: [{timeBegin, timeEnd, speaker?}]}` (seconds), and the app
81
+ * holds them as proposals until a person types into one.
82
+ */
83
+ DETECT_SPEECH: "detect-speech",
84
+ /** A conversational assistant over a project (chat turns; see plaid-agent). */
85
+ ASSIST: "assist",
86
+ /**
87
+ * Score one document's annotation against another document's of the same
88
+ * text (two annotators' copies), writing a report on the scored document's
89
+ * metadata and never an annotation. The request names the other document
90
+ * as `against`. See plaid-umr's AnCast service.
91
+ */
92
+ COMPARE: "compare",
53
93
  });
54
94
 
55
95
  /**
@@ -57,8 +97,8 @@ export const TASKS = Object.freeze({
57
97
  * declared `tasks` array. Drop once all services advertise `tasks`.
58
98
  */
59
99
  const LEGACY_TASK_PREFIXES = Object.freeze({
60
- [TASKS.TOKENIZE]: 'tok:',
61
- [TASKS.TRANSCRIBE]: 'asr:',
100
+ [TASKS.TOKENIZE]: "tok:",
101
+ [TASKS.TRANSCRIBE]: "asr:",
62
102
  });
63
103
 
64
104
  /**
@@ -74,7 +114,11 @@ export function servesTask(service, task) {
74
114
  return declared.includes(task);
75
115
  }
76
116
  const prefix = LEGACY_TASK_PREFIXES[task];
77
- return !!prefix && typeof service?.serviceId === 'string' && service.serviceId.startsWith(prefix);
117
+ return (
118
+ !!prefix &&
119
+ typeof service?.serviceId === "string" &&
120
+ service.serviceId.startsWith(prefix)
121
+ );
78
122
  }
79
123
 
80
124
  /**
@@ -104,7 +148,7 @@ export function getParamSchema(service) {
104
148
  * @returns {string}
105
149
  */
106
150
  export function getServiceSummary(service) {
107
- return service?.extras?.summary || service?.description || '';
151
+ return service?.extras?.summary || service?.description || "";
108
152
  }
109
153
 
110
154
  /** Valid option values for an enum/multiselect param. */
@@ -119,20 +163,25 @@ function optionValues(param) {
119
163
  */
120
164
  function defaultForParam(param) {
121
165
  const opts = optionValues(param);
122
- if (param.type === 'enum') {
123
- if (param.default != null && opts.includes(param.default)) return param.default;
124
- return opts[0] ?? '';
166
+ if (param.type === "enum") {
167
+ if (param.default != null && opts.includes(param.default))
168
+ return param.default;
169
+ return opts[0] ?? "";
125
170
  }
126
- if (param.type === 'multiselect') {
171
+ if (param.type === "multiselect") {
127
172
  const arr = Array.isArray(param.default) ? param.default : [];
128
173
  return opts.length ? arr.filter((x) => opts.includes(x)) : arr;
129
174
  }
130
- if (param.default !== undefined && param.default !== null) return param.default;
175
+ if (param.default !== undefined && param.default !== null)
176
+ return param.default;
131
177
  switch (param.type) {
132
- case 'number': return typeof param.min === 'number' ? param.min : 0;
133
- case 'boolean': return false;
134
- case 'string':
135
- default: return '';
178
+ case "number":
179
+ return typeof param.min === "number" ? param.min : 0;
180
+ case "boolean":
181
+ return false;
182
+ case "string":
183
+ default:
184
+ return "";
136
185
  }
137
186
  }
138
187
 
@@ -169,43 +218,48 @@ export function coerceParamValues(schema, raw) {
169
218
  if (v === undefined) v = defaultForParam(param);
170
219
 
171
220
  switch (param.type) {
172
- case 'number': {
221
+ case "number": {
173
222
  // Blank / nullish counts as "missing" → the param's default (matches the
174
223
  // Python client, where float('') raises and falls back to the default).
175
224
  let n;
176
- if (v === '' || v == null || (typeof v === 'string' && v.trim() === '')) {
225
+ if (
226
+ v === "" ||
227
+ v == null ||
228
+ (typeof v === "string" && v.trim() === "")
229
+ ) {
177
230
  n = defaultForParam(param);
178
231
  } else {
179
- n = typeof v === 'number' ? v : Number(v);
232
+ n = typeof v === "number" ? v : Number(v);
180
233
  if (Number.isNaN(n)) n = defaultForParam(param);
181
234
  }
182
- if (typeof param.min === 'number') n = Math.max(param.min, n);
183
- if (typeof param.max === 'number') n = Math.min(param.max, n);
235
+ if (typeof param.min === "number") n = Math.max(param.min, n);
236
+ if (typeof param.max === "number") n = Math.min(param.max, n);
184
237
  v = n;
185
238
  break;
186
239
  }
187
- case 'boolean':
188
- v = v === true || v === 'true';
240
+ case "boolean":
241
+ v = v === true || v === "true";
189
242
  break;
190
- case 'enum': {
243
+ case "enum": {
191
244
  const opts = optionValues(param);
192
245
  if (opts.length && !opts.includes(v)) v = defaultForParam(param);
193
246
  break;
194
247
  }
195
- case 'multiselect': {
248
+ case "multiselect": {
196
249
  const opts = optionValues(param);
197
- const arr = Array.isArray(v) ? v : (v == null || v === '' ? [] : [v]);
250
+ const arr = Array.isArray(v) ? v : v == null || v === "" ? [] : [v];
198
251
  v = opts.length ? arr.filter((x) => opts.includes(x)) : arr;
199
252
  break;
200
253
  }
201
- case 'string':
254
+ case "string":
202
255
  default:
203
- v = v == null ? '' : String(v);
256
+ v = v == null ? "" : String(v);
204
257
  break;
205
258
  }
206
259
 
207
260
  if (param.required) {
208
- const empty = v === '' || v == null || (Array.isArray(v) && v.length === 0);
261
+ const empty =
262
+ v === "" || v == null || (Array.isArray(v) && v.length === 0);
209
263
  if (empty) errors[k] = `${param.label || k} is required`;
210
264
  }
211
265
  values[k] = v;