aac-metrics 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bddb8e430a21868a5b26312fb5924f36b914446dc212d849330462768fd9b233
4
- data.tar.gz: c5ddd0e2b835678f0dabf8743099b29d462612d94a4aa4dd261e1cb7337eaa02
3
+ metadata.gz: 52fb3c41b9427605e9a78c258c5f4e5d7303c98f8988b52aba750597cb164900
4
+ data.tar.gz: 60ccc8a6c1dd2c25d29dd159134c7953e133a1f7a0bb9d710696807f853e8139
5
5
  SHA512:
6
- metadata.gz: 876e09d6be2b2e2b7579c11bb06367296ee7fbba627db0f8d421b4e60d2b7042d9a41433f9b1af10c78a6fc8ac84077170224e059a5a5ba30db6b217cab56936
7
- data.tar.gz: 5072598db0226690da77985e4e1ed760b24f0d0edc2bc99a5961e59d7f8e8bfa549073f4d0b223559ba48d7d2348556041d8b513944e5427c9c158db3b467f6c
6
+ metadata.gz: ac227f5770623d9df36635c92f71aa7a122404fa40f006dce31432be0bd17b6199c0c16ec6a4f5c777f19255106bf1c7f8811bfe4972996b9d12980a75b2911a
7
+ data.tar.gz: fd2ef145c944fea960a30282b6f3960f313d5f6f44d0db13f3a4581909744e127455010371d5b4138e546132a2e8d4cfb26ec291793f9ff83b970b54d47aeed9
data/README.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  A tool for analysing and comparing grid-based AAC board sets
4
4
 
5
+ To compare two datasets
6
+ `ruby setup.rb qc24 qc112`
7
+
8
+ To compare two datasets and export the minimal obfset for the first
9
+ `ruby setup.rb qc24 qc112 export`
10
+ `ruby setup.rb qc24 qc112 render` # updates interactive preview in sets/preview.html
11
+
12
+ To generate an obfset from an external data source
13
+ `ruby lib/ingester.rb path/to/manifest-from-unzipped-obz.json`
14
+ `ruby lib/ingester.rb path/to/compiled-set.obfset`
15
+ `ruby lib/ingester.rb path/to/root-file.obf`
16
+
17
+
18
+ NOTE: if you add an obfset to sets, it will be accessible
19
+ simply by its prefix. If you add ".common" to the filename
20
+ similar to the existing files, it will be added to the
21
+ corpus of "common" word sets.
22
+
5
23
  ## License
6
24
 
7
25
  Licensed under the MIT License.
@@ -20,6 +20,7 @@ module AACMetrics::Loader
20
20
  }
21
21
  if button['load_board'] && button['load_board']['id']
22
22
  new_button['load_board'] = {'id' => button['load_board']['id']}
23
+ new_button['load_board']['temporary_home'] = button['load_board']['temporary_home'] if button['load_board']['temporary_home'] == true || button['load_board']['temporary_home'] == 'prior'
23
24
  new_button['load_board']['add_to_sentence'] = true if button['load_board']['add_to_sentence']
24
25
  end
25
26
  new_board['buttons'].push(new_button)
@@ -73,11 +74,11 @@ module AACMetrics::Loader
73
74
  if json.is_a?(Array)
74
75
  json.each do |brd|
75
76
  brd['buttons'].each do |btn|
76
- if btn['label'].match(/^\$/)
77
+ if (btn['label'] || '').match(/^\$/)
77
78
  word = base[btn['label'].sub(/^\$/, '')]
78
79
  btn['label'] = word if word
79
80
  end
80
- btn['label'] = btn['label'].gsub(/’/, '')
81
+ btn['label'] = (btn['label'] || '').gsub(/’/, '')
81
82
  end
82
83
  end
83
84
  elsif json.is_a?(Hash)
@@ -159,6 +160,12 @@ module AACMetrics::Loader
159
160
  # record load_board reference
160
161
  btn_idx += 1
161
162
  if btn['load_board']
163
+ if !btn['load_board']['path'] && btn['load_board']['id']
164
+ bpath = File.join(File.dirname(path), btn['load_board']['id'] + '.obf')
165
+ if File.exists?(bpath)
166
+ btn['load_board']['path'] = bpath
167
+ end
168
+ end
162
169
  if btn['load_board']['path']
