2
\$\begingroup\$

I am not sure how to accurately describe what this program does in just a few words, anyway this program is very simple and easy to use, what it does is very simple and it is good at it.

Let me start from the beginning, I want to generate random pronounceable strings that closely resemble English text, and I want my program to be the best in this field in the world.

And I did some researching and found Markov Chain to be the best way to implement my ideas, and Markov Chain needs a very large population to be useful/accurate, so after a countless Google searches I have found GCIDE, and pulled 105230 words from it.

And I analyzed the corpus and used the data to write the pseudoword generation program. And it is very good, but the output of the letter mode has a probability of being unpronounceable, a probability that is higher than acceptable level. Let me be clear, I want the output to be exactly impossible to be unpronounceable, period.

The morphology of English orthography is very complex, and it allows multiple consonant letters to be adjacent to each other, a word that can be pronounced must also be able to be completely syllabified, some consonant letters put together can fit in a single syllable at either the start and/or the end, some consonant letters put together can't fit in a syllable but can fit in the end of one syllable and the start of the next syllable. And some consonant letters put together do not make sense wherever they are, if they appear then the word can't be completely syllabified and is invalidated.

It is very hard to accurately describe, whenever I see a random string of letters, I can tell if it is valid pronounceable word or garbage text, with 100% accuracy, the moment I see it.

You can too, but the computer can't. This knowledge is implicit knowledge and very hard to explicitly write down, and I need to program it.

And GCIDE is corpus, it collected various strings, most of the valid words, yet many are completely garbage text. For example, it includes acronyms (ALL CONSONANTS KIND), Roman Numerals, some made up words, some extremely rare foreign words that aren't valid in English standards...

For example, acronyms like HTTP, FTP, TCP, FBI and something simple like Mr., Ms. and Dr. Let me be clear, they are all invalid words that aren't pronounceable at all, they are all invalid garbage text(the output of my program must be all lowercase, because Python is CASE SENSITIVE, and they don't make any sense in all lowercase).

Sure, I know HTTP stands for HyperText Transfer Protocol, FTP: File Transfer Protocol, TCP: Transfer Control Protocol, FBI: Federal Bureau of Investigations... But how do you pronounce them? When you pronounce HTTP, aren't you pronouncing aitch, tee, tee, pee, or you can somehow actually pronounce a string of consonants with NO VOWELS? If they are considered pronounceable, then, PDLGFSTJ must also be considered pronounceable.

And there are garbage text included in GCIDE like houyhnhnm and eschscholtzia...

In short, I want to find all non-breaking consonants (consonant clusters that can be put into a single syllable) and which consonant can be put after which consonant and exactly how many of them can be put together without invalidating the string...

Hard code the rules, and use the rules to filter out garbage text and monitor the text generation process.

So I have written this program that helps me manually find non-breaking consonants.

You will need this file:

