Text replacements for dictation

Replace phrases and regex patterns in what you dictate on a Mac. Write a replace table in config.yaml, add it to the pipeline, and test it from the terminal.

What you will get

Three replace tables. One writes product names the way your company spells them. One expands a short phrase into an email sign-off. One writes units as symbols.

You say You get
we moved billing to northwind pay last week we moved billing to Northwind Pay last week
North Wind Cloud Plus is live for every customer Northwind Cloud+ is live for every customer
the run was twelve kilometers the run was 12 km
it is twenty two degrees celsius in the office it is 22 °C in the office
the image is two point five gigabytes the image is 2.5 GB

“Thanks for the update. Insert signature.” gives:

Thanks for the update.

Best regards,
Alex Martin

Each output above is the real result of --replace on that sentence. A replace table needs no model and costs nothing at run time.

How a replace table works

A transform with a replace: body is a table. Each key is the text to write. Its list holds what to look for.

  • A plain phrase matches as whole words, in any case. 'Northwind Pay': ['northwind pay'] also catches “Northwind pay” and “NORTHWIND PAY”.
  • A pattern between slashes is a regular expression. It also ignores case.
  • $1 in the key is what the first group in brackets captured. $2 is the second group. This works only when the source is a pattern. With a plain phrase, the key is written exactly as typed, $ included.
  • An empty key deletes what matched. The shipped fillers rule deletes “um” and “uh” this way.

A transform runs on a dictation only when transcription.pipeline has a step for it. Define the table and skip the step, and nothing happens. We checked this: with the tables below defined and no step, --replace "we moved billing to northwind pay last week" printed the sentence unchanged.

Steps

  1. Open ~/.config/parrotflow/config.yaml. From the menu bar, use Settings, then Edit Config.

  2. Add the three tables under the transforms: key that is already in the file. Do not add a second transforms: key. Put your own product names and your own name in place of ours.

    transforms:
      - name: product_names
        description: our product names, written the way marketing writes them
        replace:
          'Northwind Cloud+': ['northwind cloud plus', 'north wind cloud plus']
          'Northwind Pay': ['northwind pay', 'north wind pay']
    
      - name: signature
        description: my email sign-off
        replace:
          "\n\nBest regards,\nAlex Martin": ['/,?\s*\binsert (?:my )?signature\b\.?/']
    
      - name: units
        description: spoken units as symbols
        replace:
          '$1 km': ['/\b(\d+(?:\.\d+)?) (?:kilometers|kilometres)\b/']
          '$1 °C': ['/\b(\d+(?:\.\d+)?) degrees? (?:celsius|centigrade)\b/']
          '$1 GB': ['/\b(\d+(?:\.\d+)?) gigabytes?\b/']

    What each one does:

    • product_names lists two spellings of each name. The recogniser can write “Northwind” as one word or as “North Wind”, and a plain phrase only matches what it is given.
    • signature uses a pattern so it can take the comma before “insert signature” and the full stop after it. The key is in double quotes, so YAML turns each \n into a line break.
    • units captures the number in (\d+(?:\.\d+)?) and writes it back with $1.
  3. Add a step for each table to transcription.pipeline. units goes right after numbers_en, because it needs digits and numbers_en is the step that writes them. The other two can go at the end. This is the default pipeline with the three new steps:

    transcription:
      pipeline:
        - transform: fillers
        - transform: fillers_fr
          when: language == "fr"
        - transform: dates_en
        - transform: numbers_en
        - transform: units
        - transform: money_en
        - transform: disfluency
        - transform: product_names
        - transform: signature
  4. Save the file. The app reloads it at once.

Check that it worked

The commands below use the full path to the app. If you installed with Homebrew, parrotflow runs the same binary.

Ask the app what it loaded:

/Applications/ParrotFlow.app/Contents/MacOS/ParrotFlow --check-config

