/**
* @file nzilbb.labbcat module for communicating with <a href="https://labbcat.canterbury.ac.nz/">LaBB-CAT</a> web application servers.
*
* <h2>What is LaBB-CAT?</h2>
*
* <p>LaBB-CAT is a web-based linguistic annotation store that stores audio or video
* recordings, text transcripts, and other annotations.</p>
*
* <p>Annotations of various types can be automatically generated or manually added.</p>
*
* <p>LaBB-CAT servers are usually password-protected linguistic corpora, and can be
* accessed manually via a web browser, or programmatically using a client library like
* this one.</p>
*
* <h2>What is this library?</h2>
*
* <p>The library copies from
* <a href="https://nzilbb.github.io/ag/javadoc/nzilbb/ag/IGraphStoreQuery.html">nzilbb.ag.IGraphStoreQuery</a>
* and related Java interfaces, for standardized API calls.</p>
*
* <p><em>nzilbb.labbcat</em> is available as an <em>npm</em> package
* <a href="https://www.npmjs.com/package/@nzilbb/labbcat">here.</a></p>
*
* <p><em>nzilbb.labbcat.js</em> can also be used as a browser-importable script.</p>
*
* <p>This API is has the following object model:
* <dl>
* <dt>{@link LabbcatView}</dt><dd> implements read-only functions for a LaBB-CAT graph
* store, corresponding to <q>view</q> permissions in LaBB-CAT.</dd>
* <dt>{@link LabbcatEdit}</dt><dd> inherits all LabbcatView functions, and also
* implements some graph store editing functions, corresponding to <q>edit</q>
* permissions in LaBB-CAT.</dd>
* <dt>{@link LabbcatAdmin}</dt><dd> inherits all LabbcatEdit functions, and also
* implements some administration functions, corresponding to <q>admin</q>
* permissions in LaBB-CAT.</dd>
* </dl>
*
* @example
* const corpus = new labbcat.LabbcatView("https://sometld.com", "your username", "your password");
*
* // optionally, we can set the language that messages are returned in
* labbcat.language = "es";
*
* // get the first participant in the corpus
* corpus.getParticipantIds((ids, errors, messages)=>{
* const participantId = ids[0];
*
* // all their instances of "the" followed by a word starting with a vowel
* const pattern = [
* {"orthography" : "i"},
* {"phonemes" : "[cCEFHiIPqQuUV0123456789~#\\$@].*"}];
*
* // start searching
* corpus.search(pattern, [ participantId ], false, (response, errors, messages)=>{
* const taskId = response.threadId
*
* // wait for the search to finish
* corpus.waitForTask(taskId, 30, (task, errors, messages)=>{
*
* // get the matches
* corpus.getMatches(taskId, (result, errors, messages)=>{
* const matches = result.matches;
* console.log("There were " + matches.length + " matches for " + participantId);
*
* // get TextGrids of the utterances
* corpus.getFragments(
* matches, [ "orthography", "phonemes" ], "text/praat-textgrid",
* (textgrids, errors, messages)=>{
*
* for (let textgrid of textgrids) {
* console.log(textgrid);
* }
*
* // get the utterance recordings
* corpus.getSoundFragments(matches, (wavs, errors, messages)=>{
*
* for (let wav of wavs) {
* console.log(wav);
* }
* });
* });
* });
* });
* });
* });
*
* @author Robert Fromont robert.fromont@canterbury.ac.nz
* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL v3.0
* @copyright 2016-2020 New Zealand Institute of Language, Brain and Behaviour, University of Canterbury
*
* This file is part of LaBB-CAT.
*
* LaBB-CAT is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* LaBB-CAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LaBB-CAT; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* @lic-end
*/
(function(exports){
var runningOnNode = false;
if (typeof(require) == "function") { // running on node.js
XMLHttpRequest = require('xhr2');
FormData = require('form-data');
fs = require('fs');
path = require('path');
os = require('os');
btoa = require('btoa');
parseUrl = require('url').parse;
runningOnNode = true;
}
/**
* Callback invoked when the result of a request is available.
*
* @callback resultCallback
* @param result The result of the function. This may be null, a string, number,
* array, or complex object, depending on what function was called.
* @param {string[]} errors A list of errors, or null if there were no errors.
* @param {string[]} messages A list of messages from the server if any.
* @param {string} call The name of the function that was called
* @param {string} id The ID that was passed to the method, if any.
*/
function callComplete(evt) {
if (exports.verbose) console.log("callComplete: " + this.responseText);
var result = null;
var errors = null;
var messages = null;
try {
var response = JSON.parse(this.responseText);
if (response.model != null) {
if (response.model.result) {
result = response.model.result;
}
if (!result && result != 0) result = response.model;
}
if (exports.verbose) console.log("result: " + JSON.stringify(result));
var errors = response.errors;
if (!errors || errors.length == 0) errors = null;
var messages = response.messages;
if (!messages || messages.length == 0) messages = null;
} catch(exception) {
result = null;
errors = ["" +exception+ ": " + this.responseText];
messages = [];
}
if (evt.target.onResult) {
evt.target.onResult(result, errors, messages, evt.target.call, evt.target.id);
}
}
function callFailed(evt) {
if (exports.verbose) console.log("callFailed: "+this.responseText);
if (evt.target.onResult) {
evt.target.onResult(
null, ["failed: " + this.responseText], [], evt.target.call, evt.target.id);
}
}
function callCancelled(evt) {
if (exports.verbose) console.log("callCancelled");
if (evt.target.onResult) {
evt.target.onResult(null, ["cancelled"], [], evt.target.call, evt.target.id);
}
}
// LabbcatView class - read-only "view" access
/**
* Read-only querying of LaBB-CAT corpora, based on the
* <a href="https://nzilbb.github.io/ag/javadoc/nzilbb/ag/IGraphStoreQuery.html">nzilbb.ag.IGraphStoreQuery</a>
* interface.
* <p>This interface provides only <em>read-only</em> operations.
* @example
* // create annotation store client
* const store = new LabbcatView("https://labbcat.canterbury.ac.nz", "demo", "demo");
* // get some basic information
* store.getId((result, errors, messages, call)=>{
* console.log("id: " + result);
* });
* store.getLayerIds((layers, errors, messages, call)=>{
* for (l in result) console.log("layer: " + layers[l]);
* });
* store.getCorpusIds((corpora, errors, messages, call)=>{
* store.getTranscriptIdsInCorpus(corpora[0], (ids, errors, messages, call, id)=>{
* console.log("transcripts in: " + id);
* for (i in ids) console.log(ids[i]);
* });
* });
* @author Robert Fromont robert@fromont.net.nz
*/
class LabbcatView {
/**
* Create a query client
* @param {string} baseUrl The LaBB-CAT base URL (i.e. the address of the 'home' link)
* @param {string} username The LaBB-CAT user name.
* @param {string} password The LaBB-CAT password.
*/
constructor(baseUrl, username, password) {
if (!/\/$/.test(baseUrl)) baseUrl += "/";
this._baseUrl = baseUrl;
this._storeUrl = baseUrl + "api/store/";
this._username = username;
this._password = password;
}
/**
* The base URL - e.g. https://labbcat.canterbury.ac.nz/demo/api/store/
*/
get baseUrl() {
return this._baseUrl;
}
/**
* The graph store URL - e.g. https://labbcat.canterbury.ac.nz/demo/api/store/
*/
get storeUrl() {
return this._storeUrl;
}
set storeUrl(url) {
this._storeUrl = url;
}
/**
* The LaBB-CAT user name.
*/
get username() {
return this._username;
}
parametersToQueryString(parameters) {
var queryString = "";
if (parameters) {
for (var key in parameters) {
if (parameters[key] // parameter is not false-ish
|| parameters[key] === "" // or it's an empty string
|| parameters[key] == 0) { // or it's 0
if (parameters[key].constructor === Array) {
for (var i in parameters[key]) {
queryString += "&"+key+"="+encodeURIComponent(parameters[key][i])
}
} else {
queryString += "&"+key+"="+encodeURIComponent(parameters[key])
}
}
} // next parameter
}
queryString = queryString.replace(/^&/,"");
return queryString;
}
//
// Creates an http request.
// @param {string} call The name of the API function to call
// @param {object} parameters The arguments of the function, if any
// @param {resultCallback} onResult Invoked when the request has returned a result.
// @param {string} [url=this.storeUrl] The URL
// @param {string} [method=GET] The HTTP method e.g. "POST"
// @param {string} [storeUrl=null] The URL for the graph store.
// @param {string} [contentTypeHeader=null] The request content type e.g "application/x-www-form-urlencoded".
// @return {XMLHttpRequest} An open request.
//
createRequest(call, parameters, onResult, url, method, storeUrl, contentTypeHeader) {
if (exports.verbose) {
console.log("createRequest "+method+" "+url + " "
+ call + " " + JSON.stringify(parameters));
}
method = method || "GET";
var xhr = new XMLHttpRequest();
xhr.call = call;
if (parameters && parameters.id) xhr.id = parameters.id;
xhr.onResult = onResult;
xhr.addEventListener("load", callComplete, false);
xhr.addEventListener("error", callFailed, false);
xhr.addEventListener("abort", callCancelled, false);
var queryString = this.parametersToQueryString(parameters);
if (!url) {
storeUrl = storeUrl || this.storeUrl;
if (exports.verbose) {
console.log(method + ": "+storeUrl + call + queryString + " as " + this.username);
}
xhr.open(method, storeUrl + call + (queryString?"?"+queryString:""), true);
} else { // explicit URL, so don't append call
if (exports.verbose) {
console.log(method + ": "+url + queryString + " as " + this.username);
}
xhr.open(method, url + (queryString?"?"+queryString:""), true);
}
if (contentTypeHeader) xhr.setRequestHeader("Content-Type", contentTypeHeader);
if (this.username) {
xhr.setRequestHeader(
"Authorization", "Basic " + btoa(this.username + ":" + this._password))
}
if (exports.language) {
xhr.setRequestHeader("Accept-Language", exports.language);
}
xhr.setRequestHeader("Accept", "application/json");
return xhr;
}
/**
* Gets version information of all components of LaBB-CAT.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: {object} An object containing section objects
* "System", "Formats", "Layer Managers", etc. each containing version information
* for sub-components. e.g.
* <ul>
* <li>result["System"]["LaBB-CAT"] is the overall LaBB-CAT version,</li>
* <li>result["System"]["nzilbb.ag"] is the overall annotation graph package
* version,</li>
* <li>result["Formats"]["Praat TextGrid"] is version of the Praat TextGrid
* conversion module,</li>
* <li>result["Layer Managers"]["HTK"] is version of the HTK Layer Manager, etc.</li>
* </ul>
*/
versionInfo(onResult) {
this.createRequest("version", null, onResult, this.baseUrl+"version").send();
}
/**
* Gets the store's ID.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: {string} The annotation store's ID.
*/
getId(onResult) {
this.createRequest("getId", null, onResult).send();
}
/**
* Gets the store's information document.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: {string} An HTML document providing
* information about the corpus.
*/
getInfo(onResult) {
var xhr = new XMLHttpRequest();
xhr.onResult = onResult;
xhr.addEventListener("load", function(evt) {
onResult(this.responseText, null, null, "getInfo")
}, false);
xhr.addEventListener("error", callFailed, false);
xhr.addEventListener("abort", callCancelled, false);
xhr.open("GET", this.baseUrl + "doc/");
if (this.username) {
xhr.setRequestHeader(
"Authorization", "Basic " + btoa(this.username + ":" + this._password))
}
if (exports.language) {
xhr.setRequestHeader("Accept-Language", exports.language);
}
xhr.setRequestHeader("Accept-Language", exports.language);
xhr.setRequestHeader("Accept", "text/html");
xhr.send();
}
/**
* Gets a list of layer IDs (annotation 'types').
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: {string[]} A list of layer IDs.
*/
getLayerIds(onResult) {
this.createRequest("getLayerIds", null, onResult).send();
}
/**
* Gets a list of layer definitions.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of layer definitions.
*/
getLayers(onResult) {
this.createRequest("getLayers", null, onResult).send();
}
/**
* Gets the layer schema.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A schema defining the layers and how they
* relate to each other.
*/
getSchema(onResult) {
this.createRequest("getSchema", null, onResult).send();
}
/**
* Gets a layer definition.
* @param {string} id ID of the layer to get the definition for.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: The definition of the given layer.
*/
getLayer(id, onResult) {
var xhr = this.createRequest("getLayer", { id : id }, onResult).send();
}
/**
* Gets a list of corpus IDs.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: {string[]} A list of corpus IDs.
*/
getCorpusIds(onResult) {
this.createRequest("getCorpusIds", null, onResult).send();
}
/**
* Gets a list of participant IDs.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: {string[]} A list of participant IDs.
*/
getParticipantIds(onResult) {
this.createRequest("getParticipantIds", null, onResult).send();
}
/**
* Gets the participant record specified by the given identifier.
* @param id The ID of the participant, which could be their name or their
* database annotation ID.
* @param layerIds The IDs of the participant attribute layers to load, or null if only
* participant data is required.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: An annotation representing the participant,
* or null if the participant was not found.
*/
getParticipant(id, layerIds, onResult) {
if (typeof layerIds === "function") { // (id, onResult)
onResult = layerIds;
layerIds = null;
}
this.createRequest("getParticipant", {id : id, layerIds : layerIds}, onResult).send();
}
/**
* Counts the number of participants that match a particular pattern.
* @param {string} expression An expression that determines which participants match.
* <p> The expression language is loosely based on JavaScript; expressions such as the
* following can be used:
* <ul>
* <li><code>/Ada.+/.test(id)</code></li>
* <li><code>labels('corpus').includes('CC')</code></li>
* <li><code>labels('participant_languages').includes('en')</code></li>
* <li><code>labels('transcript_language').includes('en')</code></li>
* <li><code>!/Ada.+/.test(id) && first('corpus').label == 'CC'</code></li>
* <li><code>all('transcript_rating').length > 2</code></li>
* <li><code>all('participant_rating').length = 0</code></li>
* <li><code>!annotators('transcript_rating').includes('labbcat')</code></li>
* <li><code>first('participant_gender').label == 'NA'</code></li>
* </ul>
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: The number of matching participants.
*/
countMatchingParticipantIds(expression, onResult) {
this.createRequest("countMatchingParticipantIds", {
expression : expression
}, onResult).send();
}
/**
* Gets a list of IDs of participants that match a particular pattern.
* @param {string} expression An expression that determines which participants match.
* <p> The expression language is loosely based on JavaScript; expressions such as the
* following can be used:
* <ul>
* <li><code>/Ada.+/.test(id)</code></li>
* <li><code>labels('corpus').includes('CC')</code></li>
* <li><code>labels('participant_languages').includes('en')</code></li>
* <li><code>labels('transcript_language').includes('en')</code></li>
* <li><code>!/Ada.+/.test(id) && first('corpus').label == 'CC'</code></li>
* <li><code>all('transcript_rating').length > 2</code></li>
* <li><code>all('participant_rating').length = 0</code></li>
* <li><code>!annotators('transcript_rating').includes('labbcat')</code></li>
* <li><code>first('participant_gender').label == 'NA'</code></li>
* </ul>
* @param {int} [pageLength] The maximum number of IDs to return, or null to return all.
* @param {int} [pageNumber] The zero-based page number to return, or null to return the
* first page.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of participant IDs.
*/
getMatchingParticipantIds(expression, pageLength, pageNumber, onResult) {
if (typeof pageLength === "function") { // no pageLength, pageNumber
onResult = pageLength;
pageLength = null;
pageNumber = null;
}
this.createRequest("getMatchingParticipantIds", {
expression : expression,
pageLength : pageLength,
pageNumber : pageNumber
}, onResult).send();
}
/**
* Counts the number of transcripts that match a particular pattern.
* @param {string} expression An expression that determines which transcripts match.
* <p> The expression language is loosely based on JavaScript; expressions such as
* the following can be used:
* <ul>
* <li><code>/Ada.+/.test(id)</code></li>
* <li><code>labels('participant').includes('Robert')</code></li>
* <li><code>('CC', 'IA', 'MU').includes(first('corpus').label)</code></li>
* <li><code>first('episode').label == 'Ada Aitcheson'</code></li>
* <li><code>first('transcript_scribe').label == 'Robert'</code></li>
* <li><code>first('participant_languages').label == 'en'</code></li>
* <li><code>first('noise').label == 'bell'</code></li>
* <li><code>labels('transcript_languages').includes('en')</code></li>
* <li><code>labels('participant_languages').includes('en')</code></li>
* <li><code>labels('noise').includes('bell')</code></li>
* <li><code>all('transcript_languages').length gt; 1</code></li>
* <li><code>all('participant_languages').length gt; 1</code></li>
* <li><code>all('transcript').length gt; 100</code></li>
* <li><code>annotators('transcript_rating').includes('Robert')</code></li>
* <li><code>!/Ada.+/.test(id) && first('corpus').label == 'CC' &&
* labels('participant').includes('Robert')</code></li>
* </ul>
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: The number of matching transcripts.
*/
countMatchingTranscriptIds(expression, onResult) {
this.createRequest("countMatchingTranscriptIds", {
expression : expression
}, onResult).send();
}
/**
* <p>Gets a list of IDs of transcripts that match a particular pattern.
* <p>The results can be exhaustive, by omitting pageLength and pageNumber, or they
* can be a subset (a 'page') of results, by given pageLength and pageNumber values.</p>
* <p>The order of the list can be specified. If ommitted, the transcripts are
* listed in ID order.</p>
* @param {string} expression An expression that determines which transcripts match.
* <p> The expression language is loosely based on JavaScript; expressions such as
* the following can be used:
* <ul>
* <li><code>/Ada.+/.test(id)</code></li>
* <li><code>labels('participant').includes('Robert')</code></li>
* <li><code>('CC', 'IA', 'MU').includes(first('corpus').label)</code></li>
* <li><code>first('episode').label == 'Ada Aitcheson'</code></li>
* <li><code>first('transcript_scribe').label == 'Robert'</code></li>
* <li><code>first('participant_languages').label == 'en'</code></li>
* <li><code>first('noise').label == 'bell'</code></li>
* <li><code>labels('transcript_languages').includes('en')</code></li>
* <li><code>labels('participant_languages').includes('en')</code></li>
* <li><code>labels('noise').includes('bell')</code></li>
* <li><code>all('transcript_languages').length gt; 1</code></li>
* <li><code>all('participant_languages').length gt; 1</code></li>
* <li><code>all('transcript').length gt; 100</code></li>
* <li><code>annotators('transcript_rating').includes('Robert')</code></li>
* <li><code>!/Ada.+/.test(id) && first('corpus').label == 'CC' &&
* labels('participant').includes('Robert')</code></li>
* </ul>
* @param {int} [pageLength] The maximum number of IDs to return, or null to return all.
* @param {int} [pageNumber] The zero-based page number to return, or null to return
* the first page.
* @param {string} [order] The ordering for the list of IDs, a string containing a
* comma-separated list of
* expressions, which may be appended by " ASC" or " DESC", or null for transcript ID order.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of transcript IDs.
*/
getMatchingTranscriptIds(expression, pageLength, pageNumber, order, onResult) {
if (typeof pageLength === "function") { // (expression, onResult)
onResult = pageLength;
pageLength = null;
pageNumber = null;
order = null;
} else if (typeof pageNumber === "function") { // (order, onResult)
order = pageLength;
onResult = pageNumber;
pageLength = null;
pageNumber = null;
} else if (typeof order === "function") { // (pageLength, pageNumber, onResult)
onResult = order;
order = null;
}
this.createRequest("getMatchingTranscriptIds", {
expression : expression,
pageLength : pageLength,
pageNumber : pageNumber,
order : order
}, onResult).send();
}
/**
* Gets the number of annotations on the given layer of the given transcript.
* @param {string} id The ID of the transcript.
* @param {string} layerId The ID of the layer.
* @param {int} [maxOrdinal] The maximum ordinal for the counted annotations.
* e.g. a <var>maxOrdinal</var> of 1 will ensure that only the first annotation for each
* parent is counted. If <var>maxOrdinal</var> is null, then all annotations are
* counted, regardless of their ordinal.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A (possibly empty) array of annotations.
*/
countAnnotations(id, layerId, maxOrdinal, onResult) {
if (typeof maxOrdinal === "function") { // (id, layerId, onResult)
onResult = maxOrdinal;
maxOrdinal = null;
}
this.createRequest("countAnnotations", {
id : id,
layerId : layerId,
maxOrdinal : maxOrdinal
}, onResult).send();
}
/**
* Gets the annotations on the given layer of the given transcript.
* @param {string} id The ID of the transcript.
* @param {string} layerId The ID of the layer.
* @param {int} [maxOrdinal] The maximum ordinal for the returned annotations.
* e.g. a <var>maxOrdinal</var> of 1 will ensure that only the first annotation for each
* parent is returned. If <var>maxOrdinal</var> is null, then all annotations are
* returned, regardless of their ordinal.
* @param {int} [pageLength] The maximum number of IDs to return, or null to return all.
* @param {int} [pageNumber] The zero-based page number to return, or null to return
* the first page.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A (possibly empty) array of annotations.
*/
getAnnotations(id, layerId, maxOrdinal, pageLength, pageNumber, onResult) {
if (typeof maxOrdinal === "function") { // (id, layerId, onResult)
onResult = maxOrdinal;
maxOrdinal = null;
pageLength = null;
pageNumber = null;
} else if (typeof pageLength === "function") { // (id, layerId, maxOrdinal, onResult)
onResult = pageLength;
pageLength = null;
pageNumber = null;
} else if (typeof pageNumber === "function") { // (id, layerId, pageLength, pageNumber, onResult)
onResult = pageNumber;
pageNumber = pageLength;
pageLength = maxOrdinal;
maxOrdinal = null;
}
this.createRequest("getAnnotations", {
id : id,
layerId : layerId,
maxOrdinal : maxOrdinal,
pageLength : pageLength,
pageNumber : pageNumber
}, onResult).send();
}
/**
* Counts the number of annotations that match a particular pattern.
* @param {string} expression An expression that determines which participants match.
* <p> The expression language is loosely based on JavaScript; expressions such as
* the following can be used:
* <ul>
* <li><code>id == 'ew_0_456'</code></li>
* <li><code>!/th[aeiou].//.test(label)</code></li>
* <li><code>first('participant').label == 'Robert' && first('utterances').start.offset ==
* 12.345</code></li>
* <li><code>graph.id == 'AdaAicheson-01.trs' && layer.id == 'orthography'
* && start.offset > 10.5</code></li>
* <li><code>previous.id == 'ew_0_456'</code></li>
* </ul>
* </ul>
* <p><em>NB</em> all expressions must match by either id or layer.id.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: The number of matching annotations.
*/
countMatchingAnnotations(expression, onResult) {
this.createRequest("countMatchingAnnotations", {
expression : expression
}, onResult).send();
}
/**
* Gets a list of annotations that match a particular pattern.
* @param {string} expression An expression that determines which transcripts match.
* <p> The expression language is loosely based on JavaScript; expressions such as the
* following can be used:
* <ul>
* <li><code>id == 'ew_0_456'</code></li>
* <li><code>!/th[aeiou].//.test(label)</code></li>
* <li><code>first('participant').label == 'Robert' && first('utterances').start.offset ==
* 12.345</code></li>
* <li><code>graph.id == 'AdaAicheson-01.trs' && layer.id == 'orthography'
* && start.offset > 10.5</code></li>
* <li><code>previous.id == 'ew_0_456'</code></li>
* </ul>
* <p><em>NB</em> all expressions must match by either id or layer.id.
* @param {int} [pageLength] The maximum number of annotations to return, or null
* to return all.
* @param {int} [pageNumber] The zero-based page number to return, or null to
* return the first page.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of matching Annotations.
*/
getMatchingAnnotations(expression, pageLength, pageNumber, onResult) {
if (typeof pageLength === "function") { // (expression, onResult)
onResult = pageLength;
pageLength = null;
pageNumber = null;
}
this.createRequest("getMatchingAnnotations", {
expression : expression,
pageLength : pageLength,
pageNumber : pageNumber
}, onResult).send();
}
/**
* Gets a list of transcript IDs.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: {string[]} A list of transcript IDs.
*/
getTranscriptIds(onResult) {
this.createRequest("getTranscriptIds", null, onResult).send();
}
/**
* Gets a list of transcript IDs in the given corpus.
* @param {string} id A corpus ID.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: {string[]} A list of transcript IDs.
*/
getTranscriptIdsInCorpus(id, onResult) {
this.createRequest("getTranscriptIdsInCorpus", { id : id }, onResult).send();
}
/**
* Gets a list of IDs of transcripts that include the given participant.
* @param {string} id A participant ID.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: {string[]} A list of transcript IDs.
*/
getTranscriptIdsWithParticipant(id, onResult) {
this.createRequest("getTranscriptIdsWithParticipant", { id : id }, onResult).send();
}
/**
* Gets a transcript given its ID, containing only the given layers.
* @param {string} id The given transcript ID.
* @param {string[]} layerIds The IDs of the layers to load, or null for all
* layers. If only transcript data is required, set this to ["graph"].
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: The identified transcript.
*/
getTranscript(id, layerIds, onResult) {
this.createRequest("getTranscript", { id : id, layerIds : layerIds }, onResult).send();
}
/**
* Gets the given anchors in the given transcript.
* @param {string} id The given transcript ID.
* @param {string[]} anchorIds The IDs of the anchors to load.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: The identified transcript.
*/
getAnchors(id, anchorIds, onResult) {
this.createRequest("getAnchors", { id : id, anchorIds : anchorIds }, onResult).send();
}
/**
* List the predefined media tracks available for transcripts.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: An ordered list of media track definitions.
*/
getMediaTracks(onResult) {
this.createRequest("getMediaTracks", null, onResult).send();
}
/**
* List the media available for the given transcript.
* @param {string} id The transcript ID.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: List of media files available for the given transcript.
*/
getAvailableMedia(id, onResult) {
this.createRequest("getAvailableMedia", { id : id }, onResult).send();
}
/**
* Get a list of documents associated with the episode of the given transcript.
* @param {string} id The transcript ID.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: List of media files available for the given transcript.
*/
getEpisodeDocuments(id, onResult) {
this.createRequest("getEpisodeDocuments", { id : id }, onResult).send();
}
/**
* Gets a given media track for a given transcript.
* @param {string} id The transcript ID.
* @param {string} trackSuffix The track suffix of the media.
* @param {string} mimeType The MIME type of the media.
* @param {float} [startOffset] The start offset of the media sample, or null for
* the start of the whole recording.
* @param {float} [endOffset[ The end offset of the media sample, or null for the
* end of the whole recording.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: {string} A URL to the given media for the given
* transcript, or null if the given media doesn't exist.
*/
getMedia(id, trackSuffix, mimeType, startOffset, endOffset, onResult) {
if (typeof startOffset === "function") { // (id, trackSuffix, mimeType, onResult)
onResult = startOffset;
startOffset = null;
endOffset = null;
}
this.createRequest("getMedia", {
id : id,
trackSuffix : trackSuffix,
mimeType : mimeType,
startOffset : startOffset,
endOffset : endOffset
}, onResult).send();
}
/**
* Gets list of tasks.
* @param {resultCallback} onResult Invoked when the request has returned a
* result, which is an map of task IDs to statuses.
*/
getTasks(onResult) {
if (exports.verbose) console.log("getTasks()");
this.createRequest("getTasks", null, onResult, this.baseUrl + "threads").send();
}
/**
* Gets the status of a task.
* @param {string} id ID of the task.
* @param {resultCallback} onResult Invoked when the request has returned a result.
*/
taskStatus(id, onResult) {
this.createRequest("taskStatus", { id : id, threadId : id }, onResult, this.baseUrl+"thread").send();
}
/**
* Wait for the given task to finish.
* @param {string} threadId The task ID.
* @param {int} maxSeconds The maximum time to wait for the task, or 0 for forever.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: The final task status. To determine whether
* the task finished or waiting timed out, check <var>result.running</var>, which
* will be false if the task finished.
*/
waitForTask(threadId, maxSeconds, onResult) {
if (exports.verbose) console.log("waitForTask("+threadId+", "+maxSeconds+")");
const labbcat = this;
this.taskStatus(threadId, (thread, errors, messages)=> {
const waitTimeMS = thread && thread.refreshSeconds?
thread.refreshSeconds*1000 : 2000;
if (thread.running && maxSeconds > waitTimeMS/1000) {
setTimeout(()=>labbcat.waitForTask(
threadId, maxSeconds - waitTimeMS/1000, onResult), waitTimeMS);
} else {
if (onResult) {
onResult(thread, errors, messages);
}
}
});
}
/**
* Releases a finished task so it no longer uses resources on the server.
* @param {string} id ID of the task.
* @param {resultCallback} onResult Invoked when the request has completed.
*/
releaseTask(id, onResult) {
if (exports.verbose) console.log("releaseTask("+id+")");
this.createRequest("releaseTask", {
threadId : id,
command : "release"
}, onResult, this.baseUrl+"threads").send();
}
/**
* Cancels a running task.
* @param threadId The ID of the task.
* @param {resultCallback} onResult Invoked when the request has completed.
*/
cancelTask(threadId, onResult) {
if (exports.verbose) console.log("cancelTask("+threadId+")");
this.createRequest("cancelTask", {
threadId : threadId,
command : "cancel"
}, onResult, this.baseUrl+"threads").send();
}
/**
* Searches for tokens that match the given pattern.
* <p> Although <var>mainParticipantOnly</var>, <var>offsetThreshold</var> and
* <var>matchesPerTranscript</var> are all optional, if one of them is specified,
* then all must be specified.
* <p> The <var>pattern</var> should match the structure of the search matrix in the
* browser interface of LaBB-CAT. This is a JSON object with one attribute called
* <q>columns</q>, which is an array of JSON objects.
* <p>Each element in the <q>columns</q> array contains am JSON object named
* <q>layers</q>, whose value is a JSON object for patterns to match on each layer, and
* optionally an element named <q>adj</q>, whose value is a number representing the
* maximum distance, in tokens, between this column and the next column - if <q>adj</q>
* is not specified, the value defaults to 1, so tokens are contiguous.
* Each element in the <q>layers</q> JSON object is named after the layer it matches, and
* the value is a named list with the following possible attributes:
* <dl>
* <dt>pattern</dt> <dd>A regular expression to match against the label</dd>
* <dt>min</dt> <dd>An inclusive minimum numeric value for the label</dd>
* <dt>max</dt> <dd>An exclusive maximum numeric value for the label</dd>
* <dt>not</dt> <dd>TRUE to negate the match</dd>
* <dt>anchorStart</dt> <dd>TRUE to anchor to the start of the annotation on this layer
* (i.e. the matching word token will be the first at/after the start of the matching
* annotation on this layer)</dd>
* <dt>anchorEnd</dt> <dd>TRUE to anchor to the end of the annotation on this layer
* (i.e. the matching word token will be the last before/at the end of the matching
* annotation on this layer)</dd>
* <dt>target</dt> <dd>TRUE to make this layer the target of the search; the results will
* contain one row for each match on the target layer</dd>
* </dl>
*
* <p>Examples of valid pattern objects include:
* <pre>// words starting with 'ps...'
* const pattern1 = {
* "columns" : [
* {
* "layers" : {
* "orthography" : {
* "pattern" : "ps.*"
* }
* }
* }
* ]};
*
* // the word 'the' followed immediately or with one intervening word by
* // a hapax legomenon (word with a frequency of 1) that doesn't start with a vowel
* const pattern2 = {
* "columns" : [
* {
* "layers" : {
* "orthography" : {
* "pattern" : "the"
* }
* }
* "adj" : 2 },
* {
* "layers" : {
* "phonemes" : {
* "not" : true,
* "pattern" : "[cCEFHiIPqQuUV0123456789~#\\$@].*"}
* "frequency" {
* "max" : "2"
* }
* }
* }
* ]};
* </pre>
*
* For ease of use, the function will also accept the following abbreviated forms:
* <pre>
* // a single list representing a 'one column' search,
* // and string values, representing regular expression pattern matching
* const pattern3 = { orthography : "ps.*" };
*
* // a list containing the columns (adj defaults to 1, so matching tokens are contiguous)
* const pattrn4 = [{
* orthography : "the"
* }, {
* phonemes : {
* not : true,
* pattern : "[cCEFHiIPqQuUV0123456789~#\\$@].*" },
* frequency : {
* max = "2" }
* }];
* </pre>
* @param {object} pattern An object representing the pattern to search for, which
* mirrors the Search Matrix in the browser interface.
* @param {string[]} [participantQuery=null] An optional expression for
* identifying participants to search the utterances of. This can be any
* expression of the kind used with {@link LabbcatView#getMatchingParticipantIds}
* e.g. "['AP2505_Nelson','AP2512_MattBlack','AP2515_ErrolHitt'].includes(id)"
* @param {string[]} [transcriptQuery=null] An optional expression for
* identifying transcripts to search the utterances of. This can be any
* expression of the kind used with {@link LabbcatView#getMatchingTranscriptIds}
* e.g. "['CC','ID'].includes(first('corpus').label)
* && first('transcript_type').label == 'wordlist'"
* @param {boolean} [mainParticipantOnly=true] true to search only main-participant
* utterances, false to search all utterances.
* @param {int} [offsetThreshold=null] Optional minimum alignment confidence for
* matching word or segment annotations. A value of 50 means that annotations that
* were at least automatically aligned will be returned. Use 100 for
* manually-aligned annotations only, and 0 or no value to return all matching
* annotations regardless of alignment confidence.
* @param {int} [matchesPerTranscript=null] Optional maximum number of matches per
* transcript to return. <tt>null</tt> means all matches.
* @param {int} [overlapThreshold=null] Optional percentage overlap with other
* utterances before simultaneous speech is excluded. <tt>null</tt> means include
* all overlapping utterances.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: An object with one attribute, "threadId",
* which identifies the resulting task, which can be passed to
* {@link LabbcatView#getMatches}, {@link LabbcatView#taskStatus},
* {@link LabbcatView#waitForTask}, etc.
*/
search(pattern, participantQuery, transcriptQuery, mainParticipantOnly, offsetThreshold, matchesPerTranscript, overlapThreshold, onResult) {
if (typeof participantQuery === "function") { // (pattern, onResult)
onResult = participantQuery;
participantQuery = null;
transcriptQuery = null;
mainParticipantOnly = true;
offsetThreshold = null;
matchesPerTranscript = null;
overlapThreshold = null;
} else if (typeof transcriptQuery === "function") {
// (pattern, participantQuery, onResult)
onResult = transcriptQuery;
transcriptQuery = null;
mainParticipantOnly = true;
offsetThreshold = null;
matchesPerTranscript = null;
overlapThreshold = null;
} else if (typeof transcriptQuery === "boolean") {
// (pattern, participantQuery, mainParticipantOnly, offsetThreshold,
// matchesPerTranscript, onResult)
onResult = overlapThreshold;
overlapThreshold = matchesPerTranscript
matchesPerTranscript = offsetThreshold;
offsetThreshold = mainParticipantOnly;
mainParticipantOnly = transcriptQuery;
overlapThreshold = null;
} else if (typeof mainParticipantOnly === "function") {
// (pattern, participantQuery, transcriptQuery, onResult)
onResult = mainParticipantOnly;
mainParticipantOnly = true;
offsetThreshold = null;
matchesPerTranscript = null;
overlapThreshold = null;
}
if (typeof offsetThreshold === "function") {
// (pattern, participantIds, transcriptQuery, mainParticipantOnly, onResult)
// i.e. the original signature of this function
onResult = offsetThreshold;
offsetThreshold = null;
matchesPerTranscript = null;
overlapThreshold = null;
}
if (typeof matchesPerTranscript === "function") {
// (pattern, participantIds, mainParticipantOnly, offsetThreshold, onResult)
// i.e. the original signature of this function
onResult = matchesPerTranscript;
matchesPerTranscript = null;
overlapThreshold = null;
}
if (typeof overlapThreshold === "function") {
// (pattern, participantIds, mainParticipantOnly, offsetThreshold, matchesPerTranscript, onResult)
onResult = overlapThreshold;
overlapThreshold = null;
}
if (exports.verbose) {
console.log("search("+JSON.stringify(pattern)
+", "+participantQuery
+", "+transcriptQuery
+", "+mainParticipantOnly
+", "+offsetThreshold
+", "+matchesPerTranscript
+", "+overlapThreshold+")");
}
// for backwards compatibility, convert arrays of IDs to expressions
if (Array.isArray(participantQuery)) {
participantQuery = "["
+participantQuery
.map(s=>"'"+s.replace(/'/,"\\'")+"'")
.join(",")
+"].includes(id)";
}
if (Array.isArray(transcriptQuery)) {
transcriptQuery = "["
+transcriptQuery
.map(s=>"'"+s.replace(/'/,"\\'")+"'")
.join(",")
+"].includes(first('transcript_type').label)";
}
// first normalize the pattern...
// if pattern isn't a list with a "columns" element, wrap a list around it
if (!pattern.columns) pattern = { columns : pattern };
// if pattern.columns isn't an array wrap an array list around it
if (!(pattern.columns instanceof Array)) pattern.columns = [ pattern.columns ];
// columns contain lists with no "layers" element, wrap a list around them
for (let c = 0; c < pattern.columns.length; c++) {
if (!("layers" in pattern.columns[c])) {
pattern.columns[c] = { layers : pattern.columns[c] };
}
} // next column
// convert layer:string to layer : { pattern:string }
for (let c = 0; c < pattern.columns.length; c++) { // for each column
for (let l in pattern.columns[c].layers) { // for each layer in the column
// if the layer value isn't an object
if (typeof pattern.columns[c].layers[l] == "string") {
// wrap a list(pattern=...) around it
pattern.columns[c].layers[l] = { pattern : pattern.columns[c].layers[l] };
} // value isn't a list
} // next layer
} // next column
const parameters = {
command : "search",
searchJson : JSON.stringify(pattern),
words_context : 0
}
if (mainParticipantOnly) parameters.mainParticipantOnly = true;
if (offsetThreshold) parameters.offsetThreshold = offsetThreshold;
if (matchesPerTranscript) parameters.matchesPerTranscript = matchesPerTranscript;
if (participantQuery) parameters.participantQuery = participantQuery;
if (transcriptQuery) parameters.transcriptQuery = transcriptQuery;
if (overlapThreshold) parameters.overlapThreshold = overlapThreshold;
this.createRequest(
"search", null, onResult, this.baseUrl+"api/search",
"POST", // not GET, because the number of parameters can make the URL too long
null, "application/x-www-form-urlencoded;charset=\"utf-8\"")
.send(this.parametersToQueryString(parameters));
}
/**
* Identifies all utterances by the given participants.
* @param {string[]} participantIds A list of participant IDs to identify
* the utterances of.
* @param {string[]} [transcriptTypes=null] An optional list of transcript types to limit
* the results to. If null, all transcript types will be searched.
* @param {boolean} [mainParticipant=true] true to search only main-participant
* utterances, false to search all utterances.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: An object with one attribute, "threadId",
* which identifies the resulting task, which can be passed to
* {@link LabbcatView#getMatches}, {@link LabbcatView#taskStatus},
* {@link LabbcatView#waitForTask}, etc.
*/
allUtterances(participantIds, transcriptTypes, mainParticipant, onResult) {
if (typeof transcriptTypes === "function") { // (participantIds, onResult)
onResult = transcriptTypes;
transcriptTypes = null;
mainParticipant = true;
} else if (typeof mainParticipant === "function") {
// (participantIds, transcriptTypes, onResult)
onResult = mainParticipant;
mainParticipant = true;
} else if (typeof transcriptTypes === "boolean") {
// (participantIds, mainParticipant, onResult)
onResult = mainParticipant;
mainParticipant = transcriptTypes;
transcriptTypes = null;
}
if (exports.verbose) {
console.log("allUtterances("+JSON.stringify(participantIds)
+", "+JSON.stringify(transcriptTypes)
+", "+mainParticipant+")");
}
// first normalize the pattern...
const parameters = {
list : "list",
id : participantIds,
}
if (mainParticipant) parameters.only_main_speaker = true;
if (transcriptTypes) parameters.transcript_type = transcriptTypes;
if (exports.verbose) console.log(JSON.stringify(parameters));
this.createRequest(
"allUtterances", null, onResult, this.baseUrl+"api/utterances",
"POST", // not GET, because the number of parameters can make the URL too long
null, "application/x-www-form-urlencoded")
.send(this.parametersToQueryString(parameters));
}
/**
* Gets a list of tokens that were matched by {@link LabbcatView#search}.
* <p>If the task is still running, then this function will wait for it to finish.
* <p>This means calls can be stacked like this:
* <pre>const matches = labbcat.getMatches(
* labbcat.search(
* {"orthography", "and"},
* participantIds, true), 1);</pre>
* @param {string} threadId A task ID returned by {@link LabbcatView#search}.
* @param {int} [wordsContext=0] Number of words context to include in the <q>Before
* Match</q> and <q>After Match</q> columns in the results.
* @param {int} [pageLength] The maximum number of matches to return, or null to
* return all.
* @param {int} [pageNumber] The zero-based page number to return, or null to
* return the first page.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: An object with two attributes:
* <dl>
* <dt>name</dt><dd>The name of the search results collection</dd>
* <dt>matches</dt>
* <dd>A list of match objects, with the following attributes
* <dl>
* <dt>MatchId</dt> <dd>A string identifying the match, of the kind expected
* by {@link LabbcatView#getMatchAnnotations}</dd>
* <dt>Transcript</dt> <dd>The name of the transcript</dd>
* <dt>Participant</dt> <dd>The name of the participant</dd>
* <dt>Corpus</dt> <dd>The name of corpus the transcript belongs to</dd>
* <dt>Line</dt> <dd>The start offset of the utterance, usually in seconds</dd>
* <dt>LineEnd</dt> <dd>The end offset of the uttereance, usually in seconds</dd>
* <dt>BeforeMatch</dt> <dd>The context of the trascript text just before the
* match</dd>
* <dt>Text</dt> <dd>The transcript text that matched</dd>
* <dt>AfterMatch</dt> <dd>The context of the transcript text just after
* the match</dd>
* </dl>
* </dd>
* </dl>
*/
getMatches(threadId, wordsContext, pageLength, pageNumber, onResult) {
if (typeof wordsContext === "function") { // (threadId, onResult)
onResult = wordsContext;
wordsContext = null;
}
else if (typeof pageLength === "function") { // (threadId, wordsContext, onResult)
onResult = pageLength;
pageLength = null;
pageNumber = null;
}
else if (typeof pageNumber === "function") {
// (threadId, pageLength, pageNumber, onResult)
onResult = pageNumber;
pageNumber = pageLength;
pageLength = wordsContext;
wordsContext = null;
}
if (exports.verbose) {
console.log("getMatches("+threadId+", "+wordsContext
+", "+pageLength+", "+pageNumber+")");
}
wordsContext = wordsContext || 0;
this.createRequest("getMatches", {
threadId : threadId,
words_context : wordsContext,
pageLength : pageLength,
pageNumber : pageNumber
}, onResult, this.baseUrl+"api/results").send();
}
/**
* Gets annotations on selected layers related to search results returned by a previous
* call to {@link LabbcatView#getMatches}.
* @param {string[]|object[]} matchIds A list of MatchIds, or a list of match
* objects returned by {@link LabbcatView#getMatches}
* @param {string[]} layerIds A list of layer IDs.
* @param {int} [targetOffset=0] The distance from the original target of the match, e.g.
* <ul>
* <li>0 - find annotations of the match target itself</li>
* <li>1 - find annotations of the token immediately <em>after</em> match target</li>
* <li>-1 - find annotations of the token immediately <em>before</em> match target</li>
* </ul>
* @param {int} [annotationsPerLayer=1] The number of annotations on the given layer to
* retrieve. In most cases, there's only one annotation available. However, tokens may,
* for example, be annotated with `all possible phonemic transcriptions', in which case
* using a value of greater than 1 for this parameter provides other phonemic
* transcriptions, for tokens that have more than one.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: An array of arrays of Annotations, of
* dimensions <var>matchIds</var>.length × (<var>layerIds</var>.length *
* <var>annotationsPerLayer</var>). The first index matches the corresponding
* index in <var>matchIds</var>.
*/
getMatchAnnotations(matchIds, layerIds, targetOffset, annotationsPerLayer, onResult) {
if (typeof targetOffset === "function") { // (matchIds, layerIds, onResult)
onResult = targetOffset;
targetOffset = null;
annotationsPerLayer = null;
} else if (typeof annotationsPerLayer === "function") {
// (matchIds, layerIds, targetOffset, onResult)
onResult = annotationsPerLayer;
annotationsPerLayer = null;
}
// check that an array of matches hasn't been passed.
if (typeof matchIds[0] != "string" && matchIds[0].MatchId) {
// convert the array of matches into an array of MatchIds
matchIds = matchIds.map(match => match.MatchId);
}
if (exports.verbose) {
console.log("getMatchAnnotations("+JSON.stringify(matchIds)+", "
+JSON.stringify(layerIds)+", "+targetOffset+", "
+annotationsPerLayer+")");
}
targetOffset = targetOffset || 0;
annotationsPerLayer = annotationsPerLayer || 1;
// create form
var fd = new FormData();
fd.append("targetOffset", targetOffset);
fd.append("annotationsPerLayer", annotationsPerLayer);
fd.append("csvFieldDelimiter", ",");
fd.append("targetColumn", "0");
fd.append("copyColumns", "false");
for (let layerId of layerIds ) fd.append("layer", layerId);
// getMatchAnnotations expects an uploaded CSV file for MatchIds,
const uploadfile = "MatchId\n"+matchIds.join("\n");
fd.append("uploadfile", uploadfile, {
filename: 'uploadfile.csv',
contentType: 'text/csv',
knownLength: uploadfile.length
});
if (!runningOnNode) {
// create HTTP request
var xhr = new XMLHttpRequest();
xhr.call = "getMatchAnnotations";
xhr.id = transcript.name;
xhr.onResult = onResult;
xhr.addEventListener("load", callComplete, false);
xhr.addEventListener("error", callFailed, false);
xhr.addEventListener("abort", callCancelled, false);
xhr.open("POST", this.baseUrl + "api/getMatchAnnotations");
if (this.username) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(this.username + ":" + this.password))
}
xhr.setRequestHeader("Accept", "application/json");
xhr.send(fd);
} else { // runningOnNode
var urlParts = parseUrl(this.baseUrl + "api/getMatchAnnotations");
// for tomcat 8, we need to explicitly send the content-type and content-length headers...
var labbcat = this;
var password = this._password;
fd.getLength(function(something, contentLength) {
var requestParameters = {
port: urlParts.port,
path: urlParts.pathname,
host: urlParts.hostname,
headers: {
"Accept" : "application/json",
"content-length" : contentLength,
"Content-Type" : "multipart/form-data; boundary=" + fd.getBoundary()
}
};
if (labbcat.username && password) {
requestParameters.auth = labbcat.username+':'+password;
}
if (/^https.*/.test(labbcat.baseUrl)) {
requestParameters.protocol = "https:";
}
if (exports.verbose) {
console.log("submit: " + labbcat.baseUrl + "edit/transcript/new");
}
fd.submit(requestParameters, function(err, res) {
var responseText = "";
if (!err) {
res.on('data',function(buffer) {
//console.log('data ' + buffer);
responseText += buffer;
});
res.on('end',function(){
if (exports.verbose) console.log("response: " + responseText);
var result = null;
var errors = null;
var messages = null;
try {
var response = JSON.parse(responseText);
result = response.model.result || response.model;
errors = response.errors;
if (errors.length == 0) errors = null
messages = response.messages;
if (messages.length == 0) messages = null
} catch(exception) {
result = null
errors = ["" +exception+ ": " + labbcat.responseText];
messages = [];
}
onResult(result, errors, messages, "getMatchAnnotations");
});
} else {
onResult(null, ["" +err+ ": " + labbcat.responseText], [], "getMatchAnnotations");
}
if (res) res.resume();
});
}); // got length
} // runningOnNode
}
/**
* Downloads WAV sound fragments.
* <p>For convenience, the first three arguments, <var>transcriptIds</var>,
* <var>startOffsets</var>, and <var>endOffsets</var>, can be replaced by a single
* array of match objects of the kind returned by {@link LabbcatView#getMatches}, in
* which case the start/end times are the utterance boundaries - e.g.
* <pre>labbcat.getMatches(threadId, wordsContext (result, e, m) => {
* labbcat.getMatchAnnotations(result.matches, sampleRate, dir, (files, e, m) => {
* ...
* });
* });</pre>
* @param {string[]} transcriptIds A list of transcript IDs (transcript names).
* @param {float[]} startOffsets A list of start offsets, with one element for each
* element in <var>transcriptIds</var>.
* @param {float[]} endOffsets A list of end offsets, with one element for each element in
* <var>transcriptIds</var>.
* @param {int} [sampleRate] The desired sample rate, or null for no preference.
* @param {string} [dir] A directory in which the files should be stored, or null
* for a temporary folder. If specified, and the directory doesn't exist, it will
* be created.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of WAV files. If <var>dir</var> is
* null, these files will be stored under the system's temporary directory, so
* once processing is finished, they should be deleted by the caller, or moved to
* a more permanent location.
*/
getSoundFragments(transcriptIds, startOffsets, endOffsets, sampleRate, dir, onResult) {
if (!runningOnNode) {
onResult && onResult(
null, ["getSoundFragments is not yet implemented for browsers"], [], // TODO
"getSoundFragments");
return;
}
// ensure transcriptIds is a list of strings, not a list of matches
if (typeof transcriptIds[0] != "string" && transcriptIds[0].Transcript) {
// convert the array of matches into an arrays of transcriptIds, startOffset,
// and endOffsets...
// shift remaining arguments to the right
onResult = sampleRate
dir = endOffsets
sampleRate = startOffsets
// create arrays
startOffsets = transcriptIds.map(match => match.Line);
endOffsets = transcriptIds.map(match => match.LineEnd);
transcriptIds = transcriptIds.map(match => match.Transcript);
}
if (transcriptIds.length != startOffsets.length || transcriptIds.length != endOffsets.length) {
onResult && onResult(null, [
"transcriptIds ("+transcriptIds.length +"), startOffsets ("+startOffsets.length
+"), and endOffsets ("+endOffsets.length+") must be arrays of equal size."],
[], "getSoundFragments");
return;
}
if (typeof sampleRate === "function") {
// (transcriptIds, startOffsets, endOffsets, onResult)
onResult = sampleRate;
sampleRate = null;
dir = null;
} else if (typeof dir === "function") {
onResult = dir;
if (typeof sampleRate === "string") {
// (transcriptIds, startOffsets, endOffsets, dir, onResult)
dir = sampleRate;
sampleRate = null;
} else {
// (transcriptIds, startOffsets, endOffsets, sampleRate, onResult)
dir = null;
}
}
if (exports.verbose) {
console.log("getSoundFragments("+transcriptIds.length+" transcriptIds, "
+startOffsets.length+" startOffsets, "
+endOffsets.length+" endOffsets, "
+sampleRate+", "+dir+")");
}
if (dir == null) {
dir = os.tmpdir();
} else {
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
}
let fragments = [];
let errors = [];
// get fragments individually to ensure elements in result map 1:1 to element
// in transcriptIds
const url = this.baseUrl + "soundfragment";
const lc = this;
const nextFragment = function(i) {
if (i < transcriptIds.length) { // next file
const xhr = new XMLHttpRequest();
let queryString = "?id="+encodeURIComponent(transcriptIds[i])
+"&start="+encodeURIComponent(startOffsets[i])
+"&end="+encodeURIComponent(endOffsets[i]);
if (sampleRate) queryString += "&sampleRate="+sampleRate;
queryString += "&prefix=true"; // TODO add function parameter for this
if (exports.verbose) {
console.log("GET: "+url + "?" + queryString + " as " + lc.username);
}
xhr.open("GET", url + queryString, true);
if (lc.username) {
xhr.setRequestHeader(
"Authorization", "Basic " + btoa(lc.username + ":" + lc._password))
}
xhr.setRequestHeader("Accept", "audio/wav");
// we want binary data, not text
xhr.responseType = "arraybuffer";
xhr.addEventListener("error", function(evt) {
if (exports.verbose) {
console.log("getSoundFragments "+i+" ERROR: "+this.responseText);
}
errors.push("Could not get fragment "+i+": "+this.responseText);
fragments.push(null); // add a blank element
nextFragment(i+1);
}, false);
xhr.addEventListener("load", function(evt) {
if (exports.verbose) {
console.log("getSoundFragments "+i+" loaded.");
}
// save the result to a file
let fileName = transcriptIds[i]+"__"+startOffsets[i]+"-"+endOffsets[i]+".wav";
let contentDisposition = this.getResponseHeader("content-disposition");
if (contentDisposition != null) {
// something like attachment; filename=blah.wav
const equals = contentDisposition.indexOf("=");
if (equals > 0) {
fileName = contentDisposition.substring(equals + 1);
}
}
const filePath = path.join(dir, fileName);
fs.writeFile(filePath, Buffer.from(this.response), function(err) {
if (err) {
if (exports.verbose) {
console.log("getSoundFragments "+i+" SAVE ERROR: "+err);
}
errors.push("Could not save fragment "+i+": "+err);
}
// add the file name to the result
if (exports.verbose) console.log("wrote file " + filePath);
fragments.push(filePath); // add a blank element
nextFragment(i+1);
});
}, false);
xhr.send();
} else { // there are no more triples
if (onResult) {
onResult(fragments, errors.length?errors:null, [], "getSoundFragments");
}
}
}
nextFragment(0);
}
/**
* Get transcript fragments in a specified format.
* <p>For convenience, the first three arguments, <var>transcriptIds</var>,
* <var>startOffsets</var>, and <var>endOffsets</var>, can be replaced by a single
* array of match objects of the kind returned by {@link LabbcatView#getMatches}, in
* which case the start/end times are the utterance boundaries - e.g.
* <pre>labbcat.getMatches(threadId, wordsContext (result, e, m) => {
* labbcat.getFragments(result.matches, layerIds, mimeType, dir, (files, e, m) => {
* ...
* });
* });</pre>
* @param {string[]} transcriptIds A list of transcript IDs (transcript names).
* @param {float[]} startOffsets A list of start offsets, with one element for
* each element in <var>transcriptIds</var>.
* @param {float[]} endOffsets A list of end offsets, with one element for each element in
* <var>transcriptIds</var>.
* @param {string[]} layerIds A list of IDs of annotation layers to include in the
* fragment.
* @param {string} mimeType The desired format, for example "text/praat-textgrid" for Praat
* TextGrids, "text/plain" for plain text, etc.
* @param {string} [dir] A directory in which the files should be stored, or null
* for a temporary folder. If specified, and the directory doesn't exist, it will
* be created.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of files. If <var>dir</var> is null,
* these files will be stored under the system's temporary directory, so once
* processing is finished, they should be deleted by the caller, or moved to a
* more permanent location.
*/
getFragments(transcriptIds, startOffsets, endOffsets, layerIds, mimeType, dir, onResult) {
if (!runningOnNode) {
onResult && onResult(
null, ["getFragments is not yet implemented for browsers"], [], // TODO
"getFragments");
return;
}
// ensure transcriptIds is a list of strings, not a list of matches
if (typeof transcriptIds[0] != "string" && transcriptIds[0].Transcript) {
// convert the array of matches into an arrays of transcriptIds, startOffset,
// and endOffsets...
// shift remaining arguments to the right
onResult = mimeType
dir = layerIds
mimeType = endOffsets
layerIds = startOffsets
// create arrays
startOffsets = transcriptIds.map(match => match.Line);
endOffsets = transcriptIds.map(match => match.LineEnd);
transcriptIds = transcriptIds.map(match => match.Transcript);
}
if (transcriptIds.length != startOffsets.length || transcriptIds.length != endOffsets.length) {
onResult && onResult(
null,
["transcriptIds ("+transcriptIds.length +"), startOffsets ("+startOffsets.length
+"), and endOffsets ("+endOffsets.length+") must be arrays of equal size."],
[], "getFragments");
return;
}
if (typeof dir === "function") {
// (transcriptIds, startOffsets, endOffsets, layerIds, mimeType, onResult)
onResult = dir;
dir = null;
}
if (exports.verbose) {
console.log("getFragments("+transcriptIds.length+" transcriptIds, "
+startOffsets.length+" startOffsets, "
+endOffsets.length+" endOffsets, "
+JSON.stringify(layerIds)+", "+mimeType+", "+dir+")");
}
if (dir == null) {
dir = os.tmpdir();
} else {
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
}
let fragments = [];
let errors = [];
// get fragments individually to ensure elements in result map 1:1 to element
// in transcriptIds
let url = this.baseUrl + "api/serialize/fragment?mimeType="+encodeURIComponent(mimeType);
for (let layerId of layerIds) url += "&layerId=" + layerId;
const lc = this;
const nextFragment = function(i) {
if (i < transcriptIds.length) { // next file
const xhr = new XMLHttpRequest();
let queryString = "&id="+encodeURIComponent(transcriptIds[i])
+"&start="+encodeURIComponent(startOffsets[i])
+"&end="+encodeURIComponent(endOffsets[i]);
queryString += "&prefix=true"; // TODO add a function parameter for this
if (exports.verbose) {
console.log("GET: "+url + queryString + " as " + lc.username);
}
xhr.open("GET", url + queryString, true);
if (lc.username) {
xhr.setRequestHeader(
"Authorization", "Basic " + btoa(lc.username + ":" + lc._password))
}
xhr.setRequestHeader("Accept", mimeType);
// we want binary data, not text
xhr.responseType = "arraybuffer";
xhr.addEventListener("error", function(evt) {
if (exports.verbose) {
console.log("getFragments "+i+" ERROR: "+this.responseText);
}
errors.push("Could not get fragment "+i+": "+this.responseText);
fragments.push(null); // add a blank element
nextFragment(i+1);
}, false);
xhr.addEventListener("load", function(evt) {
if (exports.verbose) {
console.log("getSoundFragments "+i+" loaded.");
}
// save the result to a file
let fileName = transcriptIds[i]+"__"+startOffsets[i]+"-"+endOffsets[i];
let contentDisposition = this.getResponseHeader("content-disposition");
if (contentDisposition != null) {
// something like attachment; filename=blah.wav
const equals = contentDisposition.indexOf("=");
if (equals > 0) {
fileName = contentDisposition.substring(equals + 1);
}
}
const filePath = path.join(dir, fileName);
fs.writeFile(filePath, Buffer.from(this.response), function(err) {
if (err) {
if (exports.verbose) {
console.log("getFragments "+i+" SAVE ERROR: "+err);
}
errors.push("Could not save fragment "+i+": "+err);
}
// add the file name to the result
fragments.push(filePath); // add a blank element
nextFragment(i+1);
});
}, false);
xhr.send();
} else { // there are no more triples
if (onResult) {
onResult(fragments, errors.length?errors:null, [], "getSoundFragments");
}
}
}
nextFragment(0);
}
/**
* Gets transcript attribute values for given transcript IDs.
* @param {string[]} transcriptIds A list of transcript IDs (transcript names).
* @param {string[]} layerIds A list of layer IDs corresponding to transcript
* attributes. In general, these are layers whose ID is prefixed 'transcript_',
* however formally it's any layer where layer.parentId == 'graph' &&
* layer.alignment == 0, which includes 'corpus' as well as transcript attribute layers.
* @param {string} fileName The full path for the file where the results CSV
* should be saved.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: The CSV file path - i.e. <var>fileName</var>
* or null if the request failed.
*/
getTranscriptAttributes(transcriptIds, layerIds, fileName, onResult) {
if (!runningOnNode) {
onResult && onResult(
null, ["getTranscriptAttributes is not yet implemented for browsers"], [], // TODO
"getTranscriptAttributes");
return;
}
if (exports.verbose) {
console.log("getTranscriptAttributes("+transcriptIds.length+" transcriptIds, "
+JSON.stringify(layerIds)+")");
}
const xhr = new XMLHttpRequest();
const url = this.baseUrl + "api/attributes";
let queryString = "?layer=transcript";
for (let id of layerIds) queryString += "&layer="+encodeURIComponent(id);
for (let id of transcriptIds) queryString += "&id="+encodeURIComponent(id);
if (exports.verbose) {
console.log("GET: "+url + queryString + " as " + this.username);
}
xhr.open("GET", url + queryString, true);
if (this.username) {
xhr.setRequestHeader(
"Authorization", "Basic " + btoa(this.username + ":" + this._password))
}
xhr.setRequestHeader("Accept", "text/csv");
xhr.addEventListener("error", function(evt) {
if (exports.verbose) {
console.log("getTranscriptAttributes "+i+" ERROR: "+this.responseText);
}
fragments.push(null); // add a blank element
if (onResult) {
onResult(null, ["Could not get transcript attributes: "
+this.responseText], [], "getTranscriptAttributes");
}
}, false);
xhr.addEventListener("load", function(evt) {
if (exports.verbose) {
console.log("getTranscriptAttributes loaded. " + JSON.stringify(this.response));
}
fs.writeFile(fileName, Buffer.from(xhr.responseText), function(err) {
if (exports.verbose) {
console.log("getTranscriptAttributes wrote file " + fileName);
}
let errors = null;
if (err) {
if (exports.verbose) {
console.log("getTranscriptAttributes SAVE ERROR: "+err);
}
errors = ["Could not get transcript attributes: "+err];
}
onResult(fileName, errors, [], "getTranscriptAttributes");
});
}, false);
xhr.send();
}
/**
* Gets participant attribute values for given participant IDs.
* @param {string[]} participantIds A list of participant IDs.
* @param {string[]} layerIds A list of layer IDs corresponding to participant
* attributes. In general, these are layers whose ID is prefixed 'participant_',
* however formally it's any layer where layer.parentId == 'participant' &&
* layer.alignment == 0.
* @param {string} fileName The full path for the file where the results CSV
* should be saved.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: The CSV file path - i.e. <var>fileName</var>
* or null if the request failed.
*/
getParticipantAttributes(participantIds, layerIds, fileName, onResult) {
if (!runningOnNode) {
onResult && onResult(
null, ["getParticipantAttributes is not yet implemented for browsers"], [], // TODO
"getParticipantAttributes");
return;
}
if (exports.verbose) {
console.log("getParticipantAttributes("+participantIds.length+" participantIds, "
+JSON.stringify(layerIds)+")");
}
const xhr = new XMLHttpRequest();
const url = this.baseUrl + "participantsExport";
let queryString = "?type=participant&content-type=text/csv&csvFieldDelimiter=,";
for (let id of layerIds) queryString += "&layer="+encodeURIComponent(id);
for (let id of participantIds) queryString += "&participantId="+encodeURIComponent(id);
if (exports.verbose) {
console.log("GET: "+url + queryString + " as " + this.username);
}
xhr.open("GET", url + queryString, true);
if (this.username) {
xhr.setRequestHeader(
"Authorization", "Basic " + btoa(this.username + ":" + this._password))
}
xhr.setRequestHeader("Accept", "text/csv");
xhr.addEventListener("error", function(evt) {
if (exports.verbose) {
console.log("getParticipantAttributes "+i+" ERROR: "+this.responseText);
}
fragments.push(null); // add a blank element
if (onResult) {
onResult(null, ["Could not get participant attributes: "
+this.responseText], [], "getParticipantAttributes");
}
}, false);
xhr.addEventListener("load", function(evt) {
if (exports.verbose) {
console.log("getParticipantAttributes loaded. " + JSON.stringify(this.response));
}
fs.writeFile(fileName, Buffer.from(xhr.responseText), function(err) {
if (exports.verbose) {
console.log("getParticipantAttributes wrote file " + fileName);
}
let errors = null;
if (err) {
if (exports.verbose) {
console.log("getParticipantAttributes SAVE ERROR: "+err);
}
errors = ["Could not get participant attributes: "+err];
}
onResult(fileName, errors, [], "getParticipantAttributes");
});
}, false);
xhr.send();
}
/**
* Lists the descriptors of all registered serializers.
* <p> Serializers are modules that export annotation structures as a specific file
* format, e.g. Praat TextGrid, plain text, etc., so the <var>mimeType</var> of descriptors
* reflects what <var>mimeType</var>s can be specified for {@link LabbcatView#getFragments}.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of the descriptors of all registered
* serializers.
*/
getSerializerDescriptors(onResult) {
this.createRequest("getSerializerDescriptors", null, onResult).send();
}
/**
* Lists the descriptors of all registered deserializers.
* <p> Deserializers are modules that import annotation structures from a specific file
* format, e.g. Praat TextGrid, plain text, etc.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of the descriptors of all registered
* deserializers.
*/
getDeserializerDescriptors(onResult) {
this.createRequest("getSerializerDescriptors", null, onResult).send();
}
/**
* Lists the descriptors of all registered annotators.
* <p> Annotators are modules that perform automated annotation of transcripts.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of the descriptors of all registered
* annotators.
*/
getAnnotatorDescriptors(onResult) {
this.createRequest("getAnnotatorDescriptors", null, onResult).send();
}
/**
* Lists the descriptors of all registered transcribers.
* <p> Transcribers are modules that perform automated transcription of recordings
* that have not alreadye been transcribed.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of the descriptors of all registered
* transcribers.
*/
getTranscriberDescriptors(onResult) {
this.createRequest("getTranscriberDescriptors", null, onResult).send();
}
/**
* Gets the value of the given system attribute.
* @param {string} attribute Name of the attribute.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: The given attribute, with name and value properties.
*/
getSystemAttribute(attribute, onResult) {
this.createRequest(
"systemattributes", null, onResult, this.baseUrl+"api/systemattributes/" + attribute)
.send();
}
/**
* Gets a list of currently-installed layer managers.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: an array of objects with the following
* structure:
* <ul>
* <li> <q> layer_manager_id </q> : ID of the layer manager. </li>
* <li> <q> version </q> : The version of the installed implementation. </li>
* <li> <q> name </q> : The name for the layer manager. </li>
* <li> <q> description </q> : A brief description of what the layer manager does. </li>
* <li> <q> layer_type </q> : What kinds of layers the layer manager can process - a
* string composed of any of the following:
* <ul>
* <li><b> S </b> - segment layers </li>
* <li><b> W </b> - word layers </li>
* <li><b> M </b> - phrase (meta) layers </li>
* <li><b> F </b> - span (freeform) layers </li>
* </ul>
* </li>
* </ul>
*/
getLayerManagers(onResult) {
this.createRequest(
"layermanagers", null, onResult, this.baseUrl+"api/layers/managers")
.send();
}
/**
* Gets information about the current user, including the roles or groups they are
* in.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: The user record with an attribute called
* "roles" which is an array of string role names.
*/
getUserInfo(onResult) {
this.createRequest(
"user", null, onResult, this.baseUrl+"api/user")
.send();
}
/**
* For HTK dictionary-filling, this starts a task (returning it's threadId) that
* traverses the given utterances, looking for missing word pronunciations
* @param {string} seriesId search.search_id for identifying the utterances
* @param {string} tokenLayerId token layer ("orthography")
* @param {string} annotationLayerId tokens with missing annotations on this layer
* should be returned ("phonemes") the "model" returned is the threadId of the task
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: the threadId of the task.
* {@link LabbcatView#taskStatus} can be used to follow the progress of the task
* until it's finished. Once done, the resultUrl can be invoked, which returns a
* map from token labels to token IDs (one per type) i.e. the word that's missing,
* with an ID for finding its first occurance
*/
missingAnnotations(seriesId, tokenLayerId, annotationLayerId, fragmentIds, onResult) {
if (typeof fragmentIds === "function") {
// (seriesId, tokenLayerId, annotationLayerId, onResult)
onProgress = fragmentIds;
fragmentIds = null;
}
this.createRequest(
"missingAnnotations", {
seriesId : seriesId,
utterance : fragmentIds,
tokenLayerId : tokenLayerId,
annotationLayerId : annotationLayerId
}, onResult, this.baseUrl+"api/missingAnnotations").send();
}
/**
* For HTK dictionary-filling, this looks up some given words to get their entries.
* @param {string} layerId The dictionary of this layer will be used.
* @param {string} labels Space-separated list of words to look up.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: an object with the following structure:
* returned has the following structure:
* <ul>
* <li><b> words </b> - object where each key is the word, and each value is an
* array of entries for that word
* <li><b> combined </b> - the first entry of each word, concatenated together
* with a hyphen separator
* </ul>
*/
dictionaryLookup(layerId, labels, onResult) {
this.createRequest(
"lookup", {
layerId : layerId,
labels : labels
}, onResult, this.baseUrl+"api/dictionary/lookup").send();
}
/**
* For HTK dictionary-filling, this uses a layer dictionary to suggest missing entries.
* @param {string} layerId The dictionary of this layer will be used.
* @param {string} labels Space-separated list of words to suggest entries for.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: an object with the following structure:
* returned has the following structure:
* <ul>
* <li><b> words </b> - object where each key is the word, and each value is a
* array of suggestions for that word (with 0 or 1 elements)
* </ul>
*/
dictionarySuggest(layerId, labels, onResult) {
this.createRequest(
"suggest", {
layerId : layerId,
labels : labels
}, onResult, this.baseUrl+"api/dictionary/suggest").send();
}
/**
* Process with Praat.
* @param {file|string} csv The results file to upload. In a browser, this
* must be a file object, and in Node, it must be the full path to the file.
* @param {int} transcriptColumn CSV column index of the transcript name.
* @param {int} participantColumn CSV column index of the participant name.
* @param {int} startTimeColumn CSV column index of the start time.
* @param {int} endTimeColumn CSV column index of the end time name.
* @param {number} windowOffset How much surrounsing context to include, in seconds.
* @param {boolean} passThroughData Whether to include all CSV columns from the
* input file in the output file.
* @param {object} measurementParameters Parameters that define what to measure
* and output. All parameters are optional, and include:
* <dl>
*
* <dt> extractF1 (boolean) </dt><dd> Extract F1. (default: false) </dd>
* <dt> extractF2 (boolean) </dt><dd> Extract F2. (default: false) </dd>
* <dt> extractF3 (boolean) </dt><dd> Extract F3. (default: false) </dd>
* <dt> samplePoints (string) </dt><dd> Space-delimited series of real
* numbers between 0 and 1, specifying the proportional time points to measure
* formant at. e.g. "0.5" will measure only the mid-point, "0 0.2 0.4 0.6 0.8 1"
* will measure six points evenly spread across the duration of the segment,
* etc. (default: "0.5")</dd>
* <dt> formantCeilingDefault (int) </dt><dd> Maximum Formant by default. (default:
* 550) </dd>
* <dt> formantDifferentiationLayerId (string) </dt><dd> Participant attribute
* layer ID for differentiating formant settings; this will typically be
* "participant_gender" but can be any participant attribute layer. </dd>
* <dt> formantOtherPattern (string[]) </dt><dd> Array of regular expression
* strings to match against the value of that attribute identified by
* <var>formantDifferentiationLayerId</var>. If the participant's attribute value
* matches the pattern for an element in this array, the corresponding element in
* <var>formantCeilingOther</var> will be used for that participant. </dd>
* <dt> formantCeilingOther (int[]) </dt><dd> Values to use as the formant ceiling
* for participants who's attribute value matches the corresponding regular
* expression in <var>formantOtherPattern</var></dd>
* <dt> scriptFormant (string) </dt><dd> Formant extraction script command.
* (default: "To Formant (burg)... 0.0025 5 formantCeiling 0.025 50") </dd>
*
* <dt> useFastTrack (boolean) </dt><dd> Use the FastTrack plugin to generate
* optimum, smoothed formant tracks. (default: false)</dd>
* <dt> fastTrackDifferentiationLayerId (string) </dt><dd> Participant attribute
* layer ID for differentiating fastTrack settings; this will typically be
* "participant_gender" but can be any participant attribute layer. </dd>
* <dt> fastTrackOtherPattern (string[]) </dt><dd> Array of regular expression
* strings to match against the value of that attribute identified by
* <var>fastTrackDifferentiationLayerId</var>. If the participant's attribute value
* matches the pattern for an element in this array, the corresponding element in
* <var>fastTrackCeilingOther</var> will be used for that participant. </dd>
* <dt> fastTrackLowestAnalysisFrequencyDefault (int) </dt><dd> Fast Track lowest
* analysis frequency by default. </dd>
* <dt> fastTrackLowestAnalysisFrequencyOther (int[]) </dt><dd> Values to use as
* the Fast Track lowest analysis frequency for participants who's attribute
* value matches the corresponding regular expression in
* <var>fastTrackOtherPattern</var>.</dd>
* <dt> fastTrackHighestAnalysisFrequencyDefault (int) </dt><dd> Fast Track highest
* analysis frequency by default. </dd>
* <dt> fastTrackHighestAnalysisFrequencyOther (int[]) </dt><dd> Values to use as
* the Fast Track highest analysis frequency for participants who's attribute
* value matches the corresponding regular expression in
* <var>fastTrackOtherPattern</var>.</dd>
* <dt> fastTrackTimeStep </dt>
* <dd> Fast Track time_step global setting. </dd>
* <dt> fastTrackBasisFunctions </dt>
* <dd> Fast Track basis_functions global setting - "dct". </dd>
* <dt> fastTrackErrorMethod </dt>
* <dd> Fast Track error_method global setting - "mae". </dd>
* <dt> fastTrackTrackingMethod </dt>
* <dd> Fast Track tracking_method parameter for trackAutoselectProcedure; "burg" or
* "robust". </dd>
* <dt> fastTrackEnableF1FrequencyHeuristic ("true" or "false") </dt>
* <dd> Fast Track enable_F1_frequency_heuristic global setting. </dd>
* <dt> fastTrackMaximumF1FrequencyValue </dt>
* <dd> Fast Track maximum_F1_frequency_value global setting. </dd>
* <dt> fastTrackEnableF1BandwidthHeuristic </dt>
* <dd> Fast Track enable_F1_bandwidth_heuristic global setting. </dd>
* <dt> fastTrackMaximumF1BandwidthValue </dt>
* <dd> Fast Track maximum_F1_bandwidth_value global setting. </dd>
* <dt> fastTrackEnableF2BandwidthHeuristic ("true" or "false") </dt>
* <dd> Fast Track enable_F2_bandwidth_heuristic global setting. </dd>
* <dt> fastTrackMaximumF2BandwidthValue </dt>
* <dd> Fast Track maximum_F2_bandwidth_value global setting. </dd>
* <dt> fastTrackEnableF3BandwidthHeuristic ("true" or "false") </dt>
* <dd> Fast Track enable_F3_bandwidth_heuristic global setting.. </dd>
* <dt> fastTrackMaximumF3BandwidthValue </dt>
* <dd> Fast Track maximum_F3_bandwidth_value global setting. </dd>
* <dt> fastTrackEnableF4FrequencyHeuristic ("true" or "false") </dt>
* <dd> Fast Track enable_F4_frequency_heuristic global setting. </dd>
* <dt> fastTrackMinimumF4FrequencyValue </dt>
* <dd> Fast Track minimum_F4_frequency_value global setting. </dd>
* <dt> fastTrackEnableRhoticHeuristic ("true" of "false") </dt>
* <dd> Fast Track enable_rhotic_heuristic global setting. </dd>
* <dt> fastTrackEnableF3F4ProximityHeuristic </dt>
* <dd> Fast Track enable_F3F4_proximity_heuristic global setting. </dd>
* <dt> fastTrackNumberOfSteps </dt>
* <dd> Fast Track number of steps. </dd>
* <dt> fastTrackNumberOfCoefficients </dt>
* <dd> Fast Track number of coefficients for the regression function. </dd>
* <dt> fastTrackNumberOfFormants </dt>
* <dd> Fast Track number of formants. </dd>
* <dt> fastTrackCoefficients ("true" or "false") </dt>
* <dd> Whether to return the regression coefficients from FastTrack. </dd>
*
* <dt> extractMinimumPitch (boolean) </dt><dd> Extract minimum pitch.
* (default: false) </dd>
* <dt> extractMeanPitch (boolean) </dt><dd> Extract mean pitch. (default: false) </dd>
* <dt> extractMaximumPitch (boolean) </dt><dd> Extract maximum pitch.
* (default: false) </dd>
* <dt> pitchFloorDefault (int) </dt><dd> Pitch Floor by default. (default: 60) </dd>
* <dt> pitchCeilingDefault (int) </dt><dd> Pitch Ceiling by default. (default: 500) </dd>
* <dt> voicingThresholdDefault (number) </dt><dd> Voicing Threshold by default.
* (default: 0.5) </dd>
* <dt> pitchDifferentiationLayerId (string) </dt><dd> Participant attribute
* layer ID for differentiating pitch settings; this will typically be
* "participant_gender" but can be any participant attribute layer. </dd>
* <dt> pitchOtherPattern (string[]) </dt><dd> Array of regular expression
* strings to match against the value of that attribute identified by
* <var>pitchDifferentiationLayerId</var>. If the participant's attribute value
* matches the pattern for an element in this array, the corresponding element in
* <var>pitchFloorOther</var>, <var>pitchCeilingOther</var>, and
* <var>voicingThresholdOther</var> will be used for that participant. </dd>
* <dt> pitchFloorOther (int[]) </dt><dd> Values to use as the pitch floor
* for participants who's attribute value matches the corresponding regular
* expression in <var>pitchOtherPattern</var></dd>
* <dt> pitchCeilingOther (int[]) </dt><dd> Values to use as the pitch ceiling
* for participants who's attribute value matches the corresponding regular
* expression in <var>pitchOtherPattern</var></dd>
* <dt> voicingThresholdOther (int[]) </dt><dd> Values to use as the voicing threshold
* for participants who's attribute value matches the corresponding regular
* expression in <var>pitchOtherPattern</var></dd>
* <dt> scriptPitch (string) </dt><dd> Pitch extraction script command.
* (default:
* "To Pitch (ac)... 0 pitchFloor 15 no 0.03 voicingThreshold 0.01 0.35 0.14 pitchCeiling") </dd>
*
* <dt> extractMaximumIntensity (boolean) </dt><dd> Extract maximum intensity.
* (default: false) </dd>
* <dt> intensityPitchFloorDefault (int) </dt><dd> Pitch Floor by default.
* (default: 60) </dd>
* <dt> intensityDifferentiationLayerId (string) </dt><dd> Participant attribute
* layer ID for differentiating intensity settings; this will typically be
* "participant_gender" but can be any participant attribute layer. </dd>
* <dt> intensityOtherPattern (string[]) </dt><dd> Array of regular expression
* strings to match against the value of that attribute identified by
* <var>intensityDifferentiationLayerId</var>. If the participant's attribute value
* matches the pattern for an element in this array, the corresponding element in
* <var>intensityPitchFloorOther</var> will be used for that participant. </dd>
* <dt> intensityPitchFloorOther (int[]) </dt><dd> Values to use as the pitch floor
* for participants who's attribute value matches the corresponding regular
* expression in <var>intensityPitchOtherPattern</var></dd>
* <dt> scriptIntensity (string) </dt><dd> Pitch extraction script command.
* (default: "To Intensity... intensityPitchFloor 0 yes") </dd>
*
* <dt> extractCOG1 (boolean) </dt><dd> Extract COG 1. (default: false) </dd>
* <dt> extractCOG2 (boolean) </dt><dd> Extract COG 2. (default: false) </dd>
* <dt> extractCOG23 (boolean) </dt><dd> Extract COG 2/3. (default: false) </dd>
*
* <dt> script (string) </dt><dd> A user-specified custom Praat script to execute
* on each segment. </dd>
* <dt> attributes (string[]) </dt><dd> A list of participant attribute layer IDs
* to include as variables for the custom Praat <var>script</var>. </dd>
* </dl>
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: An object with one attribute,
* <var>threadId</var>.
* @param onProgress Invoked on XMLHttpRequest progress.
*/
praat(
csv, transcriptColumn, participantColumn, startTimeColumn, endTimeColumn,
windowOffset, passThroughData, measurementParameters,
onResult, onProgress) {
if (exports.verbose) {
console.log(
"praat("
+csv+", "+transcriptColumn+", "+participantColumn+", "
+startTimeColumn+", "+endTimeColumn+", "+windowOffset+", "
+passThroughData+", "+JSON.stringify(measurementParameters)+")");
}
// create form
var fd = new FormData();
fd.append("transcriptColumn", transcriptColumn);
fd.append("participantColumn", participantColumn);
fd.append("startTimeColumn", startTimeColumn);
fd.append("endTimeColumn", endTimeColumn);
fd.append("windowOffset", windowOffset);
for (var parameter in measurementParameters) {
var value = measurementParameters[parameter];
if (Array.isArray(value)) {
for (var element of value) {
fd.append(parameter, element);
} // next element
} else { // simple value
fd.append(parameter, value);
}
} // next parameter
if (!runningOnNode) {
fd.append("csv", csv);
// create HTTP request
var xhr = new XMLHttpRequest();
xhr.call = "praat";
xhr.onResult = onResult;
xhr.addEventListener("load", callComplete, false);
xhr.addEventListener("error", callFailed, false);
xhr.addEventListener("abort", callCancelled, false);
xhr.upload.addEventListener("progress", onProgress, false);
xhr.open("POST", this.baseUrl + "api/praat");
if (this.username) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(this.username + ":" + this.password))
}
xhr.setRequestHeader("Accept", "application/json");
xhr.send(fd);
} else { // runningOnNode
var csvName = csv.replace(/.*\//g, "");
if (exports.verbose) console.log("csvName: " + csvName);
fd.append("csv",
fs.createReadStream(csv).on('error', function(){
onResult(null, ["Invalid file: " + csvName], [], "praat", csvName);
}), csvName);
var urlParts = parseUrl(this.baseUrl + "api/praat");
// for tomcat 8, we need to explicitly send the content-type and content-length headers...
var labbcat = this;
var password = this._password;
fd.getLength(function(something, contentLength) {
var requestParameters = {
port: urlParts.port,
path: urlParts.pathname,
host: urlParts.hostname,
headers: {
"Accept" : "application/json",
"content-length" : contentLength,
"Content-Type" : "multipart/form-data; boundary=" + fd.getBoundary()
}
};
if (labbcat.username && password) {
requestParameters.auth = labbcat.username+':'+password;
}
if (/^https.*/.test(labbcat.baseUrl)) {
requestParameters.protocol = "https:";
}
if (exports.verbose) {
console.log("submit: " + labbcat.baseUrl + "api/praat");
}
fd.submit(requestParameters, function(err, res) {
var responseText = "";
if (!err) {
res.on('data',function(buffer) {
//console.log('data ' + buffer);
responseText += buffer;
});
res.on('end',function(){
if (exports.verbose) console.log("response: " + responseText);
var result = null;
var errors = null;
var messages = null;
try {
var response = JSON.parse(responseText);
result = response.model.result || response.model;
errors = response.errors;
if (errors.length == 0) errors = null
messages = response.messages;
if (messages.length == 0) messages = null
} catch(exception) {
result = null
errors = ["" +exception+ ": " + labbcat.responseText];
messages = [];
}
onResult(result, errors, messages, "getMatchAnnotations");
});
} else {
onResult(null, ["" +err+ ": " + labbcat.responseText], [], "praat");
}
if (res) res.resume();
});
}); // got length
} // runningOnNode
}
/**
* Supplies a list of automation tasks for the identified annotator.
* @param {string} annotatorId The ID of the annotator that will perform the task.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var>, which is a map of task IDs to descriptions.
*/
getAnnotatorTasks(annotatorId, onResult) {
this.createRequest(
"getAnnotatorTasks", {
annotatorId: annotatorId
}, onResult)
.send();
}
/**
* Supplies the given task's parameter string.
* @param {string} taskId The ID of the task, which must not already exist.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var>, which is the task parameters, serialized as a string.
*/
getAnnotatorTaskParameters(taskId, onResult) {
this.createRequest(
"getAnnotatorTaskParameters", {
taskId: taskId
}, onResult)
.send();
}
/**
* Reads a list of category records.
* @param {string} class_id What attributes to read; "transcript" or "participant".
* @param {int} [pageNumber] The zero-based page of records to return (if null, all
* records will be returned).
* @param {int} [pageLength] The length of pages (if null, the default page length is 20).
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of category records with the following
* attributes:
* <dl>
* <dt> class_id </dt> <dd> The class_id of the category. </dd>
* <dt> category </dt> <dd> The name/id of the category. </dd>
* <dt> description </dt> <dd> The description of the category. </dd>
* <dt> display_order </dt> <dd> Where the category appears among other categories. </dd>
* </dl>
*/
readOnlyCategories(class_id, pageNumber, pageLength, onResult) {
if (typeof pageNumber === "function") { // (onResult)
onResult = pageNumber;
pageNumber = null;
pageLength = null;
} else if (typeof l === "function") { // (p, onResult)
onResult = l;
pageLength = null;
}
if (class_id == "participant") class_id = "speaker";
this.createRequest(
`categories/${class_id}`, {
pageNumber:pageNumber,
pageLength:pageLength
}, onResult, `${this.baseUrl}api/categories/${class_id}`)
.send();
}
} // class LabbcatView
// LabbcatEdit class - read/write "edit" access
/**
* Read/write interaction with LaBB-CAT corpora, based on the
* <a href="https://nzilbb.github.io/ag/javadoc/nzilbb/ag/IGraphStore.html">nzilbb.ag.IGraphStore</a>.
* interface.
* <p>This class inherits the <em>read-only</em> operations of LabbcatView
* and adds some <em>write</em> operations for updating data.
* @example
* // create annotation store client
* const store = new LabbcatEdit("https://labbcat.canterbury.ac.nz", "demo", "demo");
* // get a corpus
* store.getCorpusIds((corpora, errors, messages, call)=>{
* console.log("transcripts in: " + corpora[0]);
* store.getTranscriptIdsInCorpus(corpora[0], (ids, errors, messages, call, id)=>{
* console.log("Deleting all transcripts in " + id));
* for (i in ids) {
* store.deleteTranscript(ids[i], (ids, errors, messages, call, id)=>{
* console.log("deleted " + id);
* });
* }
* });
* });
* @extends LabbcatView
* @author Robert Fromont robert@fromont.net.nz
*/
class LabbcatEdit extends LabbcatView{
/**
* The graph store URL - e.g. https://labbcat.canterbury.ac.nz/demo/api/edit/store/
*/
get storeEditUrl() {
return this._storeEditUrl;
}
/**
* Create a store client
* @param {string} baseUrl The LaBB-CAT base URL (i.e. the address of the 'home' link)
* @param {string} username The LaBB-CAT user name.
* @param {string} password The LaBB-CAT password.
*/
constructor(baseUrl, username, password) {
super(baseUrl, username, password);
this._storeEditUrl = this.baseUrl + "api/edit/store/";
}
/**
* Saves changes to the given transcript annotation graph object, which was
* previously returned from {@link #getTranscript}.
* <em>NB</em> this not be confused with the methods that upload a file:
* {@link #newTranscript} and {@link #updateTranscript}.
* <em>NB</em> Currently only transcript attributes can be updated.
* <p> The graph can be partial e.g. include only some of the layers that the
* stored version of the transcript contains.
* @param transcript The transcript to save.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: true if changes were saved, false if there
* were no changes to save.
* @see LabbcatView#getTranscript
*/
saveTranscript(transcript, onResult) {
this.createRequest(
"saveTranscript", null, onResult, null, "POST",
this.storeEditUrl, "application/json")
.send(JSON.stringify(transcript));
}
/**
* Saves the given media for the given transcript
* @param {string} id The transcript ID
* @param {string} trackSuffix The track suffix of the media.
* @param {string} mediaUrl A URL to the media content.
* @param {resultCallback} onResult Invoked when the request has returned a result.
*/
saveMedia(id, trackSuffix, mediaUrl, onResult) { // TODO
}
/**
* Saves the given source file (transcript) for the given transcript.
* @param {string} id The transcript ID
* @param {string} url A URL to the transcript.
* @param {resultCallback} onResult Invoked when the request has returned a result.
*/
saveSource(id, url, onResult) { // TODO
}
/**
* Saves the given document for the episode of the given transcript.
* @param {string} id The transcript ID
* @param {string} url A URL to the document.
* @param {resultCallback} onResult Invoked when the request has returned a result.
*/
saveEpisodeDocument(id, url, onResult) { // TODO
}
/**
* Deletes the given transcript, and all associated media, from the graph store.
* @param {string} id The transcript ID
* @param {resultCallback} onResult Invoked when the request has completed.
*/
deleteTranscript(id, onResult) {
this.createRequest(
"deleteTranscript", null, onResult, null, "POST",
this.storeEditUrl, "application/x-www-form-urlencoded")
.send(this.parametersToQueryString({id : id}));
}
/**
* Saves a participant, and all its tags, to the graph store.
* To change the ID of an existing participant, pass the old/current ID as the
* <var>id</var>, and pass the new ID as the <var>label</var>.
* If the participant ID does not already exist in the database, a new participant record
* is created.
* @param {string} id The participant ID - either the unique internal database ID,
* or their name.
* @param {string} label The new ID (name) for the participant
* @param {object} attributes Participant attribute values - the names are the
* participant attribute layer IDs, and the values are the corresponding new
* attribute values. The pass phrase for participant access can also be set by
* specifying a "_password" attribute.
* @param {resultCallback} onResult Invoked when the request has completed.
*/
saveParticipant(id, label, attributes, onResult) {
attributes["id"] = id;
attributes["label"] = label;
this.createRequest(
"saveParticipant", null, onResult, null, "POST",
this.storeEditUrl, "application/x-www-form-urlencoded")
.send(this.parametersToQueryString(attributes));
}
/**
* Deletes the given participan, and all assciated meta-data, from the graph store.
* @param {string} id The participant ID
* @param {resultCallback} onResult Invoked when the request has completed.
*/
deleteParticipant(id, onResult) {
this.createRequest(
"deleteParticipant", null, onResult, null, "POST",
this.storeEditUrl, "application/x-www-form-urlencoded")
.send(this.parametersToQueryString({id : id}));
}
/**
* Uploads a new transcript.
* @param {file|string} transcript The transcript to upload. In a browser, this
* must be a file object, and in Node, it must be the full path to the file.
* @param {file|file[]|string|string[]} media The media to upload, if any. In a
* browser, these must be file objects, and in Node, they must be the full paths
* to the files.
* @param {string} [mediaSuffix] The media suffix for the media.
* @param {string} transcriptType The transcript type.
* @param {string} corpus The corpus for the transcript.
* @param {string} [episode] The episode the transcript belongs to.
* @param {resultCallback} onResult Invoked when the request has returned a
* result, which is the task ID of the resulting annotation generation task. The
* task status can be updated using {@link LabbcatView#taskStatus}
* @param onProgress Invoked on XMLHttpRequest progress.
*/
newTranscript(transcript, media, mediaSuffix, transcriptType, corpus, episode, onResult, onProgress) {
if (typeof corpus === "function") {
// (transcript, media, transcriptType, corpus, onResult, onProgress)
onProgress = episode;
onResult = corpus;
episode = null;
corpus = transcriptType;
transcriptType = mediaSuffix;
mediaSuffix = null;
} else if (typeof episode === "function") {
// (transcript, media, transcriptType, corpus, episode, onResult, onProgress)
onProgress = onResult;
onResult = episode;
episode = corpus;
corpus= transcriptType;
transcriptType = mediaSuffix;
mediaSuffix = null;
}
if (exports.verbose) {
console.log("newTranscript(" + transcript + ", " + media + ", " + mediaSuffix
+ ", " + transcriptType + ", " + corpus + ", " + episode + ")");
}
// create form
var fd = new FormData();
fd.append("todo", "new");
fd.append("auto", "true");
if (transcriptType) fd.append("transcript_type", transcriptType);
if (corpus) fd.append("corpus", corpus);
if (episode) fd.append("episode", episode);
if (!runningOnNode) {
fd.append("uploadfile1_0", transcript);
if (media) {
if (!mediaSuffix) mediaSuffix = "";
if (media.constructor === Array) { // multiple files
for (var f in media) {
fd.append("uploadmedia"+mediaSuffix+"1", media[f]);
} // next file
} else { // a single file
fd.append("uploadmedia"+mediaSuffix+"1", media);
}
}
// create HTTP request
var xhr = new XMLHttpRequest();
xhr.call = "newTranscript";
xhr.id = transcript.name;
xhr.onResult = onResult;
xhr.addEventListener("load", callComplete, false);
xhr.addEventListener("error", callFailed, false);
xhr.addEventListener("abort", callCancelled, false);
xhr.upload.addEventListener("progress", onProgress, false);
xhr.upload.id = transcript.name; // for knowing what status to update during events
xhr.open("POST", this.baseUrl + "edit/transcript/new");
if (this.username) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(this.username + ":" + this.password))
}
xhr.setRequestHeader("Accept", "application/json");
xhr.send(fd);
} else { // runningOnNode
// on node.js, files are actually paths
var transcriptName = transcript.replace(/.*\//g, "");
if (exports.verbose) console.log("transcriptName: " + transcriptName);
fd.append("uploadfile1_0",
fs.createReadStream(transcript).on('error', function(){
onResult(null, ["Invalid transcript: " + transcriptName], [], "newTranscript", transcriptName);
}), transcriptName);
if (media) {
if (!mediaSuffix) mediaSuffix = "";
if (media.constructor === Array) { // multiple files
for (var f in media) {
var mediaName = media[f].replace(/.*\//g, "");
try {
fd.append("uploadmedia"+mediaSuffix+(f+1),
fs.createReadStream(media[f]).on('error', function(){
onResult(null, ["Invalid media: " + mediaName], [], "newTranscript", transcriptName);
}), mediaName);
} catch(error) {
onResult(null, ["Invalid media: " + mediaName], [], "newTranscript", transcriptName);
return;
}
} // next file
} else { // a single file
var mediaName = media.replace(/.*\//g, "");
fd.append("uploadmedia"+mediaSuffix+"1",
fs.createReadStream(media).on('error', function(){
onResult(null, ["Invalid media: " + mediaName], [], "newTranscript", transcriptName);
}), mediaName);
}
}
var urlParts = parseUrl(this.baseUrl + "edit/transcript/new");
// for tomcat 8, we need to explicitly send the content-type and content-length headers...
var labbcat = this;
var password = this._password;
fd.getLength(function(something, contentLength) {
var requestParameters = {
port: urlParts.port,
path: urlParts.pathname,
host: urlParts.hostname,
headers: {
"Accept" : "application/json",
"content-length" : contentLength,
"Content-Type" : "multipart/form-data; boundary=" + fd.getBoundary()
}
};
if (labbcat.username && password) {
requestParameters.auth = labbcat.username+':'+password;
}
if (/^https.*/.test(labbcat.baseUrl)) {
requestParameters.protocol = "https:";
}
if (exports.verbose) {
console.log("submit: " + labbcat.baseUrl + "edit/transcript/new");
}
fd.submit(requestParameters, function(err, res) {
var responseText = "";
if (!err) {
res.on('data',function(buffer) {
//console.log('data ' + buffer);
responseText += buffer;
});
res.on('end',function(){
if (exports.verbose) console.log("response: " + responseText);
var result = null;
var errors = null;
var messages = null;
try {
var response = JSON.parse(responseText);
result = response.model.result || response.model;
errors = response.errors;
if (errors.length == 0) errors = null
messages = response.messages;
if (messages.length == 0) messages = null
} catch(exception) {
result = null
errors = ["" +exception+ ": " + labbcat.responseText];
messages = [];
}
onResult(result, errors, messages, "newTranscript",
transcriptName);
});
} else {
onResult(null, ["" +err+ ": " + labbcat.responseText], [], "newTranscript", transcriptName);
}
if (res) res.resume();
});
}); // got length
} // runningOnNode
}
/**
* Uploads a new version of an existing transcript.
* <em>NB</em> this not be confused with the method that saves an annotation graph
* object: {@link #saveTranscript}
* @param {file|string} transcript The transcript to upload. In a browser, this
* must be a file object, and in Node, it must be the full path to the file.
* @param {boolean} suppressGeneration (optional) false (the default) to run
* automatic layer generation, true to suppress automatic layer generation.
* @param {resultCallback} onResult Invoked when the request has returned a result,
* which is the task ID of the resulting annotation generation task. The
* task status can be updated using {@link LabbcatView#taskStatus}
* @param onProgress Invoked on XMLHttpRequest progress.
*/
updateTranscript(transcript, suppressGeneration, onResult, onProgress) {
if (typeof suppressGeneration === "function") {
onProgress = onResult;
onResult = suppressGeneration;
suppressGeneration = false
}
if (exports.verbose) {
console.log("updateTranscript(" + transcript + ","+suppressGeneration+")");
}
// create form
var fd = new FormData();
fd.append("todo", "update");
fd.append("auto", "true");
if (suppressGeneration) fd.append("suppressGeneration", "true");
if (!runningOnNode) {
fd.append("uploadfile1_0", transcript);
// create HTTP request
var xhr = new XMLHttpRequest();
xhr.call = "updateTranscript";
xhr.id = transcript.name;
xhr.onResult = onResult;
xhr.addEventListener("load", callComplete, false);
xhr.addEventListener("error", callFailed, false);
xhr.addEventListener("abort", callCancelled, false);
xhr.upload.addEventListener("progress", onProgress, false);
xhr.upload.id = transcript.name; // for knowing what status to update during events
xhr.open("POST", this.baseUrl + "edit/transcript/new");
if (this.username) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(this.username + ":" + this._password))
}
xhr.setRequestHeader("Accept", "application/json");
xhr.send(fd);
} else { // runningOnNode
// on node.js, files are actually paths
var transcriptName = transcript.replace(/.*\//g, "");
fd.append("uploadfile1_0",
fs.createReadStream(transcript).on('error', function(){
onResult(null, ["Invalid transcript: " + transcriptName], [], "updateTranscript", transcriptName);
}), transcriptName);
var urlParts = parseUrl(this.baseUrl + "edit/transcript/new");
var requestParameters = {
port: urlParts.port,
path: urlParts.pathname,
host: urlParts.hostname,
headers: { "Accept" : "application/json" }
};
if (this.username && this._password) {
requestParameters.auth = this.username+':'+this._password;
}
if (/^https.*/.test(this.baseUrl)) {
requestParameters.protocol = "https:";
}
fd.submit(requestParameters, function(err, res) {
var responseText = "";
if (!err) {
res.on('data',function(buffer) {
//console.log('data ' + buffer);
responseText += buffer;
});
res.on('end',function(){
if (exports.verbose) console.log("response: " + responseText);
var result = null;
var errors = null;
var messages = null;
try {
var response = JSON.parse(responseText);
result = response.model.result || response.model;
errors = response.errors;
if (errors.length == 0) errors = null
messages = response.messages;
if (messages.length == 0) messages = null
;
} catch(exception) {
result = null
errors = ["" +exception+ ": " + labbcat.responseText];
messages = [];
}
// for this call, the result is an object with one key, whose
// value is the threadId - so just return that
onResult(result, errors, messages, "updateTranscript",
transcriptName);
});
} else {
onResult(null, ["" +err+ ": " + this.responseText], [], "updateTranscript", transcriptName);
}
if (res) res.resume();
});
}
}
/**
* For HTK dictionary-filling, this adds a new dictionary entry and updates all tokens.
* @param {string} layerId The dictionary of this layer will be used.
* @param {string} label The word label to add an entry for.
* @param {string} entry The definition (pronunciation) of the word to add.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var>.
*/
dictionaryAdd(layerId, label, entry, onResult) {
this.createRequest(
"add", {
layerId : layerId,
label : label,
entry : entry
}, onResult, this.baseUrl+"api/edit/dictionary/add").send();
}
} // class LabbcatEdit
// LabbcatAdmin class - read/write "admin" access
/**
* Read/write/administration interaction with LaBB-CAT corpora.
* <p>This class inherits the <em>read/write</em> operations of LabbcatEdit
* and adds some administration functions.
* @example
* // create annotation store client
* const store = new labbcat.LabbcatAdmin("http://localhost:8080/labbcat", "labbcat", "labbcat");
* // add a corpus
* store.createCorpus("new-corpus", "en", "New English Corpus", (corpus, errors, messages, call)=>{
* console.log("new corpus ID is: " + corpus.corpus_id);
* store.updateCorpus("new-corpus", "de", "New German Corpus", (corpus, errors, messages, call)=>{
* console.log("corpus updated, language is now: " + corpus.corpus_language);
* store.deleteCorpus("new-corpus", (result, errors, messages, call)=>{
* console.log("corpus deleted");
* });
* });
* });
* store.readCorpora((corpora, errors, messages, call)=>{
* for (let corpus of corpora) {
* console.log("corpus: " + corpus.corpus_name);
* } // next corpus
* });
* @extends LabbcatView
* @author Robert Fromont robert@fromont.net.nz
*/
class LabbcatAdmin extends LabbcatEdit {
/**
* The graph store URL - e.g. https://labbcat.canterbury.ac.nz/demo/api/edit/store/
*/
get storeAdminUrl() {
return this._storeAdminUrl;
}
/**
* Create a store client
* @param {string} baseUrl The LaBB-CAT base URL (i.e. the address of the 'home' link)
* @param {string} username The LaBB-CAT user name.
* @param {string} password The LaBB-CAT password.
*/
constructor(baseUrl, username, password) {
super(baseUrl, username, password);
this._storeAdminUrl = this.baseUrl + "api/admin/store/";
}
/**
* Adds a new layer.
* @param {string|object} layer The layer ID, if all the other attribute
* parameters are specified, or an object with all the layer attributes, in which case
* only <var>onResult</var> need be specified.
* @param {string} parentId The layer's parent layer id.
* @param {string} description The description of the layer.
* @param {number} alignment The layer's alignment
* - 0 for none, 1 for point alignment, 2 for interval alignment.
* @param {boolean} peers Whether children on this layer have peers or not.
* @param {boolean} peersOverlap Whether child peers on this layer can overlap or not.
* @param {boolean} parentIncludes Whether the parent temporally includes the child.
* @param {boolean} saturated Whether children must temporally fill the entire parent
* duration (true) or not (false).
* @param {string} type The type for labels on this layer, e.g. string, number,
* boolean, ipa.
* @param {object} validLabels List of valid label values for this layer, or null
* if the layer values are not restricted. The 'key' is the possible label value, and
* each key is associated with a description of the value (e.g. for displaying to users).
* @param {string} category Category for the layer, if any.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: The resulting layer definition.
*/
newLayer(layer, parentId, description, alignment,
peers, peersOverlap, parentIncludes, saturated, type, validLabels, category,
onResult) {
var layerDefinition = {
id: layer, parentId: parentId, description: description,
alignment: Number(alignment), // ensure a number is passed, not a string
peers: Boolean(peers), peersOverlap: Boolean(peersOverlap),
parentIncludes: Boolean(parentIncludes), saturated: Boolean(saturated),
type: type, validLabels: validLabels, category: category };
if (typeof parentId === "function") { // (layerObject, onResult)
onResult = parentId;
layerDefinition = layer;
// ensure important types are converted from strings
layerDefinition.alignment = Number(layerDefinition.alignment);
layerDefinition.peers = Boolean(layerDefinition.peers);
layerDefinition.peers = Boolean(layerDefinition.peers);
layerDefinition.peersOverlap = Boolean(layerDefinition.peersOverlap);
layerDefinition.parentIncludes = Boolean(layerDefinition.parentIncludes);
layerDefinition.saturated = Boolean(layerDefinition.saturated);
}
this.createRequest(
"newLayer", null, onResult, this.storeAdminUrl + "newLayer", "POST",
null, "application/json")
.send(JSON.stringify(layerDefinition));
}
/**
* Saves changes to a layer.
* @param {string|object} layer The layer ID, if all the other attribute
* parameters are specified, or an object with all the layer attributes, in which case
* only <var>onResult</var> need be specified.
* @param {string} parentId The layer's parent layer id.
* @param {string} description The description of the layer.
* @param {number} alignment The layer's alignment
* - 0 for none, 1 for point alignment, 2 for interval alignment.
* @param {boolean} peers Whether children on this layer have peers or not.
* @param {boolean} peersOverlap Whether child peers on this layer can overlap or not.
* @param {boolean} parentIncludes Whether the parent temporally includes the child.
* @param {boolean} saturated Whether children must temporally fill the entire parent
* duration (true) or not (false).
* @param {string} type The type for labels on this layer, e.g. string, number,
* boolean, ipa.
* @param {object} validLabels List of valid label values for this layer, or null
* if the layer values are not restricted. The 'key' is the possible label value, and
* each key is associated with a description of the value (e.g. for displaying to users).
* @param {string} category Category for the layer, if any.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: The resulting layer definition.
*/
saveLayer(layer, parentId, description, alignment,
peers, peersOverlap, parentIncludes, saturated, type, validLabels, category,
onResult) {
var layerDefinition = {
id: layer, parentId: parentId, description: description,
alignment: Number(alignment), // ensure a number is passed, not a string
peers: Boolean(peers), peersOverlap: Boolean(peersOverlap),
parentIncludes: Boolean(parentIncludes), saturated: Boolean(saturated),
type: type, validLabels: validLabels, category: category };
if (typeof parentId === "function") { // (layerObject, onResult)
onResult = parentId;
layerDefinition = layer;
// ensure important types are converted from strings
layerDefinition.alignment = Number(layerDefinition.alignment);
layerDefinition.peers = Boolean(layerDefinition.peers);
layerDefinition.peers = Boolean(layerDefinition.peers);
layerDefinition.peersOverlap = Boolean(layerDefinition.peersOverlap);
layerDefinition.parentIncludes = Boolean(layerDefinition.parentIncludes);
layerDefinition.saturated = Boolean(layerDefinition.saturated);
}
this.createRequest(
"saveLayer", null, onResult, this.storeAdminUrl + "saveLayer", "POST",
null, "application/json")
.send(JSON.stringify(layerDefinition));
}
/**
* Deletes the given layer, and all associated annotations.
* @param {string|object} id The ID layer to delete.
* @param {resultCallback} onResult Invoked when the request has completed.
*/
deleteLayer(id, onResult) {
this.createRequest(
"deleteLayer", null, onResult, this.storeAdminUrl + "deleteLayer", "POST",
null, "application/x-www-form-urlencoded")
.send(this.parametersToQueryString({
id : id
}));
}
/**
* Creates a new corpus record.
* @see LabbcatAdmin#readCorpora
* @see LabbcatAdmin#updateCorpus
* @see LabbcatAdmin#deleteCorpus
* @param {string} corpus_name The name/ID of the corpus.
* @param {string} corpus_language The ISO 639-1 code for the default language.
* @param {string} corpus_description The description of the corpus.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A copy of the corpus record,
* including <em> corpus_id </em> - The database key for the record.
*/
createCorpus(corpus_name, corpus_language, corpus_description, onResult) {
this.createRequest(
"corpora", null, onResult, this.baseUrl+"api/admin/corpora", "POST",
null, "application/json")
.send(JSON.stringify({
corpus_name : corpus_name,
corpus_language : corpus_language,
corpus_description : corpus_description}));
}
/**
* Reads a list of corpus records.
* @see LabbcatAdmin#createCorpus
* @see LabbcatAdmin#updateCorpus
* @see LabbcatAdmin#deleteCorpus
* @param {int} [pageNumber] The zero-based page of records to return (if null, all
* records will be returned).
* @param {int} [pageLength] The length of pages (if null, the default page length is 20).
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of corpus records with the following
* attributes:
* <dl>
* <dt> corpus_id </dt> <dd> The database key for the record. </dd>
* <dt> corpus_name </dt> <dd> The name/id of the corpus. </dd>
* <dt> corpus_language </dt> <dd> The ISO 639-1 code for the default language. </dd>
* <dt> corpus_description </dt> <dd> The description of the corpus. </dd>
* <dt> _cantDelete </dt> <dd> This is not a database field, but rather is present in
* records returned from the server that can not currently be deleted;
* a string representing the reason the record can't be deleted. </dd>
* </dl>
*/
readCorpora(pageNumber, pageLength, onResult) {
if (typeof pageNumber === "function") { // (onResult)
onResult = pageNumber;
pageNumber = null;
pageLength = null;
} else if (typeof l === "function") { // (p, onResult)
onResult = l;
pageLength = null;
}
this.createRequest(
"corpora", {
pageNumber:pageNumber,
pageLength:pageLength
}, onResult, this.baseUrl+"api/admin/corpora")
.send();
}
/**
* Updates an existing corpus record.
* @see LabbcatAdmin#createCorpus
* @see LabbcatAdmin#readCorpora
* @see LabbcatAdmin#deleteCorpus
* @param {string} corpus_id The database key for the record. // TODO eliminate corpus_id
* @param {string} corpus_name The name/ID of the corpus.
* @param {string} corpus_language The ISO 639-1 code for the default language.
* @param {string} corpus_description The description of the corpus.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A copy of the corpus record.
*/
updateCorpus(corpus_name, corpus_language, corpus_description, onResult) {
this.createRequest(
"corpora", null, onResult, this.baseUrl+"api/admin/corpora", "PUT")
.send(JSON.stringify({
corpus_name : corpus_name,
corpus_language : corpus_language,
corpus_description : corpus_description}));
}
/**
* Deletes an existing corpus record.
* @see LabbcatAdmin#createCorpus
* @see LabbcatAdmin#readCorpora
* @see LabbcatAdmin#updateCorpus
* @param {string} corpus_name The name/ID of the corpus.
* @param {resultCallback} onResult Invoked when the request has completed.
*/
deleteCorpus(corpus_name, onResult) {
this.createRequest(
"corpora", null, onResult, `${this.baseUrl}api/admin/corpora/${corpus_name}`,
"DELETE").send();
}
/**
* Creates a new project record.
* @see LabbcatAdmin#readProjects
* @see LabbcatAdmin#updateProject
* @see LabbcatAdmin#deleteProject
* @param {string} project The name/ID of the project.
* @param {string} description The description of the project.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A copy of the project record,
* including <em> project_id </em> - The database key for the record.
*/
createProject(project, description, onResult) {
this.createRequest(
"projects", null, onResult, this.baseUrl+"api/admin/projects", "POST",
null, "application/json")
.send(JSON.stringify({
project : project,
description : description}));
}
/**
* Reads a list of project records.
* @see LabbcatAdmin#createProject
* @see LabbcatAdmin#updateProject
* @see LabbcatAdmin#deleteProject
* @param {int} [pageNumber] The zero-based page of records to return (if null, all
* records will be returned).
* @param {int} [pageLength] The length of pages (if null, the default page length is 20).
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of project records with the following
* attributes:
* <dl>
* <dt> project_id </dt> <dd> The database key for the record. </dd>
* <dt> project </dt> <dd> The name/id of the project. </dd>
* <dt> description </dt> <dd> The description of the project. </dd>
* <dt> _cantDelete </dt> <dd> This is not a database field, but rather is present in
* records returned from the server that can not currently be deleted;
* a string representing the reason the record can't be deleted. </dd>
* </dl>
*/
readProjects(pageNumber, pageLength, onResult) {
if (typeof pageNumber === "function") { // (onResult)
onResult = pageNumber;
pageNumber = null;
pageLength = null;
} else if (typeof l === "function") { // (p, onResult)
onResult = l;
pageLength = null;
}
this.createRequest(
"projects", {
pageNumber:pageNumber,
pageLength:pageLength
}, onResult, this.baseUrl+"api/admin/projects")
.send();
}
/**
* Updates an existing project record.
* @see LabbcatAdmin#createProject
* @see LabbcatAdmin#readProjects
* @see LabbcatAdmin#deleteProject
* @param {string} project_id The database key for the record. // TODO eliminate project_id
* @param {string} project The name/ID of the project.
* @param {string} description The description of the project.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A copy of the project record.
*/
updateProject(project, description, onResult) {
this.createRequest(
"projects", null, onResult, this.baseUrl+"api/admin/projects", "PUT")
.send(JSON.stringify({
project : project,
description : description}));
}
/**
* Deletes an existing project record.
* @see LabbcatAdmin#createProject
* @see LabbcatAdmin#readProjects
* @see LabbcatAdmin#updateProject
* @param {string} project The name/ID of the project.
* @param {resultCallback} onResult Invoked when the request has completed.
*/
deleteProject(project, onResult) {
this.createRequest(
"projects", null, onResult, `${this.baseUrl}api/admin/projects/${project}`,
"DELETE").send();
}
/**
* Reads a list of category records. This overrides the LabbcatView version, and includes
* information about the possibility of deletion.
* @see LabbcatAdmin#createCategory
* @see LabbcatAdmin#updateCategory
* @see LabbcatAdmin#deleteCategory
* @param {string} class_id What attributes to read; "transcript" or "participant".
* @param {int} [pageNumber] The zero-based page of records to return (if null, all
* records will be returned).
* @param {int} [pageLength] The length of pages (if null, the default page length is 20).
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of category records with the following
* attributes:
* <dl>
* <dt> class_id </dt> <dd> The class_id of the category. </dd>
* <dt> category </dt> <dd> The name/id of the category. </dd>
* <dt> description </dt> <dd> The description of the category. </dd>
* <dt> display_order </dt> <dd> Where the category appears among other categories. </dd>
* <dt> _cantDelete </dt> <dd> This is not a database field, but rather is present in
* records returned from the server that can not currently be deleted;
* a string representing the reason the record can't be deleted. </dd>
* </dl>
*/
readCategories(class_id, pageNumber, pageLength, onResult) {
if (typeof pageNumber === "function") { // (onResult)
onResult = pageNumber;
pageNumber = null;
pageLength = null;
} else if (typeof l === "function") { // (p, onResult)
onResult = l;
pageLength = null;
}
if (class_id == "participant") class_id = "speaker";
this.createRequest(
`categories/${class_id}`, {
pageNumber:pageNumber,
pageLength:pageLength
}, onResult, `${this.baseUrl}api/admin/categories/${class_id}`)
.send();
}
/**
* Creates a new category record.
* @see LabbcatView#readCategories
* @see LabbcatAdmin#updateCategory
* @see LabbcatAdmin#deleteCategory
* @param {string} class_id What attributes the category applies to; "transcript" or
* "participant".
* @param {string} category The name/ID of the category.
* @param {string} description The description of the category.
* @param {number} display_order Where the category appears among other categories.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A copy of the category record,
* including <em> category_id </em> - The database key for the record.
*/
createCategory(class_id, category, description, display_order, onResult) {
if (class_id == "participant") class_id = "speaker";
this.createRequest(
"categories", null, onResult, this.baseUrl+"api/admin/categories", "POST",
null, "application/json")
.send(JSON.stringify({
class_id : class_id,
category : category,
description : description,
display_order : display_order}));
}
/**
* Updates an existing category record.
* @see LabbcatAdmin#createCategory
* @see LabbcatView#readCategories
* @see LabbcatAdmin#deleteCategory
* @param {string} class_id What attributes the category applies to; "transcript" or
* "participant".
* @param {string} category The name/ID of the category.
* @param {string} description The description of the category.
* @param {number} display_order Where the category appears among other categories.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A copy of the category record.
*/
updateCategory(class_id, category, description, display_order, onResult) {
if (class_id == "participant") class_id = "speaker";
this.createRequest(
"categories", null, onResult, this.baseUrl+"api/admin/categories", "PUT")
.send(JSON.stringify({
class_id : class_id,
category : category,
description : description,
display_order : display_order}));
}
/**
* Deletes an existing category record.
* @see LabbcatAdmin#createCategory
* @see LabbcatView#readCategories
* @see LabbcatAdmin#updateCategory
* @param {string} class_id What attributes the category applies to; "transcript" or
* "participant".
* @param {string} category The name/ID of the category.
* @param {resultCallback} onResult Invoked when the request has completed.
*/
deleteCategory(class_id, category, onResult) {
if (class_id == "participant") class_id = "speaker";
this.createRequest(
"categories", null, onResult,
`${this.baseUrl}api/admin/categories/${class_id}/${category}`,
"DELETE").send();
}
/**
* Creates a new media track record.
* @see LabbcatAdmin#readMediaTracks
* @see LabbcatAdmin#updateTask
* @see LabbcatAdmin#deleteTask
* @param {string} suffix The suffix of the mediaTrack.
* @param {string} description The description of the mediaTrack.
* @param {int} display_order The position of the mediaTrack relative to other mediaTracks.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A copy of the mediaTrack record.
*/
createMediaTrack(suffix, description, display_order, onResult) {
this.createRequest(
"mediaTracks", null, onResult, this.baseUrl+"api/admin/mediatracks", "POST",
null, "application/json")
.send(JSON.stringify({
suffix : suffix,
description : description,
display_order: display_order}));
}
/**
* Reads a list of media track records.
* @see LabbcatAdmin#createMediaTrack
* @see LabbcatAdmin#updateTask
* @see LabbcatAdmin#deleteTask
* @param {int} [pageNumber] The zero-based page of records to return (if null, all
* records will be returned).
* @param {int} [pageLength] The length of pages (if null, the default page length is 20).
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of mediaTrack records with the following
* attributes:
* <dl>
* <dt> suffix </dt> <dd> The suffix of the mediaTrack. </dd>
* <dt> description </dt> <dd> The description of the mediaTrack. </dd>
* <dt> display_order </dt> <dd> The position of the mediaTrack relative to other mediaTracks. </dd>
* <dt> _cantDelete </dt> <dd> This is not a database field, but rather is present in
* records returned from the server that can not currently be deleted;
* a string representing the reason the record can't be deleted. </dd>
* </dl>
*/
readMediaTracks(pageNumber, pageLength, onResult) {
if (typeof pageNumber === "function") { // (onResult)
onResult = pageNumber;
pageNumber = null;
pageLength = null;
} else if (typeof l === "function") { // (p, onResult)
onResult = l;
pageLength = null;
}
this.createRequest(
"mediaTracks", {
pageNumber:pageNumber,
pageLength:pageLength
}, onResult, this.baseUrl+"api/admin/mediatracks")
.send();
}
/**
* Updates an existing media track record.
* @see LabbcatAdmin#createMediaTrack
* @see LabbcatAdmin#readMediaTracks
* @see LabbcatAdmin#deleteMediaTrack
* @param {string} suffix The suffix of the mediaTrack.
* @param {string} description The description of the mediaTrack.
* @param {int} display_order The position of the mediaTrack relative to other mediaTracks.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A copy of the mediaTrack record.
*/
updateMediaTrack(suffix, description, display_order, onResult) {
this.createRequest(
"mediaTracks", null, onResult, this.baseUrl+"api/admin/mediatracks", "PUT")
.send(JSON.stringify({
suffix : suffix,
description : description,
display_order: display_order}));
}
/**
* Deletes an existing media track record.
* @see LabbcatAdmin#createMediaTrack
* @see LabbcatAdmin#readMediaTracks
* @see LabbcatAdmin#updateMediaTrack
* @param {string} suffix The suffix of the mediaTrack.
* @param {resultCallback} onResult Invoked when the request has completed.
*/
deleteMediaTrack(suffix, onResult) {
this.createRequest(
"mediaTracks", null, onResult, `${this.baseUrl}api/admin/mediatracks/${suffix}`,
"DELETE").send();
}
/**
* Creates a new role record.
* @see LabbcatAdmin#readRoles
* @see LabbcatAdmin#updateRole
* @see LabbcatAdmin#deleteRole
* @param {string} role_id The name/ID of the role.
* @param {string} description The description of the role.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A copy of the role record,
* including <em> role_id </em> - The database key for the record.
*/
createRole(role_id, description, onResult) {
this.createRequest(
"roles", null, onResult, this.baseUrl+"api/admin/roles", "POST",
null, "application/json")
.send(JSON.stringify({
role_id : role_id,
description : description}));
}
/**
* Reads a list of role records.
* @see LabbcatAdmin#createRole
* @see LabbcatAdmin#updateRole
* @see LabbcatAdmin#deleteRole
* @param {int} [pageNumber] The zero-based page of records to return (if null, all
* records will be returned).
* @param {int} [pageLength] The length of pages (if null, the default page length is 20).
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of role records with the following
* attributes:
* <dl>
* <dt> role_id </dt> <dd> The name/id of the role. </dd>
* <dt> description </dt> <dd> The description of the role. </dd>
* <dt> _cantDelete </dt> <dd> This is not a database field, but rather is present in
* records returned from the server that can not currently be deleted;
* a string representing the reason the record can't be deleted. </dd>
* </dl>
*/
readRoles(pageNumber, pageLength, onResult) {
if (typeof pageNumber === "function") { // (onResult)
onResult = pageNumber;
pageNumber = null;
pageLength = null;
} else if (typeof l === "function") { // (p, onResult)
onResult = l;
pageLength = null;
}
this.createRequest(
"roles", {
pageNumber:pageNumber,
pageLength:pageLength
}, onResult, this.baseUrl+"api/admin/roles")
.send();
}
/**
* Updates an existing role record.
* @see LabbcatAdmin#createRole
* @see LabbcatAdmin#readRoles
* @see LabbcatAdmin#deleteRole
* @param {string} role_id The name/ID of the role.
* @param {string} description The description of the role.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A copy of the role record.
*/
updateRole(role_id, description, onResult) {
this.createRequest(
"roles", null, onResult, this.baseUrl+"api/admin/roles", "PUT")
.send(JSON.stringify({
role_id : role_id,
description : description}));
}
/**
* Deletes an existing role record.
* @see LabbcatAdmin#createRole
* @see LabbcatAdmin#readRoles
* @see LabbcatAdmin#updateRole
* @param {string} role_id The name/ID of the role.
* @param {resultCallback} onResult Invoked when the request has completed.
*/
deleteRole(role_id, onResult) {
this.createRequest(
"roles", null, onResult, `${this.baseUrl}api/admin/roles/${role_id}`,
"DELETE").send();
}
/**
* Creates a new role permission record.
* @see LabbcatAdmin#readRolePermissions
* @see LabbcatAdmin#updateRolePermission
* @see LabbcatAdmin#deleteRolePermission
* @param {string} role_id The name/ID of the role.
* @param {string} entity A string indentifying the entities the permission
* applies to.
* @param {string} layer The ID of a a transcript attribute layer (or "corpus")
* the label of which determines the access.
* @param {string} value_pattern A regular expression; if the value of the
* label identified by <var> layer </var> matches this pattern, then
* access to the entities identfied by <var> entity </var> is granted.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A copy of the role permission record,
* including <em> rolePermission_id </em> - The database key for the record.
*/
createRolePermission(role_id, entity, layer, value_pattern, onResult) {
this.createRequest(
"rolePermissions", null, (permission, errors, messages, call, id) => {
if (permission) {
// attribute_name -> layer
if (permission.attribute_name == "corpus") {
permission.layer = permission.attribute_name;
} else {
permission.layer = "transcript_" + permission.attribute_name;
}
}
if (onResult) onResult(permission, errors, messages, call, id);
}, this.baseUrl+"api/admin/roles/permissions", "POST",
null, "application/json")
.send(JSON.stringify({
role_id : role_id,
entity : entity,
attribute_name : layer.replace(/^transcript_/,""), // layer -> attribute_name
value_pattern : value_pattern}));
}
/**
* Reads a list of role permission records for a given user role.
* @see LabbcatAdmin#createRolePermission
* @see LabbcatAdmin#updateRolePermission
* @see LabbcatAdmin#deleteRolePermission
* @param {string} role_id The name/ID of the role.
* @param {int} [pageNumber] The zero-based page of records to return (if null, all
* records will be returned).
* @param {int} [pageLength] The length of pages (if null, the default page length is 20).
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of role permission records with the following
* attributes:
* <dl>
* <dt> role_id </dt> <dd> The name/id of the rolePermission. </dd>
* <dt> entity </dt> <dd> A string indentifying the entities the permission
* applies to. </dd>
* <dt> layer </dt> <dd> The ID of a a transcript attribute layer (or "corpus")
* the label of which determines the access. </dd>
* <dt> value_pattern </dt> <dd> A regular expression; if the value of the
* label identified by <var> layer </var> matches this pattern, then
* access to the entities identfied by <var> entity </var> is granted. </dd>
* <dt> _cantDelete </dt> <dd> This is not a database field, but rather is present in
* records returned from the server that can not currently be deleted;
* a string representing the reason the record can't be deleted. </dd>
* </dl>
*/
readRolePermissions(role_id, pageNumber, pageLength, onResult) {
if (typeof pageNumber === "function") { // (onResult)
onResult = pageNumber;
pageNumber = null;
pageLength = null;
} else if (typeof l === "function") { // (p, onResult)
onResult = l;
pageLength = null;
}
this.createRequest(
"rolePermissions", {
pageNumber:pageNumber,
pageLength:pageLength
}, (permissions, errors, messages, call, id) => {
if (permissions) {
// attribute_name -> layer
for (var permission of permissions) {
if (permission.attribute_name == "corpus") {
permission.layer = permission.attribute_name;
} else {
permission.layer = "transcript_" + permission.attribute_name;
}
}
}
if (onResult) onResult(permissions, errors, messages, call, id);
}, `${this.baseUrl}api/admin/roles/permissions/${role_id}`)
.send();
}
/**
* Updates an existing role permission record.
* @see LabbcatAdmin#createRolePermission
* @see LabbcatAdmin#readRolePermissions
* @see LabbcatAdmin#deleteRolePermission
* @param {string} role_id The name/ID of the role.
* @param {string} entity A string indentifying the entities the permission
* applies to.
* @param {string} layer The ID of a a transcript attribute layer (or "corpus")
* the label of which determines the access.
* @param {string} value_pattern A regular expression; if the value of the
* label identified by <var> layer </var> matches this pattern, then
* access to the entities identfied by <var> entity </var> is granted.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A copy of the role permission record.
*/
updateRolePermission(role_id, entity, layer, value_pattern, onResult) {
var permission = this.createRequest(
"rolePermissions", null, (permission, errors, messages, call, id) => {
if (permission) {
// attribute_name -> layer
if (permission.attribute_name == "corpus") {
permission.layer = permission.attribute_name;
} else {
permission.layer = "transcript_" + permission.attribute_name;
}
}
if (onResult) onResult(permission, errors, messages, call, id);
}, this.baseUrl+"api/admin/roles/permissions", "PUT")
.send(JSON.stringify({
role_id : role_id,
entity : entity,
attribute_name : layer.replace(/^transcript_/,""), // layer -> attribute_name
value_pattern : value_pattern}));
}
/**
* Deletes an existing role permission record.
* @see LabbcatAdmin#createRolePermission
* @see LabbcatAdmin#readRolePermissions
* @see LabbcatAdmin#updateRolePermission
* @param {string} rolePermission_id The name/ID of the rolePermission.
* @param {resultCallback} onResult Invoked when the request has completed.
*/
deleteRolePermission(role_id, entity, onResult) {
this.createRequest(
"rolePermissions", null, onResult, `${this.baseUrl}api/admin/roles/permissions/${role_id}/${entity}`,
"DELETE").send();
}
/**
* Reads a list of system attribute records.
* @see LabbcatAdmin#updateSystemAttribute
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of system attribute records with the following
* attributes:
* <dl>
* <dt> attribute </dt> <dd> ID of the attribute. </dd>
* <dt> type </dt> <dd> The type of the attribute - "string", "integer",
* "boolean", "select", etc. </dd>
* <dt> style </dt> <dd> Style definition which depends on <var> type </var> -
* e.g. whether the "boolean" is shown as a checkbox or
* radio buttons, etc. </dd>
* <dt> label </dt> <dd> User-facing label for the attribute. </dd>
* <dt> description </dt> <dd> User-facing (long) description for the attribute. </dd>
* <dt> options </dt> <dd> If <var> type </var> is "select", this is an object
* defining the valid options for the attribute, where
* the attribute key is the attribute value and the attribute
* value is the user-facing label for the option. </dd>
* <dt> value </dt> <dd> The value of the attribute. </dd>
* </dl>
*/
readSystemAttributes(onResult) {
this.createRequest(
"systemattributes", null, onResult, this.baseUrl+"api/admin/systemattributes")
.send();
}
/**
* Updates an existing system attribute record.
* @see LabbcatAdmin#readSystemAttributes
* @param {string} attribute The ID of the attribute.
* @param {string} value The value of the attribute.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A copy of the system attribute record.
*/
updateSystemAttribute(attribute, value, onResult) {
this.createRequest(
"systemattributes", null, onResult, this.baseUrl+"api/admin/systemattributes", "PUT")
.send(JSON.stringify({
attribute : attribute,
value : value}));
}
/**
* Uploads an annotator module in preparation for installing it.
* @param {string|file} jarFile Annotator .jar file.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: An object describing the attributes of the
* annotator found in the jar file:
* <dl>
* <dt> jar </dt><dd> The name of the file uploaded. </dd>
* <dt> annotatorId </dt><dd> The ID of the annotator. </dd>
* <dt> version </dt><dd> The version of the annotator implementation. </dd>
* <dt> installedVersion </dt><dd> The version of the annotator that this one will
* replace, if the annotator ID has already been installed. </dd>
* <dt> hasConfigWebapp </dt><dd> Whether the annotator has a
* installation/configuration web-app. </dd>
* <dt> hasTaskWebapp </dt><dd> Whether the annotator has a task definition
* web-app. </dd>
* <dt> hasExtWebapp </dt><dd> Whether the annotator has an 'extensions' web-app. </dd>
* <dt> info </dt><dd> HTML document describing the annotator. </dd>
* </dl>
* @param onProgress Invoked on XMLHttpRequest progress.
*/
uploadAnnotator(jarFile, onResult, onProgress) {
// create form
var fd = new FormData();
// TODO nzibb/labbcat-server/user-interface thinks it's running on Node when actually
// it's running in a browser, so we need a better test for runningOnNode
if (runningOnNode || true) {
fd.append("jarFile", jarFile);
// create HTTP request
var xhr = new XMLHttpRequest();
xhr.call = "uploadAnnotator";
xhr.onResult = onResult;
xhr.addEventListener("load", callComplete, false);
xhr.addEventListener("error", callFailed, false);
xhr.addEventListener("abort", callCancelled, false);
xhr.upload.addEventListener("progress", onProgress, false);
xhr.open("POST", this.baseUrl + "admin/annotator");
if (this.username) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(this.username + ":" + this.password))
}
xhr.setRequestHeader("Accept", "application/json");
xhr.send(fd);
} else { // runningOnNode
var jarName = jarFile.replace(/.*\//g, "");
if (exports.verbose) console.log("jarName: " + jarName);
fd.append(
"jarFile",
fs.createReadStream(jarFile).on('error', function(){
onResult(null, ["Invalid jar: " + jarFile], [], "uploadAnnotator", jarFile);
}), jarName);
var urlParts = parseUrl(this.baseUrl + "admin/annotator");
// for tomcat 8, we need to explicitly send the content-type and content-length headers...
var labbcat = this;
var password = this._password;
fd.getLength(function(something, contentLength) {
var requestParameters = {
port: urlParts.port,
path: urlParts.pathname,
host: urlParts.hostname,
headers: {
"Accept" : "application/json",
"content-length" : contentLength,
"Content-Type" : "multipart/form-data; boundary=" + fd.getBoundary()
}
};
if (labbcat.username && password) {
requestParameters.auth = labbcat.username+':'+password;
}
if (/^https.*/.test(labbcat.baseUrl)) {
requestParameters.protocol = "https:";
}
if (exports.verbose) {
console.log("submit: " + labbcat.baseUrl + "edit/transcript/new");
}
fd.submit(requestParameters, function(err, res) {
var responseText = "";
if (!err) {
res.on('data',function(buffer) {
//console.log('data ' + buffer);
responseText += buffer;
});
res.on('end',function(){
if (exports.verbose) console.log("response: " + responseText);
var result = null;
var errors = null;
var messages = null;
try {
var response = JSON.parse(responseText);
result = response.model.result || response.model;
errors = response.errors;
if (errors.length == 0) errors = null
messages = response.messages;
if (messages.length == 0) messages = null
} catch(exception) {
result = null
errors = ["" +exception+ ": " + labbcat.responseText];
messages = [];
}
// for this call, the result is an object with one key, whose
// value is the threadId - so just return that
onResult(
result[jarName], errors, messages, "uploadAnnotator", jarName);
});
} else {
onResult(null, ["" +err+ ": " + labbcat.responseText], [], "uploadAnnotator", jarName);
}
if (res) res.resume();
});
}); // got length
} // runningOnNode
}
/**
* Uploads an annotator module in preparation for installing it.
* @param {string} jar Name of the annotator .jar file already uploaded, as
* specified by the "jar" attribute of the uploadAnnotator() response.
* @param {boolean} install true to install the annotator, false to cancel the
* installation.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: the annotator ID if it was installed, and null
* otherwise.
*/
installAnnotator(jar, install, onResult) {
this.createRequest(
"installAnnotator", null, onResult, this.baseUrl+"admin/annotator",
"POST", // not GET, because the number of parameters can make the URL too long
null, "application/x-www-form-urlencoded")
.send(this.parametersToQueryString({
jar : jar,
action : install?"install":"cancel"
}));
}
/**
* Uploads an annotator module in preparation for installing it.
* @param {string} annotatorId ID of the annotator to uninstall.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var>.
*/
uninstallAnnotator(annotatorId, onResult) {
this.createRequest(
"uninstallAnnotator", null, onResult, this.baseUrl+"admin/annotator",
"POST", // not GET, because the number of parameters can make the URL too long
null, "application/x-www-form-urlencoded")
.send(this.parametersToQueryString({
annotatorId : annotatorId,
action : "uninstall"
}));
}
/**
* Create a new annotator task with the given ID and description.
* @param {string} annotatorId The ID of the annotator that will perform the task.
* @param {string} taskId The ID of the task, which must not already exist.
* @param {string} description The description of the task.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var>.
*/
newAnnotatorTask(annotatorId, taskId, description, onResult) {
this.createRequest(
"newAnnotatorTask", null, onResult, null, "POST",
this.storeAdminUrl+"newAnnotatorTask", "application/x-www-form-urlencoded")
.send(this.parametersToQueryString({
annotatorId: annotatorId,
taskId: taskId,
description: description
}));
}
/**
* Update the annotator task description.
* @param {string} taskId The ID of the task, which must not already exist.
* @param {string} description The description of the task.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var>.
*/
saveAnnotatorTaskDescription(taskId, description, onResult) {
this.createRequest(
"saveAnnotatorTaskDescription", null, onResult, null, "POST",
this.storeAdminUrl+"saveAnnotatorTaskDescription", "application/x-www-form-urlencoded")
.send(this.parametersToQueryString({
taskId: taskId,
description: description
}));
}
/**
* Update the annotator task parameters.
* @param {string} taskId The ID of the task, which must not already exist.
* @param {string} parameters The task parameters, serialized as a string.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var>.
*/
saveAnnotatorTaskParameters(taskId, parameters, onResult) {
this.createRequest(
"saveAnnotatorTaskParameters", null, onResult, null, "POST",
this.storeAdminUrl+"saveAnnotatorTaskParameters", "application/x-www-form-urlencoded")
.send(this.parametersToQueryString({
taskId: taskId,
parameters: parameters
}));
}
/**
* Delete the identified automation task.
* @param {string} taskId The ID of the task, which must not already exist.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var>.
*/
deleteAnnotatorTask(taskId, onResult) {
this.createRequest(
"deleteAnnotatorTask", null, onResult, null, "POST",
this.storeAdminUrl+"deleteAnnotatorTask", "application/x-www-form-urlencoded")
.send(this.parametersToQueryString({
taskId: taskId
}));
}
/**
* Uploads an transcriber module in preparation for installing it.
* @param {string|file} jarFile Transcriber .jar file.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: An object describing the attributes of the
* transcriber found in the jar file:
* <dl>
* <dt> jar </dt><dd> The name of the file uploaded. </dd>
* <dt> transcriberId </dt><dd> The ID of the transcriber. </dd>
* <dt> version </dt><dd> The version of the transcriber implementation. </dd>
* <dt> installedVersion </dt><dd> The version of the transcriber that this one will
* replace, if the transcriber ID has already been installed. </dd>
* <dt> hasConfigWebapp </dt><dd> Whether the transcriber has a
* installation/configuration web-app. </dd>
* <dt> info </dt><dd> HTML document describing the transcriber. </dd>
* </dl>
* @param onProgress Invoked on XMLHttpRequest progress.
*/
uploadTranscriber(jarFile, onResult, onProgress) {
// create form
var fd = new FormData();
// TODO nzibb/labbcat-server/user-interface thinks it's running on Node when actually
// it's running in a browser, so we need a better test for runningOnNode
if (runningOnNode || true) {
fd.append("jarFile", jarFile);
// create HTTP request
var xhr = new XMLHttpRequest();
xhr.call = "uploadTranscriber";
xhr.onResult = onResult;
xhr.addEventListener("load", callComplete, false);
xhr.addEventListener("error", callFailed, false);
xhr.addEventListener("abort", callCancelled, false);
xhr.upload.addEventListener("progress", onProgress, false);
xhr.open("POST", this.baseUrl + "admin/transcriber");
if (this.username) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(this.username + ":" + this.password))
}
xhr.setRequestHeader("Accept", "application/json");
xhr.send(fd);
} else { // runningOnNode
var jarName = jarFile.replace(/.*\//g, "");
if (exports.verbose) console.log("jarName: " + jarName);
fd.append(
"jarFile",
fs.createReadStream(jarFile).on('error', function(){
onResult(null, ["Invalid jar: " + jarFile], [], "uploadTranscriber", jarFile);
}), jarName);
var urlParts = parseUrl(this.baseUrl + "admin/transcriber");
// for tomcat 8, we need to explicitly send the content-type and content-length headers...
var labbcat = this;
var password = this._password;
fd.getLength(function(something, contentLength) {
var requestParameters = {
port: urlParts.port,
path: urlParts.pathname,
host: urlParts.hostname,
headers: {
"Accept" : "application/json",
"content-length" : contentLength,
"Content-Type" : "multipart/form-data; boundary=" + fd.getBoundary()
}
};
if (labbcat.username && password) {
requestParameters.auth = labbcat.username+':'+password;
}
if (/^https.*/.test(labbcat.baseUrl)) {
requestParameters.protocol = "https:";
}
if (exports.verbose) {
console.log("submit: " + labbcat.baseUrl + "edit/transcript/new");
}
fd.submit(requestParameters, function(err, res) {
var responseText = "";
if (!err) {
res.on('data',function(buffer) {
//console.log('data ' + buffer);
responseText += buffer;
});
res.on('end',function(){
if (exports.verbose) console.log("response: " + responseText);
var result = null;
var errors = null;
var messages = null;
try {
var response = JSON.parse(responseText);
result = response.model.result || response.model;
errors = response.errors;
if (errors.length == 0) errors = null
messages = response.messages;
if (messages.length == 0) messages = null
} catch(exception) {
result = null
errors = ["" +exception+ ": " + labbcat.responseText];
messages = [];
}
// for this call, the result is an object with one key, whose
// value is the threadId - so just return that
onResult(
result[jarName], errors, messages, "uploadTranscriber", jarName);
});
} else {
onResult(null, ["" +err+ ": " + labbcat.responseText], [], "uploadTranscriber", jarName);
}
if (res) res.resume();
});
}); // got length
} // runningOnNode
}
/**
* Uploads an transcriber module in preparation for installing it.
* @param {string} jar Name of the transcriber .jar file already uploaded, as
* specified by the "jar" attribute of the uploadTranscriber() response.
* @param {boolean} install true to install the transcriber, false to cancel the
* installation.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: the transcriber ID if it was installed, and null
* otherwise.
*/
installTranscriber(jar, install, onResult) {
this.createRequest(
"installTranscriber", null, onResult, this.baseUrl+"admin/transcriber",
"POST", // not GET, because the number of parameters can make the URL too long
null, "application/x-www-form-urlencoded")
.send(this.parametersToQueryString({
jar : jar,
action : install?"install":"cancel"
}));
}
/**
* Uploads an transcriber module in preparation for installing it.
* @param {string} transcriberId ID of the transcriber to uninstall.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var>.
*/
uninstallTranscriber(transcriberId, onResult) {
this.createRequest(
"uninstallTranscriber", null, onResult, this.baseUrl+"admin/transcriber",
"POST", // not GET, because the number of parameters can make the URL too long
null, "application/x-www-form-urlencoded")
.send(this.parametersToQueryString({
transcriberId : transcriberId,
action : "uninstall"
}));
}
/**
* Creates a new user record.
* @see LabbcatAdmin#readUsers
* @see LabbcatAdmin#updateUser
* @see LabbcatAdmin#deleteUser
* @param {string} user The ID of the user.
* @param {string} email The email address of the user.
* @param {boolean} resetPassword Whether the user must reset their password when
* they next log in.
* @param {string[]} roles Roles or groups the user belongs to.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A copy of the user record,
* including <em> user </em> - The database key for the record.
*/
createUser(user, email, resetPassword, roles, onResult) {
this.createRequest(
"users", null, onResult, this.baseUrl+"api/admin/users", "POST",
null, "application/json")
.send(JSON.stringify({
user : user,
email : email,
resetPassword : resetPassword?1:0,
roles : roles}));
}
/**
* Reads a list of user records.
* @see LabbcatAdmin#createUser
* @see LabbcatAdmin#updateUser
* @see LabbcatAdmin#deleteUser
* @param {int} [pageNumber] The zero-based page of records to return (if null, all
* records will be returned).
* @param {int} [pageLength] The length of pages (if null, the default page length is 20).
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A list of user records with the following
* attributes:
* <dl>
* <dt> user </dt> <dd> The name/id of the user. </dd>
* <dt> email </dt> <dd> The email address of the user. </dd>
* <dt> resetPassword </dt> <dd> Whether the user must reset their password when
* they next log in. </dd>
* <dt> roles </dt> <dd> Roles or groups the user belongs to. </dd>
* <dt> _cantDelete </dt> <dd> This is not a database field, but rather is present in
* records returned from the server that can not currently be deleted;
* a string representing the reason the record can't be deleted. </dd>
* </dl>
*/
readUsers(pageNumber, pageLength, onResult) {
if (typeof pageNumber === "function") { // (onResult)
onResult = pageNumber;
pageNumber = null;
pageLength = null;
} else if (typeof l === "function") { // (p, onResult)
onResult = l;
pageLength = null;
}
this.createRequest(
"users", {
pageNumber:pageNumber,
pageLength:pageLength
}, onResult, this.baseUrl+"api/admin/users")
.send();
}
/**
* Updates an existing user record.
* @see LabbcatAdmin#createUser
* @see LabbcatAdmin#readUsers
* @see LabbcatAdmin#deleteUser
* @param {string} user The ID of the user.
* @param {string} email The email address of the user.
* @param {boolean} resetPassword Whether the user must reset their password when
* they next log in.
* @param {string[]} roles Roles or groups the user belongs to.
* @param {resultCallback} onResult Invoked when the request has returned a
* <var>result</var> which will be: A copy of the user record.
*/
updateUser(user, email, resetPassword, roles, onResult) {
if (exports.verbose) console.log("updateUser("+user+", "+email+", "+resetPassword+", "+JSON.stringify(roles));
this.createRequest(
"users", null, onResult, this.baseUrl+"api/admin/users", "PUT")
.send(JSON.stringify({
user : user,
email : email,
resetPassword : resetPassword?1:0,
roles : roles}));
}
/**
* Deletes an existing user record.
* @see LabbcatAdmin#createUser
* @see LabbcatAdmin#readUsers
* @see LabbcatAdmin#updateUser
* @param {string} user The name/ID of the user.
* @param {resultCallback} onResult Invoked when the request has completed.
*/
deleteUser(user, onResult) {
this.createRequest(
"users", null, onResult, `${this.baseUrl}api/admin/users/${user}`,
"DELETE").send();
}
/**
* Sets a given user's password.
* @param {string} user The ID of the user.
* @param {string} password The new password.
* @param {boolean} resetPassword Whether the user must reset their password when
* they next log in.
* @param {resultCallback} onResult Invoked when the request has returned.
*/
setPassword(user, password, resetPassword, onResult) {
if (exports.verbose) console.log("updateUsersetP("+user+", ****, "+resetPassword);
this.createRequest(
"users", null, onResult, this.baseUrl+"api/admin/password", "PUT")
.send(JSON.stringify({
user : user,
password : password,
resetPassword : resetPassword}));
}
}
/**
* Interpreter for match ID strings.
* <p>The schema is:</p>
* <ul>
* <li>
* when there's a defining annotation UID:<br>
* g_<i>ag_id</i>;<em>uid</em><br>
* e.g. <tt>g_243;em_12_20035</tt></li>
* <li>
* when there's anchor IDs:<br>
* g_<i>ag_id</i>;<em>startuid</em>-<em>enduid</em><br>
* e.g. <tt>g_243;n_72700-n_72709</tt></li>
* <li>
* when there's anchor offsets:<br>
* g_<i>ag_id</i>;<em>startoffset</em>-<em>endoffset</em><br>
* e.g. <tt>g_243;39.400-46.279</tt></li>
* <li>
* when there's a participant/speaker number, it will be appended:<br>
* <em>...</em>;p_<em>speakernumber</em><br>
* e.g. <tt>g_243;n_72700-n_72709;p_76</tt></li>
* <li>
* matching subparts can be identified by appending a list of annotation UIDs for insertion into {@link #mMatchAnnotationUids}, the keys being enclosed in square brackets:<br>
* ...;<em>[key]=uid;[key]=uid</em><br>
* e.g. <samp>g_243;n_72700-n_72709;[0,0]=ew_0_123;[1,0]ew_0_234</samp></li>
* <li>
* a target annotation by appending a uid prefixed by <samp>#=</samp>:<br>
* ...;#=<em>uid</em><br>
* e.g. <samp>g_243;n_72700-n_72709;#=ew_0_123</samp></li>
* <li>
* other items (search name or prefix) could then come after all that, and key=value pairs:<br>
* ...;<em>key</em>=<em>value</em><br>
* e.g. <tt>g_243;n_72700-n_72709;ew_0_123-ew_0_234;prefix=024-;name=the_aeiou</tt></li>
* <p>These can be something like:
* <ul>
* <li><q>g_3;em_11_23;n_19985-n_20003;p_4;#=ew_0_12611;prefix=001-;[0]=ew_0_12611</q></li>
* <li><q>AgnesShacklock-01.trs;60.897-67.922;prefix=001-</q></li>
* <li><q>AgnesShacklock-01.trs;60.897-67.922;m_-1_23</q></li>
* </ul>
*/
class MatchId {
/**
* String constructor.
*/
constructor(matchId) {
this._transcriptId = null;
this._startAnchorId = null;
this._endAnchorId = null;
this._startOffset = null;
this._endOffset = null;
this._utteranceId = null;
this._participantId = null;
this._targetId = null;
this._prefix = null;
if (matchId) {
const parts = matchId.split(";");
this._transcriptId = parts[0];
let intervalPart = null;
for (let part of parts) {
if (part == parts[0]) continue;
if (part.indexOf("-") > 0) {
intervalPart = part;
break;
}
} // next part
const interval = intervalPart.split("-");
if (interval[0].startsWith("n_")) { // anchor IDs
this._startAnchorId = interval[0];
this._endAnchorId = interval[1];
} else { // offsets
this._startOffset = parseFloat(interval[0]);
this._endOffset = parseFloat(interval[1]);
}
for (let part of parts) {
if (part.startsWith("prefix=")) {
this._prefix = part.substring("prefix=".length);
} else if (part.startsWith("em_") || part.startsWith("m_")) {
this._utteranceId = part;
} else if (part.startsWith("p_")) {
this._participantId = part;
} else if (part.startsWith("#=")) {
this._targetId = part.substring("#=".length);
}
} // next part
} // string was given
}
/**
* The transcript identifier.
*/
get transcriptId() { return this._transcriptId; }
/**
* ID of the start anchor.
*/
get startAnchorId() { return this._startAnchorId; }
/**
* ID of the end anchor.
*/
get endAnchorId() { return this._endAnchorId; }
/**
* Offset of the start anchor.
*/
get startOffset() { return this._startOffset; }
/**
* Offset of the end anchor.
*/
get endOffset() { return this._endOffset; }
/**
* ID of the participant.
*/
get participantId() { return this._participantId; }
/**
* ID of the match utterance.
*/
get utteranceId() { return this._utteranceId; }
/**
* ID of the match target annotation.
*/
get targetId() { return this._targetId; }
/**
* Match prefix for fragments.
*/
get prefix() { return this._prefix; }
}
exports.LabbcatView = LabbcatView;
exports.LabbcatEdit = LabbcatEdit;
exports.LabbcatAdmin = LabbcatAdmin;
exports.MatchId = MatchId;
exports.verbose = false;
exports.language = false;
}(typeof exports === 'undefined' ? this.labbcat = {} : exports));