{
    "r": 28755,
    "n": 27823,
    "t": 26506,
    "l": 24402,
    "c": 20898,
    "d": 18615,
    "s": 17766,
    "m": 17102,
    "p": 11882,
    "g": 7839,
    "b": 7734,
    "v": 7294,
    "nt": 7023,
    "st": 6564,
    "f": 5454,
    "ss": 5181,
    "h": 4820,
    "ng": 4312,
    "ll": 4237,
    "bl": 3380,
    "nd": 3315,
    "nc": 3148,
    "ph": 2974,
    "tr": 2892,
    "w": 2821,
    "th": 2545,
    "ch": 2536,
    "z": 2518,
    "pr": 2459,
    "ct": 2413,
    "k": 2109,
    "sh": 2065,
    "sc": 1961,
    "rm": 1939,
    "gr": 1907,
    "x": 1907,
    "sm": 1823,
    "rt": 1762,
    "cr": 1615,
    "ns": 1561,
    "sp": 1547,
    "rr": 1433,
    "br": 1394,
    "rd": 1358,
    "tt": 1292,
    "ck": 1215,
    "mp": 1214,
    "pl": 1206,
    "q": 1196,
    "rn": 1157,
    "str": 1074,
    "nn": 1068,
    "j": 1052,
    "dr": 1047,
    "rs": 1046,
    "cl": 1029,
    "pt": 1019,
    "mm": 937,
    "rc": 888,
    "mb": 868,
    "fl": 856,
    "lt": 813,
    "rg": 783,
    "rb": 750,
    "rl": 715,
    "ff": 698,
    "gl": 684,
    "gn": 681,
    "pp": 681,
    "ngl": 665,
    "fr": 639,
    "sl": 625,
    "nf": 614,
    "ntr": 607,
    "nch": 579,
    "cc": 573,
    "rp": 532,
    "ld": 506,
    "rv": 486,
    "ps": 468,
    "nv": 460,
    "rch": 411,
    "rk": 411,
    "wh": 398,
    "nth": 397,
    "rh": 387,
    "ght": 378,
    "lm": 377,
    "gg": 374,
    "nl": 367,
    "nk": 365,
    "sk": 365,
    "nr": 358,
    "lv": 351,
    "sn": 334,
    "chr": 323,
    "dl": 323,
    "ntl": 323,
    "ndr": 318,
    "dd": 314,
    "tch": 308,
    "lc": 305,
    "rf": 302,
    "nh": 301,
    "sw": 295,
    "mpl": 291,
    "bb": 277,
    "mn": 270,
    "scr": 270,
    "rth": 260,
    "dg": 246,
    "sq": 242,
    "ft": 241,
    "nm": 238,
    "cs": 231,
    "ls": 229,
    "nb": 229,
    "xt": 219,
    "nst": 218,
    "thr": 218,
    "mbr": 213,
    "dn": 212,
    "wn": 210,
    "xp": 209,
    "mph": 203,
    "sph": 203,
    "np": 200,
    "rph": 200,
    "ln": 199,
    "rw": 197,
    "gh": 195,
    "mpr": 194,
    "nw": 194,
    "rsh": 192,
    "bs": 191,
    "gm": 191,
    "lph": 190,
    "lg": 188,
    "tl": 186,
    "sch": 184,
    "lk": 182,
    "nct": 178,
    "tw": 178,
    "ckl": 175,
    "mbl": 174,
    "lp": 171,
    "wl": 170,
    "ctr": 164,
    "nthr": 162,
    "kn": 160,
    "wr": 158,
    "nsh": 154,
    "nq": 152,
    "ndl": 151,
    "xc": 147,
    "mpt": 146,
    "ttl": 146,
    "stl": 145,
    "lb": 143,
    "ngr": 143,
    "nstr": 143,
    "ppr": 141,
    "ncl": 136,
    "ncr": 136,
    "spr": 135,
    "lf": 134,
    "ppl": 133,
    "nsp": 130,
    "ddl": 127,
    "nj": 127,
    "phr": 123,
    "dv": 122,
    "chl": 120,
    "dm": 120,
    "xtr": 120,
    "bd": 119,
    "spl": 119,
    "ffl": 114,
    "npr": 113,
    "phth": 107,
    "sb": 106,
    "nfr": 105,
    "rst": 105,
    "sth": 104,
    "pn": 102,
    "bbl": 97,
    "nfl": 97,
    "ts": 96,
    "chn": 94,
    "bst": 93,
    "dj": 92,
    "bt": 87,
    "rrh": 86,
    "tm": 86,
    "rtl": 80,
    "scl": 80,
    "nz": 79,
    "tf": 77,
    "rtr": 74,
    "shr": 74,
    "sr": 74,
    "nsc": 73,
    "dw": 72,
    "shm": 72,
    "ml": 70,
    "dh": 68,
    "rdl": 68,
    "zz": 67,
    "ggl": 66,
    "phl": 66,
    "bj": 63,
    "ffr": 63,
    "wb": 63,
    "xpl": 63,
    "rpr": 62,
    "tb": 62,
    "ws": 62,
    "xh": 62,
    "sf": 60,
    "cch": 59,
    "mf": 59,
    "thm": 59,
    "cq": 58,
    "ms": 58,
    "chth": 56,
    "ltr": 56,
    "ngs": 56,
    "nsl": 56,
    "ds": 55,
    "nsm": 53,
    "sd": 53,
    "tn": 53,
    "wd": 53,
    "nkl": 52,
    "rpl": 52,
    "rq": 52,
    "rthr": 52,
    "kl": 51,
    "thl": 50,
    "rcl": 49,
    "sg": 49,
    "ngn": 48,
    "ttr": 48,
    "df": 47,
    "lch": 46,
    "zzl": 46,
    "cks": 45,
    "bstr": 44,
    "cn": 44,
    "lr": 44,
    "rfl": 44,
    "shl": 44,
    "ssl": 44,
    "bc": 43,
    "bm": 43,
    "rj": 43,
    "lh": 42,
    "lw": 42,
    "ntn": 42,
    "rsp": 42,
    "ckw": 41,
    "stf": 41,
    "tz": 41,
    "ckb": 40,
    "rgr": 40,
    "shn": 40,
    "stm": 40,
    "db": 39,
    "lth": 39,
    "pst": 39,
    "ggr": 38,
    "kr": 38,
    "pm": 38,
    "wf": 38,
    "ccl": 37,
    "wk": 37,
    "ghtl": 35,
    "ngh": 34,
    "npl": 34,
    "nsw": 34,
    "rbl": 34,
    "rcr": 34,
    "bn": 33,
    "ckn": 33,
    "ndm": 33,
    "nsf": 33,
    "pb": 33,
    "rbr": 33,
    "rdr": 33,
    "wm": 33,
    "xpr": 33,
    "chm": 32,
    "lst": 32,
    "nds": 32,
    "bh": 31,
    "bv": 31,
    "mv": 31,
    "nbl": 31,
    "rstr": 31,
    "ssm": 31,
    "ssn": 31,
    "ckm": 30,
    "gw": 30,
    "ghtf": 29,
    "ndw": 29,
    "ngst": 29,
    "nscr": 29,
    "ptr": 29,
    "rds": 29,
    "tp": 29,
    "bsc": 28,
    "ctl": 28,
    "llm": 28,
    "nbr": 28,
    "pf": 28,
    "pw": 28,
    "rpt": 28,
    "rtm": 28,
    "thw": 28,
    "dst": 27,
    "ndf": 27,
    "nx": 27,
    "rtn": 27,
    "thn": 27,
    "btr": 26,
    "lsh": 26,
    "mr": 26,
    "mst": 26,
    "ntm": 26,
    "rct": 26,
    "rz": 26,
    "stw": 26,
    "xs": 26,
    "kh": 25,
    "rgl": 25,
    "rkl": 25,
    "wt": 25,
    "ckh": 24,
    "hm": 24,
    "ldr": 24,
    "nchr": 24,
    "nwr": 24,
    "xcr": 24,
    "llb": 23,
    "llh": 23,
    "ngb": 23,
    "tst": 23,
    "ccr": 22,
    "gb": 22,
    "gd": 22,
    "ghb": 22,
    "nkn": 22,
    "thf": 22,
    "ckf": 21,
    "gs": 21,
    "llf": 21,
    "llw": 21,
    "rnm": 21,
    "sgr": 21,
    "xl": 21,
    "ckst": 20,
    "mps": 20,
    "ntw": 20,
    "rsl": 20,
    "sj": 20,
    "ssb": 20,
    "bf": 19,
    "dc": 19,
    "ngth": 19,
    "rthw": 19,
    "shb": 19,
    "tc": 19,
    "bp": 18,
    "ckr": 18,
    "dp": 18,
    "ngt": 18,
    "rgh": 18,
    "ssp": 18,
    "stn": 18,
    "tg": 18,
    "vr": 18,
    "wp": 18,
    "wth": 18,
    "ww": 18,
    "hl": 17,
    "ndb": 17,
    "pc": 17,
    "rml": 17,
    "rnl": 17,
    "rtf": 17,
    "ssw": 17,
    "tbr": 17,
    "ths": 17,
    "xcl": 17,
    "ckp": 16,
    "ctn": 16,
    "ldl": 16,
    "md": 16,
    "mw": 16,
    "ndh": 16,
    "ndn": 16,
    "ngd": 16,
    "psh": 16,
    "rdn": 16,
    "ssf": 16,
    "cht": 15,
    "lz": 15,
    "nts": 15,
    "rchl": 15,
    "ssh": 15,
    "bw": 14,
    "dsh": 14,
    "ghl": 14,
    "hn": 14,
    "hr": 14,
    "kw": 14,
    "lln": 14,
    "nks": 14,
    "ntsh": 14,
    "rdw": 14,
    "rfr": 14,
    "rtw": 14,
    "tchw": 14,
    "dth": 13,
    "gnm": 13,
    "ldf": 13,
    "mc": 13,
    "mpn": 13,
    "msh": 13,
    "ngf": 13,
    "ntf": 13,
    "rdb": 13,
    "rnb": 13,
    "rnf": 13,
    "rsc": 13,
    "rsw": 13,
    "shf": 13,
    "stp": 13,
    "tsh": 13,
    "bbr": 12,
    "cd": 12,
    "cz": 12,
    "ks": 12,
    "lld": 12,
    "mt": 12,
    "nsv": 12,
    "rdsh": 12,
    "rsk": 12,
    "rts": 12,
    "rwh": 12,
    "stb": 12,
    "wc": 12,
    "wsh": 12,
    "ckt": 11,
    "dgm": 11,
    "ghtw": 11,
    "kk": 11,
    "lfr": 11,
    "llr": 11,
    "mscr": 11,
    "ngm": 11,
    "nkh": 11,
    "nsk": 11,
    "rchd": 11,
    "rdf": 11,
    "rdm": 11,
    "rkm": 11,
    "rps": 11,
    "rscr": 11,
    "rthl": 11,
    "shw": 11,
    "tchm": 11,
    "wdr": 11,
    "chs": 10,
    "cksh": 10,
    "dt": 10,
    "ftl": 10,
    "ghtn": 10,
    "ghts": 10,
    "km": 10,
    "ksh": 10,
    "lgr": 10,
    "lpt": 10,
    "mfl": 10,
    "mh": 10,
    "nchm": 10,
    "ndst": 10,
    "nph": 10,
    "nsch": 10,
    "pd": 10,
    "phn": 10,
    "rkn": 10,
    "rsm": 10,
    "rtz": 10,
    "stc": 10,
    "tchb": 10,
    "td": 10,
    "tpr": 10,
    "tsp": 10,
    "tv": 10,
    "wnw": 10,
    "xch": 10,
    "btl": 9,
    "ckd": 9,
    "cm": 9,
    "dsm": 9,
    "dstr": 9,
    "ftsm": 9,
    "gf": 9,
    "ghtm": 9,
    "gt": 9,
    "kt": 9,
    "ldn": 9,
    "lls": 9,
    "msp": 9,
    "nchn": 9,
    "ndp": 9,
    "nkf": 9,
    "nshr": 9,
    "ptl": 9,
    "rkw": 9,
    "rnst": 9,
    "rwr": 9,
    "sv": 9,
    "thh": 9,
    "vv": 9,
    "wnl": 9,
    "xb": 9,
    "xf": 9,
    "chw": 8,
    "ckstr": 8,
    "ctm": 8,
    "ddr": 8,
    "ghm": 8,
    "ghth": 8,
    "gp": 8,
    "kb": 8,
    "ldw": 8,
    "llt": 8,
    "ltl": 8,
    "ngw": 8,
    "nkb": 8,
    "nwh": 8,
    "rchw": 8,
    "rmf": 8,
    "rtb": 8,
    "rtg": 8,
    "sht": 8,
    "sthm": 8,
    "tfl": 8,
    "wg": 8,
    "wst": 8,
    "xw": 8,
    "bscr": 7,
    "ctf": 7,
    "ftm": 7,
    "ghh": 7,
    "ghn": 7,
    "ghw": 7,
    "gsh": 7,
    "hs": 7,
    "kf": 7,
    "ldb": 7,
    "ldh": 7,
    "lkw": 7,
    "llst": 7,
    "mg": 7,
    "nchl": 7,
    "ngsh": 7,
    "nkw": 7,
    "nsgr": 7,
    "nsn": 7,
    "nspl": 7,
    "nthl": 7,
    "pbr": 7,
    "pg": 7,
    "pk": 7,
    "ptn": 7,
    "rchm": 7,
    "rdc": 7,
    "rdsm": 7,
    "rld": 7,
    "rsch": 7,
    "rsn": 7,
    "rthb": 7,
    "skr": 7,
    "thb": 7,
    "tk": 7,
    "zl": 7,
    "bg": 6,
    "bgl": 6,
    "bk": 6,
    "cb": 6,
    "dk": 6,
    "ftw": 6,
    "ghf": 6,
    "gnl": 6,
    "gst": 6,
    "hd": 6,
    "hv": 6,
    "hw": 6,
    "lds": 6,
    "lfl": 6,
    "lkh": 6,
    "llp": 6,
    "lq": 6,
    "lthf": 6,
    "mfr": 6,
    "mj": 6,
    "nchb": 6,
    "ndc": 6,
    "ndsh": 6,
    "ngc": 6,
    "nkst": 6,
    "ntz": 6,
    "psk": 6,
    "pstr": 6,
    "rkb": 6,
    "rks": 6,
    "rnt": 6,
    "schn": 6,
    "sdr": 6,
    "sfr": 6,
    "shp": 6,
    "stt": 6,
    "tchl": 6,
    "tcr": 6,
    "tgr": 6,
    "thdr": 6,
    "thst": 6,
    "tsc": 6,
    "tsch": 6,
    "tstr": 6,
    "wdl": 6,
    "xm": 6,
    "xq": 6,
    "zd": 6,
    "btf": 5,
    "ckc": 5,
    "cksm": 5,
    "cph": 5,
    "dbr": 5,
    "dch": 5,
    "ddh": 5,
    "dfl": 5,
    "dz": 5,
    "ffm": 5,
    "fn": 5,
    "ftn": 5,
    "kst": 5,
    "ldm": 5,
    "lkm": 5,
    "lks": 5,
    "lsp": 5,
    "ltm": 5,
    "mbd": 5,
    "mk": 5,
    "mstr": 5,
    "nctl": 5,
    "nkm": 5,
    "nsq": 5,
    "pht": 5,
    "pj": 5,
    "pph": 5,
    "rchb": 5,
    "rchpr": 5,
    "rkf": 5,
    "rldl": 5,
    "rnc": 5,
    "rnp": 5,
    "rnw": 5,
    "rstl": 5,
    "rthn": 5,
    "rtsh": 5,
    "rtsm": 5,
    "shh": 5,
    "skl": 5,
    "sst": 5,
    "tchf": 5,
    "tj": 5,
    "tsw": 5,
    "tts": 5,
    "vsk": 5,
    "wbr": 5,
    "wfl": 5,
    "wnb": 5,
    "wnh": 5,
    "wnst": 5,
    "wsl": 5,
    "xd": 5,
    "xn": 5,
    "bq": 4,
    "chf": 4,
    "ckg": 4,
    "ckj": 4,
    "ckkn": 4,
    "cksl": 4,
    "cksw": 4,
    "cp": 4,
    "cst": 4,
    "cth": 4,
    "dspr": 4,
    "ffs": 4,
    "fgh": 4,
    "fth": 4,
    "ghbr": 4,
    "ghtr": 4,
    "kd": 4,
    "lbl": 4,
    "lbr": 4,
    "lchr": 4,
    "lcr": 4,
    "ldsp": 4,
    "lfp": 4,
    "lgh": 4,
    "lkl": 4,
    "llfl": 4,
    "ltf": 4,
    "ltn": 4,
    "ltz": 4,
    "mbf": 4,
    "mbm": 4,
    "mcr": 4,
    "mpb": 4,
    "mpf": 4,
    "mphl": 4,
    "mphr": 4,
    "mpk": 4,
    "mpst": 4,
    "nchp": 4,
    "nck": 4,
    "ndbr": 4,
    "ndt": 4,
    "nkg": 4,
    "nkr": 4,
    "nscl": 4,
    "nspr": 4,
    "ntc": 4,
    "pch": 4,
    "pfl": 4,
    "pps": 4,
    "rdh": 4,
    "rgm": 4,
    "rkr": 4,
    "rksh": 4,
    "rlb": 4,
    "rlw": 4,
    "rmn": 4,
    "rms": 4,
    "rnbl": 4,
    "rnfl": 4,
    "rnn": 4,
    "rns": 4,
    "rnsh": 4,
    "rshr": 4,
    "rtbr": 4,
    "rthm": 4,
    "rtstr": 4,
    "schw": 4,
    "sphr": 4,
    "sthr": 4,
    "stpl": 4,
    "sts": 4,
    "stsc": 4,
    "tbl": 4,
    "tchc": 4,
    "tchp": 4,
    "tdr": 4,
    "thp": 4,
    "tpl": 4,
    "wch": 4,
    "wnf": 4,
    "wz": 4,
    "xtl": 4,
    "bch": 3,
    "bcr": 3,
    "bsp": 3,
    "chb": 3,
    "chh": 3,
    "chst": 3,
    "ckbr": 3,
    "ckcl": 3,
    "cksc": 3,
    "cksk": 3,
    "cksp": 3,
    "ckth": 3,
    "cts": 3,
    "dsw": 3,
    "ffh": 3,
    "ffn": 3,
    "ffsh": 3,
    "ffw": 3,
    "ftf": 3,
    "fts": 3,
    "gc": 3,
    "ghfl": 3,
    "ghg": 3,
    "ghp": 3,
    "ghtb": 3,
    "ghtsh": 3,
    "ghtsm": 3,
    "gsk": 3,
    "gtr": 3,
    "gz": 3,
    "hns": 3,
    "hsh": 3,
    "jp": 3,
    "lcl": 3,
    "lct": 3,
    "ldcr": 3,
    "ldp": 3,
    "ldsh": 3,
    "ldst": 3,
    "ldt": 3,
    "lfb": 3,
    "lfh": 3,
    "llbr": 3,
    "lml": 3,
    "lms": 3,
    "lpf": 3,
    "lpl": 3,
    "lpr": 3,
    "lsm": 3,
    "lstr": 3,
    "lthl": 3,
    "ltp": 3,
    "ltw": 3,
    "lx": 3,
    "mbk": 3,
    "mpm": 3,
    "mpsh": 3,
    "mptr": 3,
    "mq": 3,
    "mth": 3,
    "ncht": 3,
    "ndg": 3,
    "ndgr": 3,
    "ndk": 3,
    "ndsc": 3,
    "ndsl": 3,
    "ndsm": 3,
    "ngcr": 3,
    "ngk": 3,
    "ngsp": 3,
    "nksg": 3,
    "nns": 3,
    "nrh": 3,
    "nsfr": 3,
    "nskr": 3,
    "nsph": 3,
    "nssh": 3,
    "ntb": 3,
    "ntbr": 3,
    "ntg": 3,
    "ntsm": 3,
    "pdr": 3,
    "pgr": 3,
    "psc": 3,
    "psw": 3,
    "pth": 3,
    "pwr": 3,
    "rchch": 3,
    "rck": 3,
    "rdpl": 3,
    "rdt": 3,
    "rgn": 3,
    "rkh": 3,
    "rlp": 3,
    "rmh": 3,
    "rmst": 3,
    "rnd": 3,
    "rsph": 3,
    "rspr": 3,
    "rtc": 3,
    "rtgr": 3,
    "rthd": 3,
    "rthf": 3,
    "rtp": 3,
    "rx": 3,
    "schr": 3,
    "shg": 3,
    "shwh": 3,
    "skf": 3,
    "skn": 3,
    "skw": 3,
    "spn": 3,
    "ssc": 3,
    "ssr": 3,
    "sstr": 3,
    "stcl": 3,
    "std": 3,
    "stscr": 3,
    "sz": 3,
    "tchh": 3,
    "tchk": 3,
    "tcl": 3,
    "tq": 3,
    "tsb": 3,
    "tsm": 3,
    "ttw": 3,
    "wbl": 3,
    "wgr": 3,
    "wkn": 3,
    "wnc": 3,
    "wntr": 3,
    "wpl": 3,
    "wspr": 3,
    "wstr": 3,
    "wtr": 3,
    "xg": 3,
    "xsc": 3,
    "xscr": 3,
    "xsp": 3,
    "zw": 3,
    "bbs": 2,
    "bcl": 2,
    "bfl": 2,
    "bgr": 2,
    "bpr": 2,
    "bsl": 2,
    "bsph": 2,
    "bz": 2,
    "chbl": 2,
    "chcl": 2,
    "chd": 2,
    "chwh": 2,
    "ckcr": 2,
    "ckfr": 2,
    "ckgr": 2,
    "ckpl": 2,
    "cksn": 2,
    "dcl": 2,
    "dcr": 2,
    "dph": 2,
    "dpl": 2,
    "dq": 2,
    "dsc": 2,
    "dscr": 2,
    "dsk": 2,
    "dsp": 2,
    "dthw": 2,
    "fb": 2,
    "fc": 2,
    "fd": 2,
    "ffb": 2,
    "ffc": 2,
    "ffsc": 2,
    "fft": 2,
    "fk": 2,
    "fm": 2,
    "fpr": 2,
    "fst": 2,
    "ftb": 2,
    "ftp": 2,
    "gch": 2,
    "gdr": 2,
    "ggh": 2,
    "ggsh": 2,
    "ghc": 2,
    "ghdr": 2,
    "ghj": 2,
    "ghr": 2,
    "ghs": 2,
    "ghsh": 2,
    "ghtcl": 2,
    "ghwr": 2,
    "gnn": 2,
    "gsd": 2,
    "gsm": 2,
    "hlb": 2,
    "hmg": 2,
    "hnn": 2,
    "hrg": 2,
    "jj": 2,
    "kv": 2,
    "ldbr": 2,
    "ldc": 2,
    "ldgr": 2,
    "ldsm": 2,
    "lfc": 2,
    "lfk": 2,
    "lfm": 2,
    "lfn": 2,
    "lft": 2,
    "lfw": 2,
    "lj": 2,
    "lkc": 2,
    "lkn": 2,
    "llc": 2,
    "llg": 2,
    "llk": 2,
    "llpr": 2,
    "llsh": 2,
    "lltr": 2,
    "llwh": 2,
    "lmm": 2,
    "lmn": 2,
    "lmsg": 2,
    "lmsm": 2,
    "lpm": 2,
    "lsgr": 2,
    "lsk": 2,
    "lsw": 2,
    "ltb": 2,
    "ltc": 2,
    "lts": 2,
    "lwr": 2,
    "mbb": 2,
    "mbn": 2,
    "mbsk": 2,
    "mpstr": 2,
    "mptl": 2,
    "msk": 2,
    "msm": 2,
    "mz": 2,
    "nchw": 2,
    "ncm": 2,
    "nctn": 2,
    "ndcr": 2,
    "ndd": 2,
    "ndfl": 2,
    "ndq": 2,
    "ndsw": 2,
    "ndthr": 2,
    "ndwr": 2,
    "ngstr": 2,
    "ngthf": 2,
    "ngthl": 2,
    "ngthw": 2,
    "ngtr": 2,
    "nkp": 2,
    "nksh": 2,
    "nkt": 2,
    "nnk": 2,
    "nsd": 2,
    "nsfl": 2,
    "nsr": 2,
    "nss": 2,
    "nthm": 2,
    "nxt": 2,
    "pkn": 2,
    "psp": 2,
    "pspr": 2,
    "ptf": 2,
    "pthr": 2,
    "pts": 2,
    "rbst": 2,
    "rchg": 2,
    "rchn": 2,
    "rchr": 2,
    "rchsh": 2,
    "rchtr": 2,
    "rfm": 2,
    "rghm": 2,
    "rksm": 2,
    "rkt": 2,
    "rlst": 2,
    "rmw": 2,
    "rncr": 2,
    "rng": 2,
    "rnh": 2,
    "rnwr": 2,
    "rpn": 2,
    "rpsh": 2,
    "rsd": 2,
    "rspl": 2,
    "rsth": 2,
    "rtcl": 2,
    "rtd": 2,
    "rthp": 2,
    "rthq": 2,
    "rthst": 2,
    "sbl": 2,
    "sbr": 2,
    "sfl": 2,
    "sgl": 2,
    "shc": 2,
    "shcl": 2,
    "shk": 2,
    "skm": 2,
    "ssbl": 2,
    "ssbr": 2,
    "ssgr": 2,
    "stbr": 2,
    "stcr": 2,
    "stgl": 2,
    "stwh": 2,
    "stwr": 2,
    "swr": 2,
    "tdw": 2,
    "thbr": 2,
    "thpl": 2,
    "tkn": 2,
    "tsk": 2,
    "tsl": 2,
    "tspr": 2,
    "tth": 2,
    "tthr": 2,
    "twh": 2,
    "twr": 2,
    "tzp": 2,
    "vl": 2,
    "vts": 2,
    "wgl": 2,
    "wkb": 2,
    "wkw": 2,
    "wld": 2,
    "wls": 2,
    "wlw": 2,
    "wnbr": 2,
    "wnm": 2,
    "wnp": 2,
    "wns": 2,
    "wnsm": 2,
    "wnstr": 2,
    "wsm": 2,
    "wsp": 2,
    "wsw": 2,
    "wv": 2,
    "xsh": 2,
    "xst": 2,
    "xth": 2,
    "zc": 2,
    "zt": 2,
    "bcd": 1,
    "bdr": 1,
    "bfr": 1,
    "blr": 1,
    "bpl": 1,
    "brd": 1,
    "bsq": 1,
    "bsw": 1,
    "bth": 1,
    "btm": 1,
    "bwh": 1,
    "cbm": 1,
    "cf": 1,
    "cg": 1,
    "chc": 1,
    "chcr": 1,
    "chsh": 1,
    "chsr": 1,
    "chsst": 1,
    "chtm": 1,
    "chts": 1,
    "chtsm": 1,
    "ckch": 1,
    "ckdr": 1,
    "ckfl": 1,
    "ckk": 1,
    "cksch": 1,
    "ckscr": 1,
    "ckthr": 1,
    "cktr": 1,
    "ckwh": 1,
    "cphr": 1,
    "cthl": 1,
    "ctsh": 1,
    "ctz": 1,
    "cw": 1,
    "ddb": 1,
    "ddf": 1,
    "ddm": 1,
    "ddn": 1,
    "dds": 1,
    "dfr": 1,
    "dgl": 1,
    "dgr": 1,
    "dgsh": 1,
    "dkn": 1,
    "dpr": 1,
    "dsl": 1,
    "dsn": 1,
    "dthl": 1,
    "dthr": 1,
    "dwh": 1,
    "dzh": 1,
    "fch": 1,
    "ffbr": 1,
    "ffd": 1,
    "ffg": 1,
    "ffpr": 1,
    "ffsk": 1,
    "ffspr": 1,
    "fj": 1,
    "fkl": 1,
    "fnm": 1,
    "fs": 1,
    "fsk": 1,
    "ftg": 1,
    "ftgr": 1,
    "fthl": 1,
    "ftr": 1,
    "ftsp": 1,
    "ftt": 1,
    "fw": 1,
    "gfr": 1,
    "ggn": 1,
    "ggpl": 1,
    "ggs": 1,
    "ghch": 1,
    "ghd": 1,
    "ghsc": 1,
    "ghsch": 1,
    "ghsp": 1,
    "ghst": 1,
    "ghstr": 1,
    "ghtc": 1,
    "ghtcr": 1,
    "ghtdr": 1,
    "ghtg": 1,
    "ghthl": 1,
    "ghtj": 1,
    "ghtpr": 1,
    "ghtsc": 1,
    "ghtst": 1,
    "ghtt": 1,
    "ghtv": 1,
    "ghwh": 1,
    "gj": 1,
    "gk": 1,
    "gnb": 1,
    "gnc": 1,
    "gnf": 1,
    "gnh": 1,
    "gnp": 1,
    "gnsh": 1,
    "gnt": 1,
    "gsc": 1,
    "gsl": 1,
    "gsn": 1,
    "gssp": 1,
    "gsw": 1,
    "gth": 1,
    "gv": 1,
    "gwh": 1,
    "hf": 1,
    "hj": 1,
    "hlst": 1,
    "hlw": 1,
    "hmm": 1,
    "hms": 1,
    "hnhnm": 1,
    "hrst": 1,
    "hrw": 1,
    "htr": 1,
    "hvh": 1,
    "jh": 1,
    "jr": 1,
    "kc": 1,
    "kcr": 1,
    "khs": 1,
    "kpl": 1,
    "kpr": 1,
    "ksb": 1,
    "ksd": 1,
    "kthr": 1,
    "lchm": 1,
    "lddr": 1,
    "lfcl": 1,
    "lff": 1,
    "lfs": 1,
    "lfsb": 1,
    "lfsk": 1,
    "lfth": 1,
    "lftht": 1,
    "lftw": 1,
    "lgl": 1,
    "lkb": 1,
    "lkf": 1,
    "lkp": 1,
    "lksh": 1,
    "lksl": 1,
    "lksr": 1,
    "lkst": 1,
    "lkt": 1,
    "llcl": 1,
    "lldr": 1,
    "llfr": 1,
    "llgr": 1,
    "lll": 1,
    "llpl": 1,
    "llsn": 1,
    "llsp": 1,
    "llspr": 1,
    "llsw": 1,
    "llth": 1,
    "llv": 1,
    "llwr": 1,
    "lmbr": 1,
    "lmcr": 1,
    "lmr": 1,
    "lmsd": 1,
    "lmsf": 1,
    "lmsh": 1,
    "lmw": 1,
    "lnh": 1,
    "lphl": 1,
    "lphth": 1,
    "lpn": 1,
    "lptr": 1,
    "lpw": 1,
    "lsb": 1,
    "lsc": 1,
    "lshm": 1,
    "lsl": 1,
    "lsth": 1,
    "ltch": 1,
    "lths": 1,
    "lthw": 1,
    "ltsch": 1,
    "ltschm": 1,
    "ltsf": 1,
    "ltspr": 1,
    "ltst": 1,
    "ltt": 1,
    "lwh": 1,
    "mbbr": 1,
    "mbcl": 1,
    "mbpr": 1,
    "mbs": 1,
    "mbscr": 1,
    "mbsh": 1,
    "mbst": 1,
    "mcc": 1,
    "mck": 1,
    "mcl": 1,
    "mdm": 1,
    "mdr": 1,
    "mmh": 1,
    "mnl": 1,
    "mnn": 1,
    "mpbl": 1,
    "mpc": 1,
    "mpd": 1,
    "mptn": 1,
    "mpw": 1,
    "msb": 1,
    "msc": 1,
    "msd": 1,
    "msl": 1,
    "mstv": 1,
    "mtr": 1,
    "mtsch": 1,
    "mwh": 1,
    "nchc": 1,
    "nchf": 1,
    "ncp": 1,
    "ndch": 1,
    "ndcl": 1,
    "nddr": 1,
    "ndgl": 1,
    "ndj": 1,
    "ndsbr": 1,
    "ndsk": 1,
    "ndsp": 1,
    "ndspr": 1,
    "ndsth": 1,
    "ndstr": 1,
    "ndth": 1,
    "ndtr": 1,
    "ndwh": 1,
    "ngp": 1,
    "ngsg": 1,
    "ngthn": 1,
    "ngthr": 1,
    "ngtz": 1,
    "ngwh": 1,
    "nhh": 1,
    "nkc": 1,
    "nkfl": 1,
    "nkpl": 1,
    "nksm": 1,
    "nkwr": 1,
    "nnh": 1,
    "nnw": 1,
    "nnwfn": 1,
    "nnwn": 1,
    "nps": 1,
    "nschl": 1,
    "ntch": 1,
    "ntd": 1,
    "ntgr": 1,
    "nths": 1,
    "ntj": 1,
    "ntt": 1,
    "nttr": 1,
    "ntv": 1,
    "ntwh": 1,
    "nxm": 1,
    "nxr": 1,
    "pbl": 1,
    "pcl": 1,
    "pfr": 1,
    "phsl": 1,
    "psl": 1,
    "psm": 1,
    "psn": 1,
    "pspl": 1,
    "psq": 1,
    "ptc": 1,
    "pthl": 1,
    "ptm": 1,
    "pv": 1,
    "pwh": 1,
    "ql": 1,
    "rbs": 1,
    "rchf": 1,
    "rchp": 1,
    "rdbr": 1,
    "rdgr": 1,
    "rdk": 1,
    "rdp": 1,
    "rdsp": 1,
    "rdst": 1,
    "rdsw": 1,
    "rdv": 1,
    "rfb": 1,
    "rfd": 1,
    "rff": 1,
    "rfg": 1,
    "rfh": 1,
    "rfsk": 1,
    "rggr": 1,
    "rghb": 1,
    "rghbr": 1,
    "rghf": 1,
    "rgschr": 1,
    "rgst": 1,
    "rkd": 1,
    "rkscr": 1,
    "rksp": 1,
    "rlc": 1,
    "rlcr": 1,
    "rlf": 1,
    "rlfr": 1,
    "rlh": 1,
    "rlk": 1,
    "rll": 1,
    "rmb": 1,
    "rmc": 1,
    "rmch": 1,
    "rmcl": 1,
    "rmg": 1,
    "rmgl": 1,
    "rmp": 1,
    "rmpl": 1,
    "rmr": 1,
    "rmsp": 1,
    "rmth": 1,
    "rmthl": 1,
    "rnbr": 1,
    "rnk": 1,
    "rnpl": 1,
    "rnsm": 1,
    "rnsn": 1,
    "rnsp": 1,
    "rnv": 1,
    "rphr": 1,
    "rpm": 1,
    "rrf": 1,
    "rrfl": 1,
    "rrnh": 1,
    "rrst": 1,
    "rrw": 1,
    "rsb": 1,
    "rscht": 1,
    "rschw": 1,
    "rshb": 1,
    "rshch": 1,
    "rshl": 1,
    "rshn": 1,
    "rsht": 1,
    "rsq": 1,
    "rstb": 1,
    "rstf": 1,
    "rstn": 1,
    "rstw": 1,
    "rstwh": 1,
    "rtcr": 1,
    "rtgl": 1,
    "rthbr": 1,
    "rthc": 1,
    "rthdr": 1,
    "rthg": 1,
    "rthh": 1,
    "rthpl": 1,
    "rthsh": 1,
    "rthwh": 1,
    "rtq": 1,
    "rtsch": 1,
    "rtst": 1,
    "rtsw": 1,
    "rtt": 1,
    "rtwh": 1,
    "rtwr": 1,
    "schl": 1,
    "schm": 1,
    "schsch": 1,
    "shd": 1,
    "shfl": 1,
    "shq": 1,
    "shsk": 1,
    "shst": 1,
    "shv": 1,
    "shz": 1,
    "skc": 1,
    "skj": 1,
    "slt": 1,
    "sml": 1,
    "spb": 1,
    "sscr": 1,
    "ssfl": 1,
    "ssg": 1,
    "ssj": 1,
    "sspl": 1,
    "sspr": 1,
    "sssh": 1,
    "stfr": 1,
    "stg": 1,
    "stgr": 1,
    "stkn": 1,
    "stpr": 1,
    "stsh": 1,
    "stsph": 1,
    "stv": 1,
    "stz": 1,
    "tchbl": 1,
    "tchbr": 1,
    "tchcr": 1,
    "tchd": 1,
    "tchdr": 1,
    "tchfl": 1,
    "tchn": 1,
    "tchst": 1,
    "tchstr": 1,
    "tcht": 1,
    "tfr": 1,
    "tgl": 1,
    "thbl": 1,
    "thc": 1,
    "thcl": 1,
    "thcr": 1,
    "thd": 1,
    "thg": 1,
    "thml": 1,
    "thsh": 1,
    "thsk": 1,
    "thsm": 1,
    "thsp": 1,
    "tht": 1,
    "thth": 1,
    "thv": 1,
    "tph": 1,
    "tsf": 1,
    "ttd": 1,
    "ttm": 1,
    "ttv": 1,
    "tzg": 1,
    "tzkr": 1,
    "tzn": 1,
    "tzschk": 1,
    "tzsk": 1,
    "vd": 1,
    "vn": 1,
    "wcl": 1,
    "wdn": 1,
    "wdst": 1,
    "wgh": 1,
    "wj": 1,
    "wkl": 1,
    "wks": 1,
    "wksb": 1,
    "wlb": 1,
    "wldr": 1,
    "wlf": 1,
    "wll": 1,
    "wln": 1,
    "wlst": 1,
    "wnd": 1,
    "wng": 1,
    "wnn": 1,
    "wnr": 1,
    "wnsf": 1,
    "wnsh": 1,
    "wnsl": 1,
    "wnsp": 1,
    "wnthr": 1,
    "wq": 1,
    "wsb": 1,
    "wsc": 1,
    "wsk": 1,
    "wsr": 1,
    "wss": 1,
    "wthf": 1,
    "wthr": 1,
    "wwh": 1,
    "wwr": 1,
    "xfl": 1,
    "xgl": 1,
    "xk": 1,
    "xr": 1,
    "xstr": 1,
    "xsw": 1,
    "xthl": 1,
    "xtm": 1,
    "xx": 1,
    "zg": 1,
    "zj": 1,
    "zm": 1,
    "zn": 1,
    "zp": 1,
    "zq": 1,
    "zv": 1,
    "zzs": 1
}