The pipeline line names the new steps in order:

  · pipeline          sentence_repair → vocabulary → transform fillers → transform fillers_fr when language == "fr" → transform dates_en → transform numbers_en → transform units → transform money_en → transform disfluency → transform product_names → transform signature

The replace section counts the rules in each table:

      table     product_names — 4 rule(s), our product names, written the way marketing writes them
      table     signature — 1 rule(s), my email sign-off
      table     units — 3 rule(s), spoken units as symbols

Then run sentences through your pipeline. --replace uses your config and skips the steps that call a model.

/Applications/ParrotFlow.app/Contents/MacOS/ParrotFlow --replace "the run was twelve kilometers"
the run was 12 km

Try sentences that must not change too. These came back as they went in:

Sentence Why it stays
upgrade to northwind cloud No rule lists “northwind cloud” without “plus”.
a few kilometers away The pattern needs a number before “kilometers”.
please sign the contract The pattern needs the words “insert signature”.

Last, dictate one of the sentences into any text field.

To see what the recogniser really wrote for a phrase, dictate it once and read the log. Each dictation adds a transcribed: line:

grep "transcribed:" ~/Library/Logs/ParrotFlow.log | tail -1

Add that spelling to the list if your rule missed it.

Order matters

Between steps

Steps run from top to bottom. Each step gets the text as the steps above it left it.

We moved units above numbers_en and ran the same sentence:

the run was 12 kilometers

units ran first and saw “twelve kilometers”, with no digits, so it did nothing. Then numbers_en wrote the digits. With units below numbers_en, the result is the run was 12 km.

Inside one table

The order of the keys inside one table is not fixed. Do not put two rules that match the same words in one table.

We tested this table on “northwind cloud plus is live” ten times:

replace:
  'NW Cloud': ['northwind cloud']
  'NW Cloud+': ['northwind cloud plus']

It gave NW Cloud+ is live six times and NW Cloud plus is live four times. When the shorter rule ran first, the longer one had nothing left to match.

Put the longer phrase in its own transform, and give it the earlier step:

transforms:
  - name: long_names
    description: product names with a suffix
    replace:
      'NW Cloud+': ['northwind cloud plus']
  - name: short_names
    description: product names without a suffix
    replace:
      'NW Cloud': ['northwind cloud']

transcription:
  pipeline:
    # the steps already in your config stay here
    - transform: long_names
    - transform: short_names

On “northwind cloud plus is live, northwind cloud is not” this gave NW Cloud+ is live, NW Cloud is not on all six runs.

Write patterns in single quotes

In single quotes, YAML keeps a backslash as it is. In double quotes, YAML reads \b and \d as its own escapes, and the config does not load. --check-config shows the error:

  ✗ 213:22: error: scanner: while parsing a quoted scalar in line 213, column 17
found unknown escape character:
      '$1 mi': ["/\b(\d+) miles\b/"]
                     ^

Use double quotes only for a key that needs a real line break, like the signature above.

Capture groups

$1 is the first group in brackets, $2 the second, and so on. A group that starts with (?: captures nothing, so it has no number.

Write $1, not ${1}. We tested '${1} km' on “ran 12 kilometers” and got ran ${1} km.

If the key names a group the pattern does not have, the rule still runs and writes nothing in that place. --check-config reports it. We changed the units key to '$1 $2 km':

  ✗ replacements: "/\b(\d+(?:\.\d+)?) (?:kilometers|kilometres)\b/" writes $2, but the pattern captures 1 group(s) — $2 comes out as nothing

Limits

  • A table only changes what the recogniser wrote. If it writes “North Wind” one day and “Northwind” the next, list both.
  • A name the recogniser gets wrong in many ways belongs in your vocabulary, not in a table. See Teach ParrotFlow a word.
  • A table cannot change the case of what it captured. Use a script for that. See Post-process dictation with a Python script.
  • A table runs in every app. To limit a step to some apps, add app: to it. See Apps in pipelines.md.
  • --replace skips the vocabulary pass and every prompt step. A live dictation runs them too.