speech_rule_engine.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. import { AuditoryDescription, AuditoryList } from '../audio/auditory_description.js';
  2. import { Span } from '../audio/span.js';
  3. import { Debugger } from '../common/debugger.js';
  4. import * as DomUtil from '../common/dom_util.js';
  5. import { Engine } from '../common/engine.js';
  6. import * as EngineConst from '../common/engine_const.js';
  7. import { evalXPath, updateEvaluator } from '../common/xpath_util.js';
  8. import * as SpeechRules from '../speech_rules/speech_rules.js';
  9. import * as SpeechRuleStores from '../speech_rules/speech_rule_stores.js';
  10. import { BrailleStore, EuroStore } from './braille_store.js';
  11. import { Axis, DynamicCstr } from './dynamic_cstr.js';
  12. import { Grammar } from './grammar.js';
  13. import { MathStore } from './math_store.js';
  14. import { ActionType } from './speech_rule.js';
  15. import { Trie } from '../indexing/trie.js';
  16. export class SpeechRuleEngine {
  17. static getInstance() {
  18. SpeechRuleEngine.instance =
  19. SpeechRuleEngine.instance || new SpeechRuleEngine();
  20. return SpeechRuleEngine.instance;
  21. }
  22. static debugSpeechRule(rule, node) {
  23. const prec = rule.precondition;
  24. const queryResult = rule.context.applyQuery(node, prec.query);
  25. Debugger.getInstance().output(prec.query, queryResult ? queryResult.toString() : queryResult);
  26. prec.constraints.forEach((cstr) => Debugger.getInstance().output(cstr, rule.context.applyConstraint(node, cstr)));
  27. }
  28. static debugNamedSpeechRule(name, node) {
  29. const rules = SpeechRuleEngine.getInstance().trie.collectRules();
  30. const allRules = rules.filter((rule) => rule.name == name);
  31. for (let i = 0, rule; (rule = allRules[i]); i++) {
  32. Debugger.getInstance().output('Rule', name, 'DynamicCstr:', rule.dynamicCstr.toString(), 'number', i);
  33. SpeechRuleEngine.debugSpeechRule(rule, node);
  34. }
  35. }
  36. evaluateNode(node) {
  37. updateEvaluator(node);
  38. const timeIn = new Date().getTime();
  39. let result = [];
  40. try {
  41. result = this.evaluateNode_(node);
  42. }
  43. catch (err) {
  44. console.log(err);
  45. console.error('Something went wrong computing speech.');
  46. Debugger.getInstance().output(err);
  47. }
  48. const timeOut = new Date().getTime();
  49. Debugger.getInstance().output('Time:', timeOut - timeIn);
  50. return result;
  51. }
  52. toString() {
  53. const allRules = this.trie.collectRules();
  54. return allRules.map((rule) => rule.toString()).join('\n');
  55. }
  56. runInSetting(settings, callback) {
  57. const engine = Engine.getInstance();
  58. const save = {};
  59. for (const [key, val] of Object.entries(settings)) {
  60. save[key] = engine[key];
  61. engine[key] = val;
  62. }
  63. engine.setDynamicCstr();
  64. const result = callback();
  65. for (const [key, val] of Object.entries(save)) {
  66. engine[key] = val;
  67. }
  68. engine.setDynamicCstr();
  69. return result;
  70. }
  71. static addStore(set) {
  72. const store = storeFactory(set);
  73. if (store.kind !== 'abstract') {
  74. store
  75. .getSpeechRules()
  76. .forEach((x) => SpeechRuleEngine.getInstance().trie.addRule(x));
  77. }
  78. SpeechRuleEngine.getInstance().addEvaluator(store);
  79. }
  80. processGrammar(context, node, grammar) {
  81. const assignment = {};
  82. for (const [key, val] of Object.entries(grammar)) {
  83. assignment[key] =
  84. typeof val === 'string' ? context.constructString(node, val) : val;
  85. }
  86. Grammar.getInstance().pushState(assignment);
  87. }
  88. addEvaluator(store) {
  89. const fun = store.evaluateDefault.bind(store);
  90. const loc = this.evaluators_[store.locale];
  91. if (loc) {
  92. loc[store.modality] = fun;
  93. return;
  94. }
  95. const mod = {};
  96. mod[store.modality] = fun;
  97. this.evaluators_[store.locale] = mod;
  98. }
  99. getEvaluator(locale, modality) {
  100. const loc = this.evaluators_[locale] ||
  101. this.evaluators_[DynamicCstr.DEFAULT_VALUES[Axis.LOCALE]];
  102. return loc[modality] || loc[DynamicCstr.DEFAULT_VALUES[Axis.MODALITY]];
  103. }
  104. enumerate(opt_info) {
  105. return this.trie.enumerate(opt_info);
  106. }
  107. constructor() {
  108. this.trie = null;
  109. this.evaluators_ = {};
  110. this.trie = new Trie();
  111. }
  112. evaluateNode_(node) {
  113. if (!node) {
  114. return [];
  115. }
  116. this.updateConstraint_();
  117. let result = this.evaluateTree_(node);
  118. result = processAnnotations(result);
  119. return result;
  120. }
  121. evaluateTree_(node) {
  122. const engine = Engine.getInstance();
  123. let result;
  124. Debugger.getInstance().output(engine.mode !== EngineConst.Mode.HTTP ? node.toString() : node);
  125. Grammar.getInstance().setAttribute(node);
  126. const rule = this.lookupRule(node, engine.dynamicCstr);
  127. if (!rule) {
  128. if (engine.strict) {
  129. return [];
  130. }
  131. result = this.getEvaluator(engine.locale, engine.modality)(node);
  132. if (node.attributes) {
  133. this.addPersonality_(result, {}, false, node);
  134. }
  135. return result;
  136. }
  137. Debugger.getInstance().generateOutput(() => [
  138. 'Apply Rule:',
  139. rule.name,
  140. rule.dynamicCstr.toString(),
  141. engine.mode === EngineConst.Mode.HTTP
  142. ? DomUtil.serializeXml(node)
  143. : node.toString()
  144. ]);
  145. Grammar.getInstance().processSingles();
  146. const context = rule.context;
  147. const components = rule.action.components;
  148. result = [];
  149. for (let i = 0, component; (component = components[i]); i++) {
  150. let descrs = [];
  151. const content = component.content || '';
  152. const attributes = component.attributes || {};
  153. let multi = false;
  154. if (component.grammar) {
  155. this.processGrammar(context, node, component.grammar);
  156. }
  157. let saveEngine = null;
  158. if (attributes.engine) {
  159. saveEngine = Engine.getInstance().dynamicCstr.getComponents();
  160. const features = Object.assign({}, saveEngine, Grammar.parseInput(attributes.engine));
  161. Engine.getInstance().setDynamicCstr(features);
  162. this.updateConstraint_();
  163. }
  164. switch (component.type) {
  165. case ActionType.NODE:
  166. {
  167. const selected = context.applyQuery(node, content);
  168. if (selected) {
  169. descrs = this.evaluateTree_(selected);
  170. }
  171. }
  172. break;
  173. case ActionType.MULTI:
  174. {
  175. multi = true;
  176. const selects = context.applySelector(node, content);
  177. if (selects.length > 0) {
  178. descrs = this.evaluateNodeList_(context, selects, attributes['sepFunc'], context.constructString(node, attributes['separator']), attributes['ctxtFunc'], context.constructString(node, attributes['context']));
  179. }
  180. }
  181. break;
  182. case ActionType.TEXT:
  183. {
  184. const xpath = attributes['span'];
  185. let attrs = {};
  186. if (xpath) {
  187. const nodes = evalXPath(xpath, node);
  188. attrs = nodes.length
  189. ? Span.getAttributes(nodes[0])
  190. : { kind: xpath };
  191. }
  192. const str = context.constructSpan(node, content, attrs);
  193. descrs = str.map(function (span) {
  194. return AuditoryDescription.create({ text: span.speech, attributes: span.attributes }, { adjust: true });
  195. });
  196. }
  197. break;
  198. case ActionType.PERSONALITY:
  199. default:
  200. descrs = [AuditoryDescription.create({ text: content })];
  201. }
  202. if (descrs[0] && !multi) {
  203. if (attributes['context']) {
  204. descrs[0]['context'] =
  205. context.constructString(node, attributes['context']) +
  206. (descrs[0]['context'] || '');
  207. }
  208. if (attributes['annotation']) {
  209. descrs[0]['annotation'] = attributes['annotation'];
  210. }
  211. }
  212. this.addLayout(descrs, attributes, multi);
  213. if (component.grammar) {
  214. Grammar.getInstance().popState();
  215. }
  216. result = result.concat(this.addPersonality_(descrs, attributes, multi, node));
  217. if (saveEngine) {
  218. Engine.getInstance().setDynamicCstr(saveEngine);
  219. this.updateConstraint_();
  220. }
  221. }
  222. Grammar.getInstance().popState();
  223. return result;
  224. }
  225. evaluateNodeList_(context, nodes, sepFunc, sepStr, ctxtFunc, ctxtStr) {
  226. if (!nodes.length) {
  227. return [];
  228. }
  229. const sep = sepStr || '';
  230. const cont = ctxtStr || '';
  231. const cFunc = context.contextFunctions.lookup(ctxtFunc);
  232. const ctxtClosure = cFunc
  233. ? cFunc(nodes, cont)
  234. : function () {
  235. return cont;
  236. };
  237. const sFunc = context.contextFunctions.lookup(sepFunc);
  238. const sepClosure = sFunc
  239. ? sFunc(nodes, sep)
  240. : function () {
  241. return [
  242. AuditoryDescription.create({ text: sep }, { translate: true })
  243. ];
  244. };
  245. let result = [];
  246. for (let i = 0, node; (node = nodes[i]); i++) {
  247. const descrs = this.evaluateTree_(node);
  248. if (descrs.length > 0) {
  249. descrs[0]['context'] = ctxtClosure() + (descrs[0]['context'] || '');
  250. result = result.concat(descrs);
  251. if (i < nodes.length - 1) {
  252. const text = sepClosure();
  253. result = result.concat(text);
  254. }
  255. }
  256. }
  257. return result;
  258. }
  259. addLayout(descrs, props, _multi) {
  260. const layout = props.layout;
  261. if (!layout) {
  262. return;
  263. }
  264. if (layout.match(/^begin/)) {
  265. descrs.unshift(new AuditoryDescription({ text: '', layout: layout }));
  266. return;
  267. }
  268. if (layout.match(/^end/)) {
  269. descrs.push(new AuditoryDescription({ text: '', layout: layout }));
  270. return;
  271. }
  272. descrs.unshift(new AuditoryDescription({ text: '', layout: `begin${layout}` }));
  273. descrs.push(new AuditoryDescription({ text: '', layout: `end${layout}` }));
  274. }
  275. addPersonality_(descrs, props, multi, node) {
  276. const personality = {};
  277. let pause = null;
  278. for (const key of EngineConst.personalityPropList) {
  279. const value = props[key];
  280. if (typeof value === 'undefined') {
  281. continue;
  282. }
  283. const numeral = parseFloat(value);
  284. const realValue = isNaN(numeral)
  285. ? value.charAt(0) === '"'
  286. ? value.slice(1, -1)
  287. : value
  288. : numeral;
  289. if (key === EngineConst.personalityProps.PAUSE) {
  290. pause = realValue;
  291. }
  292. else {
  293. personality[key] = realValue;
  294. }
  295. }
  296. for (let i = 0, descr; (descr = descrs[i]); i++) {
  297. this.addRelativePersonality_(descr, personality);
  298. this.addExternalAttributes_(descr, node);
  299. }
  300. if (multi && descrs.length) {
  301. delete descrs[descrs.length - 1].personality[EngineConst.personalityProps.JOIN];
  302. }
  303. if (pause && descrs.length) {
  304. const last = descrs[descrs.length - 1];
  305. if (last.text || Object.keys(last.personality).length) {
  306. descrs.push(AuditoryDescription.create({
  307. text: '',
  308. personality: { pause: pause }
  309. }));
  310. }
  311. else {
  312. last.personality[EngineConst.personalityProps.PAUSE] = pause;
  313. }
  314. }
  315. return descrs;
  316. }
  317. addExternalAttributes_(descr, node) {
  318. if (descr.attributes['id'] === undefined) {
  319. descr.attributes['id'] = node.getAttribute('id');
  320. }
  321. if (node.hasAttributes()) {
  322. const attrs = node.attributes;
  323. for (let i = attrs.length - 1; i >= 0; i--) {
  324. const key = attrs[i].name;
  325. if (!descr.attributes[key] && key.match(/^ext/)) {
  326. descr.attributes[key] = attrs[i].value;
  327. }
  328. }
  329. }
  330. }
  331. addRelativePersonality_(descr, personality) {
  332. if (!descr['personality']) {
  333. descr['personality'] = personality;
  334. return descr;
  335. }
  336. const descrPersonality = descr['personality'];
  337. for (const [key, val] of Object.entries(personality)) {
  338. if (descrPersonality[key] &&
  339. typeof descrPersonality[key] == 'number' &&
  340. typeof val == 'number') {
  341. descrPersonality[key] = (descrPersonality[key] + val).toString();
  342. }
  343. else if (!descrPersonality[key]) {
  344. descrPersonality[key] = val;
  345. }
  346. }
  347. return descr;
  348. }
  349. updateConstraint_() {
  350. const dynamic = Engine.getInstance().dynamicCstr;
  351. const strict = Engine.getInstance().strict;
  352. const trie = this.trie;
  353. const props = {};
  354. let locale = dynamic.getValue(Axis.LOCALE);
  355. let modality = dynamic.getValue(Axis.MODALITY);
  356. let domain = dynamic.getValue(Axis.DOMAIN);
  357. if (!trie.hasSubtrie([locale, modality, domain])) {
  358. domain = DynamicCstr.DEFAULT_VALUES[Axis.DOMAIN];
  359. if (!trie.hasSubtrie([locale, modality, domain])) {
  360. modality = DynamicCstr.DEFAULT_VALUES[Axis.MODALITY];
  361. if (!trie.hasSubtrie([locale, modality, domain])) {
  362. locale = DynamicCstr.DEFAULT_VALUES[Axis.LOCALE];
  363. }
  364. }
  365. }
  366. props[Axis.LOCALE] = [locale];
  367. props[Axis.MODALITY] = [
  368. modality !== 'summary'
  369. ? modality
  370. : DynamicCstr.DEFAULT_VALUES[Axis.MODALITY]
  371. ];
  372. props[Axis.DOMAIN] = [
  373. modality !== 'speech' ? DynamicCstr.DEFAULT_VALUES[Axis.DOMAIN] : domain
  374. ];
  375. const order = dynamic.getOrder();
  376. for (let i = 0, axis; (axis = order[i]); i++) {
  377. if (!props[axis]) {
  378. const value = dynamic.getValue(axis);
  379. const valueSet = this.makeSet_(value, dynamic.preference);
  380. const def = DynamicCstr.DEFAULT_VALUES[axis];
  381. if (!strict && value !== def) {
  382. valueSet.push(def);
  383. }
  384. props[axis] = valueSet;
  385. }
  386. }
  387. dynamic.updateProperties(props);
  388. }
  389. makeSet_(value, preferences) {
  390. if (!preferences || !Object.keys(preferences).length) {
  391. return [value];
  392. }
  393. return value.split(':');
  394. }
  395. lookupRule(node, dynamic) {
  396. if (!node ||
  397. (node.nodeType !== DomUtil.NodeType.ELEMENT_NODE &&
  398. node.nodeType !== DomUtil.NodeType.TEXT_NODE)) {
  399. return null;
  400. }
  401. const matchingRules = this.lookupRules(node, dynamic);
  402. return matchingRules.length > 0
  403. ? this.pickMostConstraint_(dynamic, matchingRules)
  404. : null;
  405. }
  406. lookupRules(node, dynamic) {
  407. return this.trie.lookupRules(node, dynamic.allProperties());
  408. }
  409. pickMostConstraint_(_dynamic, rules) {
  410. const comparator = Engine.getInstance().comparator;
  411. rules.sort(function (r1, r2) {
  412. return (comparator.compare(r1.dynamicCstr, r2.dynamicCstr) ||
  413. r2.precondition.priority - r1.precondition.priority ||
  414. r2.precondition.constraints.length -
  415. r1.precondition.constraints.length ||
  416. r2.precondition.rank - r1.precondition.rank);
  417. });
  418. Debugger.getInstance().generateOutput((() => {
  419. return rules.map((x) => x.name + '(' + x.dynamicCstr.toString() + ')');
  420. }).bind(this));
  421. return rules[0];
  422. }
  423. }
  424. const stores = new Map();
  425. function getStore(locale, modality) {
  426. if (modality === 'braille' && locale === 'euro') {
  427. return new EuroStore();
  428. }
  429. if (modality === 'braille') {
  430. return new BrailleStore();
  431. }
  432. return new MathStore();
  433. }
  434. function storeFactory(set) {
  435. const name = `${set.locale}.${set.modality}.${set.domain}`;
  436. if (set.kind === 'actions') {
  437. const store = stores.get(name);
  438. store.parse(set);
  439. return store;
  440. }
  441. SpeechRuleStores.init();
  442. if (set && !set.functions) {
  443. set.functions = SpeechRules.getStore(set.locale, set.modality, set.domain);
  444. }
  445. const store = getStore(set.locale, set.modality);
  446. stores.set(name, store);
  447. if (set.inherits) {
  448. store.inherits = stores.get(`${set.inherits}.${set.modality}.${set.domain}`);
  449. }
  450. store.parse(set);
  451. store.initialize();
  452. return store;
  453. }
  454. Engine.nodeEvaluator = SpeechRuleEngine.getInstance().evaluateNode.bind(SpeechRuleEngine.getInstance());
  455. const punctuationMarks = ['⠆', '⠒', '⠲', '⠦', '⠴', '⠄'];
  456. function processAnnotations(descrs) {
  457. const alist = new AuditoryList(descrs);
  458. for (const item of alist.annotations) {
  459. const descr = item.data;
  460. if (descr.annotation === 'punctuation') {
  461. const prev = alist.prevText(item);
  462. if (!prev)
  463. continue;
  464. const last = prev.data;
  465. if (last.annotation !== 'punctuation' &&
  466. last.text !== '⠀' &&
  467. descr.text.length === 1 &&
  468. punctuationMarks.indexOf(descr.text) !== -1) {
  469. descr.text = '⠸' + descr.text;
  470. }
  471. }
  472. }
  473. return alist.toList();
  474. }