In short I used this regex '[bcdfghjklmnpqrstvwxyz]+' on the comma joined corpus to find all consonant chunks and turned it into a Counter and sorted it in the order of (-x[1], x[0]) and dumped it using json( all in one line).

Program

import json
import re
import sys
from pathlib import Path
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from PyQt6.sip import delete

DIRECTORY = str(Path(__file__).absolute().parent).replace('\\', '/')

ALIGNMENT = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
FLAG = Qt.ItemFlag

QGROUPBOXHIGHLIGHT = """
    QGroupBox {
        border: 3px groove #4080ff;
        border-radius: 6px;
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #0080c0, stop: 1 #4080c0);
    }
    QLabel {
        border: 0px solid black;
        color: #00ffff;
    }
"""

QGROUPBOXNORMAL = """
    QGroupBox {
        border: 3px groove #204080;
        border-radius: 6px;
        background: #002050;
    }
    QLabel {
        border: 0px solid black;
        color: #87CEEB;
    }
"""

MAINSTYLE = """
    QFrame {{
        border: 3px groove #123456;
        border-radius: 6px;
    }}
    QHeaderView::section {{
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #004060, stop: 1 #102372);
        color: #00ffff;
        border: 4px groove #102030;
        padding-left: 4px;
    }}
    QTableWidget {{
        background: #002050;
        alternate-background-color: #200050;
        show-decoration-selected: 1;
    }}
    QTableWidget QTableCornerButton::section {{
        background-color: #003b6f;
        border: 2px solid #202040;
    }}
    QTableWidget::indicator {{
        border: 0px solid black;
        width: 16px;
        height: 16px;
    }}
    QTableWidget::indicator:unchecked {{
        image: url({0}/icons/checkbox-unchecked.png);
    }}
    QTableWidget::indicator:checked {{
        image: url({0}/icons/checkbox-checked.png);
    }}
    QTableWidget::indicator:indeterminate {{
        image: url({0}/icons/checkbox-indeterminate.png);
    }}
    QTableWidget::indicator:hover {{
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #b2ffff, stop: 1 #4080c0);
    }}
    QTableWidget::item {{
        border: 2px groove #204080;
        height: 20px;
        color: #73b0c8;
    }}
    QTableWidget::item:hover {{
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #244872, stop: 1 #00c0ff);
        color: #b2ffff;
        border: 2px groove #bfcde4;
    }}
    QTableWidget::item:selected {{
        border: 3px groove #4080ff;
        border-radius: 6px;
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #0080c0, stop: 1 #4080c0);
        color: #00ffff;
    }}
    QScrollBar::vertical {{
        border: 2px solid #204060;
        background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #004060, stop: 1 #00b2e4);
        position: absolute;
        width: 16px;
    }}
    QScrollBar::handle:vertical {{
        min-height: 32px;
        background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #00c0ff, stop: 1 #102372);
        margin: 18px;
    }}
    QTextEdit {{
        border: 2px solid #306090;
        border-radius: 6px;
        background: #202040;
        color: #b2ffff;
    }}
    QGroupBox {{
        border: 3px groove #204080;
        border-radius: 6px;
        background: #002050;
        color: #b2ffff;
    }}
    QGroupBox::title {{
        top: 3px;
        left: 3px;
    }}
    QGroupBox::indicator {{
        border: 0px solid black;
        width: 14px;
        height: 14px;
    }}
    QGroupBox::indicator:unchecked {{
        image: url({0}/icons/checkbox-unchecked.png);
    }}
    QGroupBox::indicator:checked {{
        image: url({0}/icons/checkbox-checked.png);
    }}
    QGroupBox::indicator:indeterminate {{
        image: url({0}/icons/checkbox-indeterminate.png);
    }}
    QGroupBox::indicator:hover {{
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #b2ffff, stop: 1 #4080c0);
    }}
    QCheckBox {{
        color: #8080ff;
        spacing: 8px;
    }}
    QCheckBox::indicator {{
        border: 0px solid black;
        width: 16px;
        height: 16px;
    }}
    QCheckBox::indicator:unchecked {{
        image: url({0}/icons/checkbox-unchecked.png);
    }}
    QCheckBox::indicator:checked {{
        image: url({0}/icons/checkbox-checked.png);
    }}
    QCheckBox::indicator:indeterminate {{
        image: url({0}/icons/checkbox-indeterminate.png);
    }}
    QCheckBox::indicator:hover {{
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #b2ffff, stop: 1 #4080c0);
    }}
    QCheckBox:hover {{
        color: blue;
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #00ffff, stop: 1 #4080c0);
    }}
    QCheckBox:pressed {{
        color: #00ffff;
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #005572, stop: 1 #4080c0);
    }}
    QMainWindow {{
        background: #202030;
    }}
    QPushButton:disabled {{
        border: 3px outset #73808d;
        border-radius: 4px;
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #73808d, stop: 0.75 #5b7d89, stop: 1 #73808d);
        color: #4c4c73;
    }}
    QPushButton {{
        border: 3px outset #4080c0;
        border-radius: 4px;
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #00aae4, stop: 1 #4080c0);
        color: blue;
    }}
    QPushButton:hover {{
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #b2ffff, stop: 1 #4080c0);
    }}
    QPushButton:pressed {{
        border: 3px inset #4080c0;
        background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 0.75, x3: 0, y3: 1, stop: 0 #4080c0, stop: 0.75 #005572, stop: 1 #4080c0);
        color: #00ffff;
    }}
""".format(DIRECTORY)