163
170
  if visited_paths[btn['load_board']['path']]
164
171
  new_btn['load_board'] = {'id' => "brd#{visited_paths[btn['load_board']['path']]}"}
@@ -176,8 +183,10 @@ module AACMetrics::Loader
176
183
  new_btn['load_board'] = {'tmp_path' => btn['load_board']['data_url']}
177
184
  end
178
185
  else
186
+ puts path
179
187
  puts "Link found with no access #{btn['load_board'].to_json}"
180
188
  end
189
+ new_btn['load_board']['temporary_home'] = true if new_btn['load_board'] && btn['load_board']['temporary_home']
181
190
  new_btn['load_board']['add_to_sentence'] = true if new_btn['load_board'] && btn['load_board']['add_to_sentence']
182
191
  elsif btn['action']
183
192
  # TODO: track keyboard actions and don't
@@ -30,14 +30,16 @@ module AACMetrics::Metrics
30
30
  # as the link used to get there if they share an id
31
31
  def self.analyze(obfset, output=true, include_obfset=false)
32
32
  locale = nil
33
- buttons = []
33
+ buttons = nil
34
34
  set_refs = {}
35
35
  grid = {}
36
+ alt_scores = {}
36
37
 
37
38
  if obfset.is_a?(Hash) && obfset['buttons']
38
39
  locale = obfset['locale'] || 'en'
39
40
  set_refs = obfset['reference_counts']
40
41
  grid = obfset['grid']
42
+ alt_scores = obfset['alternates']
41
43
  buttons = []
42
44
  obfset['buttons'].each do |btn|
