stenodojo

stenodojo
git clone git@git.zachrice.app:repos/stenodojo.git
Log | Files | Refs

phrases.py (11563B)


      1 """Jeff's Phrasing dictionary integration for stenography practice.
      2 
      3 Loads the Jeff's Phrasing Plover dictionary plugin and provides functions
      4 to generate phrasing-based practice content.
      5 """
      6 
      7 import os
      8 import random
      9 import sys
     10 
     11 # ---------------------------------------------------------------------------
     12 # Import Jeff's Phrasing dictionary
     13 # ---------------------------------------------------------------------------
     14 
     15 JEFF_PHRASING = None
     16 
     17 _jeff_phrasing_dir = os.path.expanduser(
     18     "~/.config/plover/dicts/jeff-phrasing"
     19 )
     20 
     21 try:
     22     if _jeff_phrasing_dir not in sys.path:
     23         sys.path.insert(0, _jeff_phrasing_dir)
     24     import importlib
     25     JEFF_PHRASING = importlib.import_module("jeff-phrasing")
     26 except Exception:
     27     JEFF_PHRASING = None
     28 
     29 # ---------------------------------------------------------------------------
     30 # Starters and enders used to generate phrase combinations
     31 # ---------------------------------------------------------------------------
     32 
     33 # Pronoun starters: stroke -> display word
     34 STARTER_STROKES = {
     35     "SWR": "I",
     36     "KPWR": "you",
     37     "KWHR": "he",
     38     "SKWHR": "she",
     39     "KPWH": "it",
     40     "TWR": "we",
     41     "TWH": "they",
     42 }
     43 
     44 # Common ender strokes (present tense only for generation)
     45 ENDER_STROKES = {
     46     "RB": "ask",
     47     "B": "be",
     48     "RPBG": "become",
     49     "BL": "believe",
     50     "RBLG": "call",
     51     "BGS": "can",
     52     "RZ": "care",
     53     "PBGZ": "change",
     54     "BG": "come",
     55     "RP": "do",
     56     "PGS": "expect",
     57     "LT": "feel",
     58     "PBLG": "find",
     59     "RG": "forget",
     60     "GS": "get",
     61     "GZ": "give",
     62     "G": "go",
     63     "GT": "go to",
     64     "T": "have",
     65     "TS": "have to",
     66     "PZ": "happen",
     67     "PG": "hear",
     68     "RPS": "hope",
     69     "PLG": "imagine",
     70     "PBGS": "keep",
     71     "PB": "know",
     72     "RPBS": "learn",
     73     "LS": "let",
     74     "BLG": "like",
     75     "LZ": "live",
     76     "L": "look",
     77     "LG": "love",
     78     "RPBL": "make",
     79     "PL": "may",
     80     "PBL": "mean",
     81     "PLZ": "move",
     82     "PBLGS": "must",
     83     "RPG": "need",
     84     "RPGT": "need to",
     85     "PS": "put",
     86     "RPL": "remember",
     87     "R": "run",
     88     "BS": "say",
     89     "S": "see",
     90     "PLS": "seem",
     91     "RBL": "shall",
     92     "RBGS": "will",
     93     "RBGSZ": "would",
     94     "RBS": "wish",
     95     "RBG": "work",
     96     "RBT": "take",
     97     "RLT": "tell",
     98     "PBG": "think",
     99     "RT": "try",
    100     "RPB": "understand",
    101     "Z": "use",
    102     "P": "want",
    103     "PT": "want to",
    104 }
    105 
    106 # Middle/structure modifiers to combine with starters + enders
    107 MIDDLE_STROKES = {
    108     "": "simple present/past",
    109     "*": "negative",
    110     "A": "can/could",
    111     "A*": "can't/couldn't",
    112     "O": "shall/should",
    113     "AO": "will/would",
    114     "AO*": "won't/wouldn't",
    115 }
    116 
    117 # ---------------------------------------------------------------------------
    118 # Phrase generation via Jeff's Phrasing lookup
    119 # ---------------------------------------------------------------------------
    120 
    121 def _try_lookup(stroke):
    122     """Attempt to look up a single stroke via Jeff's Phrasing. Returns text or None."""
    123     if JEFF_PHRASING is None:
    124         return None
    125     try:
    126         return JEFF_PHRASING.lookup((stroke,)).strip()
    127     except (KeyError, TypeError, AttributeError):
    128         return None
    129 
    130 
    131 def _build_stroke(starter, middle, ender):
    132     """Combine starter, middle, and ender stroke fragments into a single stroke string."""
    133     import re
    134     hyphen_chars = re.compile(r"[AO*EU-]")
    135 
    136     combined = starter + middle + ender
    137 
    138     # Add hyphen between left and right bank when needed
    139     if not hyphen_chars.search(starter + middle) and ender:
    140         combined = starter + middle + "-" + ender
    141 
    142     return combined
    143 
    144 
    145 def _generate_phrase_list():
    146     """Build a list of (phrase_text, stroke) tuples from starter + ender combinations."""
    147     if JEFF_PHRASING is None:
    148         return []
    149 
    150     phrases = []
    151     seen = set()
    152 
    153     # Simple starter + ender (present tense)
    154     for s_stroke in STARTER_STROKES:
    155         for e_stroke in ENDER_STROKES:
    156             stroke = _build_stroke(s_stroke, "", e_stroke)
    157             text = _try_lookup(stroke)
    158             if text and text not in seen:
    159                 phrases.append((text, stroke))
    160                 seen.add(text)
    161 
    162     # Starter + middle + ender
    163     for s_stroke in STARTER_STROKES:
    164         for m_stroke in MIDDLE_STROKES:
    165             if not m_stroke:
    166                 continue
    167             for e_stroke in ENDER_STROKES:
    168                 stroke = _build_stroke(s_stroke, m_stroke, e_stroke)
    169                 text = _try_lookup(stroke)
    170                 if text and text not in seen:
    171                     phrases.append((text, stroke))
    172                     seen.add(text)
    173 
    174     # Past tense: starter + ender with D suffix
    175     for s_stroke in STARTER_STROKES:
    176         for e_stroke, verb in ENDER_STROKES.items():
    177             past_stroke = e_stroke + "D" if not e_stroke.endswith("D") else e_stroke
    178             stroke = _build_stroke(s_stroke, "", past_stroke)
    179             text = _try_lookup(stroke)
    180             if text and text not in seen:
    181                 phrases.append((text, stroke))
    182                 seen.add(text)
    183 
    184     # Structure variations: be/have forms
    185     structure_mods = ["E", "F", "EF"]  # be, have, have been
    186     for s_stroke in STARTER_STROKES:
    187         for struct in structure_mods:
    188             for e_stroke in ENDER_STROKES:
    189                 stroke = _build_stroke(s_stroke, struct, e_stroke)
    190                 text = _try_lookup(stroke)
    191                 if text and text not in seen:
    192                     phrases.append((text, stroke))
    193                     seen.add(text)
    194 
    195     return phrases
    196 
    197 
    198 # ---------------------------------------------------------------------------
    199 # Pre-computed phrase cache
    200 # ---------------------------------------------------------------------------
    201 
    202 _PHRASE_CACHE = []
    203 _PHRASE_TEXT_SET = set()
    204 
    205 
    206 def _init_cache():
    207     global _PHRASE_CACHE, _PHRASE_TEXT_SET
    208     if not _PHRASE_CACHE:
    209         _PHRASE_CACHE = _generate_phrase_list()
    210         _PHRASE_TEXT_SET = {p[0] for p in _PHRASE_CACHE}
    211 
    212 
    213 _init_cache()
    214 
    215 # ---------------------------------------------------------------------------
    216 # Public API
    217 # ---------------------------------------------------------------------------
    218 
    219 def get_phrase_strokes(phrase_text):
    220     """Return a list of stroke tuples for the given phrase text using Jeff's reverse_lookup.
    221 
    222     Example return: [("SWRPB",), ("SWR-PB",)]
    223     Returns an empty list if Jeff's Phrasing is not available or the phrase is not found.
    224     """
    225     if JEFF_PHRASING is None:
    226         return []
    227     try:
    228         return JEFF_PHRASING.reverse_lookup(phrase_text)
    229     except Exception:
    230         return []
    231 
    232 
    233 def get_practice_phrases(count=20):
    234     """Return a list of random phrase texts for practice.
    235 
    236     Each item is a dict with 'text' and 'strokes' keys.
    237     """
    238     if not _PHRASE_CACHE:
    239         return [{"text": s, "strokes": []} for s in random.sample(
    240             CURATED_SENTENCES, min(count, len(CURATED_SENTENCES))
    241         )]
    242 
    243     phrases = random.sample(_PHRASE_CACHE, min(count, len(_PHRASE_CACHE)))
    244     return [{"text": text, "strokes": get_phrase_strokes(text)} for text, stroke in phrases]
    245 
    246 
    247 def get_all_cached_phrases():
    248     """Return the full list of pre-computed (phrase_text, stroke) tuples."""
    249     return list(_PHRASE_CACHE)
    250 
    251 
    252 def get_sentence_words(count=10):
    253     """Generate simple practice sentences that combine phrases with common words.
    254 
    255     Returns a list of sentence strings suitable for steno practice.
    256     """
    257     sentences = list(CURATED_SENTENCES)
    258     random.shuffle(sentences)
    259 
    260     if count <= len(sentences):
    261         return sentences[:count]
    262 
    263     # If we need more than curated, also generate from phrase combinations
    264     generated = []
    265     completions = [
    266         "now", "here", "today", "again", "soon", "there",
    267         "at home", "at work", "in the morning", "every day",
    268         "to the store", "for a while", "right now", "this time",
    269         "a lot", "so much", "very well", "too much",
    270         "about it", "with you", "for me", "to them",
    271     ]
    272 
    273     if _PHRASE_CACHE:
    274         for text, stroke in random.sample(_PHRASE_CACHE, min(50, len(_PHRASE_CACHE))):
    275             completion = random.choice(completions)
    276             generated.append(f"{text} {completion}")
    277 
    278     combined = sentences + generated
    279     random.shuffle(combined)
    280     return combined[:count]
    281 
    282 
    283 # ---------------------------------------------------------------------------
    284 # Hand-curated practice sentences
    285 # ---------------------------------------------------------------------------
    286 
    287 CURATED_SENTENCES = [
    288     # Short phrases (2-4 words)
    289     "I can do it",
    290     "you have to go",
    291     "I want to know",
    292     "she will come",
    293     "they have to work",
    294     "we can see it",
    295     "he will go",
    296     "I need to go",
    297     "you can do it",
    298     "it will be",
    299     "I have to say",
    300     "they want to come",
    301     "she can go",
    302     "we need to talk",
    303     "he has to go",
    304     "I would like to",
    305     "you should go",
    306     "they will come",
    307     "we have to go",
    308     "I can tell",
    309     "she will be",
    310     "you have to come",
    311     "it can be",
    312     "he will come",
    313     "I should go",
    314 
    315     # Medium phrases (4-6 words)
    316     "they don't want to come",
    317     "she has been working",
    318     "I don't think so",
    319     "you can't do that",
    320     "we have been here",
    321     "he doesn't want to go",
    322     "I have been working",
    323     "they will have to come",
    324     "she can't believe it",
    325     "you don't have to go",
    326     "we should try to go",
    327     "I don't want to go",
    328     "he has been looking",
    329     "they don't know how",
    330     "you should have come",
    331     "I can't believe it",
    332     "she doesn't want to come",
    333     "we don't have to go",
    334     "he will have to come",
    335     "I have to tell you",
    336     "they can't come today",
    337     "you don't need to go",
    338     "she would like to come",
    339     "we will have to go",
    340     "it doesn't have to be",
    341     "I used to live here",
    342     "you have to believe me",
    343     "they have been working",
    344     "she will need to go",
    345     "he can't seem to find",
    346 
    347     # Longer sentences (6-10 words)
    348     "I think that you should go to the store",
    349     "they have been working on it all day",
    350     "she will come to the house in the morning",
    351     "we don't want to go to work today",
    352     "he has been trying to find a new home",
    353     "I would like to tell you about it",
    354     "you should try to come here every day",
    355     "they don't believe that it will work",
    356     "she has to go to the store today",
    357     "we need to find a way to do it",
    358     "I can't remember how to get there",
    359     "you will have to come back again soon",
    360     "they should have been here by now",
    361     "he doesn't seem to care about it",
    362     "I have been trying to call you all day",
    363     "she will need to learn how to do it",
    364     "we can't go to the store right now",
    365     "you don't have to do it today",
    366     "they used to live in a big house",
    367     "I would like to know what you think",
    368     "he can't seem to make it work",
    369     "she has been wanting to come here",
    370     "we should try to get there on time",
    371     "you have to tell me what you know",
    372     "they will need to come back tomorrow",
    373 
    374     # Common conversational phrases
    375     "I don't know",
    376     "you have to",
    377     "I want to",
    378     "she will be there",
    379     "they have been",
    380     "we can do it",
    381     "he doesn't know",
    382     "I will try",
    383     "you should know",
    384     "it has to be",
    385     "I can see",
    386     "they don't care",
    387     "we will go",
    388     "she has been",
    389     "he will try",
    390     "I have been here before",
    391     "you can come with me",
    392     "they should have told us",
    393     "she would like to help",
    394     "we need to go home now",
    395     "he used to work here",
    396     "I will have to think about it",
    397     "you don't seem to understand",
    398     "they can't find it",
    399     "she has to come back",
    400 ]