diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aae6ff2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,160 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+# *.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+# *.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..1625c17
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+ HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display,
+ communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+ likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+ v. rights protecting the extraction, dissemination, use and reuse of data
+ in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation
+ thereof, including any amended or successor version of such
+ directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+ world based on applicable law or treaty, and any national
+ implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+ warranties of any kind concerning the Work, express, implied,
+ statutory or otherwise, including without limitation warranties of
+ title, merchantability, fitness for a particular purpose, non
+ infringement, or the absence of latent or other defects, accuracy, or
+ the present or absence of errors, whether or not discoverable, all to
+ the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without
+ limitation any person's Copyright and Related Rights in the Work.
+ Further, Affirmer disclaims responsibility for obtaining any necessary
+ consents, permissions or other rights required for any use of the
+ Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to
+ this CC0 or use of the Work.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..de3f07d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,146 @@
+
+
+
+
+
+[![Contributors][contributors-shield]][contributors-url]
+[![Forks][forks-shield]][forks-url]
+[![Stargazers][stars-shield]][stars-url]
+[![Issues][issues-shield]][issues-url]
+
+
+
+
+
+
+
+
+
Audiotext
+
+
+ A program that transcribes audio from a file or microphone to text in any supported language by Google API.
+
+ Report Bug · Request Feature
+
+
+
+
+
+ Table of Contents
+
+ -
+ About The Project
+
+
+ -
+ Getting Started
+
+ - Contributing
+ - Contact
+ - Acknowledgments
+
+
+
+
+
+## About The Project
+
+[![Main screenshot light][main-system]](https://github.com/HenestrosaConH/audiotext)
+
+The project structure is as follows:
+
+- `docs`: Contains files related to the documentation of the project.
+- `res`: Contains all the static resources used by the app, which are the app icon (located in the `img` folder) and the i18n files (located in the `locales` folder).
+- `src`: Contains the source code files of the app.
+
+Besides those directories, there are also these two files (apart from the .gitignore, README.md and LICENSE):
+
+- `audiotext.spec`: Used to generate a .exe file with [PyInstaller](https://pyinstaller.org/en/stable/). Notice that, inside the file, there are some annotations, which are `ROOT DIRECTORY PATH`, `PATH TO CUSTOM TKINTER` and `PATH TO RES FOLDER`. You will have to replace them by the indicated path of your computer. For example, my `PATH TO CUSTOMTKINTER` is `c:\\users\\JC\\appdata\\local\\programs\\python\\python310\\lib\\site-packages\\customtkinter`.
+- `requirements.txt`: Lists the names and versions of each package used to build this project.
+
+(back to top)
+
+
+
+### Built With
+
+- [pydub](https://github.com/jiaaro/pydub)
+- [CustomTkinter](https://github.com/TomSchimansky/CustomTkinter)
+- [SpeechRecognition](https://pypi.org/project/SpeechRecognition/)
+- [PyAudio](https://pypi.org/project/PyAudio/)
+
+(back to top)
+
+
+
+## Getting Started
+
+**Important**: You need to install [FFmpeg](https://ffmpeg.org) to execute the program. Otherwise, it won't be able to process the audio files. You can download FFmpeg from the [official site](https://ffmpeg.org/download.html)
+
+If you want to execute the program:
+- Go to [releases](https://github.com/HenestrosaConH/audiotext/releases) and download the latest one. Once you download it and uncompress it, open the `audiotext` folder and open the `audiotext.exe` file.
+
+If you want to open the code:
+- Clone the project with the `git clone https://github.com/HenestrosaConH/audiotext.git` command and then open it with your favourite IDE (mine is [PyCharm](https://www.jetbrains.com/pycharm/)).
+- Please bear in mind that you cannot generate a single .exe file for this project with PyInstaller due to the dependency with the CustonTkinter package (reason [here](https://github.com/TomSchimansky/CustomTkinter/wiki/Packaging)).
+
+(back to top)
+
+
+
+## Contributing
+
+Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
+
+If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag `enhancement`.
+Don't forget to give the project a star! Thanks again!
+
+1. Fork the Project
+2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
+3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
+4. Push to the Branch (`git push origin feature/AmazingFeature`)
+5. Open a Pull Request
+
+(back to top)
+
+
+
+## Contact
+
+Email - [LinkedIn][linkedin-url]
+
+(back to top)
+
+
+
+## Acknowledgments
+
+I've made use of the following repositories to make this README:
+
+- [Best-README-Template](https://github.com/othneildrew/Best-README-Template/)
+- [Img Shields](https://shields.io)
+
+(back to top)
+
+
+
+
+[contributors-shield]: https://img.shields.io/github/contributors/HenestrosaConH/audiotext.svg?style=for-the-badge
+[contributors-url]: https://github.com/HenestrosaConH/audiotext/graphs/contributors
+[forks-shield]: https://img.shields.io/github/forks/HenestrosaConH/audiotext.svg?style=for-the-badge
+[forks-url]: https://github.com/HenestrosaConH/audiotext/network/members
+[stars-shield]: https://img.shields.io/github/stars/HenestrosaConH/audiotext.svg?style=for-the-badge
+[stars-url]: https://github.com/HenestrosaConH/audiotext/stargazers
+[issues-shield]: https://img.shields.io/github/issues/HenestrosaConH/audiotext.svg?style=for-the-badge
+[issues-url]: https://github.com/HenestrosaConH/audiotext/issues
+[linkedin-url]: https://linkedin.com/in/henestrosaconh
+[main-system]: docs/main-system.png
+[icon]: docs/icon.png
diff --git a/audiotext.spec b/audiotext.spec
new file mode 100644
index 0000000..ccbb792
--- /dev/null
+++ b/audiotext.spec
@@ -0,0 +1,54 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+
+block_cipher = None
+
+
+a = Analysis(
+ ['D:\\Mio\\Proyectos\\Python\\audiotext\\src\\main.py'],
+ pathex=[],
+ binaries=[],
+ datas=[
+ ('PATH TO CUSTOMTKINTER', 'customtkinter/'),
+ ('PATH TO RES FOLDER', 'res/')
+ ],
+ hiddenimports=[],
+ hookspath=[],
+ hooksconfig={},
+ runtime_hooks=[],
+ excludes=[],
+ win_no_prefer_redirects=False,
+ win_private_assemblies=False,
+ cipher=block_cipher,
+ noarchive=False,
+)
+pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
+
+exe = EXE(
+ pyz,
+ a.scripts,
+ [],
+ exclude_binaries=True,
+ name='audiotext',
+ debug=False,
+ bootloader_ignore_signals=False,
+ strip=False,
+ upx=True,
+ console=False,
+ disable_windowed_traceback=False,
+ argv_emulation=False,
+ target_arch=None,
+ codesign_identity=None,
+ entitlements_file=None,
+ icon=['ROOT DIRECTORY PATH\\audiotext\\res\\img\\icon.ico'],
+)
+coll = COLLECT(
+ exe,
+ a.binaries,
+ a.zipfiles,
+ a.datas,
+ strip=False,
+ upx=True,
+ upx_exclude=[],
+ name='audiotext',
+)
diff --git a/docs/icon.png b/docs/icon.png
new file mode 100644
index 0000000..d13057d
Binary files /dev/null and b/docs/icon.png differ
diff --git a/docs/main-light.png b/docs/main-light.png
new file mode 100644
index 0000000..8c7b38d
Binary files /dev/null and b/docs/main-light.png differ
diff --git a/docs/main-system.png b/docs/main-system.png
new file mode 100644
index 0000000..8aac30e
Binary files /dev/null and b/docs/main-system.png differ
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..d2f7893
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+customtkinter==5.0.5
+pydub==0.25.1
+SpeechRecognition==3.9.0
+pyaudio==0.2.13
\ No newline at end of file
diff --git a/res/img/icon.ico b/res/img/icon.ico
new file mode 100644
index 0000000..63d6494
Binary files /dev/null and b/res/img/icon.ico differ
diff --git a/res/locales/en/LC_MESSAGES/all.mo b/res/locales/en/LC_MESSAGES/all.mo
new file mode 100644
index 0000000..8b8ae03
Binary files /dev/null and b/res/locales/en/LC_MESSAGES/all.mo differ
diff --git a/res/locales/en/LC_MESSAGES/all.po b/res/locales/en/LC_MESSAGES/all.po
new file mode 100644
index 0000000..6aec3f5
--- /dev/null
+++ b/res/locales/en/LC_MESSAGES/all.po
@@ -0,0 +1,104 @@
+# Copyright (C) 2023 HenestrosaConH
+# José Carlos López Henestrosa , 2023.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: 1.00\n"
+"POT-Creation-Date: 2023-01-26 11:32+0100\n"
+"PO-Revision-Date: 2023-01-26 11:32+0100\n"
+"Last-Translator: José Carlos López Henestrosa \n"
+"Language-Team: José Carlos López Henestrosa \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: pygettext.py 1.5\n"
+
+
+#: audiotext\src\main.py:53
+#: audiotext\src\main.py:118
+msgid "Audio language"
+msgstr "Audio language"
+
+#: audiotext\src\main.py:65
+#: audiotext\src\main.py:119
+msgid "Select audio file"
+msgstr "Select audio file"
+
+#: audiotext\src\main.py:72
+msgid "Transcribe from microphone"
+msgstr "Transcribe from microphone"
+
+#: audiotext\src\main.py:81
+#: audiotext\src\main.py:120
+msgid "Generate text"
+msgstr "Generate text"
+
+#: audiotext\src\main.py:89
+#: audiotext\src\main.py:121
+msgid "App language"
+msgstr "App language"
+
+#: audiotext\src\main.py:103
+#: audiotext\src\main.py:122
+msgid "Appearance mode"
+msgstr "Appearance mode"
+
+#: audiotext\src\main.py:109
+#: audiotext\src\main.py:123
+#: audiotext\src\main.py:124
+#: audiotext\src\main.py:155
+msgid "System"
+msgstr "System"
+
+#: audiotext\src\main.py:109
+#: audiotext\src\main.py:123
+#: audiotext\src\main.py:153
+msgid "Dark"
+msgstr "Dark"
+
+#: audiotext\src\main.py:109
+#: audiotext\src\main.py:123
+#: audiotext\src\main.py:154
+msgid "Light"
+msgstr "Light"
+
+#: audiotext\src\main.py:125
+#: audiotext\src\main.py:142
+msgid "Save transcription"
+msgstr "Save transcription"
+
+#: audiotext\src\main.py:175
+msgid "Error: No audio file selected, please select one before generating text."
+msgstr "Error: No audio file selected, please select one before generating text."
+
+#: audiotext\src\main.py:219
+msgid "Error: The selected audio language is not valid."
+msgstr "Error: The selected audio language is not valid."
+
+#: audiotext\src\logic.py:20
+msgid "Select a file"
+msgstr "Select a file"
+
+#: audiotext\src\logic.py:21
+msgid "Audio files"
+msgstr "Audio files"
+
+#: audiotext\src\logic.py:102
+msgid "Say something"
+msgstr "Say something"
+
+#: audiotext\src\logic.py:109
+msgid "Error: Microphone not available."
+msgstr "Error: Microphone not available."
+
+#: audiotext\src\logic.py:126
+msgid "Save as"
+msgstr "Save as"
+
+#: audiotext\src\logic.py:128
+msgid "All Files"
+msgstr "All Files"
+
+#: audiotext\src\logic.py:128
+msgid "Text file"
+msgstr "Text file"
diff --git a/res/locales/en/LC_MESSAGES/logic.po b/res/locales/en/LC_MESSAGES/logic.po
new file mode 100644
index 0000000..0c568a6
--- /dev/null
+++ b/res/locales/en/LC_MESSAGES/logic.po
@@ -0,0 +1,44 @@
+# Copyright (C) 2023 HenestrosaConH
+# José Carlos López Henestrosa , 2023.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: 1.00\n"
+"POT-Creation-Date: 2023-01-26 11:32+0100\n"
+"PO-Revision-Date: 2023-01-26 11:32+0100\n"
+"Last-Translator: José Carlos López Henestrosa \n"
+"Language-Team: José Carlos López Henestrosa \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: pygettext.py 1.5\n"
+
+
+#: audiotext\src\logic.py:20
+msgid "Select a file"
+msgstr "Select a file"
+
+#: audiotext\src\logic.py:21
+msgid "Audio files"
+msgstr "Audio files"
+
+#: audiotext\src\logic.py:102
+msgid "Say something"
+msgstr "Say something"
+
+#: audiotext\src\logic.py:109
+msgid "Error: Microphone not available."
+msgstr "Error: Microphone not available."
+
+#: audiotext\src\logic.py:126
+msgid "Save as"
+msgstr "Save as"
+
+#: audiotext\src\logic.py:128
+msgid "All Files"
+msgstr "All Files"
+
+#: audiotext\src\logic.py:128
+msgid "Text file"
+msgstr "Text file"
+
diff --git a/res/locales/en/LC_MESSAGES/main.po b/res/locales/en/LC_MESSAGES/main.po
new file mode 100644
index 0000000..3bbf900
--- /dev/null
+++ b/res/locales/en/LC_MESSAGES/main.po
@@ -0,0 +1,77 @@
+# Copyright (C) 2023 HenestrosaConH
+# José Carlos López Henestrosa , 2023.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: 1.00\n"
+"POT-Creation-Date: 2023-01-26 11:32+0100\n"
+"PO-Revision-Date: 2023-01-26 11:32+0100\n"
+"Last-Translator: José Carlos López Henestrosa \n"
+"Language-Team: José Carlos López Henestrosa \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: pygettext.py 1.5\n"
+
+
+#: audiotext\src\main.py:53
+#: audiotext\src\main.py:118
+msgid "Audio language"
+msgstr "Audio language"
+
+#: audiotext\src\main.py:65
+#: audiotext\src\main.py:119
+msgid "Select audio file"
+msgstr "Select audio file"
+
+#: audiotext\src\main.py:72
+msgid "Transcribe from microphone"
+msgstr "Transcribe from microphone"
+
+#: audiotext\src\main.py:81
+#: audiotext\src\main.py:120
+msgid "Generate text"
+msgstr "Generate text"
+
+#: audiotext\src\main.py:89
+#: audiotext\src\main.py:121
+msgid "App language"
+msgstr "App language"
+
+#: audiotext\src\main.py:103
+#: audiotext\src\main.py:122
+msgid "Appearance mode"
+msgstr "Appearance mode"
+
+#: audiotext\src\main.py:109
+#: audiotext\src\main.py:123
+#: audiotext\src\main.py:124
+#: audiotext\src\main.py:155
+msgid "System"
+msgstr "System"
+
+#: audiotext\src\main.py:109
+#: audiotext\src\main.py:123
+#: audiotext\src\main.py:153
+msgid "Dark"
+msgstr "Dark"
+
+#: audiotext\src\main.py:109
+#: audiotext\src\main.py:123
+#: audiotext\src\main.py:154
+msgid "Light"
+msgstr "Light"
+
+#: audiotext\src\main.py:125
+#: audiotext\src\main.py:142
+msgid "Save transcription"
+msgstr "Save transcription"
+
+#: audiotext\src\main.py:175
+msgid "Error: No audio file selected, please select one before generating text."
+msgstr "Error: No audio file selected, please select one before generating text."
+
+#: audiotext\src\main.py:219
+msgid "Error: The selected audio language is not valid."
+msgstr "Error: The selected audio language is not valid."
+
diff --git a/res/locales/es/LC_MESSAGES/all.mo b/res/locales/es/LC_MESSAGES/all.mo
new file mode 100644
index 0000000..ac9549a
Binary files /dev/null and b/res/locales/es/LC_MESSAGES/all.mo differ
diff --git a/res/locales/es/LC_MESSAGES/all.po b/res/locales/es/LC_MESSAGES/all.po
new file mode 100644
index 0000000..e647051
--- /dev/null
+++ b/res/locales/es/LC_MESSAGES/all.po
@@ -0,0 +1,108 @@
+# Copyright (C) 2023 HenestrosaConH
+# José Carlos López Henestrosa , 2023.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: 1.00\n"
+"POT-Creation-Date: 2023-01-26 11:32+0100\n"
+"PO-Revision-Date: 2023-01-26 11:32+0100\n"
+"Last-Translator: José Carlos López Henestrosa \n"
+"Language-Team: José Carlos López Henestrosa \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: pygettext.py 1.5\n"
+
+#: audiotext\src\main.py:53
+#: audiotext\src\main.py:118
+msgid "Audio language"
+msgstr "Idioma del audio"
+
+#: audiotext\src\main.py:65
+#: audiotext\src\main.py:119
+msgid "Select audio file"
+msgstr "Seleccionar archivo de audio"
+
+#: audiotext\src\main.py:72
+msgid "Transcribe from microphone"
+msgstr "Transcribir del micrófono"
+
+#: audiotext\src\main.py:81
+#: audiotext\src\main.py:120
+msgid "Generate text"
+msgstr "Generar texto"
+
+#: audiotext\src\main.py:89
+#: audiotext\src\main.py:121
+msgid "App language"
+msgstr "Idioma de la app"
+
+#: audiotext\src\main.py:103
+#: audiotext\src\main.py:122
+msgid "Appearance mode"
+msgstr "Tema actual"
+
+#: audiotext\src\main.py:86
+#: audiotext\src\main.py:109
+#: audiotext\src\main.py:123
+#: audiotext\src\main.py:124
+#: audiotext\src\main.py:155
+msgid "System"
+msgstr "Sistema"
+
+#: audiotext\src\main.py:109
+#: audiotext\src\main.py:123
+#: audiotext\src\main.py:153
+msgid "Dark"
+msgstr "Oscuro"
+
+#: audiotext\src\main.py:109
+#: audiotext\src\main.py:123
+#: audiotext\src\main.py:154
+msgid "Light"
+msgstr "Claro"
+
+#: audiotext\src\main.py:125
+#: audiotext\src\main.py:142
+msgid "Save transcription"
+msgstr "Guardar transcripción"
+
+#: audiotext\src\main.py:175
+msgid "Error: No audio file selected, please select one before generating text."
+msgstr "Error: No se ha seleccionado un archivo de audio, por favor seleccione uno antes de generar el texto."
+
+#: audiotext\src\main.py:219
+msgid "Error: The selected audio language is not valid."
+msgstr "Error: El idioma del audio seleccionado no es válido."
+
+#: audiotext\src\logic.py:20
+msgid "Select a file"
+msgstr "Selecciona un archivo"
+
+#: audiotext\src\logic.py:21
+msgid "Audio files"
+msgstr "Archivos de audio"
+
+#: audiotext\src\logic.py:102
+msgid "Say something"
+msgstr "Di algo"
+
+#: audiotext\src\logic.py:111
+msgid "Error: Microphone not available."
+msgstr "Error: Micrófono no disponible."
+
+#: audiotext\src\logic.py:113
+msgid "Error: Please, try again."
+msgstr "Error: Por favor, inténtalo de nuevo."
+
+#: audiotext\src\logic.py:126
+msgid "Save as"
+msgstr "Guardar como"
+
+#: audiotext\src\logic.py:128
+msgid "All Files"
+msgstr "Todos los archivos"
+
+#: audiotext\src\logic.py:130
+msgid "Text file"
+msgstr "Archivo de texto"
diff --git a/res/locales/es/LC_MESSAGES/logic.po b/res/locales/es/LC_MESSAGES/logic.po
new file mode 100644
index 0000000..acc77f3
--- /dev/null
+++ b/res/locales/es/LC_MESSAGES/logic.po
@@ -0,0 +1,48 @@
+# Copyright (C) 2023 HenestrosaConH
+# José Carlos López Henestrosa , 2023.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: 1.00\n"
+"POT-Creation-Date: 2023-01-26 11:32+0100\n"
+"PO-Revision-Date: 2023-01-26 11:32+0100\n"
+"Last-Translator: José Carlos López Henestrosa \n"
+"Language-Team: José Carlos López Henestrosa \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: pygettext.py 1.5\n"
+
+
+#: audiotext\src\logic.py:20
+msgid "Select a file"
+msgstr "Selecciona un archivo"
+
+#: audiotext\src\logic.py:21
+msgid "Audio files"
+msgstr "Archivos de audio"
+
+#: audiotext\src\logic.py:102
+msgid "Say something"
+msgstr "Di algo"
+
+#: audiotext\src\logic.py:111
+msgid "Error: Microphone not available."
+msgstr "Error: Micrófono no disponible."
+
+#: audiotext\src\logic.py:113
+msgid "Error: Please, try again."
+msgstr "Error: Por favor, inténtalo de nuevo."
+
+#: audiotext\src\logic.py:126
+msgid "Save as"
+msgstr "Guardar como"
+
+#: audiotext\src\logic.py:128
+msgid "All Files"
+msgstr "Todos los archivos"
+
+#: audiotext\src\logic.py:130
+msgid "Text file"
+msgstr "Archivo de texto"
+
diff --git a/res/locales/es/LC_MESSAGES/main.po b/res/locales/es/LC_MESSAGES/main.po
new file mode 100644
index 0000000..5299a06
--- /dev/null
+++ b/res/locales/es/LC_MESSAGES/main.po
@@ -0,0 +1,78 @@
+# Copyright (C) 2023 HenestrosaConH
+# José Carlos López Henestrosa , 2023.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: 1.00\n"
+"POT-Creation-Date: 2023-01-26 11:32+0100\n"
+"PO-Revision-Date: 2023-01-26 11:32+0100\n"
+"Last-Translator: José Carlos López Henestrosa \n"
+"Language-Team: José Carlos López Henestrosa \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: pygettext.py 1.5\n"
+
+
+#: audiotext\src\main.py:53
+#: audiotext\src\main.py:118
+msgid "Audio language"
+msgstr "Idioma del audio"
+
+#: audiotext\src\main.py:65
+#: audiotext\src\main.py:119
+msgid "Select audio file"
+msgstr "Seleccionar archivo de audio"
+
+#: audiotext\src\main.py:72
+msgid "Transcribe from microphone"
+msgstr "Transcribir del micrófono"
+
+#: audiotext\src\main.py:81
+#: audiotext\src\main.py:120
+msgid "Generate text"
+msgstr "Generar texto"
+
+#: audiotext\src\main.py:89
+#: audiotext\src\main.py:121
+msgid "App language"
+msgstr "Idioma de la app"
+
+#: audiotext\src\main.py:103
+#: audiotext\src\main.py:122
+msgid "Appearance mode"
+msgstr "Tema actual"
+
+#: audiotext\src\main.py:86
+#: audiotext\src\main.py:109
+#: audiotext\src\main.py:123
+#: audiotext\src\main.py:124
+#: audiotext\src\main.py:155
+msgid "System"
+msgstr "Sistema"
+
+#: audiotext\src\main.py:109
+#: audiotext\src\main.py:123
+#: audiotext\src\main.py:153
+msgid "Dark"
+msgstr "Oscuro"
+
+#: audiotext\src\main.py:109
+#: audiotext\src\main.py:123
+#: audiotext\src\main.py:154
+msgid "Light"
+msgstr "Claro"
+
+#: audiotext\src\main.py:125
+#: audiotext\src\main.py:142
+msgid "Save transcription"
+msgstr "Guardar transcripción"
+
+#: audiotext\src\main.py:175
+msgid "Error: No audio file selected, please select one before generating text."
+msgstr "Error: No se ha seleccionado un archivo de audio, por favor seleccione uno antes de generar el texto."
+
+#: audiotext\src\main.py:219
+msgid "Error: The selected audio language is not valid."
+msgstr "Error: El idioma del audio seleccionado no es válido."
+
diff --git a/src/constants.py b/src/constants.py
new file mode 100644
index 0000000..3dc4166
--- /dev/null
+++ b/src/constants.py
@@ -0,0 +1,140 @@
+LANGUAGES = {
+ "af": "Afrikaans",
+ "am": "አማርኛ",
+ "ar": "العربية",
+ "ar_AE": "(العربية (الإمارات",
+ "ar_BH": "(العربية (البحرين",
+ "ar_DZ": "(العربية (الجزائر",
+ "ar_EG": "(العربية (مصر",
+ "ar_IL": "(العربية (إسرائيل",
+ "ar_JO": "(العربية (الأردن",
+ "ar_KW": "(العربية (الكويت",
+ "ar_LB": "(العربية (لبنان",
+ "ar_MA": "(العربية (المغرب",
+ "ar_OM": "(العربية (عُمان",
+ "ar_PS": "(العربية (فلسطين",
+ "ar_QA": "(العربية (قطر",
+ "ar_SA": "(العربية (السعودية",
+ "ar_TN": "(العربية (تونس",
+ "az": "Azərbaycan",
+ "be": "беларуская",
+ "bg": "Български",
+ "bn": "বাংলা",
+ "bn_BD": "বাংলা (বাংলাদেশ)",
+ "bn_IN": "বাংলা (ভারত)",
+ "ca": "Català",
+ "cs": "Čeština",
+ "da": "Dansk",
+ "de": "Deutsch",
+ "de_CH": "Schweizer Hochdeutsch",
+ "el": "Ελληνικά",
+ "en": "English (United States)",
+ "en_AU": "English (Australia)",
+ "en_CA": "English (Canada)",
+ "en_GB": "English (United Kingdom)",
+ "en_GH": "English (Ghana)",
+ "en_IE": "English (Ireland)",
+ "en_IN": "English (India)",
+ "en_KE": "English (Kenya)",
+ "en_NZ": "English (New Zealand)",
+ "en_NG": "English (Nigeria)",
+ "en_PH": "English (Philippines)",
+ "en_TZ": "English (Tanzania)",
+ "en_ZA": "English (South Africa)",
+ "es": "Español",
+ "es_419": "Español (Latinoamérica)",
+ "es_AR": "Español (Argentina)",
+ "es_BO": "Español (Bolivia)",
+ "es_CL": "Español (Chile)",
+ "es_CO": "Español (Colombia)",
+ "es_CR": "Español (Costa Rica)",
+ "es_DO": "Español (República Dominicana)",
+ "es_EC": "Español (Ecuador)",
+ "es_ES": "Español (España)",
+ "es_GT": "Español (Guatemala)",
+ "es_HN": "Español (Honduras)",
+ "es_MX": "Español (México)",
+ "es_NI": "Español (Nicaragua)",
+ "es_PA": "Español (Panamá)",
+ "es_PE": "Español (Perú)",
+ "es_PR": "Español (Puerto Rico)",
+ "es_PY": "Español (Paraguay)",
+ "es_SV": "Español (El Salvador)",
+ "es_US": "Español (Estados Unidos)",
+ "es_UY": "Español (Uruguay)",
+ "es_VE": "Español (Venezuela)",
+ "et": "Eesti",
+ "eu": "Euskara",
+ "fa": "فارسی",
+ "fi": "Suomi",
+ "fil": "Filipino",
+ "fr": "Français",
+ "fr_CA": "Français (Canada)",
+ "gl": "Galego",
+ "gu": "ગુજરાતી",
+ "hi": "हिन्दी",
+ "hr": "Hrvatski",
+ "hu": "Magyar",
+ "hy": "հայերեն",
+ "id": "Bahasa Indonesia",
+ "is": "Íslenska",
+ "it": "Italiano",
+ "it_CH": "Italiano (Svizzera)",
+ "it_IT": "Italiano (Italia)",
+ "iw": "עברית",
+ "ja": "日本語",
+ "jv": "Basa Jawa",
+ "ka": "ქართულად",
+ "kk": "Қазақ",
+ "km": "ខ្មែរ",
+ "kn": "ಕನ್ನಡ",
+ "ko": "한국어",
+ "lo": "ລາວ",
+ "lt": "Lietuvių",
+ "lv": "Latviešu",
+ "ml": "മലയാളം",
+ "mn": "Монгол",
+ "mr": "मराठी",
+ "ms": "Bahasa Melayu",
+ "mt": "Malti",
+ "ne": "नेपाली",
+ "nl": "Nederlands",
+ "nn": "Norsk (Nynorsk)",
+ "no": "Norsk (Bokmål)",
+ "pa": "ਪੰਜਾਬੀ",
+ "pl": "Polski",
+ "pt_BR": "Português (Brasil)",
+ "pt_PT": "Português (Portugal)",
+ "ro": "Română",
+ "ru": "Русский",
+ "si": "සිංහල",
+ "sk": "Slovenčina",
+ "sl": "Slovenščina",
+ "sr": "Српски",
+ "su": "Basa Sunda",
+ "sv": "Svenska",
+ "sw": "Kiswahili",
+ "sw_KE": "Kiswahili (Kenya)",
+ "sw_TZ": "Kiswahili (Tanzania)",
+ "ta": "தமிழ்",
+ "ta_IN": "தமிழ் (இந்தியா)",
+ "ta_LK": "தமிழ் (இலங்கை)",
+ "ta_MY": "தமிழ் (மலேஷியா)",
+ "ta_SG": "தமிழ் (சிங்கப்பூர்)",
+ "te": "తెలుగు",
+ "th": "ไทย",
+ "tr": "Türkçe",
+ "uk": "Українська",
+ "ur": "اردو",
+ "ur_IN": "(اردو (بھارت",
+ "ur_PK": "(اردو (پاکستان",
+ "vi": "Tiếng Việt",
+ "zh_CN": "中文(中国)",
+ "zh_HK": "中文(香港)",
+ "zh_TW": "中文(台灣)",
+ "zu": "Isizulu",
+}
+
+AUDIO_FILE_EXTENSIONS = [".mp3", ".wav", ".ogg", ".opus", ".mpeg"]
+FILE = "file"
+MIC = "mic"
diff --git a/src/logic.py b/src/logic.py
new file mode 100644
index 0000000..041e48c
--- /dev/null
+++ b/src/logic.py
@@ -0,0 +1,131 @@
+import constants
+import os
+import shutil
+import speech_recognition as sr
+import utils
+from pathlib import Path
+from pydub import AudioSegment
+from pydub.silence import split_on_silence
+from tkinter import filedialog
+
+
+def open_file() -> str:
+ """
+ Prompts a file explorer to determine the audio file path to transcribe.
+
+ :return str: Filepath.
+ """
+ filepath = filedialog.askopenfilename(
+ initialdir="/",
+ title=utils._("Select a file"),
+ filetypes=[(utils._("Audio files"), constants.AUDIO_FILE_EXTENSIONS)]
+ )
+ return filepath
+
+
+async def generate_file_transcription(filepath: str, language_code: str) -> str:
+ """
+ Splits a large audio file into chunks
+ and applies speech recognition on each one.
+
+ :param str filepath: Path of the file to transcribe.
+ :param str language_code: Code of the language spoken in the audio.
+ :return str: Audio transcription.
+ """
+ # Create a speech recognition object
+ r = sr.Recognizer()
+
+ # Open the audio file using pydub
+ content_type = Path(filepath).suffix
+
+ if "wav" in content_type:
+ sound = AudioSegment.from_wav(filepath)
+ elif "ogg" in content_type or "opus" in content_type:
+ sound = AudioSegment.from_ogg(filepath)
+ elif "mp3" in content_type or "mpeg" in content_type:
+ sound = AudioSegment.from_mp3(filepath)
+
+ # Split audio sound where silence is 700 miliseconds or more and get chunks
+ chunks = split_on_silence(
+ sound,
+ # Experiment with this value for your target audio file
+ min_silence_len=500,
+ # Adjust this per requirement
+ silence_thresh=sound.dBFS - 14,
+ # Keep the silence for 1 second, adjustable as well
+ keep_silence=500,
+ )
+
+ # Create a directory to store the audio chunks
+ folder_name = utils.ROOT_PATH / "audio-chunks"
+ folder_name.mkdir(exist_ok=True)
+
+ whole_text = ""
+ # Process each chunk
+ for i, audio_chunk in enumerate(chunks, start=1):
+ # Export audio chunk and save it in the `folder_name` directory.
+ chunk_filename = os.path.join(folder_name, f"chunk{i}.wav")
+ audio_chunk.export(chunk_filename, format="wav")
+
+ # Recognize the chunk
+ with sr.AudioFile(chunk_filename) as source:
+ audio_listened = r.record(source)
+
+ # Try converting it to text
+ try:
+ text = r.recognize_google(audio_listened, language=language_code)
+ except Exception:
+ shutil.rmtree(folder_name)
+ return utils._("Error: Please, try again.")
+
+ text = f"{text.capitalize()}. "
+ whole_text += text
+
+ # Delete temporal directory and files
+ shutil.rmtree(folder_name)
+
+ # Return the text for all chunks detected
+ return whole_text
+
+
+async def generate_mic_transcription(language_code: str) -> str:
+ """
+ Generates the transcription from a microphone as
+ the source of the audio.
+
+ :param str language_code: Code of the language spoken.
+ :return str: Transcription.
+ """
+ with sr.Microphone() as mic:
+ try:
+ r = sr.Recognizer()
+ print(f"{utils._('Say something')}…")
+ r.energy_threshold = 300
+ r.adjust_for_ambient_noise(mic)
+ audio = r.listen(mic, timeout=3, phrase_time_limit=3)
+ text = r.recognize_google(audio, language=language_code)
+ return text
+ except OSError:
+ return utils._("Error: Microphone not available.")
+
+
+def save_transcription(filepath, transcription):
+ """
+ Prompts a file explorer to determine the file to save the
+ generated transcription.
+
+ :param str | None filepath: Filepath of the audio file, in case that there is one.
+ :param str transcription: Text to save in the file.
+ """
+ file = filedialog.asksaveasfile(
+ mode='w',
+ initialdir=Path(filepath).parent,
+ initialfile=f'{Path(filepath).stem}.txt',
+ title=utils._("Save as"),
+ defaultextension='.txt',
+ filetypes=[(utils._("Text file"), "*.txt"), (utils._("All Files"), "*.*")],
+ )
+
+ if file:
+ file.write(transcription)
+ file.close()
diff --git a/src/main.py b/src/main.py
new file mode 100644
index 0000000..409c07c
--- /dev/null
+++ b/src/main.py
@@ -0,0 +1,236 @@
+import asyncio
+import constants
+import customtkinter
+import locale
+import logic
+import threading
+import utils
+from pathlib import Path
+
+
+class App(customtkinter.CTk):
+ def __init__(self):
+ super().__init__()
+ self.filepath = "/"
+ self.transcription = None
+
+ self.configure_window()
+ self.create_sidebar()
+ self.create_main_content()
+
+ def configure_window(self):
+ # Modes: "System" (standard), "Dark", "Light"
+ customtkinter.set_appearance_mode("System")
+ # Themes: "blue" (standard), "green", "dark-blue"
+ customtkinter.set_default_color_theme("blue")
+
+ self.title("Audiotext")
+ self.wm_iconbitmap(utils.ROOT_PATH / "res/img/icon.ico")
+ self.geometry(f"{1000}x{700}")
+ self.minsize(750, 550)
+
+ self.grid_columnconfigure(1, weight=1)
+ self.grid_rowconfigure(1, weight=1)
+
+ # VIEWS INITIALIZATION
+
+ def create_sidebar(self):
+ # Sidebar frame
+ self.frm_sidebar = customtkinter.CTkFrame(self, width=140, corner_radius=0)
+ self.frm_sidebar.grid(row=0, column=0, rowspan=4, sticky="nsew")
+ self.frm_sidebar.grid_rowconfigure(6, weight=1)
+
+ # Logo label
+ self.lbl_logo = customtkinter.CTkLabel(
+ self.frm_sidebar,
+ text="Audiotext",
+ font=customtkinter.CTkFont(size=20, weight="bold"),
+ )
+ self.lbl_logo.grid(row=0, column=0, padx=20, pady=(20, 10))
+
+ # Audio language
+ self.lbl_audio_language = customtkinter.CTkLabel(
+ self.frm_sidebar, text=f'{utils._("Audio language")}:', anchor="w"
+ )
+ self.lbl_audio_language.grid(row=1, column=0, padx=20, pady=(20, 0))
+
+ self.cbx_audio_language = customtkinter.CTkComboBox(
+ self.frm_sidebar, values=list(constants.LANGUAGES.values())
+ )
+ self.cbx_audio_language.grid(row=2, column=0, padx=20, pady=10)
+ self.cbx_audio_language.set(constants.LANGUAGES[locale.getdefaultlocale()[0]])
+
+ # Select file button
+ self.btn_select_file = customtkinter.CTkButton(
+ self.frm_sidebar, text=utils._("Select audio file"), command=self.open_file
+ )
+ self.btn_select_file.grid(row=3, column=0, padx=20, pady=(30, 20))
+
+ # Transcribe from microphone button
+ self.btn_transcribe_from_mic = customtkinter.CTkButton(
+ self.frm_sidebar,
+ text=utils._("Transcribe from microphone"),
+ command=lambda: self.get_transcription(constants.MIC)
+ )
+ self.btn_transcribe_from_mic.grid(row=4, column=0, padx=20, pady=(20, 30))
+
+ # Generate text button
+ self.btn_generate_text = customtkinter.CTkButton(
+ self.frm_sidebar,
+ fg_color="green",
+ text=utils._("Generate text"),
+ command=lambda: self.get_transcription(constants.FILE)
+ )
+ self.btn_generate_text.grid(row=5, column=0, padx=20, pady=10)
+ self.btn_generate_text.grid_remove() # hidden at start
+
+ # App language
+ self.lbl_app_language = customtkinter.CTkLabel(
+ self.frm_sidebar, text=f'{utils._("App language")}:', anchor="w"
+ )
+ self.lbl_app_language.grid(row=7, column=0, padx=20, pady=(20, 0))
+
+ self.omn_app_language = customtkinter.CTkOptionMenu(
+ self.frm_sidebar,
+ values=[constants.LANGUAGES["es"], constants.LANGUAGES["en"]],
+ command=self.change_app_language,
+ )
+ self.omn_app_language.grid(row=8, column=0, padx=20, pady=10)
+ self.omn_app_language.set(constants.LANGUAGES[locale.getdefaultlocale()[0]])
+
+ # Appearance mode
+ self.lbl_appearance_mode = customtkinter.CTkLabel(
+ self.frm_sidebar, text=f'{utils._("Appearance mode")}:', anchor="w"
+ )
+ self.lbl_appearance_mode.grid(row=9, column=0, padx=20, pady=(10, 0))
+
+ self.omn_appearance_mode = customtkinter.CTkOptionMenu(
+ self.frm_sidebar,
+ values=[utils._("System"), utils._("Light"), utils._("Dark")],
+ command=self.change_appearance_mode_event,
+ )
+ self.omn_appearance_mode.grid(row=10, column=0, padx=20, pady=10)
+
+ def change_app_language(self, language_name):
+ language_code = [i for i in constants.LANGUAGES if constants.LANGUAGES[i] == language_name][0]
+ utils.load_translation(language_code)
+
+ self.lbl_audio_language.configure(text=f'{utils._("Audio language")}:')
+ self.btn_select_file.configure(text=utils._("Select audio file"))
+ self.btn_transcribe_from_mic.configure(text=utils._("Transcribe from microphone"))
+ self.btn_generate_text.configure(text=utils._("Generate text"))
+ self.lbl_app_language.configure(text=f'{utils._("App language")}:')
+ self.lbl_appearance_mode.configure(text=f'{utils._("Appearance mode")}:')
+ self.omn_appearance_mode.configure(values=[utils._("System"), utils._("Light"), utils._("Dark")])
+ self.omn_appearance_mode.set(utils._("System"))
+ self.btn_save.configure(text=utils._("Save transcription"))
+ customtkinter.set_appearance_mode(utils._(self.omn_appearance_mode.get()))
+
+ def create_main_content(self):
+ # Selected file entry
+ self.ent_selected_file = customtkinter.CTkEntry(self, state="disabled")
+ self.ent_selected_file.grid(row=0, column=1, padx=20, pady=(20, 0), sticky="new")
+ self.ent_selected_file.grid_remove() # hidden at start
+
+ # Text audio textbox
+ self.tbx_transcription = customtkinter.CTkTextbox(self, wrap="word")
+ self.tbx_transcription.grid(row=1, column=1, padx=20, pady=20, sticky="nsew")
+
+ # Save text button
+ self.btn_save = customtkinter.CTkButton(
+ self,
+ fg_color="green",
+ text=utils._("Save transcription"),
+ command=self.save_transcription
+ )
+ self.btn_save.grid(row=2, column=1, padx=20, pady=(0, 20), sticky="sew")
+ self.btn_save.grid_remove() # hidden at start
+
+ # EVENTS
+
+ @staticmethod
+ def change_appearance_mode_event(new_appearance_mode: str):
+ appearance_mode_map = {
+ utils._("Dark"): "Dark",
+ utils._("Light"): "Light",
+ utils._("System"): "System"
+ }
+ appearance_mode = appearance_mode_map.get(new_appearance_mode, "System")
+ customtkinter.set_appearance_mode(appearance_mode)
+
+ def open_file(self):
+ # Open the file dialog
+ filepath = logic.open_file()
+
+ if filepath:
+ self.filepath = filepath
+ self.ent_selected_file.grid()
+ self.ent_selected_file.configure(textvariable=customtkinter.StringVar(self, filepath))
+ self.btn_generate_text.grid()
+
+ def get_transcription(self, source):
+ selected_path = Path(self.filepath)
+ is_file_valid = selected_path.is_file() and selected_path.suffix in constants.AUDIO_FILE_EXTENSIONS
+
+ if source == constants.FILE and not is_file_valid:
+ self.display_text(utils._("Error: No audio file selected, please select one before generating text."))
+ else:
+ threading.Thread(
+ target=lambda loop: loop.run_until_complete(self.async_get_transcription(source)),
+ args=(asyncio.new_event_loop(),)
+ ).start()
+
+ async def async_get_transcription(self, source):
+ try:
+ # Get the selected language code
+ language_code = [
+ key
+ for key, value in constants.LANGUAGES.items()
+ if value.lower() == self.cbx_audio_language.get().strip().lower()
+ ][0]
+
+ # Show progress bar
+ self.progress_bar = customtkinter.CTkProgressBar(self)
+ self.progress_bar.grid(row=1, column=1, padx=40, pady=0, sticky="ew")
+ self.progress_bar.configure(mode="indeterminnate")
+ self.progress_bar.start()
+
+ # Disable action buttons to avoid multiple requests at the same time
+ self.btn_generate_text.configure(state="disabled")
+ self.btn_transcribe_from_mic.configure(state="disabled")
+
+ # Get transcription
+ if source == constants.FILE:
+ self.transcription = await logic.generate_file_transcription(self.filepath, language_code)
+ elif source == constants.MIC:
+ self.transcription = await logic.generate_mic_transcription(language_code)
+
+ # Display transcription
+ self.display_text(self.transcription)
+
+ # Re-enable action buttons
+ self.btn_generate_text.configure(state="enabled")
+ self.btn_transcribe_from_mic.configure(state="enabled")
+
+ # Hide generate button and remove progress bar
+ self.progress_bar.grid_forget()
+ self.btn_generate_text.grid_remove()
+
+ # Show save button if transcription is not empty
+ if self.transcription:
+ self.btn_save.grid()
+ except IndexError:
+ self.display_text(utils._("Error: The selected audio language is not valid."))
+
+ def display_text(self, message):
+ self.tbx_transcription.delete("1.0", "end")
+ self.tbx_transcription.insert("0.0", message)
+
+ def save_transcription(self):
+ logic.save_transcription(self.ent_selected_file.get(), self.tbx_transcription.get("1.0", "end-1c"))
+
+
+if __name__ == "__main__":
+ app = App()
+ app.eval('tk::PlaceWindow . center')
+ app.mainloop()
diff --git a/src/utils.py b/src/utils.py
new file mode 100644
index 0000000..f248722
--- /dev/null
+++ b/src/utils.py
@@ -0,0 +1,35 @@
+import gettext
+import locale
+import sys
+from pathlib import Path
+
+
+def get_path(relative_path: str = ""):
+ """Get absolute path, works for dev and for PyInstaller"""
+ try:
+ # PyInstaller creates a temp folder and stores path in _MEIPASS
+ base_path = Path(sys._MEIPASS)
+ except (Exception,):
+ base_path = Path(__file__).parent.parent.resolve()
+
+ return (base_path / relative_path).resolve()
+
+
+ROOT_PATH = get_path("")
+
+_ = None
+
+
+def load_translation(language_code: str):
+ translation = gettext.translation(
+ "all",
+ localedir=ROOT_PATH / "res/locales",
+ languages=[language_code],
+ fallback=True
+ )
+ translation.install()
+ global _
+ _ = translation.gettext
+
+
+load_translation(locale.getdefaultlocale()[0])