├── correct.mp3 ├── favicon.ico ├── audio ├── G. N. Trenité - The Chaos.mp3 └── advanced-pronunciation-fun-english.mp3 ├── README.md ├── index.html ├── homophones.txt ├── index.css ├── index.js ├── diff_match_patch.js └── texts.txt /correct.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/little-brother/english-speech/HEAD/correct.mp3 -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/little-brother/english-speech/HEAD/favicon.ico -------------------------------------------------------------------------------- /audio/G. N. Trenité - The Chaos.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/little-brother/english-speech/HEAD/audio/G. N. Trenité - The Chaos.mp3 -------------------------------------------------------------------------------- /audio/advanced-pronunciation-fun-english.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/little-brother/english-speech/HEAD/audio/advanced-pronunciation-fun-english.mp3 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is an [application](https://little-brother.github.io/english-speech/) to the practice of English speech via Google Speech Recognition API.
2 | 3 | 4 | If app don't work properly, e.g. you don't listen any sound or you see permanent "Loading..." instead "Start"-button on first screen, it's means that your browser is obsolete. 5 | Use Chrome to avoid troubles.
6 | Minimal supported Android version is 4.1 with Chrome 69.
7 | 8 | If you have any questions, comments or suggestions, just let me know lb.im@yandex.ru. -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |
Powered by
24 | 25 |
Loading...
26 |
Allow access to microphone...
27 |
Start
28 |
29 |
30 | 31 | 32 |
33 |
34 |
35 |
Help
36 |
Option
37 |
38 | 39 |
40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 | 48 |
49 |
50 |
51 | 52 |
53 |
 
54 |
55 |
56 |
Listen
57 |
Record
58 |
59 |
60 |
61 |
62 | 63 |
64 |
65 |
New
66 |
67 |
68 | 69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | 77 |
78 |
79 |
Save
80 |
81 |
82 | 83 |
84 |
85 | 86 |
87 |
File...
88 |
89 | 90 |
91 |
92 | 93 | 94 |
95 | 96 |
97 |
98 |
99 |
100 |
101 | 102 |
103 |
104 |
105 | 106 |
107 |
108 |
Option
109 |
110 |
111 | 112 |
113 |
114 |
No
115 |
Yes
116 |
117 | 118 |
119 |
No
120 |
Yes
121 |
122 | 123 |
124 |
No
125 |
Yes
126 |
127 | 128 |
129 |
130 | 131 |
132 |
0.7
133 |
0.8
134 |
0.9
135 |
1
136 |
1.1
137 |
138 |
139 |
140 | 141 |
142 |
143 |
Help
144 |
145 |
146 |
147 |

This is an application to the practice of English speech via Google Speech Recognition API: 148 |

153 |

154 |

155 | The phrases are grouped in text: one line is one phrase.
156 | You can choose a predefined text or create your own by click on a left icon in a header on the main screen. 157 |

158 |

Keep in mind
159 | Recognition does not always work well.
160 | Some phrases can be recognized incorrectly even with perfect pronunciation.
161 |

162 | 163 |

164 | Recommendation
165 | Use a high-quality microphone to improve recognition. 166 |

167 | 168 |

If you have any questions, comments or suggestions, just let me know lb.im@yandex.ru or visit project page.

169 | 170 |

171 |

See also
172 | 177 |

178 |
179 |
180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /homophones.txt: -------------------------------------------------------------------------------- 1 | accessary;accessory 2 | ad;add 3 | ail;ale 4 | air;heir 5 | aisle;I'll;isle 6 | all;awl 7 | allowed;aloud 8 | alms;arms 9 | altar;alter 10 | arc;ark 11 | aren't;aunt 12 | ate;eight 13 | auger;augur 14 | auk;orc 15 | aural;oral 16 | away;aweigh 17 | awe;oar;or;ore 18 | axel;axle 19 | aye;eye;I 20 | bail;bale 21 | bait;bate 22 | baize;bays 23 | bald;bawled 24 | ball;bawl 25 | band;banned 26 | bard;barred 27 | bare;bear 28 | bark;barque 29 | baron;barren 30 | base;bass 31 | bay;bey 32 | bazaar;bizarre 33 | be;bee 34 | beach;beech 35 | bean;been 36 | beat;beet 37 | beau;bow 38 | beer;bier 39 | bel;bell;belle 40 | berry;bury 41 | berth;birth 42 | bight;bite;byte 43 | billed;build 44 | bitten;bittern 45 | blew;blue 46 | bloc;block 47 | boar;bore 48 | board;bored 49 | boarder;border 50 | bold;bowled 51 | boos;booze 52 | born;borne 53 | bough;bow 54 | boy;buoy 55 | brae;bray 56 | braid;brayed 57 | braise;brays;braze 58 | brake;break 59 | bread;bred 60 | brews;bruise 61 | bridal;bridle 62 | broach;brooch 63 | bur;burr 64 | but;butt 65 | buy;by;bye 66 | buyer;byre 67 | calendar;calender 68 | call;caul 69 | canvas;canvass 70 | cast;caste 71 | caster;castor 72 | caught;court 73 | caw;core;corps 74 | cede;seed 75 | ceiling;sealing 76 | cell;sell 77 | cells;sells 78 | censer;censor;sensor 79 | cent;scent;sent 80 | cereal;serial 81 | cheap;cheep 82 | check;cheque 83 | choir;quire 84 | chord;cord 85 | chews;choose 86 | cite;sight;site 87 | clack;claque 88 | clew;clue 89 | climb;clime 90 | close;cloze 91 | coal;kohl 92 | coarse;course 93 | coign;coin 94 | colonel;kernel 95 | complacent;complaisant 96 | complement;compliment 97 | coo;coup 98 | cops;copse 99 | council;counsel 100 | cousin;cozen 101 | creak;creek 102 | crews;cruise 103 | cue;kyu;queue 104 | curb;kerb 105 | currant;current 106 | cymbol;symbol 107 | dam;damn 108 | days;daze 109 | dear;deer 110 | descent;dissent 111 | desert;dessert 112 | deviser;divisor 113 | dew;due 114 | die;dye 115 | discreet;discrete 116 | doe;doh;dough 117 | done;dun 118 | douse;dowse 119 | draft;draught 120 | dual;duel 121 | earn;urn 122 | eery;eyrie 123 | energised;energized 124 | everyday;every day 125 | ewe;yew;you 126 | faint;feint 127 | fah;far 128 | fair;fare 129 | farther;father 130 | fate;fête 131 | faun;fawn 132 | fay;fey 133 | faze;phase 134 | feat;feet 135 | ferrule;ferule 136 | few;phew 137 | fie;phi 138 | file;phial 139 | find;fined 140 | fir;fur 141 | fizz;phiz 142 | flair;flare 143 | flaw;floor 144 | flea;flee 145 | flex;flecks 146 | flew;flu;flue 147 | floe;flow 148 | flour;flower 149 | foaled;fold 150 | for;fore;four 151 | foreword;forward 152 | fort;fought 153 | forth;fourth 154 | foul;fowl 155 | franc;frank 156 | freeze;frieze 157 | friar;fryer 158 | furs;furze 159 | gait;gate 160 | galipot;gallipot 161 | gallop;galop 162 | gamble;gambol 163 | gays;gaze 164 | genes;jeans 165 | gild;guild 166 | gilt;guilt 167 | giro;gyro 168 | gnaw;nor 169 | gneiss;nice 170 | gorilla;guerilla 171 | grate;great 172 | greave;grieve 173 | greys;graze 174 | grisly;grizzly 175 | groan;grown 176 | guessed;guest 177 | hail;hale 178 | hair;hare 179 | hall;haul 180 | hangar;hanger 181 | hart;heart 182 | haw;hoar;whore 183 | hay;hey 184 | heal;heel;he'll 185 | hear;here 186 | heard;herd 187 | he'd;heed 188 | heroin;heroine 189 | hew;hue 190 | hi;high 191 | hiccough;hiccup 192 | higher;hire 193 | him;hymn 194 | ho;hoe 195 | hoard;horde 196 | hoarse;horse 197 | hole;whole 198 | holey;holy;wholly 199 | hour;our 200 | idle;idol 201 | in;inn 202 | indict;indite 203 | it's;its 204 | jewel;joule 205 | key;quay 206 | knave;nave 207 | knead;need 208 | knew;new 209 | knight;night 210 | knit;nit 211 | knob;nob 212 | knock;nock 213 | knot;not 214 | know;no 215 | knows;nose 216 | laager;lager 217 | lac;lack 218 | lade;laid 219 | lain;lane 220 | lam;lamb 221 | laps;lapse 222 | larva;lava 223 | lase;laze 224 | law;lore 225 | lay;ley 226 | lea;lee 227 | leach;leech 228 | lead;led 229 | leak;leek 230 | lean;lien 231 | lessen;lesson 232 | learned;learnt 233 | levee;levy 234 | liar;lyre 235 | licence;license 236 | licker;liquor 237 | lie;lye 238 | lieu;loo 239 | links;lynx 240 | lo;low 241 | load;lode 242 | loan;lone 243 | locks;lox 244 | loop;loupe 245 | loot;lute 246 | made;maid 247 | mail;male 248 | main;mane 249 | maize;maze 250 | mall;maul 251 | manna;manner 252 | mantel;mantle 253 | mare;mayor 254 | mark;marque 255 | marshal;martial 256 | marten;martin 257 | mask;masque 258 | maw;more 259 | me;mi 260 | mean;mien 261 | meat;meet;mete 262 | medal;meddle 263 | metal;mettle 264 | meter;metre 265 | might;mite 266 | miner;minor;mynah 267 | mind;mined 268 | missed;mist 269 | moat;mote 270 | mode;mowed 271 | moor;more 272 | moose;mousse 273 | morning;mourning 274 | muscle;mussel 275 | naval;navel 276 | nay;neigh 277 | nigh;nye 278 | none;nun 279 | od;odd 280 | ode;owed 281 | o;oh;owe 282 | one;won 283 | packed;pact 284 | packs;pax 285 | pail;pale 286 | pain;pane 287 | pair;pare;pear 288 | palate;palette;pallet 289 | pascal;paschal 290 | paten;patten;pattern 291 | pause;paws;pores;pours 292 | pawn;porn 293 | pea;pee 294 | peace;piece 295 | peak;peek;peke;pique 296 | peal;peel 297 | pearl;purl 298 | pedal;peddle 299 | peer;pier 300 | pi;pie 301 | pica;pika 302 | place;plaice 303 | plain;plane 304 | pleas;please 305 | plum;plumb 306 | pole;poll 307 | poof;pouffe 308 | practice;practise 309 | practiced;practised 310 | praise;prays;preys 311 | principal;principle 312 | profit;prophet 313 | quarts;quartz 314 | quean;queen 315 | rain;reign;rein 316 | raise;rays;raze 317 | rap;wrap 318 | raw;roar 319 | read;reed 320 | read;red 321 | real;reel 322 | reek;wreak 323 | rest;wrest 324 | retch;wretch 325 | review;revue 326 | rheum;room 327 | right;rite;wright;write 328 | ring;wring 329 | road;rode 330 | roe;row 331 | role;roll 332 | roo;roux;rue 333 | rood;rude 334 | root;route 335 | rose;rows 336 | rota;rotor 337 | rote;wrote 338 | rough;ruff 339 | rouse;rows 340 | rung;wrung 341 | rye;wry 342 | saver;savour 343 | spade;spayed 344 | sale;sail 345 | sane;seine 346 | satire;satyr 347 | sauce;source 348 | saw;soar;sore 349 | scene;seen 350 | scull;skull 351 | sea;see 352 | seam;seem 353 | sear;seer;sere 354 | seas;sees;seize 355 | sew;so;sow 356 | shake;sheikh 357 | shear;sheer 358 | shoe;shoo 359 | show;shew 360 | sic;sick 361 | side;sighed 362 | sign;sine 363 | sink;synch 364 | slay;sleigh 365 | sloe;slow 366 | sole;soul 367 | some;sum 368 | son;sun 369 | sort;sought 370 | spa;spar 371 | staid;stayed 372 | stair;stare 373 | stake;steak 374 | stalk;stork 375 | stationary;stationery 376 | steal;steel 377 | stile;style 378 | storey;story 379 | straight;strait 380 | susie;suzy 381 | sweet;suite 382 | swat;swot 383 | tacks;tax 384 | tale;tail 385 | talk;torque 386 | tare;tear 387 | taught;taut;tort 388 | te;tea;tee 389 | team;teem 390 | tear;tier 391 | teas;tease 392 | terce;terse 393 | tern;turn 394 | there;their;they're 395 | threw;through 396 | throes;throws 397 | throne;thrown 398 | thyme;time 399 | tic;tick 400 | tide;tied 401 | tire;tyre 402 | to;too;two 403 | toad;toed;towed 404 | told;tolled 405 | tole;toll 406 | ton;tun 407 | tor;tore 408 | tough;tuff 409 | troop;troupe 410 | tuba;tuber 411 | vain;vane;vein 412 | vale;veil 413 | vial;vile 414 | wail;wale;whale 415 | wain;wane 416 | waist;waste 417 | wait;weight 418 | waive;wave 419 | wall;waul 420 | war;wore 421 | ware;wear;where 422 | warn;worn 423 | wart;wort 424 | watt;what 425 | wax;whacks 426 | way;weigh;whey 427 | we;wee;whee 428 | weak;week 429 | we'd;weed 430 | weal;we'll;wheel 431 | wean;ween 432 | weather;whether 433 | weaver;weever 434 | weir;we're 435 | were;whirr 436 | wet;whet 437 | wheald;wheeled 438 | which;witch 439 | whig;wig 440 | while;wile 441 | whine;wine 442 | whirl;whorl 443 | whirled;world 444 | whit;wit 445 | white;wight 446 | who's;whose 447 | woe;whoa 448 | wood;would 449 | yaw;yore;your;you're 450 | yoke;yolk 451 | you'll;yule -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | * {margin: 0; padding: 0;} 2 | html, body { padding: 0; margin: 0; height: 100%;} 3 | input, textarea {font-size: 24px;} 4 | 5 | .page {font-size: 24px;} 6 | .page:not([current]) {display: none !important;} 7 | .page.center {height: 100%; display: grid; } 8 | .page.center .content {margin: auto; position: relative;} 9 | .page.absolute {position: absolute; top: 0; bottom: 0; left: 0; right: 0;} 10 | .page.absolute .content {margin: auto; position: absolute; top: 48px; right: 0; left: 0; bottom: 0; overflow: auto;} 11 | 12 | .page .caption {position: fixed; text-align: center; right: 0; left: 0; color: #fff; background: #aaf; font-weight: bold; padding: 10px; text-transform: uppercase; z-index: 10;} 13 | .page .caption > * {display: inline-block;} 14 | .page .caption .left {float: left; cursor: pointer;} 15 | 16 | .page .content-padding {padding: 20px;} 17 | 18 | #page-start .content {top: -20px; text-align: center;} 19 | #page-start #logo {text-decoration: none; color: #000; width: 200px; display: inline-block; margin: 0; font-size: 20px; vertical-align: top; margin-top: 15px; margin-bottom: 15px;} 20 | #page-start #logo::before {content: ''; display: inline-block; background-size: contain; background-position: center; background-repeat: no-repeat; background-image: url(https://www.google.com/chrome/static/images/chrome-logo.svg); width: 160px; height: 160px; } 21 | #page-start #loading {color: #bbb; padding: 10px 20px; margin-top: 10px; position: relative; left: 10px;} 22 | #page-start #loading ~ #button-start {display: none !important;} 23 | #page-start #loading ~ #microphone {display: none !important;} 24 | #page-start #microphone ~ #button-start {display: none;} 25 | #page-start #microphone {text-align: center;} 26 | #page-start #button-start {padding: 10px 20px; margin-top: 10px; text-align: center; cursor: pointer; border-radius: 5px; background: #0f0;} 27 | 28 | #page-main #button-text-selector {display: inline-block; float: left; cursor: pointer; margin-left: 5px; position: relative; height: 28px; width: 30px; top: 1px;} 29 | #page-main #button-text-selector::before {content: ""; position: absolute; left: 0; width: 30px; height: 5px; background: #fff; box-shadow: 0 10px 0 0 #fff, 0 20px 0 0 #fff;} 30 | 31 | #page-main .content {margin: 10px; text-align: center;} 32 | #page-main #panel-counter {display: block; z-index: 1; text-align: center;} 33 | #page-main #panel-counter #phrase-number-input {display: none; width: 100px; text-align: center; background: #fff;} 34 | #page-main #panel-counter #phrase-number-input:focus ~ #phrase-number {display: none;} 35 | #page-main #panel-counter .button-arrow {cursor: pointer;} 36 | #page-main #panel-counter > * {display: inline-block; font-size: 56px;} 37 | #page-main #panel-counter #caption {display: block; font-size: 14px;} 38 | #page-main #panel-phrase { margin-bottom: 10px; position: absolute; left: 0; right: 0; bottom: 50%;} 39 | #page-main #panel-phrase .button {padding: 0px; margin: 0 5px; font-size: 36px; color: #bbb;} 40 | #page-main #panel-phrase .button[current="true"] {color: #0b0;} 41 | #page-main #panel-phrase #phrase {border-bottom: 1px solid #bbb; padding: 10px;} 42 | #page-main #panel-phrase #phrase > * { -webkit-touch-callout:none; -webkit-user-select:none; -khtml-user-select:none; -moz-user-select:none; -ms-user-select:none; user-select:none; -webkit-tap-highlight-color:rgba(0,0,0,0);} 43 | #page-main #panel-phrase #phrase > *[current] {box-shadow: 0 1px 0px #ccc;} 44 | #page-main #panel-recognition {position: absolute; left: 0; right: 0; top: 50%; bottom: 0;} 45 | #page-main #panel-recognition #recognition {color: #b00;} 46 | #page-main #panel-recognition #recognition[confidence]:not([confidence="0%"])::after {content: attr(confidence); position: relative; bottom: -10px; font-size: 10px;} 47 | #page-main #panel-recognition #recognition[correct="true"] {color: #0b0;} 48 | #page-main #panel-recognition[mode="recognition"] #compare {display: none;} 49 | #page-main #panel-recognition[mode="compare"] #recognition {display: none;} 50 | 51 | #page-main #panel-recognition #panel-record {position: absolute; bottom: 0; width: 100%;} 52 | #page-main #panel-recognition #panel-record #button-record {background: #f00; color: #fff;} 53 | #page-main #panel-recognition #panel-record #button-record[record="true"] {background: #f99;} 54 | #page-main #panel-recognition #panel-record #button-listen {background: #66f; color: #fff;} 55 | #page-main #panel-recognition #panel-record #button-listen[hidden] {display: none;} 56 | 57 | #page-text-selector .content > * {vertical-align: top; height: 100%; overflow-y: auto;} 58 | #page-text-selector #texts {display: inline-block; float: left; width: 350px; border-right: 1px solid #bbb;} 59 | #page-text-selector #texts > div {padding: 10px; border-bottom: 1px dashed #bbb; cursor: pointer; position: relative;} 60 | #page-text-selector #texts > div[current] {background: #eef;} 61 | #page-text-selector #texts #view {display: none; color: #bbb;} 62 | #page-text-selector #texts #remove {position: absolute; top: 5px; right: 15px; z-index: 2; color: #bbb;} 63 | #page-text-selector #text-wrapper { width: auto; display: block; height: auto; max-height: 100%; min-height: 100%;} 64 | #page-text-selector #text {white-space: pre; height: 100%; margin: 10px;} 65 | #page-text-selector #text audio {display: block; margin-bottom: 10px;} 66 | @media only screen and (max-width: 700px) { 67 | #page-text-selector #texts {width: 100% !important;} 68 | #page-text-selector #texts #view {display: inline-block !important; padding: 0 10px;} 69 | #page-text-selector #text-wrapper {display: none !important;} 70 | } 71 | 72 | #page-text-add #caption-block {margin-right: 80px;} 73 | #page-text-add #caption {width: 100%; border: none; text-align: left;} 74 | #page-text-add #text-block {position: absolute; top: 60px; left: 20px; right: 20px; bottom: 28px;} 75 | #page-text-add #text {width: 100%; height: 100%; resize: none; border: none; border-top: 1px solid #bbb; padding: 10px 0; font-family: inherit;} 76 | #page-text-add #button-load-file {position: absolute; top: 20px; right: 25px; cursor: pointer;} 77 | 78 | #page-text-view .content {white-space: pre; overflow: auto;} 79 | #page-text-view .content audio {display: block; margin-bottom: 10px;} 80 | 81 | #page-option .selector {display: block; margin-bottom: 20px;} 82 | #page-option .selector::before{content: attr(title); display: block; text-align: left; margin-bottom: 5px;} 83 | #page-option .selector > div {display: inline-block; width: 100px; cursor: pointer; text-align: center; line-height: 28px; font-size: 16px; background: #eee;} 84 | #page-option .selector > div[current]::before {content: '\2714'; margin-right: 5px;} 85 | #page-option .selector > div[value="yes"] {background: #afa;} 86 | #page-option .selector > div[value="no"] {background: #faa;} 87 | #page-option .selector.col1 > div {width: 310px;} 88 | #page-option .selector.col2 > div {width: 153px;} 89 | #page-option .selector.col3 > div {width: 100px;} 90 | #page-option .selector.col4 > div {width: 73px;} 91 | #page-option .selector.col5 > div {width: 57px;} 92 | #page-option .selector.col6 > div {width: 47px;} 93 | 94 | 95 | #page-help .content a {text-decoration: none;} 96 | #page-help .content p {margin-bottom: 20px;} 97 | #page-help .content ul {margin-left: 30px;} 98 | 99 | .button {padding: 10px 30px; text-align: center; cursor: pointer; border-radius: 5px; background: #0f0; display: inline-block; -webkit-touch-callout:none; -webkit-user-select:none; -khtml-user-select:none; -moz-user-select:none; -ms-user-select:none; user-select:none; -webkit-tap-highlight-color:rgba(0,0,0,0);} 100 | .caption-button {cursor: pointer; float: right; margin: 0 10px;} 101 | .close {position: absolute !important; cursor: pointer; transform: rotate(45deg); z-index: 10; border-radius: 100%; top: 2px; right: 2px; margin: 2px 10px; display: inline-block; width: 40px; height: 40px;} 102 | .close::before, .close::after {content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: #fff;} 103 | .close::before {width: 6px; margin: 6px auto;} 104 | .close::after {margin: auto 6px; height: 6px;} -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', function() { 2 | if (typeof speechSynthesis == 'undefined' || typeof webkitSpeechRecognition == 'undefined') 3 | return; 4 | 5 | var texts, homophones; 6 | var current_phrase_no, nextPhraseTimer; 7 | var $success = new Audio('correct.mp3'); 8 | 9 | Promise.all(['texts.txt', 'homophones.txt'].map(f => fetch(f).then(res => res.text()))) 10 | .then(function (res) { 11 | texts = parseTexts(res[0]); 12 | homophones = parseHomophones(res[1]); 13 | 14 | for (id in texts) 15 | addText(id, texts[id].name, texts[id].text); 16 | 17 | loadVoices(initOptions); 18 | if (speechSynthesis.onvoiceschanged !== undefined) 19 | speechSynthesis.onvoiceschanged = () => loadVoices(initOptions); 20 | 21 | $('#page-start #loading').remove(); 22 | }); 23 | 24 | $('#page-start #button-start').addEventListener('click', function () { 25 | setPage('main'); 26 | var $e = $('#page-text-selector #texts div[id = "' + localStorage.getItem('speech-current-text') + '"]') || $('#texts').children[0]; 27 | if ($e) 28 | $e.click(); 29 | }); 30 | $('#page-option #speech-success-ring [value="yes"]').addEventListener('click', () => $success.play()); 31 | $('#page-main #button-help').addEventListener('click', () => setPage('help')); 32 | $('#page-main #button-option').addEventListener('click', () => setPage('option')); 33 | $('#page-main #button-text-selector').addEventListener('click', function () { 34 | setPage('text-selector'); 35 | $('#page-text-selector #texts').scrollTop = $('#texts div[current]').offsetTop; 36 | }); 37 | $('#page-main #phrase').addEventListener('click', (event) => speakText(event.target.textContent)); 38 | $('.close', $e => $e.addEventListener('click', () => setPage($e.getAttribute('back')))); 39 | 40 | $('#page-text-selector #button-text-add').addEventListener('click', function () { 41 | $('#page-text-add #caption').value = ''; 42 | $('#page-text-add #text').value = ''; 43 | setPage('text-add'); 44 | }); 45 | 46 | $('#page-text-add #button-text-save').addEventListener('click', function () { 47 | setPage('text-selector'); 48 | 49 | var caption = $('#page-text-add #caption').value.trim() || 'My text'; 50 | var text = $('#page-text-add #text').value.trim(); 51 | if (!text) 52 | return; 53 | 54 | var id = new Date().getTime(); 55 | texts[id] = {id, name, text, phrases: text.split('\n').map(e => e.trim()).filter(e => !!e)}; 56 | addText(id, caption, text); 57 | $('#page-text-selector #texts div[id = "'+ id + '"]').click(); 58 | $('#page-text-selector #texts').scrollTop = $('#texts').scrollHeight; 59 | 60 | var user_texts = (localStorage.getItem('speech-user-texts') || ''); 61 | var user_text = '\nid: ' + id + '\nname: ' + caption + '\n\n' + text; 62 | localStorage.setItem('speech-user-texts', user_texts + '\n===' + user_text); 63 | }) 64 | 65 | function addText(id, name, text) { 66 | var $div = document.createElement('div'); 67 | $div.setAttribute('id', id); 68 | $div.innerHTML = name; 69 | $div.addEventListener('click', function () { 70 | if ($div.hasAttribute('current')) 71 | return; 72 | 73 | for (var i = 0; i < $('#texts').children.length; i++) 74 | $('#texts').children[i].removeAttribute('current'); 75 | this.setAttribute('current', true); 76 | 77 | $('#text').innerHTML = text; 78 | localStorage.setItem('speech-current-text', this.id); 79 | setPhrase($('.page[current]').id == 'page-text-selector' ? 0 : localStorage.getItem('speech-phrase')); 80 | }); 81 | 82 | var $view = document.createElement('div'); 83 | $view.setAttribute('id', 'view'); 84 | $view.innerHTML = '➤'; 85 | $view.addEventListener('click', function (event) { 86 | event.stopImmediatePropagation(); 87 | 88 | if (!$div.hasAttribute('current')) 89 | $div.click(); 90 | 91 | setPage('text-view'); 92 | $('#page-text-view .caption').children[0].innerHTML = name; 93 | $('#page-text-view .content').innerHTML = text; 94 | }) 95 | $div.appendChild($view); 96 | 97 | var $remove = document.createElement('div'); 98 | $remove.setAttribute('id', 'remove'); 99 | $remove.innerHTML = '✖'; 100 | $remove.addEventListener('click', function (event) { 101 | event.stopImmediatePropagation(); 102 | 103 | if (!confirm('Are you sure you want to remove "' + name +'"?')) 104 | return; 105 | 106 | if ($div.hasAttribute('current')) 107 | ($div.previousSibling || $div.nextSibling).click(); 108 | 109 | var deleted = localStorage.getItem('speech-deleted-texts') || ''; 110 | localStorage.setItem('speech-deleted-texts', deleted + ';' + id); 111 | $div.remove(); 112 | }) 113 | $div.appendChild($remove); 114 | 115 | $('#texts').appendChild($div); 116 | } 117 | 118 | function loadFile() { 119 | var file = $('#page-text-add #upload').files[0]; 120 | if (file) { 121 | var reader = new FileReader(); 122 | reader.onload = (event) => $('#page-text-add #text').value = event.target.result; 123 | reader.readAsText(file); 124 | } 125 | } 126 | $('#page-text-add #upload').addEventListener('change', loadFile, false); 127 | $('#page-text-add #button-load-file').addEventListener('click', () => $('#page-text-add #upload').click()); 128 | 129 | $('#page-main #phrase-number').addEventListener('click', function () { 130 | $('#page-main #phrase-number-input').style.display = 'inline-block'; 131 | $('#page-main #phrase-number-input').value = current_phrase_no + 1; 132 | $('#page-main #phrase-number-input').focus(); 133 | }); 134 | 135 | $('#page-main #phrase-number-input').addEventListener('blur', (event) => event.target.style.display = 'none'); 136 | $('#page-main #phrase-number-input').addEventListener('keypress', function (event) { 137 | if (event.keyCode == 13 && !isNaN(this.value)) { 138 | this.blur(); 139 | setPhrase(this.value - 1); 140 | } 141 | }); 142 | $('#page-main #panel-counter #button-prev-phrase').addEventListener('click', () => setPhrase(current_phrase_no - 1)); 143 | $('#page-main #panel-counter #button-next-phrase').addEventListener('click', () => setPhrase(current_phrase_no + 1)); 144 | 145 | function setPhrase (no) { 146 | clearTimeout(nextPhraseTimer); 147 | 148 | no = parseInt(no) || 0; 149 | var id = $('#texts div[current]').id; 150 | var text = texts[id]; 151 | 152 | if (no < 0) 153 | return setPhrase(0); 154 | 155 | if (no > text.phrases.length - 1) 156 | return setPhrase(text.phrases.length - 1); 157 | 158 | current_phrase_no = no; 159 | $('#page-main #panel-counter #button-prev-phrase').style.visibility = no == 0 ? 'hidden' : 'visible'; 160 | $('#page-main #panel-counter #button-next-phrase').style.visibility = no == text.phrases.length - 1 ? 'hidden' : 'visible'; 161 | 162 | $('#page-main #phrase-number').innerHTML = no + 1 + '/' + text.phrases.length; 163 | $('#page-main #panel-counter #caption').innerHTML = text.name; 164 | $('#page-main #phrase').innerHTML = text.phrases[no].split(' ').map((e) => e.indexOf('<') == -1 ? '' + e + '' : e).join(' '); 165 | $('#page-main #phrase > *', e => e.addEventListener('click', (event) => event.stopImmediatePropagation() || speakText(event.target.textContent))); 166 | $('#page-main #button-listen').setAttribute('hidden', true); 167 | $('#page-main #recognition').innerHTML = ''; 168 | $('#page-main #recognition').removeAttribute('confidence'); 169 | $('#page-main #compare').innerHTML = ''; 170 | 171 | if (getOption('speech-auto-read') == 'yes' && $('#page-main').hasAttribute('current')) 172 | speakText(text.phrases[no]); 173 | 174 | localStorage.setItem('speech-phrase', no); 175 | } 176 | 177 | function initOptions() { 178 | $('#page-option .content > div', function ($opt) { 179 | var opt = $opt.id; 180 | var $e = $('#' + opt); 181 | for(var i = 0; i < $e.children.length; i++) 182 | $e.children[i].addEventListener('click', (event) => setOption(opt, event.target.getAttribute('value'))); 183 | setOption(opt, localStorage.getItem(opt)); 184 | }); 185 | } 186 | 187 | function setOption(opt, value) { 188 | var $e = $('#' + opt); 189 | var def = $e.getAttribute('default'); 190 | localStorage.setItem(opt, value || def); 191 | for(var i = 0; i < $e.children.length; i++) 192 | $e.children[i].removeAttribute('current'); 193 | 194 | var $curr = $e.querySelector('[value="' + value + '"]') || $e.querySelector('[value="' + def + '"]') 195 | $curr.setAttribute('current', true); 196 | } 197 | 198 | function getOption(opt) { 199 | var $e = $('#' + opt + ' [current]'); 200 | return $e ? $e.getAttribute('value') : $('#' + opt).getAttribute('default'); 201 | } 202 | 203 | var voices = []; 204 | function speakText(text) { 205 | var utterance = new SpeechSynthesisUtterance(text); 206 | utterance.voice = voices[getOption('speech-voice')]; 207 | utterance.rate = getOption('speech-voice-speed') || 1; 208 | speechSynthesis.speak(utterance); 209 | }; 210 | 211 | function loadVoices (cb) { 212 | voices = speechSynthesis.getVoices(); 213 | var $voices = $('#page-option #speech-voice'); 214 | if ($voices.innerHTML.trim()) 215 | return; 216 | 217 | function speakExample () { 218 | setTimeout(() => speakText('Where is a cat?'), 200); 219 | } 220 | 221 | voices.forEach(function(e, i) { 222 | if (e.lang.indexOf('en') == 0 && (e.lang.indexOf('US') != -1 || e.lang.indexOf('UK') != -1 || e.lang.indexOf('GB') != -1)) { 223 | var $div = document.createElement('div'); 224 | $div.setAttribute('value', i); 225 | $div.setAttribute('title', e.name); 226 | $div.innerHTML = $voices.children.length + 1; 227 | $div.addEventListener('click', speakExample); 228 | $voices.appendChild($div); 229 | $voices.appendChild(document.createTextNode(' ')); 230 | } 231 | }); 232 | 233 | if (!$voices.children.length) 234 | return; 235 | 236 | var current = ($('#page-option #speech-voice [value="' + localStorage.getItem('speech-voice') + '"]') || $voices.children[0]).getAttribute('value'); 237 | $voices.setAttribute('default', current); 238 | $voices.classList.add('col' + $voices.children.length); 239 | setOption('speech-voice', current); 240 | 241 | $('#page-option #speech-voice-speed > div', $e => $e.onclick = speakExample); 242 | cb(); 243 | } 244 | 245 | navigator.mediaDevices.getUserMedia({audio: true}).then (function (stream) { 246 | $('#page-start #microphone').remove(); 247 | 248 | var recognition = new webkitSpeechRecognition(); 249 | var chunks, userAudio; 250 | 251 | var mediaRecorder = new MediaRecorder(stream); 252 | mediaRecorder.addEventListener('dataavailable', event => chunks.push(event.data)); 253 | 254 | var time; 255 | var isMobile = (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator && navigator.userAgent || '')); 256 | 257 | var $panel_recognition = $('#page-main #panel-recognition'); 258 | var $recognition = $('#page-main #recognition'); 259 | var $compare = $('#page-main #compare'); 260 | 261 | function startRecord(event) { 262 | event.stopImmediatePropagation(); 263 | 264 | if (event.type != 'touchstart' && event.type == 'mousedown' && event.which != 1) 265 | return; 266 | 267 | if ($('#page-main #button-record').hasAttribute('record')) 268 | return; 269 | 270 | time = new Date().getTime(); 271 | $('#page-main #button-record').setAttribute('record', true); 272 | $panel_recognition.setAttribute('mode', 'recognition'); 273 | $compare.innerHTML = ''; 274 | $recognition.innerHTML = ''; 275 | $recognition.removeAttribute('confidence'); 276 | 277 | chunks = []; 278 | userAudio = null; 279 | 280 | var dmp = new diff_match_patch(); 281 | dmp.Diff_Timeout = parseFloat(100); 282 | dmp.Diff_EditCost = parseFloat(4); 283 | 284 | mediaRecorder.start(); 285 | 286 | recognition.lang = isMobile ? 'en-US' : 'en-UK'; 287 | recognition.interimResults = false; 288 | recognition.continuous = false; 289 | 290 | recognition.onresult = function (event) { 291 | stopRecord(new MouseEvent('mouseup', {'which': 1})); 292 | 293 | var res = event.results[0][0]; 294 | 295 | var phrase = $('#page-main #phrase').textContent.trim(); 296 | var transcript = (res.transcript || '').replace(/\d+/g, num2text); 297 | transcript = replaceHomophones(phrase, transcript); 298 | 299 | $recognition.innerHTML = transcript.split(' ').map((e) => '' + e + '').join(' '); 300 | $recognition.querySelectorAll('*').forEach((e) => e.addEventListener('click', (event) => speakText(event.target.textContent))); 301 | $recognition.setAttribute('confidence', (parseInt(res.confidence * 100) + '%')); 302 | 303 | var clear = (text) => text.replace(/[^\w\s]|_/g, '').replace(/\s+/g, ' ').trim().toLowerCase(); 304 | phrase = clear(phrase); 305 | transcript = clear(transcript); 306 | 307 | $panel_recognition.setAttribute('mode', (transcript.length || 0) < phrase.length * 0.7 || phrase == transcript ? 'recognition' : 'compare'); 308 | 309 | var compare = ''; 310 | if (phrase != transcript) { 311 | var d = dmp.diff_main(phrase, transcript); 312 | dmp.diff_cleanupEfficiency(d); 313 | compare = dmp.diff_prettyHtml(d); 314 | } 315 | 316 | $compare.innerHTML = compare; 317 | $recognition.setAttribute('correct', phrase == transcript); 318 | 319 | if (phrase == transcript && getOption('speech-auto-next') == 'yes') 320 | nextPhraseTimer = setTimeout(() => setPhrase(current_phrase_no + 1), 1500); 321 | 322 | if (phrase == transcript && getOption('speech-success-ring') == 'yes') 323 | $success.play(); 324 | } 325 | 326 | recognition.onerror = function (event) { 327 | $panel_recognition.setAttribute('mode', 'recognition'); 328 | $recognition.removeAttribute('confidence'); 329 | $recognition.innerHTML = 'Error: ' + event.message; 330 | } 331 | 332 | recognition.start(); 333 | } 334 | 335 | function stopRecord(event) { 336 | event.stopImmediatePropagation(); 337 | 338 | if (event.type != 'touchend' && event.type == 'mouseup' && event.which != 1) 339 | return; 340 | 341 | if (new Date().getTime() - time < 300) 342 | return; 343 | 344 | if (mediaRecorder.state == 'recording') 345 | mediaRecorder.stop(); 346 | $('#page-main #button-listen').removeAttribute('hidden'); 347 | $('#page-main #button-record').removeAttribute('record'); 348 | 349 | if (recognition) 350 | recognition.stop(); 351 | 352 | setTimeout(function () { 353 | var blob = new Blob(chunks, {type : 'audio/ogg; codecs=opus'}); 354 | var url = URL.createObjectURL(blob); 355 | userAudio = new Audio(url); 356 | }, 500); 357 | } 358 | 359 | $('#page-main #button-record').addEventListener('click', (event) => event.stopImmediatePropagation()); 360 | $('#page-main #button-record').addEventListener(isMobile ? 'touchstart' : 'mousedown', startRecord); 361 | $('#page-main #button-record').addEventListener(isMobile ? 'touchend' : 'mouseup', stopRecord); 362 | $('#page-main #button-listen').addEventListener('click', function() { 363 | event.stopImmediatePropagation(); 364 | 365 | if (!userAudio || !userAudio.duration) 366 | return; 367 | 368 | if (userAudio.paused) 369 | return userAudio.play(); 370 | 371 | userAudio.pause(); 372 | userAudio.currentTime = 0; 373 | }); 374 | 375 | $panel_recognition.addEventListener('click', function (event) { 376 | var mode = this.getAttribute('mode'); 377 | this.setAttribute('mode', mode == 'recognition' && $compare.textContent.trim() ? 'compare' : 'recognition'); 378 | }) 379 | }).catch((err) => alert(err.message)); 380 | 381 | function setPage(page) { 382 | $('audio', $e => $e.pause()); 383 | $('.page', $e => $e.removeAttribute('current')); 384 | $('#page-' + page).setAttribute('current', true); 385 | } 386 | 387 | history.pushState({}, '', window.location.pathname); 388 | window.addEventListener('popstate', function(event) { 389 | var page = $('.page[current]'); 390 | if (page.id == 'page-start') 391 | return history.back(); 392 | 393 | history.pushState(null, null, window.location.pathname); 394 | if (page.id == 'page-main') 395 | return setPage('start'); 396 | 397 | page.querySelector('.close').click(); 398 | }, false); 399 | 400 | function $ (selector, apply) { 401 | return apply ? Array.prototype.slice.call(document.querySelectorAll(selector) || []).forEach(apply) : document.querySelector(selector); 402 | } 403 | 404 | // UTILS 405 | function parseTexts (data) { 406 | var texts = {}; 407 | var deleted = (localStorage.getItem('speech-deleted-texts') || '').split(';'); 408 | 409 | data += '===' + (localStorage.getItem('speech-user-texts') || ''); 410 | data.split('===').forEach(function(text) { 411 | var e = {phrases: []}; 412 | 413 | var header_mode = true; 414 | text.split('\n').forEach(function (line, i) { 415 | line = line.trim(); 416 | 417 | if (!line && i == 0) 418 | return; 419 | 420 | if (!line) 421 | header_mode = false; 422 | 423 | if (line && header_mode) 424 | e[line.substr(0, line.indexOf(':')).trim()] = line.substr(line.indexOf(':') + 1).trim(); 425 | else 426 | e.phrases.push(line); 427 | }) 428 | 429 | e.text = (e.audio ? '' : '') + e.phrases.join('\n').trim(); 430 | e.phrases = e.phrases.filter(e => !!e); 431 | 432 | if (e.id && e.phrases.length > 0 && e.name && deleted.indexOf(e.id) == -1) 433 | texts[e.id] = e; 434 | }) 435 | 436 | return texts; 437 | } 438 | 439 | function parseHomophones(data) { 440 | var res = {}; 441 | data.split('\n').map(e => e.split(';')).forEach(e => e.forEach(w => res[w] = e)); 442 | return res; 443 | } 444 | 445 | function replaceHomophones (phrase, transcript) { 446 | var phrase_words = phrase.split(' ').map((w) => w.toLowerCase().replace(/(^\W*)|(\W*$)/g, '')); 447 | return transcript.split(' ').map(function(word, i) { 448 | var w = word.toLowerCase(); 449 | var ws = homophones[w]; 450 | if (!ws) 451 | return word; 452 | 453 | var check = (w) => i > 1 && phrase_words[i - 1] == w || phrase_words[i] == w || i + 1 < phrase_words.length && phrase_words[i + 1] == w 454 | for (var j = 0; j < ws.length; j++) { 455 | if (check(ws[j])) 456 | return ws[j]; 457 | } 458 | 459 | return word; 460 | }).join(' '); 461 | } 462 | 463 | var num = 'zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen'.split(' '); 464 | var tens = 'twenty thirty forty fifty sixty seventy eighty ninety'.split(' '); 465 | function num2text(n){ 466 | if (n < 20) return num[n]; 467 | var digit = n%10; 468 | if (n < 100) return tens[~~(n/10)-2] + (digit? ' ' + num[digit]: ''); 469 | if (n < 1000) return num[~~(n/100)] +' hundred' + (n%100 == 0? '': ' ' + num2text(n%100)); 470 | return num2text(~~(n/1000)) + ' thousand' + (n%1000 != 0? ' ' + num2text(n%1000): ''); 471 | } 472 | }); -------------------------------------------------------------------------------- /diff_match_patch.js: -------------------------------------------------------------------------------- 1 | var diff_match_patch=function(){this.Diff_Timeout=1;this.Diff_EditCost=4;this.Match_Threshold=.5;this.Match_Distance=1E3;this.Patch_DeleteThreshold=.5;this.Patch_Margin=4;this.Match_MaxBits=32},DIFF_DELETE=-1,DIFF_INSERT=1,DIFF_EQUAL=0;diff_match_patch.Diff=function(a,b){this[0]=a;this[1]=b};diff_match_patch.Diff.prototype.length=2;diff_match_patch.Diff.prototype.toString=function(){return this[0]+","+this[1]}; 2 | diff_match_patch.prototype.diff_main=function(a,b,c,d){"undefined"==typeof d&&(d=0>=this.Diff_Timeout?Number.MAX_VALUE:(new Date).getTime()+1E3*this.Diff_Timeout);if(null==a||null==b)throw Error("Null input. (diff_main)");if(a==b)return a?[new diff_match_patch.Diff(DIFF_EQUAL,a)]:[];"undefined"==typeof c&&(c=!0);var e=c,f=this.diff_commonPrefix(a,b);c=a.substring(0,f);a=a.substring(f);b=b.substring(f);f=this.diff_commonSuffix(a,b);var g=a.substring(a.length-f);a=a.substring(0,a.length-f);b=b.substring(0, 3 | b.length-f);a=this.diff_compute_(a,b,e,d);c&&a.unshift(new diff_match_patch.Diff(DIFF_EQUAL,c));g&&a.push(new diff_match_patch.Diff(DIFF_EQUAL,g));this.diff_cleanupMerge(a);return a}; 4 | diff_match_patch.prototype.diff_compute_=function(a,b,c,d){if(!a)return[new diff_match_patch.Diff(DIFF_INSERT,b)];if(!b)return[new diff_match_patch.Diff(DIFF_DELETE,a)];var e=a.length>b.length?a:b,f=a.length>b.length?b:a,g=e.indexOf(f);return-1!=g?(c=[new diff_match_patch.Diff(DIFF_INSERT,e.substring(0,g)),new diff_match_patch.Diff(DIFF_EQUAL,f),new diff_match_patch.Diff(DIFF_INSERT,e.substring(g+f.length))],a.length>b.length&&(c[0][0]=c[2][0]=DIFF_DELETE),c):1==f.length?[new diff_match_patch.Diff(DIFF_DELETE, 5 | a),new diff_match_patch.Diff(DIFF_INSERT,b)]:(e=this.diff_halfMatch_(a,b))?(b=e[1],f=e[3],a=e[4],e=this.diff_main(e[0],e[2],c,d),c=this.diff_main(b,f,c,d),e.concat([new diff_match_patch.Diff(DIFF_EQUAL,a)],c)):c&&100c);t++){for(var v=-t+p;v<=t-x;v+=2){var n=f+v;var r=v==-t||v!=t&&h[n-1]d)x+=2;else if(y>e)p+=2;else if(m&&(n=f+k-v,0<=n&&n= 9 | u)return this.diff_bisectSplit_(a,b,r,y,c)}}for(v=-t+w;v<=t-q;v+=2){n=f+v;u=v==-t||v!=t&&l[n-1]d)q+=2;else if(r>e)w+=2;else if(!m&&(n=f+k-v,0<=n&&n=u)))return this.diff_bisectSplit_(a,b,r,y,c)}}return[new diff_match_patch.Diff(DIFF_DELETE,a),new diff_match_patch.Diff(DIFF_INSERT,b)]}; 10 | diff_match_patch.prototype.diff_bisectSplit_=function(a,b,c,d,e){var f=a.substring(0,c),g=b.substring(0,d);a=a.substring(c);b=b.substring(d);f=this.diff_main(f,g,!1,e);e=this.diff_main(a,b,!1,e);return f.concat(e)}; 11 | diff_match_patch.prototype.diff_linesToChars_=function(a,b){function c(a){for(var b="",c=0,g=-1,h=d.length;gd?a=a.substring(c-d):c=a.length?[h,k,l,m,g]:null}if(0>=this.Diff_Timeout)return null; 16 | var d=a.length>b.length?a:b,e=a.length>b.length?b:a;if(4>d.length||2*e.lengthd[4].length?g:d:d:g;else return null;if(a.length>b.length){d=g[0];e=g[1];var h=g[2];var l=g[3]}else h=g[0],l=g[1],d=g[2],e=g[3];return[d,e,h,l,g[4]]}; 17 | diff_match_patch.prototype.diff_cleanupSemantic=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=0,h=0,l=0,k=0;f=e){if(d>=b.length/2||d>=c.length/2)a.splice(f,0,new diff_match_patch.Diff(DIFF_EQUAL,c.substring(0,d))),a[f-1][1]=b.substring(0,b.length-d),a[f+1][1]=c.substring(d),f++}else if(e>=b.length/2||e>=c.length/2)a.splice(f,0,new diff_match_patch.Diff(DIFF_EQUAL,b.substring(0,e))),a[f-1][0]=DIFF_INSERT,a[f-1][1]=c.substring(0,c.length-e),a[f+1][0]=DIFF_DELETE, 19 | a[f+1][1]=b.substring(e),f++;f++}f++}}; 20 | diff_match_patch.prototype.diff_cleanupSemanticLossless=function(a){function b(a,b){if(!a||!b)return 6;var c=a.charAt(a.length-1),d=b.charAt(0),e=c.match(diff_match_patch.nonAlphaNumericRegex_),f=d.match(diff_match_patch.nonAlphaNumericRegex_),g=e&&c.match(diff_match_patch.whitespaceRegex_),h=f&&d.match(diff_match_patch.whitespaceRegex_);c=g&&c.match(diff_match_patch.linebreakRegex_);d=h&&d.match(diff_match_patch.linebreakRegex_);var k=c&&a.match(diff_match_patch.blanklineEndRegex_),l=d&&b.match(diff_match_patch.blanklineStartRegex_); 21 | return k||l?5:c||d?4:e&&!g&&h?3:g||h?2:e||f?1:0}for(var c=1;c=k&&(k=m,g=d,h=e,l=f)}a[c-1][1]!=g&&(g?a[c-1][1]=g:(a.splice(c- 22 | 1,1),c--),a[c][1]=h,l?a[c+1][1]=l:(a.splice(c+1,1),c--))}c++}};diff_match_patch.nonAlphaNumericRegex_=/[^a-zA-Z0-9]/;diff_match_patch.whitespaceRegex_=/\s/;diff_match_patch.linebreakRegex_=/[\r\n]/;diff_match_patch.blanklineEndRegex_=/\n\r?\n$/;diff_match_patch.blanklineStartRegex_=/^\r?\n\r?\n/; 23 | diff_match_patch.prototype.diff_cleanupEfficiency=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=!1,h=!1,l=!1,k=!1;fb)break;e=c;f=d}return a.length!=g&&a[g][0]===DIFF_DELETE?f:f+(b-e)}; 28 | diff_match_patch.prototype.diff_prettyHtml=function(a){for(var b=[],c=/&/g,d=//g,f=/\n/g,g=0;g");switch(h){case DIFF_INSERT:b[g]=''+l+"";break;case DIFF_DELETE:b[g]=''+l+"";break;case DIFF_EQUAL:b[g]=""+l+""}}return b.join("")}; 29 | diff_match_patch.prototype.diff_text1=function(a){for(var b=[],c=0;cthis.Match_MaxBits)throw Error("Pattern too long for this browser.");var e=this.match_alphabet_(b),f=this,g=this.Match_Threshold,h=a.indexOf(b,c);-1!=h&&(g=Math.min(d(0,h),g),h=a.lastIndexOf(b,c+b.length),-1!=h&&(g=Math.min(d(0,h),g)));var l=1<=k;q--){var t=e[a.charAt(q-1)];m[q]=0===w?(m[q+1]<<1|1)&t:(m[q+1]<<1|1)&t|(x[q+1]|x[q])<<1|1|x[q+1];if(m[q]&l&&(t=d(w,q-1),t<=g))if(g=t,h=q-1,h>c)k=Math.max(1,2*c-h);else break}if(d(w+1,c)>g)break;x=m}return h}; 36 | diff_match_patch.prototype.match_alphabet_=function(a){for(var b={},c=0;c=2*this.Patch_Margin&&e&&(this.patch_addContext_(a,h),c.push(a),a=new diff_match_patch.patch_obj,e=0,h=d,f=g)}k!==DIFF_INSERT&&(f+=m.length);k!==DIFF_DELETE&&(g+=m.length)}e&&(this.patch_addContext_(a,h),c.push(a));return c}; 42 | diff_match_patch.prototype.patch_deepCopy=function(a){for(var b=[],c=0;cthis.Match_MaxBits){var k=this.match_main(b,h.substring(0,this.Match_MaxBits),g);-1!=k&&(l=this.match_main(b,h.substring(h.length-this.Match_MaxBits),g+h.length-this.Match_MaxBits),-1==l||k>=l)&&(k=-1)}else k=this.match_main(b,h, 44 | g);if(-1==k)e[f]=!1,d-=a[f].length2-a[f].length1;else if(e[f]=!0,d=k-g,g=-1==l?b.substring(k,k+h.length):b.substring(k,l+this.Match_MaxBits),h==g)b=b.substring(0,k)+this.diff_text2(a[f].diffs)+b.substring(k+h.length);else if(g=this.diff_main(h,g,!1),h.length>this.Match_MaxBits&&this.diff_levenshtein(g)/h.length>this.Patch_DeleteThreshold)e[f]=!1;else{this.diff_cleanupSemanticLossless(g);h=0;var m;for(l=0;le[0][1].length){var f=b-e[0][1].length;e[0][1]=c.substring(e[0][1].length)+e[0][1];d.start1-=f;d.start2-=f;d.length1+=f;d.length2+=f}d=a[a.length-1];e=d.diffs; 47 | 0==e.length||e[e.length-1][0]!=DIFF_EQUAL?(e.push(new diff_match_patch.Diff(DIFF_EQUAL,c)),d.length1+=b,d.length2+=b):b>e[e.length-1][1].length&&(f=b-e[e.length-1][1].length,e[e.length-1][1]+=c.substring(0,f),d.length1+=f,d.length2+=f);return c}; 48 | diff_match_patch.prototype.patch_splitMax=function(a){for(var b=this.Match_MaxBits,c=0;c2*b?(h.length1+=k.length,e+=k.length,l=!1,h.diffs.push(new diff_match_patch.Diff(g,k)),d.diffs.shift()):(k=k.substring(0,b-h.length1-this.Patch_Margin),h.length1+=k.length,e+=k.length,g===DIFF_EQUAL?(h.length2+=k.length,f+=k.length):l=!1,h.diffs.push(new diff_match_patch.Diff(g,k)),k==d.diffs[0][1]?d.diffs.shift():d.diffs[0][1]=d.diffs[0][1].substring(k.length))}g=this.diff_text2(h.diffs); 50 | g=g.substring(g.length-this.Patch_Margin);k=this.diff_text1(d.diffs).substring(0,this.Patch_Margin);""!==k&&(h.length1+=k.length,h.length2+=k.length,0!==h.diffs.length&&h.diffs[h.diffs.length-1][0]===DIFF_EQUAL?h.diffs[h.diffs.length-1][1]+=k:h.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL,k)));l||a.splice(++c,0,h)}}};diff_match_patch.prototype.patch_toText=function(a){for(var b=[],c=0;c