43
45
  buttons << {
@@ -51,6 +53,7 @@ module AACMetrics::Metrics
51
53
  end
52
54
  total_boards = obfset['total_boards']
53
55
  else
56
+ start_boards = [obfset[0]]
54
57
  visited_board_ids = {}
55
58
  to_visit = [{board: obfset[0], level: 0, entry_x: 1.0, entry_y: 1.0}]
56
59
  set_refs = {}
@@ -87,6 +90,17 @@ module AACMetrics::Metrics
87
90
  set_refs[id] += 1
88
91
  end
89
92
  end
93
+ board['buttons'].each do |link_btn|
94
+ if link_btn['load_board'] && link_btn['load_board']['id'] && link_btn['load_board']['temporary_home']
95
+ # TODO: buttons can have multiple efforts, depending
96
+ # on if they are navigable from a temporary_home
97
+ if link_btn['load_board']['temporary_home'] == 'prior'
98
+ start_boards << board
99
+ elsif link_btn['load_board']['temporary_home'] == true
100
+ start_boards << obfset.detect{|b| b['id'] == link_btn['load_board']['id']}
101
+ end
102
+ end
103
+ end
90
104
  end
91
105
  # If the average grid size is much different than the root
92
106
  # grid size, only then use the average as the size for this board set
@@ -99,7 +113,52 @@ module AACMetrics::Metrics
99
113
  loc = id.split(/-/)[1]
100
114
  set_pcts[id] = cnt.to_f / (cell_refs[loc] || obfset.length).to_f
101
115
  end
102
- locale = obfset[0]['locale']
116
+
117
+ total_boards = nil
118
+ locale = nil
119
+ clusters = nil
120
+ # TODO: this list used to be reversed, but I don't know why.
121
+ # What we want is for these analyses to be run for the root
122
+ # board, don't we?
123
+ # puts JSON.pretty_generate(obfset[0])
124
+ start_boards.uniq.each do |brd|
125
+ analysis = analyze_for(obfset, brd, set_pcts, output)
126
+ buttons ||= analysis[:buttons]
127
+ if brd != obfset[0]
128
+ alt_scores[brd['id']] = {
129
+ buttons: analysis[:buttons],
130
+ levels: analysis[:levels]
131
+ }
132
+ end
133
+ total_boards ||= analysis[:total_boards]
134
+ clusters ||= analysis[:levels]
135
+ locale ||= analysis[:locale]
136
+ end
137
+ end
138
+ res = {
139
+ analysis_version: AACMetrics::VERSION,
140
+ locale: locale,
141
+ total_boards: total_boards,
142
+ total_buttons: buttons.length,
143
+ reference_counts: set_refs,
144
+ grid: {
145
+ rows: root_rows,
146
+ columns: root_cols
147
+ },
148
+ buttons: buttons,
149
+ levels: clusters,
150
+ alternates: alt_scores
151
+ }
152
+ if include_obfset
153
+ res[:obfset] = obfset
154
+ end
155
+ res
156
+ end
157
+
158
+ def self.analyze_for(obfset, brd, set_pcts, output)
159
+ visited_board_ids = {}
160
+ to_visit = [{board: brd, level: 0, entry_x: 1.0, entry_y: 1.0}]
161
+ locale = brd['locale'] || 'en'
103
162
  known_buttons = {}
104
163
  while to_visit.length > 0
105
164
  board = to_visit.shift
@@ -173,7 +232,7 @@ module AACMetrics::Metrics
173
232
  button_id = (board[:board]['grid']['order'][row_idx] || [])[col_idx]
174
233
  button = board[:board]['buttons'].detect{|b| b['id'] == button_id }
175
234
  # prior_buttons += 0.1 if !button
176
- next unless button
235
+ next unless button && (button['label'] || button['vocalization'] || '').length > 0
177
236
  x = (btn_width / 2) + (btn_width * col_idx)
178
237
  y = (btn_height / 2) + (btn_height * row_idx)
179
238
  # prior_buttons = (row_idx * board[:board]['grid']['columns']) + col_idx
@@ -271,10 +330,14 @@ module AACMetrics::Metrics
271
330
  next_board = obfset.detect{|brd| brd['id'] == button['load_board']['id'] }
272
331
  change_effort = BOARD_CHANGE_PROCESSING_EFFORT
273
332
  if next_board
333
+ temp_home_id = board[:temporary_home_id]
334
+ temp_home_id = board[:board]['id'] if button['load_board']['temporary_home'] == 'prior'
335
+ temp_home_id = button['load_board']['id'] if button['load_board']['temporary_home'] == true
274
336
  to_visit.push({
275
337
  board: next_board,
276
338
  level: board[:level] + 1,
277
339
  prior_effort: effort + change_effort,
340
+ temporary_home_id: temp_home_id,
278
341
  entry_x: x,
279
342
  entry_y: y,
280
343
  entry_clone_id: button['clone_id'],
@@ -286,29 +349,34 @@ module AACMetrics::Metrics
286
349
  if !button['load_board'] || button['load_board']['add_to_sentence']
287
350
  word = button['label']
288
351
  existing = known_buttons[word]
289
- if !existing || effort < existing[:effort] #board[:level] < existing[:level]
290
- if board_pcts[button['clone_id']]
291
- effort -= [BOARD_CHANGE_PROCESSING_EFFORT, BOARD_CHANGE_PROCESSING_EFFORT * 0.3 / board_pcts[button['clone_id']]].min
292
- elsif board_pcts[button['semantic_id']]
293
- effort -= [BOARD_CHANGE_PROCESSING_EFFORT, BOARD_CHANGE_PROCESSING_EFFORT * 0.5 / board_pcts[button['semantic_id']]].min
294
- end
295
-
296
- known_buttons[word] = {
352
+ if board_pcts[button['clone_id']]
353
+ effort -= [BOARD_CHANGE_PROCESSING_EFFORT, BOARD_CHANGE_PROCESSING_EFFORT * 0.3 / board_pcts[button['clone_id']]].min
354
+ elsif board_pcts[button['semantic_id']]
355
+ effort -= [BOARD_CHANGE_PROCESSING_EFFORT, BOARD_CHANGE_PROCESSING_EFFORT * 0.5 / board_pcts[button['semantic_id']]].min
356
+ end
357
+ if !existing || effort < existing[:effort]
358
+ ww = {
297
359
  id: "#{button['id']}::#{board[:board]['id']}",
298
360
  label: word,
299
361
  level: board[:level],
300
362
  effort: effort,
301
363
  }
364
+ # If a board set has any temporary_home links,
365
+ # then that can possibly affect the effort
366
+ # score for sentences
367
+ if board[:temporary_home_id]
368
+ ww[:temporary_home_id] = board[:temporary_home_id]
369
+ end
370
+ known_buttons[word] = ww
302
371
  end
303
372
  end
304
373
  button['effort'] = effort
305
-
306
374
  end
307
375
  end
308
- end
376
+ end # end to_visit list
309
377
  buttons = known_buttons.to_a.map(&:last)
310
378
  total_boards = visited_board_ids.keys.length
311
- end
379
+
312
380
  buttons = buttons.sort_by{|b| [b[:effort] || 1, b[:label] || ""] }
313
381
  clusters = {}
314
382
  buttons.each do |btn|
@@ -320,25 +388,38 @@ module AACMetrics::Metrics
320
388
  locale: locale,
321
389
  total_boards: total_boards,
322
390
  total_buttons: buttons.length,
323
- reference_counts: set_refs,
324
- grid: {
325
- rows: root_rows,
326
- columns: root_cols
327
- },
328
391
  buttons: buttons,
329
392
  levels: clusters
330
393
  }
331
- if include_obfset
332
- res[:obfset] = obfset
333
- end
334
394
  res
335
395
  end
336
396
 
397
+ class ExtraFloat < Numeric
398
+ include Math
399
+ def initialize(float=0.0)
400
+ @float = Float(float)
401
+ end
402
+
403
+ def to_f
404
+ @float.to_f
405
+ end
406
+
407
+ def method_missing(message, *args, &block)
408
+ if block_given?
409
+ @float.public_send(message, *args, &block)
410
+ else
411
+ @float.public_send(message, *args)
412
+ end
413
+ end
414
+ end
415
+
337
416
  SQRT2 = Math.sqrt(2)
338
417
  BUTTON_SIZE_MULTIPLIER = 0.09
339
418
  FIELD_SIZE_MULTIPLIER = 0.005
340
419
  VISUAL_SCAN_MULTIPLIER = 0.015
341
420
  BOARD_CHANGE_PROCESSING_EFFORT = 1.0
421
+ BOARD_HOME_EFFORT = 1.0
422
+ COMBINED_WORDS_REMEMBERING_EFFORT = 1.0
342
423
  DISTANCE_MULTIPLIER = 0.4
343
424
  DISTANCE_THRESHOLD_TO_SKIP_VISUAL_SCAN = 0.1
344
425
  SKIPPED_VISUAL_SCAN_DISTANCE_MULTIPLIER = 0.5
@@ -386,9 +467,21 @@ module AACMetrics::Metrics
386
467
  compare[:buttons].each do |btn|
387
468
  compare_words << btn[:label]
388
469
  compare_buttons[btn[:label]] = btn
389
- comp_efforts[btn[:label]] = btn[:effort]
470
+ comp_efforts[btn[:label]] = ExtraFloat.new(btn[:effort])
471
+ comp_efforts[btn[:label]].instance_variable_set('@temp_home_id', btn[:temporary_home_id])
390
472
  comp_levels[btn[:label]] = btn[:level]
391
473
  end
474
+ compare[:alternates].each do |id, alt|
475
+ efforts = {}
476
+ levels = {}
477
+ alt[:buttons].each do |btn|
478
+ efforts[btn[:label]] = ExtraFloat.new(btn[:effort])
479
+ efforts[btn[:label]].instance_variable_set('@temp_home_id', btn[:temporary_home_id])
480
+ levels[btn[:label]] = btn[:level]
481
+ end
482
+ comp_efforts["H:#{id}"] = efforts
483
+ comp_levels["H:#{id}"] = levels
484
+ end
392
485
 
393
486
  sortable_efforts = {}
394
487
  target_efforts = {}
@@ -400,7 +493,9 @@ module AACMetrics::Metrics
400
493
  # very frequent core words and use that when available
401
494
  res[:buttons].each{|b|
402
495
  target_words << b[:label]
403
- target_efforts[b[:label]] = b[:effort]
496
+ target_efforts[b[:label]] = ExtraFloat.new(b[:effort])
497
+ target_efforts[b[:label]].instance_variable_set('@temp_home_id', b[:temporary_home_id])
498
+
404
499
  target_levels[b[:label]] = b[:level]
405
500
  sortable_efforts[b[:label]] = b[:effort]
406
501
  comp = compare_buttons[b[:label]]
@@ -409,6 +504,18 @@ module AACMetrics::Metrics
409
504
  b[:comp_effort] = comp[:effort]
410
505
  end
411
506
  }
507
+ res[:alternates].each do |id, alt|
508
+ efforts = {}
509
+ levels = {}
510
+ alt[:buttons].each do |btn|
511
+ efforts[btn[:label]] = ExtraFloat.new(btn[:effort])
512
+ efforts[btn[:label]].instance_variable_set('@temp_home_id', btn[:temporary_home_id])
513
+ levels[btn[:label]] = btn[:level]
514
+ end
515
+ target_efforts["H:#{id}"] = efforts
516
+ target_levels["H:#{id}"] = levels
517
+ end
518
+ res.delete(:alternates)
412
519
  # Effort scores are the mean of thw scores from the
413
520
  # two sets, or just a singular value if in only one set
414
521
  compare[:buttons].each{|b|
@@ -441,7 +548,6 @@ module AACMetrics::Metrics
441
548
  end
442
549
  end
443
550
  end
444
-
445
551
 
446
552
  missing = (compare_words - target_words).sort_by{|w| sortable_efforts[w] }
447
553
  missing = missing.select do |word|
@@ -519,20 +625,11 @@ module AACMetrics::Metrics
519
625
  end
520
626
 
521
627
  # Calculate the effort for the target and comp sets
522
- effort = target_efforts[word]
523
- if !effort
524
- words.each{|w| effort ||= target_efforts[w] }
525
- end
526
- # Fallback penalty for missing word
527
- effort ||= spelling_effort(word)
628
+ effort, level = best_match(word, target_efforts, nil, synonyms)
528
629
  reffort = effort
529
630
  list_effort += effort
530
631
 
531
- effort = comp_efforts[word]
532
- if !effort
533
- words.each{|w| effort ||= comp_efforts[w] }
534
- end
535
- effort ||= spelling_effort(word)
632
+ effort, level = best_match(word, comp_efforts, nil, synonyms)
536
633
  comp_effort += effort
537
634
  # puts "#{word} - #{reffort.round(1)} - #{effort.round(1)}"
538
635
  end
@@ -552,50 +649,18 @@ module AACMetrics::Metrics
552
649
  res[:care_components][:comp_core] = (comp_effort_tally / core_lists.to_a.length) * 5.0
553
650
  comp_effort_tally = res[:care_components][:comp_core]
554
651
 
555
- # TODO: Assemble or allow a battery of word combinations,
652
+ # Assemble or allow a battery of word combinations,
556
653
  # and calculate the level of effort for each sequence,
557
654
  # as well as an average level of effort across combinations.
655
+ # TODO: sets with temporary_home settings will have custom
656
+ # effort scores for subsequent words in the sentence
558
657
  res[:sentences] = []
559
658
  sentences.each do |words|
560
- target_effort_score = 0.0
561
- comp_effort_score = 0.0
562
- words.each_with_index do |word, idx|
563
- synonym_words = [word] + (synonyms[word] || [])
564
- effort = target_efforts[word] || target_efforts[word.downcase]
565
- level = target_levels[word] || target_levels[word.downcase]
566
- if !effort
567
- synonym_words.each do |w|
568
- if !effort && target_efforts[w]
569
- effort = target_efforts[w]
570
- level = target_levels[w]
571
- end
572
- end
573
- end
574
- effort ||= spelling_effort(word)
575
- if level && level > 0 && idx > 0
576
- effort += BOARD_CHANGE_PROCESSING_EFFORT
577
- end
578
- ee = effort
579
- target_effort_score += effort
580
-
581
- effort = comp_efforts[word] || comp_efforts[word.downcase]
582
- level = comp_levels[word] || comp_levels[word.downcase]
583
- if !effort
584
- synonym_words.each do |w|
585
- if !effort && comp_efforts[w]
586
- effort = comp_efforts[w]
587
- level = comp_levels[w]
588
- end
589
- end
590
- end
591
- effort ||= spelling_effort(word)
592
- if level && level > 0 && idx > 0
593
- effort += BOARD_CHANGE_PROCESSING_EFFORT
594
- end
595
- comp_effort_score += effort
596
- end
597
- target_effort_score = target_effort_score / words.length
598
- comp_effort_score = comp_effort_score / words.length
659
+ sequence = best_combo(words, target_efforts, target_levels, synonyms)
660
+ target_effort_score = sequence.map{|w, e| e }.sum.to_f / words.length.to_f
661
+ sequence = best_combo(words, comp_efforts, comp_levels, synonyms)
662
+ comp_effort_score = sequence.map{|w, e| e }.sum.to_f / words.length.to_f
663
+
599
664
  res[:sentences] << {sentence: words.join(' '), words: words, effort: target_effort_score, comp_effort: comp_effort_score}
600
665
  end
601
666
  res[:care_components][:sentences] = res[:sentences].map{|s| s[:effort] }.sum.to_f / res[:sentences].length.to_f * 3.0
@@ -607,20 +672,11 @@ module AACMetrics::Metrics
607
672
  fringe.each do |word|
608
673
  target_effort_score = 0.0
609
674
  comp_effort_score = 0.0
610
- synonym_words = [word] + (synonyms[word] || [])
611
- effort = target_efforts[word] || target_efforts[word.downcase]
612
- if !effort
613
- synonym_words.each{|w| effort ||= target_efforts[w] }
614
- end
615
- # puts "!#{word}" unless effort
616
- effort ||= spelling_effort(word)
675
+
676
+ effort, level = best_match(word, target_efforts, nil, synonyms)
617
677
  target_effort_score += effort
618
678
 
619
- effort = comp_efforts[word] || comp_efforts[word.downcase]
620
- if !effort
621
- synonym_words.each{|w| effort ||= comp_efforts[w] }
622
- end
623
- effort ||= spelling_effort(word)
679
+ effort, level = best_match(word, comp_efforts, nil, synonyms)
624
680
  comp_effort_score += effort
625
681
  res[:fringe_words] << {word: word, effort: target_effort_score, comp_effort: comp_effort_score}
626
682
  end
@@ -633,19 +689,10 @@ module AACMetrics::Metrics
633
689
  common_fringe.each do |word|
634
690
  target_effort_score = 0.0
635
691
  comp_effort_score = 0.0
636
- synonym_words = [word] + (synonyms[word] || [])
637
- effort = target_efforts[word] || target_efforts[word.downcase]
638
- if !effort
639
- synonym_words.each{|w| effort ||= target_efforts[w] }
640
- end
641
- effort ||= spelling_effort(word)
692
+ effort, level = best_match(word, target_efforts, nil, synonyms)
642
693
  target_effort_score += effort
643
694
 
644
- effort = comp_efforts[word] || comp_efforts[word.downcase]
645
- if !effort
646
- synonym_words.each{|w| effort ||= comp_efforts[w] }
647
- end
648
- effort ||= spelling_effort(word)
695
+ effort, level = best_match(word, comp_efforts, nil, synonyms)
649
696
  comp_effort_score += effort
650
697
  res[:common_fringe_words] << {word: word, effort: target_effort_score, comp_effort: comp_effort_score}
651
698
  end
@@ -657,13 +704,6 @@ module AACMetrics::Metrics
657
704
  target_effort_tally += 70 # placeholder value for future added calculations
658
705
  comp_effort_tally += 70
659
706
 
660
- # puts target_effort_tally
661
- # puts comp_effort_tally
662
- # puts (target_effort_tally - comp_effort_tally).abs
663
- # puts JSON.pretty_generate(res[:care_components])
664
- # raise 'arf'
665
-
666
-
667
707
  res[:target_effort_score] = [0.0, 350.0 - target_effort_tally].max
668
708
  res[:comp_effort_score] = [0.0, 350.0 - comp_effort_tally].max
669
709
  # puts "CONSIDER MAKING EASIER"
@@ -674,4 +714,94 @@ module AACMetrics::Metrics
674
714
  # puts too_easy.join(' ')
675
715
  res
676
716
  end
717
+
718
+ # Find the effort for a word, its synonyms, or its spelling.
719
+ # Always returns a non-nil effort score
720
+ def self.best_match(word, target_efforts, target_levels, synonyms)
721
+ synonym_words = [word] + (synonyms[word] || [])
722
+ effort = target_efforts[word] || target_efforts[word.downcase]
723
+ target_levels ||= {}
724
+ level = target_levels[word] || target_levels[word.downcase]
725
+ if !effort
726
+ synonym_words.each do |w|
727
+ if !effort && target_efforts[w]
728
+ effort = target_efforts[w]
729
+ level = target_levels[w]
730
+ end
731
+ end
732
+ end
733
+ # Fallback penalty for missing word
734
+ fallback_effort = spelling_effort(word)
735
+ effort = fallback_effort if !effort || fallback_effort < effort
736
+
737
+ [effort, level || 0]
738
+ end
739
+
740
+ def self.best_combo(words, efforts, levels, synonyms)
741
+ options = [{next_idx: 0, list: []}]
742
+ words.length.times do |idx|
743
+ options.each do |option|
744
+ home_id = option[:temporary_home_id]
745
+ if option[:next_idx] == idx
746
+ combos = forward_combos(words, idx, efforts, levels)
747
+ if home_id
748
+ # Effort of hitting home button, and processing change, plus usual
749
+ combos.each{|c| c[:effort] += BOARD_HOME_EFFORT + BOARD_CHANGE_PROCESSING_EFFORT}
750
+ more_combos = forward_combos(words, idx, efforts["H:#{home_id}"] || {}, levels["H:#{home_id}"] || {})
751
+ more_combos.each{|c| c[:temporary_home_id] ||= home_id }
752
+ combos += more_combos
753
+ end
754
+ combos.each do |combo|
755
+ if idx > 0 && combo[:level] && combo[:level] > 0
756
+ combo[:effort] += BOARD_CHANGE_PROCESSING_EFFORT
757
+ end
758
+ options << {
759
+ next_idx: idx + combo[:size],
760
+ list: option[:list] + [[combo[:partial], combo[:effort]]],
761
+ temporary_home_id: combo[:temporary_home_id]
762
+ }
763
+ end
764
+ effort, level = best_match(words[idx], efforts, levels, synonyms)
765
+ option[:temporary_home_id] = effort.instance_variable_get('@temp_home_id')
766
+ effort += BOARD_CHANGE_PROCESSING_EFFORT if idx > 0 && level && level > 0
767
+ if home_id
768
+ effort += BOARD_HOME_EFFORT + BOARD_CHANGE_PROCESSING_EFFORT
769
+ other_effort, other_level = best_match(words[idx], efforts["H:#{home_id}"] || {}, levels["H:#{home_id}"] || {}, synonyms)
770
+ new_home_id = other_effort.instance_variable_get('@temp_home_id') || home_id
771
+ other_effort += BOARD_CHANGE_PROCESSING_EFFORT if idx > 0 && other_level && other_level > 0
772
+ other_list = option[:list] + [[words[idx], other_effort]]
773
+ options << {next_idx: idx + 1, list: other_list, temporary_home_id: new_home_id}
774
+ end
775
+ option[:list] << [words[idx], effort]
776
+ option[:next_idx] = idx + 1
777
+ end
778
+ end
779
+ end
780
+ options.sort_by{|o| o[:list].map{|w, e| e}.sum }.reverse[0][:list]
781
+ end
782
+
783
+ # Checks if any buttons will work for multiple words in a sentence
784
+ def self.forward_combos(words, idx, target_efforts, target_levels)
785
+ words_left = words.length - idx
786
+ combos = []
787
+ skip = 0
788
+ temp_home_id = nil
789
+ if words_left > 1
790
+ (words_left - 1).times do |minus|
791
+ partial = words[idx, words_left - minus].join(' ')
792
+ if target_efforts[partial] || target_efforts[partial.downcase]
793
+ effort = (target_efforts[partial] || target_efforts[partial.downcase]) + COMBINED_WORDS_REMEMBERING_EFFORT
794
+ level = target_levels[partial] || target_levels[partial.downcase]
795
+ combos << {
796
+ partial: partial,
797
+ effort: effort,
798
+ temporary_home_id: effort.instance_variable_get('@temp_home_id'),
799
+ level: level,
800
+ size: words_left - minus
801
+ }
802
+ end
803
+ end
804
+ end
805
+ combos
806
+ end
677
807
  end
data/lib/ingester.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # Imports an obf dataset for future access.
2
+
1
3
  lib_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
4
  $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
3
5