class Font(QFont):
    def __init__(self, size):
        super().__init__()
        self.setFamily("Noto Serif")
        self.setStyleHint(QFont.StyleHint.Times)
        self.setStyleStrategy(QFont.StyleStrategy.PreferAntialias)
        self.setPointSize(size)
        self.setHintingPreference(QFont.HintingPreference.PreferFullHinting)


class Button(QPushButton):
    def __init__(self, text):
        super().__init__()
        font = Font(8)
        self.setFont(font)
        self.setFixedSize(60, 20)
        self.setText(text)


class Editor(QTextEdit):
    doubleClicked = pyqtSignal(QTextEdit)
    
    def __init__(self, text):
        super().__init__()
        self.setReadOnly(True)
        font = Font(9)
        self.setFont(font)
        self.setText(text)
        self.setFixedHeight(30)
        self.setFixedWidth(80)
        self.setLayoutDirection(Qt.LayoutDirection.LeftToRight)
        self.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)

    def mouseDoubleClickEvent(self, e: QMouseEvent) -> None:
        self.doubleClicked.emit(self)
        return super().mouseDoubleClickEvent(e)


class TextCell(QTextEdit):    
    def __init__(self):
        super().__init__()
        self.setReadOnly(True)
        font = Font(9)
        self.setFont(font)
        self.setFixedHeight(30)
        self.setFixedWidth(200)
        self.setLayoutDirection(Qt.LayoutDirection.LeftToRight)
        self.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)


