elan.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. /**
  2. * @typedef {Object} ElanPluginParams
  3. * @property {string|HTMLElement} container CSS selector or HTML element where
  4. * the ELAN information should be renderer.
  5. * @property {string} url The location of ELAN XML data
  6. * @property {?boolean} deferInit Set to true to manually call
  7. * @property {?Object} tiers If set only shows the data tiers with the `TIER_ID`
  8. * in this map.
  9. */
  10. /**
  11. * Downloads and renders ELAN audio transcription documents alongside the
  12. * waveform.
  13. *
  14. * @implements {PluginClass}
  15. * @extends {Observer}
  16. * @example
  17. * // es6
  18. * import ElanPlugin from 'wavesurfer.elan.js';
  19. *
  20. * // commonjs
  21. * var ElanPlugin = require('wavesurfer.elan.js');
  22. *
  23. * // if you are using <script> tags
  24. * var ElanPlugin = window.WaveSurfer.elan;
  25. *
  26. * // ... initialising wavesurfer with the plugin
  27. * var wavesurfer = WaveSurfer.create({
  28. * // wavesurfer options ...
  29. * plugins: [
  30. * ElanPlugin.create({
  31. * // plugin options ...
  32. * })
  33. * ]
  34. * });
  35. */
  36. export default class ElanPlugin {
  37. /**
  38. * Elan plugin definition factory
  39. *
  40. * This function must be used to create a plugin definition which can be
  41. * used by wavesurfer to correctly instantiate the plugin.
  42. *
  43. * @param {ElanPluginParams} params parameters use to initialise the plugin
  44. * @return {PluginDefinition} an object representing the plugin
  45. */
  46. static create(params) {
  47. return {
  48. name: 'elan',
  49. deferInit: params && params.deferInit ? params.deferInit : false,
  50. params: params,
  51. instance: ElanPlugin
  52. };
  53. }
  54. Types = {
  55. ALIGNABLE_ANNOTATION: 'ALIGNABLE_ANNOTATION',
  56. REF_ANNOTATION: 'REF_ANNOTATION'
  57. }
  58. constructor(params, ws) {
  59. this.data = null;
  60. this.params = params;
  61. this.container = 'string' == typeof params.container ?
  62. document.querySelector(params.container) : params.container;
  63. if (!this.container) {
  64. throw Error('No container for ELAN');
  65. }
  66. }
  67. init() {
  68. this.bindClick();
  69. if (this.params.url) {
  70. this.load(this.params.url);
  71. }
  72. }
  73. destroy() {
  74. this.container.removeEventListener('click', this._onClick);
  75. this.container.removeChild(this.table);
  76. }
  77. load(url) {
  78. this.loadXML(url, xml => {
  79. this.data = this.parseElan(xml);
  80. this.render();
  81. this.fireEvent('ready', this.data);
  82. });
  83. }
  84. loadXML(url, callback) {
  85. const xhr = new XMLHttpRequest();
  86. xhr.open('GET', url, true);
  87. xhr.responseType = 'document';
  88. xhr.send();
  89. xhr.addEventListener('load', e => {
  90. callback && callback(e.target.responseXML);
  91. });
  92. }
  93. parseElan(xml) {
  94. const _forEach = Array.prototype.forEach;
  95. const _map = Array.prototype.map;
  96. const data = {
  97. media: {},
  98. timeOrder: {},
  99. tiers: [],
  100. annotations: {},
  101. alignableAnnotations: []
  102. };
  103. const header = xml.querySelector('HEADER');
  104. const inMilliseconds = header.getAttribute('TIME_UNITS') == 'milliseconds';
  105. const media = header.querySelector('MEDIA_DESCRIPTOR');
  106. data.media.url = media.getAttribute('MEDIA_URL');
  107. data.media.type = media.getAttribute('MIME_TYPE');
  108. const timeSlots = xml.querySelectorAll('TIME_ORDER TIME_SLOT');
  109. const timeOrder = {};
  110. _forEach.call(timeSlots, slot => {
  111. let value = parseFloat(slot.getAttribute('TIME_VALUE'));
  112. // If in milliseconds, convert to seconds with rounding
  113. if (inMilliseconds) {
  114. value = Math.round(value * 1e2) / 1e5;
  115. }
  116. timeOrder[slot.getAttribute('TIME_SLOT_ID')] = value;
  117. });
  118. data.tiers = _map.call(xml.querySelectorAll('TIER'), tier => ({
  119. id: tier.getAttribute('TIER_ID'),
  120. linguisticTypeRef: tier.getAttribute('LINGUISTIC_TYPE_REF'),
  121. defaultLocale: tier.getAttribute('DEFAULT_LOCALE'),
  122. annotations: _map.call(
  123. tier.querySelectorAll('REF_ANNOTATION, ALIGNABLE_ANNOTATION'), node => {
  124. const annot = {
  125. type: node.nodeName,
  126. id: node.getAttribute('ANNOTATION_ID'),
  127. ref: node.getAttribute('ANNOTATION_REF'),
  128. value: node.querySelector('ANNOTATION_VALUE')
  129. .textContent.trim()
  130. };
  131. if (this.Types.ALIGNABLE_ANNOTATION == annot.type) {
  132. // Add start & end to alignable annotation
  133. annot.start = timeOrder[node.getAttribute('TIME_SLOT_REF1')];
  134. annot.end = timeOrder[node.getAttribute('TIME_SLOT_REF2')];
  135. // Add to the list of alignable annotations
  136. data.alignableAnnotations.push(annot);
  137. }
  138. // Additionally, put into the flat map of all annotations
  139. data.annotations[annot.id] = annot;
  140. return annot;
  141. }
  142. )
  143. }));
  144. // Create JavaScript references between annotations
  145. data.tiers.forEach(tier => {
  146. tier.annotations.forEach(annot => {
  147. if (null != annot.ref) {
  148. annot.reference = data.annotations[annot.ref];
  149. }
  150. });
  151. });
  152. // Sort alignable annotations by start & end
  153. data.alignableAnnotations.sort((a, b) => {
  154. let d = a.start - b.start;
  155. if (d == 0) {
  156. d = b.end - a.end;
  157. }
  158. return d;
  159. });
  160. data.length = data.alignableAnnotations.length;
  161. return data;
  162. }
  163. render() {
  164. // apply tiers filter
  165. let tiers = this.data.tiers;
  166. if (this.params.tiers) {
  167. tiers = tiers.filter(tier => tier.id in this.params.tiers);
  168. }
  169. // denormalize references to alignable annotations
  170. const backRefs = {};
  171. let indeces = {};
  172. tiers.forEach((tier, index) => {
  173. tier.annotations.forEach(annot => {
  174. if (annot.reference && annot.reference.type == this.Types.ALIGNABLE_ANNOTATION) {
  175. if (!(annot.reference.id in backRefs)) {
  176. backRefs[annot.ref] = {};
  177. }
  178. backRefs[annot.ref][index] = annot;
  179. indeces[index] = true;
  180. }
  181. });
  182. });
  183. indeces = Object.keys(indeces).sort();
  184. this.renderedAlignable = this.data.alignableAnnotations.filter(alignable => backRefs[alignable.id]);
  185. // table
  186. const table = this.table = document.createElement('table');
  187. table.className = 'wavesurfer-annotations';
  188. // head
  189. const thead = document.createElement('thead');
  190. const headRow = document.createElement('tr');
  191. thead.appendChild(headRow);
  192. table.appendChild(thead);
  193. const th = document.createElement('th');
  194. th.textContent = 'Time';
  195. th.className = 'wavesurfer-time';
  196. headRow.appendChild(th);
  197. indeces.forEach(index => {
  198. const tier = tiers[index];
  199. const th = document.createElement('th');
  200. th.className = 'wavesurfer-tier-' + tier.id;
  201. th.textContent = tier.id;
  202. th.style.width = this.params.tiers[tier.id];
  203. headRow.appendChild(th);
  204. });
  205. // body
  206. const tbody = document.createElement('tbody');
  207. table.appendChild(tbody);
  208. this.renderedAlignable.forEach(alignable => {
  209. const row = document.createElement('tr');
  210. row.id = 'wavesurfer-alignable-' + alignable.id;
  211. tbody.appendChild(row);
  212. const td = document.createElement('td');
  213. td.className = 'wavesurfer-time';
  214. td.textContent = alignable.start.toFixed(1) + '–' +
  215. alignable.end.toFixed(1);
  216. row.appendChild(td);
  217. const backRef = backRefs[alignable.id];
  218. indeces.forEach(index => {
  219. const tier = tiers[index];
  220. const td = document.createElement('td');
  221. const annotation = backRef[index];
  222. if (annotation) {
  223. td.id = 'wavesurfer-annotation-' + annotation.id;
  224. td.dataset.ref = alignable.id;
  225. td.dataset.start = alignable.start;
  226. td.dataset.end = alignable.end;
  227. td.textContent = annotation.value;
  228. }
  229. td.className = 'wavesurfer-tier-' + tier.id;
  230. row.appendChild(td);
  231. });
  232. });
  233. this.container.innerHTML = '';
  234. this.container.appendChild(table);
  235. }
  236. bindClick() {
  237. this._onClick = e => {
  238. const ref = e.target.dataset.ref;
  239. if (null != ref) {
  240. const annot = this.data.annotations[ref];
  241. if (annot) {
  242. this.fireEvent('select', annot.start, annot.end);
  243. }
  244. }
  245. };
  246. this.container.addEventListener('click', this._onClick);
  247. }
  248. getRenderedAnnotation(time) {
  249. let result;
  250. this.renderedAlignable.some(annotation => {
  251. if (annotation.start <= time && annotation.end >= time) {
  252. result = annotation;
  253. return true;
  254. }
  255. return false;
  256. });
  257. return result;
  258. }
  259. getAnnotationNode(annotation) {
  260. return document.getElementById(
  261. 'wavesurfer-alignable-' + annotation.id
  262. );
  263. }
  264. }