├── .gitattributes ├── README.md ├── LICENSE ├── transcribe.py └── .gitignore /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # voice-memo-transcription 2 | A quickly hacked up script to transcribe Apple Voice Memos using [OpenAI's Whisper](https://github.com/openai/whisper). I don't plan to support or substantially improve this. 3 | 4 | Usage: 5 | ```shell 6 | python transcribe.py 7 | ``` 8 | 9 | The script performs the following steps: 10 | - Tries to detect the current username 11 | - Looks at the default path for Apple Voice Memos 12 | - Loads up Whisper's medium English model 13 | - Transcribes all the voice memos in the Voice Memo folder 14 | - Outputs each transcription to a separate file, along with a master transcription file containing all transcriptions 15 | 16 | Prerequisites: 17 | - [Whisper](https://github.com/openai/whisper) 18 | - Python 3 19 | - Apple Voice Memos synced to your Mac -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Michael Griscom 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /transcribe.py: -------------------------------------------------------------------------------- 1 | import whisper 2 | import os 3 | import pwd 4 | 5 | def get_username(): 6 | return pwd.getpwuid(os.getuid())[0] 7 | 8 | def processTranscript(text): 9 | return f"- {text}\n" 10 | 11 | username = get_username() 12 | print("detected username", username) 13 | 14 | voiceMemoFolder = f"/Users/{username}/Library/Application Support/com.apple.voicememos/Recordings" 15 | print("reading voice memos from", voiceMemoFolder) 16 | 17 | outputFolder = "./transcriptions" 18 | print("loading model") 19 | model = whisper.load_model("medium.en") 20 | print("model loaded") 21 | 22 | files = list(filter(lambda x: x.endswith(('.m4a')), sorted(os.listdir(voiceMemoFolder)))) 23 | masterTranscription = "" 24 | 25 | for index, file in enumerate(files): 26 | print("processing file", index + 1, "of", len(files)) 27 | inputFile = os.path.join(voiceMemoFolder, file) 28 | print("next file ", inputFile) 29 | 30 | fileName = os.path.splitext(file)[0]; 31 | outputFile = os.path.join(outputFolder, fileName + '.txt') 32 | if os.path.isfile(outputFile): 33 | print("transcription exists, grabbing text") 34 | textfile = open(outputFile, 'r') 35 | masterTranscription += processTranscript(textfile.read()) 36 | textfile.close() 37 | continue 38 | 39 | print("transcribing") 40 | result = model.transcribe(inputFile, fp16=False, language='English') 41 | masterTranscription += processTranscript(result["text"]) 42 | 43 | print("outputting to", outputFile) 44 | textfile = open(outputFile, 'w') 45 | textfile.write(result["text"]) 46 | textfile.close() 47 | print("file complete") 48 | 49 | print("all files complete, outputting master transcription") 50 | masterFile = os.path.join(outputFolder, 'master_transcription.txt') 51 | textfile = open(masterFile, 'w') 52 | textfile.write(masterTranscription) 53 | textfile.close() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | transcriptions/ 2 | 3 | .DS_Store 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 109 | __pypackages__/ 110 | 111 | # Celery stuff 112 | celerybeat-schedule 113 | celerybeat.pid 114 | 115 | # SageMath parsed files 116 | *.sage.py 117 | 118 | # Environments 119 | .env 120 | .venv 121 | env/ 122 | venv/ 123 | ENV/ 124 | env.bak/ 125 | venv.bak/ 126 | 127 | # Spyder project settings 128 | .spyderproject 129 | .spyproject 130 | 131 | # Rope project settings 132 | .ropeproject 133 | 134 | # mkdocs documentation 135 | /site 136 | 137 | # mypy 138 | .mypy_cache/ 139 | .dmypy.json 140 | dmypy.json 141 | 142 | # Pyre type checker 143 | .pyre/ 144 | 145 | # pytype static type analyzer 146 | .pytype/ 147 | 148 | # Cython debug symbols 149 | cython_debug/ 150 | 151 | # PyCharm 152 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 153 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 154 | # and can be added to the global gitignore or merged into this file. For a more nuclear 155 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 156 | #.idea/ 157 | --------------------------------------------------------------------------------