class Table(QTableWidget):
    checkStates = [Qt.CheckState.Unchecked, Qt.CheckState.Checked]
    def __init__(self):
        super().__init__()
        self.setColumnCount(3)
        font = Font(9)
        self.setFont(font)
        self.verticalHeader().setFixedWidth(50)
        self.horizontalHeader().setFont(font)
        self.setHorizontalHeaderLabels(['Consonant', 'Frequency', 'Breaked'])
        self.setColumnWidth(0, 80)
        self.setColumnWidth(1, 80)
        self.horizontalHeader().setStretchLastSection(True)
        self.setAlternatingRowColors(True)
        self.lastChecked = None
        self.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
        for k, v in json.loads(Path('D:/consonant_chunks.json').read_text()).items():
            self.addEntry(k, v)
        self.cellClicked.connect(self.onClick)
        self.cellChanged.connect(self.onCheck)
    
    def addEntry(self, key, val):
        row = self.rowCount()
        key = QTableWidgetItem(key)
        key.setFlags(FLAG.ItemIsEnabled | FLAG.ItemIsSelectable | FLAG.ItemIsUserCheckable)
        key.setCheckState(Qt.CheckState.Unchecked)
        self.insertRow(row)
        self.setItem(row, 0, key)
        val = QTableWidgetItem(str(val))
        val.setFlags(FLAG.ItemIsEnabled | FLAG.ItemIsSelectable)
        self.setItem(row, 1, val)
        three = QTableWidgetItem('')
        three.setFlags(FLAG.ItemIsEnabled | FLAG.ItemIsSelectable)
        self.setItem(row, 2, three)
    
    def onClick(self, row, column):
        item = self.item(row, 0)
        state_index = item.checkState() == Qt.CheckState.Checked
        other = bool(1 - state_index)
        new_state = Table.checkStates[other]
        for i in range(3):
            self.item(row, i).setSelected(other)
        item.setCheckState(new_state)
    
    def onCheck(self, row, column):
        if column == 0:
            self.lastChecked = row
            item = self.item(row, 0)
            text = item.text()
            text_lines = textpane.toPlainText().splitlines()
            if item.checkState() == Qt.CheckState.Checked:
                text_lines.append(text)
            else:
                text_lines.remove(text)
            textpane.setText('\n'.join(text_lines))
    
    def process(self):
        pattern = '(' + '|'.join(sorted(textpane.toPlainText().splitlines(), key=lambda x: -len(x))) + ')'
        for i in range(self.rowCount()):
            key = self.item(i, 0).text()
            text = ', '.join(i for i in re.split(pattern, key) if i)
            self.setItem(i, 2, QTableWidgetItem(text))
    
    def removeSelected(self):
        row = self.lastChecked
        if row is not None:
            key = self.item(row, 0).text()
            text_lines = textpane.toPlainText().splitlines()
            if key in text_lines:
                text_lines.remove(key)
                textpane.setText('\n'.join(text_lines))
            self.removeRow(row)
            self.lastChecked = None


class TextPane(QTextEdit):
    def __init__(self):
        super().__init__()
        self.setReadOnly(True)
        font = Font(9)
        self.setFont(font)
        self.setLayoutDirection(Qt.LayoutDirection.LeftToRight)
        self.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)


class ScrollArea(QScrollArea):
    def __init__(self, parent):
        super().__init__()
        self.setParent(parent)
        self.setWidgetResizable(True)
        self.Widget = QWidget()
        palette = QPalette()
        brush = QBrush(QColor(0, 32, 64, 255))
        brush.setStyle(Qt.BrushStyle.SolidPattern)
        palette.setBrush(QPalette.ColorGroup.Active, QPalette.ColorRole.Window, brush)
        palette.setBrush(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Window, brush)
        self.Widget.setPalette(palette)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
        self.Layout = QVBoxLayout(self.Widget)
        self.Layout.setAlignment(ALIGNMENT)
        self.Layout.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize)
        self.setWidget(self.Widget)
        self.setAlignment(ALIGNMENT)
        self.setFrameShape(QFrame.Shape.Box)
        self.setFrameShadow(QFrame.Shadow.Raised)


class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        rect = app.primaryScreen().availableGeometry()
        self.resize(int(0.9 * rect.width()), int(0.9 * rect.height()))
        self.setMinimumSize(int(0.67 * rect.width()), int(0.67 * rect.height()))
        frame = self.frameGeometry()
        center = self.screen().availableGeometry().center()
        frame.moveCenter(center)
        self.move(frame.topLeft())
        font = Font(10)
        self.setFont(font)
        self.centralwidget = QWidget(self)
        self.vbox = QVBoxLayout(self.centralwidget)
        self.table = table
        self.bottom = QVBoxLayout()
        self.vbox.addWidget(self.table)
        self.vbox.addLayout(self.bottom)
        self.setCentralWidget(self.centralwidget)
        self.setStyleSheet(MAINSTYLE)

if __name__ == '__main__':
    app = QApplication([])
    app.setStyle("Fusion")
    textpane = TextPane()
    table = Table()
    window = Window()
    window.bottom.addWidget(textpane)
    buttonbar = QHBoxLayout()
    sort_button = Button('Sort')
    def sort_text():
        textpane.setText('\n'.join(sorted(textpane.toPlainText().splitlines())))
    sort_button.clicked.connect(sort_text)
    export_button = Button('Export')
    def export():
        Path('D:/nonbreaking_consonants.txt').write_text(textpane.toPlainText())
    export_button.clicked.connect(export)
    process_button = Button('Process')
    process_button.clicked.connect(table.process)
    delete_button = Button('Delete')
    delete_button.clicked.connect(table.removeSelected)
    buttonbar.addWidget(sort_button)
    buttonbar.addWidget(export_button)
    buttonbar.addWidget(process_button)
    buttonbar.addWidget(delete_button)
    window.bottom.addLayout(buttonbar)
    window.showMaximized()
    sys.exit(app.exec())

Screenshot: enter image description here

Let me explain, the window is organized using a QVBoxLayout, in the upper half of the layout is a QScrollArea, the lower half are a QTextEdit and three buttons: Sort, Export and Process.

All textboxs are read-only.

The QScrollArea contains exactly 1615 QGroupBoxs, each QGroupBox corresponds to exactly one entry (key-value pair) in the dictionary.

Each QGroupBox contains a QVBoxLayout, the QVBoxLayout has two QHBoxLayouts, the upper layout contains three QTextEdits, the first two are on the left and the last one is on the right. The lower layout contains a QPushButton named Delete on the right, initially hidden.

The first corresponds to the key, the second corresponds to value, the third is initialy empty. Double clicking either of the first two textboxs reveals the delete button.

The groupboxs are checkable, if you check the groupbox, it will highlight itself, and add the text in its first textbox to the big textbox in the lower half, uncheck it dehighlights itself and removes the text. Delete button deletes the groupbox and deletes its key from the big textbox.

Sort button sorts the text in the big textbox, export button exports the text in the textbox to a file, and process button composes a regex using the text in the big textbox and processes the keys in each groupbox using inclusive re.split and writes the result to the third textbox in the respective groupbox.

How can it be improved?

Currently I can think of two improvements: the program is slow to start and uses considerable CPU time on starting, I think it is caused by creating all those QGroupBoxs and I think it will be better implemented as QTableView or QTableWidget but I don't know exactly how to do this, I am very inexperienced in GUI programming.

And the groupboxes, currently I can only click precisely the checkbox to change the check status, and I want that to be changed, so that I can click anywhere inside the groupbox to change the checkstatus without having to click the checkbox exactly...

Are there any other possible improvements?


Update

Icons: checkbox-checked.png enter image description here

checkbox-disabled.png enter image description here

checkbox-indeterminate.png enter image description here

checkbox-unchecked.png enter image description here

Put these icons in a folder named icons at the directory where you put the script (icons folder should be a subdirectory of the parent directory of the script path).


Update

enter image description here

Reimplemented my idea using QTableWidget, it was easier than I originally thought, now the program is much better, I am planning to add more columns and add tabs and add something to the right side, but for now I have done what I originally intended when I wrote the program.

However there is still a minor problem that I am unable to solve, I want to highlight the entire rows where the checkbox is checked, not just the cell checked, and mouse hover also highlight entire rows, but dabbling with QStyleSheet only highlights the individual cells and not the rows...

I of course have Googled it and simply put Google will never return what you want if you type a keyword that is a sentence, all methods I have found can only highlight the cell and not the row, which I have already used...

I have tried to automatically select all cells in a row, that does highlight the entire row, but all rows previously checked are dehighlighted...

How can I highlight entire rows?


Update

I figured it out all by myself, and Google is of absolutely no use, it turns out I need to set multiselection mode... QTableWidget defaults to extended selection mode (or single selection mode I dunno, what's important is it isn't what I want)...

enter image description here

\$\endgroup\$
2
  • \$\begingroup\$ Side note, the third column's label is not a typo, yes it is nonstandard, but I deliberately chose it because the meaning I tried to express through the past participle isn't the usual meaning associated with broken. A string of letters cannot be broken, it cannot stop being functional or integral, but they can indeed be broken into smaller strings, however the meaning of this broken is different from what broken usually means when it is used alone, for the lack of a better word I chose that "typo", I am busy improving my code just so you know... \$\endgroup\$ Nov 11, 2021 at 13:09
  • \$\begingroup\$ This project as a whole sounds complex and wonderful, congratulations. If you are looking into improving the UI or in particular its look and feel, please take look at qt-material. This module can convert the UI elements to resemble the material UI from Android. The XML based stylesheets are supported, making custom themes easy too. pypi.org/project/qt-material \$\endgroup\$ Nov 13, 2021 at 19:03

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.