├── scripts
├── CPU_INSTALL.txt
├── NVIDIA_INSTALL.txt
├── assets
│ ├── logo.ico
│ └── Gemma_License.txt
├── nsis-plugins
│ ├── x86-ansi
│ │ ├── Banner.dll
│ │ ├── Dialer.dll
│ │ ├── INetC.dll
│ │ ├── Math.dll
│ │ ├── NSISdl.dll
│ │ ├── Splash.dll
│ │ ├── System.dll
│ │ ├── VPatch.dll
│ │ ├── ZipDLL.dll
│ │ ├── nsExec.dll
│ │ ├── BgImage.dll
│ │ ├── LangDLL.dll
│ │ ├── TypeLib.dll
│ │ ├── UserInfo.dll
│ │ ├── nsUnzip.dll
│ │ ├── AdvSplash.dll
│ │ ├── StartMenu.dll
│ │ ├── nsDialogs.dll
│ │ ├── AccessControl.dll
│ │ └── InstallOptions.dll
│ └── x86-unicode
│ │ ├── INetC.dll
│ │ ├── Math.dll
│ │ ├── Banner.dll
│ │ ├── BgImage.dll
│ │ ├── Dialer.dll
│ │ ├── LangDLL.dll
│ │ ├── NSISdl.dll
│ │ ├── Splash.dll
│ │ ├── System.dll
│ │ ├── TypeLib.dll
│ │ ├── UserInfo.dll
│ │ ├── VPatch.dll
│ │ ├── ZipDLL.dll
│ │ ├── nsExec.dll
│ │ ├── nsUnzip.dll
│ │ ├── AdvSplash.dll
│ │ ├── StartMenu.dll
│ │ ├── nsDialogs.dll
│ │ ├── AccessControl.dll
│ │ └── InstallOptions.dll
├── hooks
│ └── hook-llama_cpp.py
└── install.nsi
├── client_requirements.txt
├── src
├── FreeScribe.client
│ ├── assets
│ │ └── logo.ico
│ ├── whisper-assets
│ │ └── mel_filters.npz
│ ├── markdown
│ │ ├── help
│ │ │ ├── about.md
│ │ │ └── settings.md
│ │ └── welcome.md
│ ├── utils
│ │ ├── ip_utils.py
│ │ └── file_utils.py
│ ├── presets
│ │ ├── ChatGPT.json
│ │ ├── Local AI.json
│ │ └── ClinicianFOCUS Toolbox.json
│ ├── UI
│ │ ├── MarkdownWindow.py
│ │ ├── Widgets
│ │ │ ├── CustomTextBox.py
│ │ │ ├── MicrophoneSelector.py
│ │ │ └── AudioMeter.py
│ │ ├── DebugWindow.py
│ │ ├── LoadingWindow.py
│ │ ├── MainWindow.py
│ │ ├── MainWindowUI.py
│ │ └── SettingsWindow.py
│ ├── ContainerManager.py
│ ├── Model.py
│ └── clientfasterwhisper.py
└── Freescribe.server
│ ├── server.py
│ ├── serverwhisperx.py
│ └── serverfasterwhisper.py
├── .gitignore
├── mac
├── distribution.xml
└── scripts
│ └── postinstall
├── .github
└── workflows
│ ├── README.md
│ └── release.yml
├── client_requirements_nvidia.txt
└── README.md
/scripts/CPU_INSTALL.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/NVIDIA_INSTALL.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client_requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/client_requirements.txt
--------------------------------------------------------------------------------
/scripts/assets/logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/assets/logo.ico
--------------------------------------------------------------------------------
/src/FreeScribe.client/assets/logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/src/FreeScribe.client/assets/logo.ico
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/Banner.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/Banner.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/Dialer.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/Dialer.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/INetC.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/INetC.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/Math.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/Math.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/NSISdl.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/NSISdl.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/Splash.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/Splash.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/System.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/System.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/VPatch.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/VPatch.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/ZipDLL.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/ZipDLL.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/nsExec.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/nsExec.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/BgImage.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/BgImage.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/LangDLL.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/LangDLL.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/TypeLib.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/TypeLib.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/UserInfo.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/UserInfo.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/nsUnzip.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/nsUnzip.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/INetC.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/INetC.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/Math.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/Math.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/AdvSplash.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/AdvSplash.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/StartMenu.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/StartMenu.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/nsDialogs.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/nsDialogs.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/Banner.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/Banner.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/BgImage.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/BgImage.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/Dialer.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/Dialer.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/LangDLL.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/LangDLL.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/NSISdl.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/NSISdl.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/Splash.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/Splash.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/System.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/System.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/TypeLib.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/TypeLib.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/UserInfo.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/UserInfo.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/VPatch.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/VPatch.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/ZipDLL.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/ZipDLL.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/nsExec.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/nsExec.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/nsUnzip.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/nsUnzip.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/AccessControl.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/AccessControl.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/AdvSplash.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/AdvSplash.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/StartMenu.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/StartMenu.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/nsDialogs.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/nsDialogs.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-ansi/InstallOptions.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-ansi/InstallOptions.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/AccessControl.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/AccessControl.dll
--------------------------------------------------------------------------------
/scripts/nsis-plugins/x86-unicode/InstallOptions.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/scripts/nsis-plugins/x86-unicode/InstallOptions.dll
--------------------------------------------------------------------------------
/src/FreeScribe.client/whisper-assets/mel_filters.npz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/1984Doc/AI-Scribe/HEAD/src/FreeScribe.client/whisper-assets/mel_filters.npz
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | aiscribe.txt
2 | aiscribe2.txt
3 | settings.txt
4 |
5 | .venv
6 | __pycache__
7 | build
8 | dist
9 | recording.wav
10 | output
11 | client.spec
12 | .aider*
13 | .DS_Store
14 |
15 | realtime.wav
16 | src/FreeScribe.client/models
17 | freescribe-client.spec
18 | freescribe-client-cpu.spec
19 | freescribe-client-nvidia.spec
20 | scripts/FreeScribeInstaller.exe
21 |
--------------------------------------------------------------------------------
/mac/distribution.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | FreeScribe Installer
4 |
5 |
6 |
7 |
8 |
9 |
10 | #installer.pkg
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/FreeScribe.client/markdown/help/about.md:
--------------------------------------------------------------------------------
1 | # AI-Scribe
2 |
3 | ## Introduction
4 |
5 | > This is a script that I worked on to help empower physicians to alleviate the burden of documentation by utilizing a medical scribe to create SOAP notes. Expensive solutions could potentially share personal health information with their cloud-based operations. It utilizes `Koboldcpp` and `Whisper` on a local server that is concurrently running the `Server.py` script. The `Client.py` script can then be used by physicians on their device to record patient-physician conversations after a signed consent is obtained and process the result into a SOAP note.
6 | >
7 | > Regards,
8 | >
9 | > Braedon Hendy
10 |
--------------------------------------------------------------------------------
/scripts/hooks/hook-llama_cpp.py:
--------------------------------------------------------------------------------
1 | # How to use this file
2 | #
3 | # 1. create a folder called "hooks" in your repo
4 | # 2. copy this file there
5 | # 3. add the --additional-hooks-dir flag to your pyinstaller command:
6 | # ex: `pyinstaller --name binary-name --additional-hooks-dir=./hooks entry-point.py`
7 |
8 |
9 | from PyInstaller.utils.hooks import collect_data_files, get_package_paths
10 | import os, sys
11 |
12 | # Get the package path
13 | package_path = get_package_paths('llama_cpp')[0]
14 |
15 | # Collect data files
16 | datas = collect_data_files('llama_cpp')
17 |
18 | # Append the additional .dll or .so file
19 | dll_path = os.path.join(package_path, 'llama_cpp', 'lib', 'llama.dll')
20 | datas.append((dll_path, 'llama_cpp'))
21 |
--------------------------------------------------------------------------------
/.github/workflows/README.md:
--------------------------------------------------------------------------------
1 | # Pushing and Building Release on GitHub Workflow
2 |
3 | ## Steps to Push and Build a Release
4 |
5 | 1. **Checkout the main branch:**
6 |
7 | ```sh
8 | git checkout main
9 | ```
10 |
11 | 2. **Pull the latest changes from the main branch:**
12 |
13 | ```sh
14 | git pull origin main
15 | ```
16 |
17 | 3. **Tag the release:**
18 |
19 | ```sh
20 | git tag
21 | ```
22 |
23 | Example:
24 |
25 | ```sh
26 | git tag v0.0.1
27 | ```
28 |
29 | 4. **Push the changes and the tag to the remote repository:**
30 |
31 | ```sh
32 | git push origin
33 | ```
34 |
35 | Example:
36 |
37 | ```sh
38 | git push origin v0.0.1
39 | ```
40 |
41 | ## Tagging Conventions
42 |
43 | - `vx.x.x` tags latest release.
44 | - `vx.x.x.alpha` tags pre-release.
45 |
--------------------------------------------------------------------------------
/mac/scripts/postinstall:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Set Hugging Face model URL and output directory
4 | MODEL_URL="https://huggingface.co/lmstudio-community/gemma-2-2b-it-GGUF/resolve/main/gemma-2-2b-it-Q8_0.gguf?download=true"
5 | OUTPUT_DIR="/Users/Shared/FreeScribe/models"
6 |
7 | # Ensure the output directory exists
8 | mkdir -p "$OUTPUT_DIR"
9 |
10 | echo "Checking if the model already exists..."
11 | if [ -f "$OUTPUT_DIR/gemma-2-2b-it-Q8_0.gguf" ]; then
12 | echo "Model already exists. Skipping download."
13 | exit 0
14 | fi
15 | echo "Downloading model from Hugging Face..."
16 | curl -L "$MODEL_URL" -o "$OUTPUT_DIR/gemma-2-2b-it-Q8_0.gguf"
17 |
18 | # Check if the download succeeded
19 | if [ $? -eq 0 ]; then
20 | echo "Model downloaded successfully."
21 | else
22 | echo "Failed to download the model. Please check your internet connection."
23 | exit 1
24 | fi
25 |
--------------------------------------------------------------------------------
/src/FreeScribe.client/utils/ip_utils.py:
--------------------------------------------------------------------------------
1 | import ipaddress
2 | from urllib.parse import urlparse
3 | import re
4 |
5 |
6 | def extract_ip_from_url(url):
7 | """
8 | Extract the IP address from a URL.
9 |
10 | :param url: The URL to extract the IP address from.
11 | :type url: str
12 | :return: The extracted IP address.
13 | :rtype: str
14 | """
15 | parsed_url = urlparse(url)
16 | return parsed_url.hostname
17 |
18 | def is_private_ip(ip_or_url):
19 | """
20 | Check if the given IP address is a private IP address (RFC 1918).
21 |
22 | :param ip: The IP address to check.
23 | :type ip: str
24 | :return: True if the IP address is private, False otherwise.
25 | :rtype: bool
26 | """
27 | try:
28 | ip = extract_ip_from_url(ip_or_url) if '://' in ip_or_url else ip_or_url
29 | ip_obj = ipaddress.ip_address(ip)
30 | return ip_obj.is_private
31 | except ValueError:
32 | # Handle invalid IP address
33 | return False
34 |
35 | def is_valid_url(url):
36 | """
37 | Validates if the provided string is a URL.
38 |
39 | A URL is considered valid if it starts with 'http://' or 'https://',
40 | optionally includes 'www.', and contains a combination of alphanumeric
41 | characters, dots, and slashes.
42 |
43 | Args:
44 | url (str): The URL string to validate.
45 |
46 | Returns:
47 | bool: True if the string is a valid URL, False otherwise.
48 | """
49 |
50 | url_regex = re.compile(r'https?://(?:www\.)?[a-zA-Z0-9./]+')
51 | return re.match(url_regex, url) is not None
52 |
--------------------------------------------------------------------------------
/client_requirements_nvidia.txt:
--------------------------------------------------------------------------------
1 | --index-url https://abetlen.github.io/llama-cpp-python/whl/cu121 --extra-index-url https://pypi.org/simple
2 | altgraph==0.17.4
3 | annotated-types==0.7.0
4 | anyio==4.6.0
5 | catalogue==2.0.10
6 | certifi==2024.8.30
7 | charset-normalizer==3.3.2
8 | click==8.1.7
9 | colorama==0.4.6
10 | dateparser==1.2.0
11 | distro==1.9.0
12 | exceptiongroup==1.2.2
13 | Faker==30.0.0
14 | filelock==3.16.1
15 | fsspec==2024.9.0
16 | h11==0.14.0
17 | httpcore==1.0.5
18 | httpx==0.27.2
19 | idna==3.10
20 | Jinja2==3.1.4
21 | jiter==0.5.0
22 | joblib==1.4.2
23 | llvmlite==0.43.0
24 | MarkupSafe==2.1.5
25 | more-itertools==10.5.0
26 | mpmath==1.3.0
27 | networkx==3.3
28 | nltk==3.9.1
29 | numba==0.60.0
30 | numpy==1.26.4
31 | openai==1.50.2
32 | openai-whisper==20240927
33 | packaging==24.1
34 | pefile==2024.8.26
35 | phonenumbers==8.13.46
36 | PyAudio==0.2.14
37 | pydantic==2.9.2
38 | pydantic_core==2.23.4
39 | pyinstaller==6.10.0
40 | pyinstaller-hooks-contrib==2024.8
41 | pyperclip==1.9.0
42 | python-dateutil==2.9.0.post0
43 | python-stdnum==1.20
44 | pytz==2024.2
45 | pywin32-ctypes==0.2.3
46 | regex==2024.9.11
47 | requests==2.32.3
48 | scikit-learn==1.5.2
49 | scipy==1.14.1
50 | scrubadub==2.0.1
51 | six==1.16.0
52 | sniffio==1.3.1
53 | SpeechRecognition==3.10.4
54 | sympy==1.13.3
55 | textblob==0.15.3
56 | threadpoolctl==3.5.0
57 | tiktoken==0.7.0
58 | torch==2.2.2
59 | tqdm==4.66.5
60 | typing_extensions==4.12.2
61 | tzdata==2024.2
62 | tzlocal==5.2
63 | urllib3==2.2.3
64 | docker==7.1.0
65 | markdown==3.7
66 | tkhtmlview==0.3.1
67 | llama-cpp-python==v0.2.90
68 |
--------------------------------------------------------------------------------
/src/FreeScribe.client/markdown/welcome.md:
--------------------------------------------------------------------------------
1 | # Welcome to the FreeScribe Project
2 |
3 | Welcome to the FreeScribe project! This project aims to provide an intelligent medical scribe application that assists healthcare professionals by transcribing conversations and generating medical notes.
4 |
5 | ## Introduction
6 |
7 | The FreeScribe project leverages advanced machine learning models to transcribe conversations between healthcare providers and patients. It also generates structured medical notes based on the transcriptions, helping to streamline the documentation process in clinical settings.
8 |
9 | ## Features
10 |
11 | - **Real-time Transcription**: Transcribe conversations in real-time using advanced speech recognition models.
12 | - **Medical Note Generation**: Automatically generate structured medical notes from transcriptions.
13 | - **User-Friendly Interface**: Intuitive and easy-to-use interface for healthcare professionals.
14 | - **Customizable Settings**: Customize the application settings to suit your workflow.
15 | - **Docker Integration**: Easily manage the application using Docker containers.
16 |
17 | ## Discord Community
18 |
19 | Join our Discord community to connect with other users, get support, and collaborate on the AI Medical Scribe project. Our community is a great place to ask questions, share ideas, and stay updated on the latest developments.
20 |
21 | [Join our Discord Community](https://discord.gg/6DnPENSn)
22 |
23 | ## Contributing
24 |
25 | We welcome contributions to the FreeScribe project! To contribute:
26 |
27 | 1. Fork the [repository](https://github.com/ClinicianFOCUS/FreeScribe).
28 | 2. Create a new branch (`git checkout -b feature/your-feature`).
29 | 3. Make your changes and commit them (`git commit -m 'Add some feature'`).
30 | 4. Push to the branch (`git push origin feature/your-feature`).
31 | 5. Open a pull request.
32 |
33 | Please ensure your code adheres to our coding standards and includes appropriate tests.
34 |
35 | ## License
36 |
37 | This project is licensed under the MIT License. See the [LICENSE](https://github.com/ClinicianFOCUS/FreeScribe/blob/main/LICENSE.txt) file for more information.
38 |
--------------------------------------------------------------------------------
/src/Freescribe.server/server.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2023 Braedon Hendy
2 | # This software is released under the GNU General Public License v3.0
3 |
4 | from http.server import BaseHTTPRequestHandler, HTTPServer
5 | import whisper
6 | import cgi
7 | import json
8 | import os
9 | import tempfile
10 |
11 | # Initialize Whisper model
12 | model = whisper.load_model("medium")
13 |
14 | class RequestHandler(BaseHTTPRequestHandler):
15 | def do_POST(self):
16 | if self.path == '/whisperaudio':
17 | ctype, pdict = cgi.parse_header(self.headers.get('content-type'))
18 | if ctype == 'multipart/form-data':
19 | pdict['boundary'] = bytes(pdict['boundary'], "utf-8")
20 | fields = cgi.parse_multipart(self.rfile, pdict)
21 | audio_data = fields.get('audio')[0]
22 |
23 | # Save the audio file temporarily
24 | with tempfile.NamedTemporaryFile(delete=False) as temp_audio_file:
25 | temp_audio_file.write(audio_data)
26 | temp_file_path = temp_audio_file.name
27 |
28 | try:
29 | # Process the file with Whisper
30 | result = model.transcribe(temp_file_path)
31 |
32 | # Send response
33 | self.send_response(200)
34 | self.send_header('Content-type', 'application/json')
35 | self.end_headers()
36 | response_data = json.dumps({"text": result["text"]})
37 | self.wfile.write(response_data.encode())
38 | finally:
39 | # Clean up the temporary file
40 | os.remove(temp_file_path)
41 | else:
42 | self.send_error(400, "Invalid content type")
43 | else:
44 | self.send_error(404, "File not found")
45 |
46 | def run(server_class=HTTPServer, handler_class=RequestHandler, port=8000):
47 | server_address = ('', port)
48 | httpd = server_class(server_address, handler_class)
49 | print(f'Server running at http://localhost:{port}/')
50 | httpd.serve_forever()
51 |
52 | if __name__ == '__main__':
53 | run()
54 |
--------------------------------------------------------------------------------
/src/FreeScribe.client/utils/file_utils.py:
--------------------------------------------------------------------------------
1 | import ctypes
2 | from functools import lru_cache
3 | import os
4 | import sys
5 |
6 | def get_file_path(*file_names: str) -> str:
7 | """
8 | Get the full path to a files. Use Temporary directory at runtime for bundled apps, otherwise use the current working directory.
9 |
10 | :param file_names: The names of the directories and the file.
11 | :type file_names: str
12 | :return: The full path to the file.
13 | :rtype: str
14 | """
15 | base = sys._MEIPASS if hasattr(sys, '_MEIPASS') else os.path.abspath('.')
16 | return os.path.join(base, *file_names)
17 |
18 | def get_resource_path(filename: str) -> str:
19 | """
20 | Get the path to the files. Use User data directory for bundled apps, otherwise use the current working directory.
21 |
22 | :param filename: The name of the file.
23 | :type filename: str
24 | :return: The full path to the file.
25 | :rtype: str
26 | """
27 | if hasattr(sys, '_MEIPASS'):
28 | base = _get_user_data_dir()
29 | freescribe_dir = os.path.join(base, 'FreeScribe')
30 |
31 | # Check if the FreeScribe directory exists, if not, create it
32 | try:
33 | if not os.path.exists(freescribe_dir):
34 | os.makedirs(freescribe_dir)
35 | except OSError as e:
36 | raise RuntimeError(f"Failed to create FreeScribe directory: {e}")
37 |
38 | return os.path.join(freescribe_dir, filename)
39 | else:
40 | return os.path.abspath(filename)
41 |
42 | def _get_user_data_dir() -> str:
43 | """
44 | Get the user data directory for the current platform.
45 |
46 | :return: The path to the user data directory.
47 | :rtype: str
48 | """
49 | if sys.platform == "win32": # Windows
50 | buf = ctypes.create_unicode_buffer(1024)
51 | ctypes.windll.shell32.SHGetFolderPathW(None, 0x001a, None, 0, buf)
52 | return buf.value
53 | elif sys.platform == "darwin": # macOS
54 | return os.path.expanduser("~/Library/Application Support")
55 | else: # Linux
56 | path = os.environ.get("XDG_DATA_HOME", "")
57 | if not path.strip():
58 | path = os.path.expanduser("~/.local/share")
59 | return self._append_app_name_and_version(path)
60 |
61 |
--------------------------------------------------------------------------------
/src/FreeScribe.client/presets/ChatGPT.json:
--------------------------------------------------------------------------------
1 | {
2 | "openai_api_key": "None",
3 | "editable_settings": {
4 | "Model": "gpt-4o-mini",
5 | "Model Endpoint": "https://api.openai.com/v1/",
6 | "use_story": 0,
7 | "use_memory": 0,
8 | "use_authors_note": 0,
9 | "use_world_info": 0,
10 | "max_context_length": 5000,
11 | "max_length": 400,
12 | "rep_pen": "1.1",
13 | "rep_pen_range": 5000,
14 | "rep_pen_slope": "0.7",
15 | "temperature": "0.1",
16 | "tfs": "0.97",
17 | "top_a": "0.8",
18 | "top_k": 30,
19 | "top_p": "0.4",
20 | "typical": "0.19",
21 | "sampler_order": "[6, 0, 1, 3, 4, 2, 5]",
22 | "singleline": 0,
23 | "frmttriminc": 0,
24 | "frmtrmblln": 0,
25 | "Local Whisper": 1,
26 | "Whisper Endpoint": "https://localhost:2224/whisperaudio",
27 | "Whisper Server API Key": "None",
28 | "Whisper Model": "small.en",
29 | "Real Time": 1,
30 | "Real Time Audio Length": "5",
31 | "Real Time Silence Length": 1,
32 | "Silence cut-off": 0.035003662109375,
33 | "LLM Container Name": "ollama",
34 | "LLM Caddy Container Name": "caddy-ollama",
35 | "Whisper Container Name": "speech-container",
36 | "Whisper Caddy Container Name": "caddy",
37 | "Auto Shutdown Containers on Exit": 1,
38 | "Use Docker Status Bar": 0,
39 | "Preset": "Custom",
40 | "Use Local LLM": 0,
41 | "Architecture": "CPU",
42 | "best_of": "2",
43 | "Use best_of": 0,
44 | "LLM Authentication Container Name": "authentication-ollama",
45 | "Show Welcome Message": 0,
46 | "Enable Scribe Template": 0,
47 | "Use Pre-Processing": 1,
48 | "Use Post-Processing": 0,
49 | "AI Server Self-Signed Certificates": 0,
50 | "S2T Server Self-Signed Certificates": 0,
51 | "Pre-Processing": "Please break down the conversation into a list of facts. Take the conversation and transform it to a easy to read list:\n\n",
52 | "Post-Processing": "\n\nUsing the provided list of facts, review the SOAP note for accuracy. Verify that all details align with the information provided in the list of facts and ensure consistency throughout. Update or adjust the SOAP note as necessary to reflect the listed facts without offering opinions or subjective commentary. Ensure that the revised note excludes a \"Notes\" section and does not include a header for the SOAP note. Provide the revised note after making any necessary corrections.",
53 | "Show Scrub PHI": 1
54 | },
55 | "api_style": "OpenAI"
56 | }
57 |
--------------------------------------------------------------------------------
/src/FreeScribe.client/presets/Local AI.json:
--------------------------------------------------------------------------------
1 | {
2 | "openai_api_key": "None",
3 | "editable_settings": {
4 | "Model": "gemma2:2b-instruct-q8_0",
5 | "Model Endpoint": "http://localhost:3334/v1/",
6 | "use_story": 0,
7 | "use_memory": 0,
8 | "use_authors_note": 0,
9 | "use_world_info": 0,
10 | "max_context_length": 5000,
11 | "max_length": 400,
12 | "rep_pen": "1.1",
13 | "rep_pen_range": 5000,
14 | "rep_pen_slope": "0.7",
15 | "temperature": "0.1",
16 | "tfs": "0.97",
17 | "top_a": "0.8",
18 | "top_k": 30,
19 | "top_p": "0.4",
20 | "typical": "0.19",
21 | "sampler_order": "[6, 0, 1, 3, 4, 2, 5]",
22 | "singleline": 0,
23 | "frmttriminc": 0,
24 | "frmtrmblln": 0,
25 | "Local Whisper": 1,
26 | "Whisper Endpoint": "https://localhost:2224/whisperaudio/",
27 | "Whisper Server API Key": "None",
28 | "Whisper Model": "small.en",
29 | "Real Time": 1,
30 | "Real Time Audio Length": "5",
31 | "Real Time Silence Length": 1,
32 | "Silence cut-off": 0.035003662109375,
33 | "LLM Container Name": "ollama",
34 | "LLM Caddy Container Name": "caddy-ollama",
35 | "Whisper Container Name": "speech-container",
36 | "Whisper Caddy Container Name": "caddy",
37 | "Auto Shutdown Containers on Exit": 1,
38 | "Use Docker Status Bar": 0,
39 | "Preset": "Custom",
40 | "Use Local LLM": 1,
41 | "Architecture": "CPU",
42 | "best_of": "2",
43 | "Use best_of": 0,
44 | "LLM Authentication Container Name": "authentication-ollama",
45 | "Show Welcome Message": 0,
46 | "Enable Scribe Template": 0,
47 | "Use Pre-Processing": 1,
48 | "Use Post-Processing": 0,
49 | "AI Server Self-Signed Certificates": 0,
50 | "S2T Server Self-Signed Certificates": 0,
51 | "Pre-Processing": "Please break down the conversation into a list of facts. Take the conversation and transform it to a easy to read list:\n\n",
52 | "Post-Processing": "\n\nUsing the provided list of facts, review the SOAP note for accuracy. Verify that all details align with the information provided in the list of facts and ensure consistency throughout. Update or adjust the SOAP note as necessary to reflect the listed facts without offering opinions or subjective commentary. Ensure that the revised note excludes a \"Notes\" section and does not include a header for the SOAP note. Provide the revised note after making any necessary corrections.",
53 | "Show Scrub PHI": 1
54 | },
55 | "api_style": "OpenAI"
56 | }
57 |
--------------------------------------------------------------------------------
/src/FreeScribe.client/presets/ClinicianFOCUS Toolbox.json:
--------------------------------------------------------------------------------
1 | {
2 | "openai_api_key": "None",
3 | "editable_settings": {
4 | "Model": "gemma-2-2b-it-Q8_0.gguf",
5 | "Model Endpoint": "http://localhost:3334/v1/",
6 | "use_story": 0,
7 | "use_memory": 0,
8 | "use_authors_note": 0,
9 | "use_world_info": 0,
10 | "max_context_length": 5000,
11 | "max_length": 400,
12 | "rep_pen": "1.1",
13 | "rep_pen_range": 5000,
14 | "rep_pen_slope": "0.7",
15 | "temperature": "0.1",
16 | "tfs": "0.97",
17 | "top_a": "0.8",
18 | "top_k": 30,
19 | "top_p": "0.4",
20 | "typical": "0.19",
21 | "sampler_order": "[6, 0, 1, 3, 4, 2, 5]",
22 | "singleline": 0,
23 | "frmttriminc": 0,
24 | "frmtrmblln": 0,
25 | "Local Whisper": 0,
26 | "Whisper Endpoint": "https://localhost:2224/whisperaudio/",
27 | "Whisper Server API Key": "None",
28 | "Whisper Model": "small.en",
29 | "Real Time": 1,
30 | "Real Time Audio Length": "5",
31 | "Real Time Silence Length": 1,
32 | "Silence cut-off": 0.035003662109375,
33 | "LLM Container Name": "ollama",
34 | "LLM Caddy Container Name": "caddy-ollama",
35 | "Whisper Container Name": "speech-container",
36 | "Whisper Caddy Container Name": "caddy",
37 | "Auto Shutdown Containers on Exit": 1,
38 | "Use Docker Status Bar": 0,
39 | "Preset": "Custom",
40 | "Use Local LLM": 0,
41 | "Architecture": "CPU",
42 | "best_of": "2",
43 | "Use best_of": 0,
44 | "LLM Authentication Container Name": "authentication-ollama",
45 | "Show Welcome Message": 0,
46 | "Enable Scribe Template": 0,
47 | "Use Pre-Processing": 1,
48 | "Use Post-Processing": 0,
49 | "AI Server Self-Signed Certificates": 0,
50 | "S2T Server Self-Signed Certificates": 0,
51 | "Pre-Processing": "Please break down the conversation into a list of facts. Take the conversation and transform it to a easy to read list:\n\n",
52 | "Post-Processing": "\n\nUsing the provided list of facts, review the SOAP note for accuracy. Verify that all details align with the information provided in the list of facts and ensure consistency throughout. Update or adjust the SOAP note as necessary to reflect the listed facts without offering opinions or subjective commentary. Ensure that the revised note excludes a \"Notes\" section and does not include a header for the SOAP note. Provide the revised note after making any necessary corrections.",
53 | "Show Scrub PHI": 0
54 | },
55 | "api_style": "OpenAI"
56 | }
57 |
--------------------------------------------------------------------------------
/src/Freescribe.server/serverwhisperx.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2023 Braedon Hendy
2 | # This software is released under the GNU General Public License v3.0
3 |
4 | from http.server import BaseHTTPRequestHandler, HTTPServer
5 | import whisperx
6 | import cgi
7 | import json
8 | import os
9 | import tempfile
10 |
11 | # Initialize Whisper model
12 | model_size = "medium.en"
13 |
14 | class RequestHandler(BaseHTTPRequestHandler):
15 | def do_POST(self):
16 | if self.path == '/whisperaudio':
17 | ctype, pdict = cgi.parse_header(self.headers.get('content-type'))
18 | if ctype == 'multipart/form-data':
19 | pdict['boundary'] = bytes(pdict['boundary'], "utf-8")
20 | fields = cgi.parse_multipart(self.rfile, pdict)
21 | audio_data = fields.get('audio')[0]
22 |
23 | # Save the audio file temporarily
24 | with tempfile.NamedTemporaryFile(delete=False) as temp_audio_file:
25 | temp_audio_file.write(audio_data)
26 | temp_file_path = temp_audio_file.name
27 |
28 | try:
29 | # Process the file with Whisper
30 | model = whisperx.load_model(model_size, device="cuda", compute_type="float16")
31 | audio = whisperx.load_audio(temp_file_path)
32 | result = model.transcribe(audio)
33 | text_segments = [segment['text'] for segment in result['segments']]
34 | transcription = " ".join(text_segments)
35 |
36 | # Send response
37 | self.send_response(200)
38 | self.send_header('Content-type', 'application/json')
39 | self.end_headers()
40 | response_data = json.dumps({"text": transcription})
41 | self.wfile.write(response_data.encode())
42 | finally:
43 | # Clean up the temporary file
44 | os.remove(temp_file_path)
45 | else:
46 | self.send_error(400, "Invalid content type")
47 | else:
48 | self.send_error(404, "File not found")
49 |
50 | def run(server_class=HTTPServer, handler_class=RequestHandler, port=8000):
51 | server_address = ('', port)
52 | httpd = server_class(server_address, handler_class)
53 | print(f'Server running at http://localhost:{port}/')
54 | httpd.serve_forever()
55 |
56 | if __name__ == '__main__':
57 | run()
58 |
--------------------------------------------------------------------------------
/src/Freescribe.server/serverfasterwhisper.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2023 Braedon Hendy
2 | # This software is released under the GNU General Public License v3.0
3 |
4 | from http.server import BaseHTTPRequestHandler, HTTPServer
5 | from faster_whisper import WhisperModel
6 | import cgi
7 | import json
8 | import os
9 | import tempfile
10 |
11 | # Initialize Whisper model
12 | model_size = "medium.en"
13 |
14 | class RequestHandler(BaseHTTPRequestHandler):
15 | def do_POST(self):
16 | if self.path == '/whisperaudio':
17 | ctype, pdict = cgi.parse_header(self.headers.get('content-type'))
18 | if ctype == 'multipart/form-data':
19 | pdict['boundary'] = bytes(pdict['boundary'], "utf-8")
20 | fields = cgi.parse_multipart(self.rfile, pdict)
21 | audio_data = fields.get('audio')[0]
22 |
23 | # Save the audio file temporarily
24 | with tempfile.NamedTemporaryFile(delete=False) as temp_audio_file:
25 | temp_audio_file.write(audio_data)
26 | temp_file_path = temp_audio_file.name
27 |
28 | try:
29 | # Process the file with Whisper
30 | model = WhisperModel(model_size, device="cuda", compute_type="float16")
31 | # or run on GPU with INT8
32 | # model = WhisperModel(model_size, device="cuda", compute_type="int8_float16")
33 | # or run on CPU with INT8
34 | # model = WhisperModel(model_size, device="cpu", compute_type="int8")
35 | segments, info = model.transcribe(temp_file_path, beam_size=5)
36 | print("Detected language '%s' with probability %f" % (info.language, info.language_probability))
37 | transcription = "".join(segment.text for segment in segments)
38 |
39 | # Send response
40 | self.send_response(200)
41 | self.send_header('Content-type', 'application/json')
42 | self.end_headers()
43 | response_data = json.dumps({"text": transcription})
44 | self.wfile.write(response_data.encode())
45 | finally:
46 | # Clean up the temporary file
47 | os.remove(temp_file_path)
48 | else:
49 | self.send_error(400, "Invalid content type")
50 | else:
51 | self.send_error(404, "File not found")
52 |
53 | def run(server_class=HTTPServer, handler_class=RequestHandler, port=8000):
54 | server_address = ('', port)
55 | httpd = server_class(server_address, handler_class)
56 | print(f'Server running at http://localhost:{port}/')
57 | httpd.serve_forever()
58 |
59 | if __name__ == '__main__':
60 | run()
61 |
--------------------------------------------------------------------------------
/src/FreeScribe.client/UI/MarkdownWindow.py:
--------------------------------------------------------------------------------
1 | from tkinter import Toplevel, messagebox
2 | import markdown as md
3 | import tkinter as tk
4 | from tkhtmlview import HTMLLabel
5 | from utils.file_utils import get_file_path
6 |
7 | """
8 | A class to create a window displaying rendered Markdown content.
9 | Attributes:
10 | -----------
11 | window : Toplevel
12 | The top-level window for displaying the Markdown content.
13 | Methods:
14 | --------
15 | __init__(parent, title, file_path, callback=None):
16 | Initializes the MarkdownWindow with the given parent, title, file path, and optional callback.
17 | _on_close(var, callback):
18 | Handles the window close event, invoking the callback with the state of the checkbox.
19 | """
20 | class MarkdownWindow:
21 | """
22 | Initializes the MarkdownWindow.
23 | Parameters:
24 | -----------
25 | parent : widget
26 | The parent widget.
27 | title : str
28 | The title of the window.
29 | file_path : str
30 | The path to the Markdown file to be rendered.
31 | callback : function, optional
32 | A callback function to be called when the window is closed, with the state of the checkbox.
33 | """
34 | def __init__(self, parent, title, file_path, callback=None):
35 | self.window = Toplevel(parent)
36 | self.window.title(title)
37 | self.window.transient(parent)
38 | self.window.grab_set()
39 | self.window.iconbitmap(get_file_path('assets','logo.ico'))
40 |
41 | try:
42 | with open(file_path, "r") as file:
43 | content = md.markdown(file.read(), extensions=["extra", "smarty"])
44 |
45 | except FileNotFoundError:
46 | print(f"File not found: {file_path}")
47 | self.window.destroy()
48 | messagebox.showerror("Error", "File not found")
49 | return
50 |
51 | # Create a frame to hold the HTMLLabel and scrollbar
52 | frame = tk.Frame(self.window)
53 | frame.pack(fill="both", expand=True, padx=10, pady=10)
54 |
55 | # Create the HTMLLabel widget
56 | html_label = HTMLLabel(frame, html=content)
57 | html_label.pack(side="left", fill="both", expand=True)
58 |
59 | # Create the scrollbar
60 | scrollbar = tk.Scrollbar(frame, orient="vertical", command=html_label.yview)
61 | scrollbar.pack(side="right", fill="y")
62 |
63 | # Configure the HTMLLabel to use the scrollbar
64 | html_label.config(yscrollcommand=scrollbar.set)
65 |
66 | if callback:
67 | var = tk.BooleanVar()
68 | tk.Checkbutton(self.window, text="Don't show this message again",
69 | variable=var).pack(side=tk.BOTTOM, pady=10)
70 | self.window.protocol("WM_DELETE_WINDOW",
71 | lambda: self._on_close(var, callback))
72 |
73 | # Add a close button at the bottom center
74 | close_button = tk.Button(self.window, text="Close", command=lambda: self._on_close(var, callback))
75 | close_button.pack(side=tk.BOTTOM) # Extra padding for separation from the checkbox
76 | else:
77 | # Add a close button at the bottom center
78 | close_button = tk.Button(self.window, text="Close", command= self.window.destroy)
79 | close_button.pack(side=tk.BOTTOM , pady=5) # Extra padding for separation from the checkbox
80 |
81 | self.window.geometry("900x900")
82 | self.window.lift()
83 |
84 |
85 | """
86 | Handles the window close event.
87 | Parameters:
88 | -----------
89 | var : BooleanVar
90 | The Tkinter BooleanVar associated with the checkbox.
91 | callback : function
92 | The callback function to be called with the state of the checkbox.
93 | """
94 | def _on_close(self, var, callback):
95 | callback(var.get())
96 | self.window.destroy()
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Build Workflow
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*.*.*"
7 | - "v*.*.*.alpha"
8 |
9 | jobs:
10 | build-windows:
11 | runs-on: windows-latest
12 | permissions:
13 | contents: write
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v1
17 |
18 | - name: Install Python
19 | uses: actions/setup-python@v1
20 | with:
21 | python-version: "3.10"
22 | architecture: "x64"
23 |
24 | # Create CUDA-enabled executable
25 | - name: Install CUDA-enabled llama_cpp
26 | run: |
27 | pip install --index-url https://abetlen.github.io/llama-cpp-python/whl/cu121 --extra-index-url https://pypi.org/simple llama-cpp-python==v0.2.90
28 |
29 | - name: Install requirements
30 | run: |
31 | pip install -r client_requirements.txt
32 |
33 | - name: Run PyInstaller for NVIDIA
34 | run: |
35 | pyinstaller --additional-hooks-dir=.\scripts\hooks --add-data ".\scripts\NVIDIA_INSTALL.txt:install_state" --add-data ".\src\FreeScribe.client\whisper-assets:whisper\assets" --add-data ".\src\FreeScribe.client\markdown:markdown" --add-data ".\src\FreeScribe.client\assets:assets" --name freescribe-client-nvidia --icon=.\src\FreeScribe.client\assets\logo.ico --noconsole .\src\FreeScribe.client\client.py
36 |
37 | # Create CPU-only executable
38 | - name: Uninstall CUDA-enabled llama_cpp (if necessary) and install CPU-only llama_cpp
39 | run: |
40 | pip uninstall -y llama-cpp-python
41 | pip install --index-url https://abetlen.github.io/llama-cpp-python/whl/cpu --extra-index-url https://pypi.org/simple llama-cpp-python==v0.2.90
42 |
43 | - name: Run PyInstaller for CPU-only
44 | run: |
45 | pyinstaller --additional-hooks-dir=.\scripts\hooks --add-data ".\scripts\CPU_INSTALL.txt:install_state" --add-data ".\src\FreeScribe.client\whisper-assets:whisper\assets" --add-data ".\src\FreeScribe.client\markdown:markdown" --add-data ".\src\FreeScribe.client\assets:assets" --name freescribe-client-cpu --icon=.\src\FreeScribe.client\assets\logo.ico --noconsole .\src\FreeScribe.client\client.py
46 |
47 | - name: Set up NSIS
48 | uses: joncloud/makensis-action@1c9f4bf2ea0c771147db31a2f3a7f5d8705c0105
49 | with:
50 | script-file: .\scripts\install.nsi
51 | additional-plugin-paths: "./scripts/nsis-plugins"
52 |
53 | - name: Check if alpha release
54 | id: check_alpha
55 | run: |
56 | if ("${{ github.ref }}" -like "*.alpha") {
57 | echo "is_alpha=true" >> $env:GITHUB_OUTPUT
58 | } else {
59 | echo "is_alpha=false" >> $env:GITHUB_OUTPUT
60 | }
61 | shell: pwsh
62 |
63 | - name: Create release
64 | id: create_release
65 | uses: actions/create-release@v1
66 | env:
67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
68 | with:
69 | tag_name: ${{ github.ref }}
70 | release_name: Release ${{ github.ref }}
71 | body: |
72 | ## What's Changed
73 | ${{ steps.changelog.outputs.CHANGELOG }}
74 |
75 | For full changelog, see [the commits since last release](${{ github.server_url }}/${{ github.repository }}/compare/${{ steps.changelog.last_tag }}...${{ github.ref }})
76 | draft: false
77 | prerelease: ${{ steps.check_alpha.outputs.is_alpha == 'true' }}
78 |
79 | - name: Upload Installer
80 | id: upload-installer
81 | uses: actions/upload-release-asset@v1
82 | env:
83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
84 | with:
85 | upload_url: ${{steps.create_release.outputs.upload_url}}
86 | asset_path: dist/FreeScribeInstaller.exe
87 | asset_name: FreeScribeInstaller_windows.exe
88 | asset_content_type: application/octet-stream
89 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AI-Scribe
2 |
3 | ## Introduction
4 |
5 | > This is a script that I worked on to help empower physicians to alleviate the burden of documentation by utilizing a medical scribe to create SOAP notes. Expensive solutions could potentially share personal health information with their cloud-based operations. It utilizes `Koboldcpp` and `Whisper` on a local server that is concurrently running the `Server.py` script. The `Client.py` script can then be used by physicians on their device to record patient-physician conversations after a signed consent is obtained and process the result into a SOAP note.
6 | >
7 | > Regards,
8 | >
9 | > Braedon Hendy
10 |
11 | ## Demo
12 | [Youtube Demo](https://www.youtube.com/watch?v=w8kUB8Y3A30)
13 |
14 | ## Changelog
15 |
16 | - **2024-03-17** - updated `client.py` to allow for `OpenAI` token access when `GPT` button is selected. A prompt will show to allow for scrubbing of any personal health information.
17 | - **2024-03-28** - updated `client.py` to allow for `Whisper` to run locally when set to `True` in the settings.
18 | - **2024-03-29** - added `Scrubadub` to be used to remove personal information prior to `OpenAI` token access.
19 | - **2024-04-26** - added alternative server file to use `Faster-Whisper`
20 | - **2024-05-03** - added alternative server file to use `WhisperX`
21 | - **2024-05-06** - added real-time `Whisper` processing
22 | - **2024-05-13** - added `SSL` and OHIP scrubbing
23 | - **2024-05-14** - `GPT` model selection
24 | - **2024-06-01** - template options and further fine-tuning for local and remote real-time `Whisper`
25 |
26 | ## Setup on a Local Machine
27 |
28 | Example instructions for running on a single machine:
29 |
30 | I will preface that this will run slowly if you are not using a GPU but will demonstrate the capability.
31 |
32 | Install `Python` `3.10.9` [HERE](https://www.python.org/downloads/release/python-3109/). (if the hyperlink doesn't work https://www.python.org/downloads/release/python-3109/). Make sure you click the checkbox to select "`Add Python to Path`".
33 |
34 | Next, you need to install software to convert the audio file to be processed. Press `Windows key` + `R`, you can run the command line by typing `powershell`. Copy/type the following:
35 |
36 | ```powershell
37 | Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
38 | Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
39 | scoop install ffmpeg
40 | ```
41 |
42 | If this was successful, you need to download the files that I wrote [HERE](https://github.com/1984Doc/AI-Scribe). Unzip the files (if the hyperlink doesn't work https://github.com/1984Doc/AI-Scribe).
43 |
44 | Run the `client.py` (it may prompt for installation of various dependencies via `pip`)
45 |
46 | I would recommend using the `GPT` option using an `API key`. The cost for running each model may determine the overall choice and can be selected in the `Settings` menu of the program.
47 |
48 | ## Setup on a Server
49 |
50 | Example instructions for running on a server with a GPU:
51 |
52 | Install `Python` `3.10.9` [HERE](https://www.python.org/downloads/release/python-3109/). (if the hyperlink doesn't work https://www.python.org/downloads/release/python-3109/). Make sure you click the checkbox to select "`Add Python to Path`".
53 |
54 | Press `Windows key` + `R`, you can run the command line by typing `cmd`. Copy/type the following, running each line by pressing `Enter`:
55 |
56 | ```sh
57 | pip install openai-whisper
58 | ```
59 |
60 | Now you need to download the AI model (it is large). I recommend the `Mistral 7B v0.2` or `Meta Llama 3` models. These can be found on [HuggingFace.](https://huggingface.co/)
61 |
62 | You now need to launch the AI model with the following software that you can download [HERE](https://github.com/LostRuins/koboldcpp/releases). It will download automatically and you will need to open it (if hyperlink doesn't work https://github.com/LostRuins/koboldcpp/releases). If you have an **NVidia RTX**-based card, the below instructions can be modified using `Koboldcpp.exe` rather than `koboldcpp_nocuda.exe`.
63 |
64 | Once the `Koboldcpp.exe` is opened, click the `Browse` button and select the model downloaded. Now click the `Launch` button.
65 |
66 | You should see a window open and can ask it questions to test!
67 |
68 | If this was successful, you need to download the files that I wrote [HERE](https://github.com/1984Doc/AI-Scribe). Unzip the files (if the hyperlink doesn't work https://github.com/1984Doc/AI-Scribe).
69 |
70 | Run the `server.py` file. This will download the files to help organize the text after converting from audio.
71 |
72 | Run the `client.py` file and edit the IP addresses in the `Settings` menu.
73 |
74 | # How to run with JanAI
75 | 1. Download and install janAI and configure with your LLM of choice.
76 | 2. Start the JanAI server.
77 | 3. Open the python client applications and set the Model Endpoint to your settings in the JanAI (Typically http://localhost:1337/v1/ by default)
78 | 4. Set your model to the one of choice (Gemma 2 2b recommended Model ID: "gemma-2-2b-it")
79 | 5. Save the settings
80 | 6. Click the KoboldCPP button to enable custom endpoint.
81 |
--------------------------------------------------------------------------------
/src/FreeScribe.client/UI/Widgets/CustomTextBox.py:
--------------------------------------------------------------------------------
1 | """
2 | CustomTextBox.py
3 |
4 | This software is released under the AGPL-3.0 license
5 | Copyright (c) 2023-2024 Braedon Hendy
6 |
7 | Further updates and packaging added in 2024 through the ClinicianFOCUS initiative,
8 | a collaboration with Dr. Braedon Hendy and Conestoga College Institute of Applied
9 | Learning and Technology as part of the CNERG+ applied research project,
10 | Unburdening Primary Healthcare: An Open-Source AI Clinician Partner Platform".
11 | Prof. Michael Yingbull (PI), Dr. Braedon Hendy (Partner),
12 | and Research Students - Software Developer Alex Simko, Pemba Sherpa (F24), and Naitik Patel.
13 |
14 | Classes:
15 | CustomTextBox: Custom text box with copy text button overlay.
16 | """
17 |
18 | import tkinter as tk
19 | from tkinter import messagebox
20 |
21 | class CustomTextBox(tk.Frame):
22 | """
23 | A custom text box widget with a built-in copy button.
24 |
25 | This widget extends the `tk.Frame` class and contains a `tk.scrolledtext.ScrolledText` widget
26 | with an additional copy button placed in the bottom-right corner. The copy button allows
27 | users to copy the entire content of the text widget to the clipboard.
28 |
29 | :param parent: The parent widget.
30 | :type parent: tk.Widget
31 | :param height: The height of the text widget in lines of text. Defaults to 10.
32 | :type height: int, optional
33 | :param state: The state of the text widget, which can be 'normal' or 'disabled'. Defaults to 'normal'.
34 | :type state: str, optional
35 | :param kwargs: Additional keyword arguments to pass to the `tk.Frame` constructor.
36 | """
37 | def __init__(self, parent, height=10, state='normal', **kwargs):
38 | tk.Frame.__init__(self, parent, **kwargs)
39 |
40 | # Create scrolled text widget
41 | self.scrolled_text = tk.scrolledtext.ScrolledText(self, wrap="word", height=height, state=state)
42 | self.scrolled_text.pack(side="left", fill="both", expand=True)
43 |
44 | # Create copy button in bottom right corner
45 | self.copy_button = tk.Button(
46 | self.scrolled_text,
47 | text="Copy Text",
48 | command=self.copy_text,
49 | relief="raised",
50 | borderwidth=1
51 | )
52 | self.copy_button.place(relx=1.0, rely=1.0, x=-2, y=-2, anchor="se")
53 |
54 | def copy_text(self):
55 | """
56 | Copy all text from the text widget to the clipboard.
57 |
58 | If an error occurs during the copy operation, a message box will display the error message.
59 | """
60 | try:
61 | # Clear clipboard and append new text
62 | self.clipboard_clear()
63 | text_content = self.scrolled_text.get("1.0", "end-1c")
64 | self.clipboard_append(text_content)
65 | except Exception as e:
66 | messagebox.showerror("Error", f"Failed to copy text: {str(e)}")
67 |
68 | def configure(self, **kwargs):
69 | """
70 | Configure the text widget with the given keyword arguments.
71 |
72 | :param kwargs: Keyword arguments to pass to the `configure` method of the `ScrolledText` widget.
73 | """
74 | self.scrolled_text.configure(**kwargs)
75 |
76 | def insert(self, index, text):
77 | """
78 | Insert text into the widget at the specified index.
79 |
80 | If the widget is in a 'disabled' state, it will be temporarily set to 'normal' to allow insertion.
81 |
82 | :param index: The index at which to insert the text.
83 | :type index: str
84 | :param text: The text to insert.
85 | :type text: str
86 | """
87 | current_state = self.scrolled_text['state']
88 | self.scrolled_text.configure(state='normal')
89 | self.scrolled_text.insert(index, text)
90 | self.scrolled_text.configure(state=current_state)
91 |
92 | def delete(self, start, end=None):
93 | """
94 | Delete text from the widget between the specified start and end indices.
95 |
96 | If the widget is in a 'disabled' state, it will be temporarily set to 'normal' to allow deletion.
97 |
98 | :param start: The start index of the text to delete.
99 | :type start: str
100 | :param end: The end index of the text to delete. If None, deletes from `start` to the end of the text.
101 | :type end: str, optional
102 | """
103 | current_state = self.scrolled_text['state']
104 | self.scrolled_text.configure(state='normal')
105 | self.scrolled_text.delete(start, end)
106 | self.scrolled_text.configure(state=current_state)
107 |
108 | def get(self, start, end=None):
109 | """
110 | Get text from the widget between the specified start and end indices.
111 |
112 | :param start: The start index of the text to retrieve.
113 | :type start: str
114 | :param end: The end index of the text to retrieve. If None, retrieves from `start` to the end of the text.
115 | :type end: str, optional
116 | :return: The text between the specified indices.
117 | :rtype: str
118 | """
119 | return self.scrolled_text.get(start, end)
--------------------------------------------------------------------------------
/src/FreeScribe.client/UI/DebugWindow.py:
--------------------------------------------------------------------------------
1 | """
2 | Input/output utilities for debugging and logging with tkinter GUI support.
3 |
4 | This module provides classes for dual output handling (console and buffer)
5 | and a debug window interface built with tkinter.
6 | """
7 |
8 | import tkinter as tk
9 | import io
10 | import sys
11 | from datetime import datetime
12 | from collections import deque
13 |
14 | class DualOutput:
15 | buffer = None
16 | MAX_BUFFER_SIZE = 2500 # Maximum number of lines in the buffer
17 |
18 | def __init__(self):
19 | """
20 | Initialize the dual output handler.
21 |
22 | Creates a deque buffer with a max length and stores references to original stdout/stderr streams.
23 | """
24 | DualOutput.buffer = deque(maxlen=DualOutput.MAX_BUFFER_SIZE) # Buffer with a fixed size
25 | self.original_stdout = sys.stdout # Save the original stdout
26 | self.original_stderr = sys.stderr # Save the original stderr
27 |
28 | def write(self, message):
29 | """
30 | Write a message to both the buffer and original stdout.
31 |
32 | :param message: The message to be written
33 | :type message: str
34 | """
35 | if message.strip():
36 | timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
37 | if "\n" in message: # Handle multi-line messages
38 | for line in message.splitlines():
39 | if line.strip():
40 | formatted_message = f"{timestamp} - {line}\n"
41 | DualOutput.buffer.append(formatted_message)
42 | else:
43 | formatted_message = f"{timestamp} - {message}"
44 | DualOutput.buffer.append(formatted_message)
45 | else:
46 | DualOutput.buffer.append("\n")
47 | if self.original_stdout is not None:
48 | self.original_stdout.write(message)
49 |
50 | def flush(self):
51 | """
52 | Flush the original stdout to ensure output is written immediately.
53 | """
54 | if self.original_stdout is not None:
55 | self.original_stdout.flush()
56 |
57 | @staticmethod
58 | def get_buffer_content():
59 | """
60 | Retrieve all content stored in the buffer.
61 |
62 | :return: The complete buffer contents as a single string.
63 | :rtype: str
64 | """
65 | return ''.join(DualOutput.buffer)
66 |
67 | class DebugPrintWindow:
68 | """
69 | Creates and manages a tkinter window for displaying debug output.
70 |
71 | Provides a GUI interface for viewing buffered output with scroll functionality
72 | and manual refresh capability.
73 | """
74 |
75 | def __init__(self, parent):
76 | """
77 | Initialize the debug window interface.
78 |
79 | :param parent: Parent tkinter window
80 | :type parent: tk.Tk or tk.Toplevel
81 | """
82 | self.window = tk.Toplevel(parent)
83 | self.window.title("Debug Output")
84 | self.window.geometry("650x450")
85 |
86 | # Create a Text widget for displaying captured output
87 | self.text_widget = tk.Text(self.window, wrap="none", width=80, height=20)
88 | self.text_widget.pack(padx=10, pady=(10, 0), fill=tk.BOTH, expand=True)
89 |
90 | # Create vertical scrollbar
91 | scrollbar = tk.Scrollbar(self.text_widget)
92 | scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
93 |
94 | # Create horizontal scrollbar
95 | hscrollbar = tk.Scrollbar(self.text_widget, orient=tk.HORIZONTAL)
96 | hscrollbar.pack(side=tk.BOTTOM, fill=tk.X)
97 |
98 | # Configure scrollbar-text widget interactions
99 | scrollbar.config(command=self.text_widget.yview)
100 | self.text_widget.config(yscrollcommand=scrollbar.set)
101 | hscrollbar.config(command=self.text_widget.xview)
102 | self.text_widget.config(xscrollcommand=hscrollbar.set)
103 |
104 | # Add refresh button
105 | refresh_button = tk.Button(self.window, text="Refresh", command=self.refresh_output)
106 | refresh_button.pack(side=tk.RIGHT,pady=10, padx=10)
107 |
108 | # Add copy to clipboard button
109 | copy_button = tk.Button(self.window, text="Copy to Clipboard", command=self._copy_to_clipboard)
110 | copy_button.pack(side=tk.LEFT, pady=10, padx=10)
111 |
112 | self.refresh_output()
113 |
114 | def _copy_to_clipboard(self):
115 | # Copy the content of the Text widget to the clipboard
116 | content = self.text_widget.get("1.0", tk.END).strip()
117 | self.window.clipboard_clear()
118 | self.window.clipboard_append(content)
119 | self.window.update_idletasks()
120 |
121 |
122 | def refresh_output(self):
123 | """
124 | Update the debug window with latest buffer contents.
125 |
126 | Preserves scroll position when updating content and only updates
127 | if there are changes in the buffer.
128 | """
129 | content = DualOutput.get_buffer_content()
130 | current_content = self.text_widget.get("1.0", tk.END).strip()
131 |
132 | if content != current_content:
133 | top_line_index = self.text_widget.index("@0,0")
134 | self.text_widget.delete("1.0", tk.END)
135 | self.text_widget.insert(tk.END, content)
136 | self.text_widget.see(top_line_index)
--------------------------------------------------------------------------------
/src/FreeScribe.client/UI/LoadingWindow.py:
--------------------------------------------------------------------------------
1 | import tkinter as tk
2 | from tkinter import ttk
3 | from utils.file_utils import get_file_path
4 |
5 | class LoadingWindow:
6 | """
7 | A class to create and manage an animated processing popup window.
8 |
9 | This class creates a popup window with an animated progress bar to indicate
10 | ongoing processing and a cancel button to abort the operation.
11 |
12 | :param parent: The parent window for this popup
13 | :type parent: tk.Tk or tk.Toplevel or None
14 | :param title: The title of the popup window
15 | :type title: str
16 | :param initial_text: The initial text to display in the popup
17 | :type initial_text: str
18 | :param on_cancel: Callback function to execute when cancel is pressed
19 | :type on_cancel: callable or None
20 |
21 | :ivar popup: The main popup window
22 | :type popup: tk.Toplevel
23 | :ivar popup_label: The label widget containing the text
24 | :type popup_label: tk.Label
25 | :ivar cancelled: Flag indicating if the operation was cancelled
26 | :type cancelled: bool
27 |
28 | Example
29 | -------
30 | >>> root = tk.Tk()
31 | >>> def cancel_callback():
32 | ... print("Operation cancelled")
33 | >>> processing = LoadingWindow(root, on_cancel=cancel_callback)
34 | >>> # Do some work here
35 | >>> if not processing.cancelled:
36 | ... # Complete the operation
37 | >>> processing.destroy()
38 | """
39 |
40 | def __init__(self, parent=None, title="Processing", initial_text="Loading", on_cancel=None):
41 | """
42 | Initialize the processing popup window.
43 |
44 | :param parent: Parent window for this popup
45 | :type parent: tk.Tk or tk.Toplevel or None
46 | :param title: Title of the popup window
47 | :type title: str
48 | :param initial_text: Initial text to display
49 | :type initial_text: str
50 | :param on_cancel: Callback function to execute when cancel is pressed
51 | :type on_cancel: callable or None
52 | """
53 | try:
54 | self.title = title
55 | self.initial_text = initial_text
56 | self.parent = parent
57 | self.on_cancel = on_cancel
58 | self.cancelled = False
59 |
60 | self.popup = tk.Toplevel(parent)
61 | self.popup.title(title)
62 | self.popup.geometry("200x105") # Increased height for cancel button
63 | self.popup.iconbitmap(get_file_path('assets','logo.ico'))
64 |
65 | if parent:
66 | # Center the popup window on the parent window
67 | parent.update_idletasks()
68 | x = parent.winfo_x() + (parent.winfo_width() - self.popup.winfo_reqwidth()) // 2
69 | y = parent.winfo_y() + (parent.winfo_height() - self.popup.winfo_reqheight()) // 2
70 | self.popup.geometry(f"+{x}+{y}")
71 | self.popup.transient(parent)
72 |
73 | # Disable the parent window
74 | parent.wm_attributes('-disabled', True)
75 |
76 | # Use label and progress bar
77 | self.label = tk.Label(self.popup, text=initial_text)
78 | self.label.pack(pady=(10,5))
79 | self.progress = ttk.Progressbar(self.popup, mode='indeterminate')
80 | self.progress.pack(padx=20, pady=(0,10), fill='x')
81 | self.progress.start()
82 |
83 | # Add cancel button
84 | self.cancel_button = ttk.Button(self.popup, text="Cancel", command=self._handle_cancel)
85 | self.cancel_button.pack(pady=(4,0))
86 |
87 | # Not Resizable
88 | self.popup.resizable(False, False)
89 |
90 | # Disable closing of the popup manually
91 | self.popup.protocol("WM_DELETE_WINDOW", lambda: None)
92 | except Exception:
93 | # Enable the window on exception
94 | if parent:
95 | parent.wm_attributes('-disabled', False)
96 | raise
97 |
98 | def _handle_cancel(self):
99 | """
100 | Internal method to handle cancel button press.
101 | Sets the cancelled flag and calls the user-provided callback if any.
102 | """
103 | self.cancelled = True
104 | if callable(self.on_cancel):
105 | try:
106 | self.on_cancel()
107 | except Exception:
108 | self.destroy()
109 |
110 | self.destroy()
111 |
112 | def destroy(self):
113 | """
114 | Clean up and destroy the popup window.
115 |
116 | This method performs the following cleanup operations:
117 | 1. Stops the progress bar animation
118 | 2. Re-enables the parent window
119 | 3. Destroys the popup window
120 |
121 | Note
122 | ----
123 | This method should be called when you want to close the popup window,
124 | rather than destroying the window directly.
125 |
126 | Example
127 | -------
128 | >>> popup = LoadingWindow()
129 | >>> # Do some processing
130 | >>> popup.destroy() # Properly clean up and close the window
131 | """
132 | if self.popup:
133 | # Enable the parent window
134 | if self.parent:
135 | self.parent.wm_attributes('-disabled', False)
136 |
137 | if self.progress.winfo_exists():
138 | self.progress.stop()
139 |
140 | if self.popup.winfo_exists():
141 | self.popup.destroy()
--------------------------------------------------------------------------------
/src/FreeScribe.client/markdown/help/settings.md:
--------------------------------------------------------------------------------
1 | # Settings Documentation
2 | ## General Settings
3 | - **Show Welcome Message**
4 | - Description: Display welcome message on startup
5 | - Default: `true`
6 | - Type: boolean
7 | - **Show Scrub PHI**
8 | - Description: Enable/Disable Scrub PHI (Only for local llm and private network RFC 18/19)
9 | - Default: `false`
10 | - Type: boolean
11 | ## Whisper Settings
12 | - **Whisper Endpoint**
13 | - Description: API endpoint for Whisper service
14 | - Default: `https://localhost:2224/whisperaudio`
15 | - Type: string
16 | - **Whisper Server API Key**
17 | - Description: API key for Whisper service authentication
18 | - Default: `None`
19 | - Type: string
20 | - **Whisper Model**
21 | - Description: Whisper model to use for speech recognition
22 | - Default: `small.en`
23 | - Type: string
24 | - **Local Whisper**
25 | - Description: Use local Whisper instance instead of cloud service
26 | - Default: `false`
27 | - Type: boolean
28 | - **Real Time**
29 | - Description: Enable real-time processing
30 | - Default: `false`
31 | - Type: boolean
32 | ## LLM Settings
33 | - **Model Endpoint**
34 | - Description: API endpoint URL for the model service
35 | - Default: `https://api.openai.com/v1/`
36 | - Type: string
37 | - **Use Local LLM**
38 | - Description: Toggle to use a locally hosted language model instead of cloud service
39 | - Default: `false`
40 | - Type: boolean
41 | ## Advanced Settings
42 | - **use_story**
43 | - Description: Enable story context for generation
44 | - Default: `false`
45 | - Type: boolean
46 | - **use_memory**
47 | - Description: Enable memory context for generation
48 | - Default: `false`
49 | - Type: boolean
50 | - **use_authors_note**
51 | - Description: Enable author's notes in generation
52 | - Default: `false`
53 | - Type: boolean
54 | - **use_world_info**
55 | - Description: Enable world information in context
56 | - Default: `false`
57 | - Type: boolean
58 | - **Enable Scribe Template**
59 | - Description: Enable Scribe template functionality
60 | - Default: `false`
61 | - Type: boolean
62 | - **max_context_length**
63 | - Description: Maximum number of tokens in the context window
64 | - Default: `5000`
65 | - Type: integer
66 | - **max_length**
67 | - Description: Maximum length of generated text
68 | - Default: `400`
69 | - Type: integer
70 | - **rep_pen**
71 | - Description: Repetition penalty factor
72 | - Default: `1.1`
73 | - Type: float
74 | - **rep_pen_range**
75 | - Description: Token range for repetition penalty
76 | - Default: `5000`
77 | - Type: integer
78 | - **rep_pen_slope**
79 | - Description: Slope of repetition penalty curve
80 | - Default: `0.7`
81 | - Type: float
82 | - **temperature**
83 | - Description: Controls randomness in generation (higher = more random)
84 | - Default: `0.1`
85 | - Type: float
86 | - **tfs**
87 | - Description: Tail free sampling parameter
88 | - Default: `0.97`
89 | - Type: float
90 | - **top_a**
91 | - Description: Top-A sampling parameter
92 | - Default: `0.8`
93 | - Type: float
94 | - **top_k**
95 | - Description: Top-K sampling parameter
96 | - Default: `30`
97 | - Type: integer
98 | - **top_p**
99 | - Description: Top-P (nucleus) sampling parameter
100 | - Default: `0.4`
101 | - Type: float
102 | - **typical**
103 | - Description: Typical sampling parameter
104 | - Default: `0.19`
105 | - Type: float
106 | - **sampler_order**
107 | - Description: Order of sampling methods to apply
108 | - Default: `[6, 0, 1, 3, 4, 2, 5]`
109 | - Type: string (JSON array)
110 | - **singleline**
111 | - Description: Output single line responses only
112 | - Default: `false`
113 | - Type: boolean
114 | - **frmttriminc**
115 | - Description: Trim incomplete sentences from output
116 | - Default: `false`
117 | - Type: boolean
118 | - **frmtrmblln**
119 | - Description: Remove blank lines from output
120 | - Default: `false`
121 | - Type: boolean
122 | - **Use best_of**
123 | - Description: Enable best-of sampling
124 | - Default: `false`
125 | - Type: boolean
126 | - **best_of**
127 | - Description: Number of completions to generate and select from
128 | - Default: `2`
129 | - Type: integer
130 | - **Real Time Audio Length**
131 | - Description: Length of audio segments for real-time processing (seconds)
132 | - Default: `5`
133 | - Type: integer
134 | - **Use Pre-Processing**
135 | - Description: Enable text pre-processing
136 | - Default: `true`
137 | - Type: boolean
138 | - **Use Post-Processing**
139 | - Description: Enable text post-processing
140 | - Default: `false`
141 | - Type: boolean
142 | ## Docker Settings
143 | - **LLM Container Name**
144 | - Description: Docker container name for LLM service
145 | - Default: `ollama`
146 | - Type: string
147 | - **LLM Caddy Container Name**
148 | - Description: Docker container name for Caddy reverse proxy
149 | - Default: `caddy-ollama`
150 | - Type: string
151 | - **LLM Authentication Container Name**
152 | - Description: Docker container name for authentication service
153 | - Default: `authentication-ollama`
154 | - Type: string
155 | - **Whisper Container Name**
156 | - Description: Docker container name for Whisper service
157 | - Default: `speech-container`
158 | - Type: string
159 | - **Whisper Caddy Container Name**
160 | - Description: Docker container name for Whisper Caddy service
161 | - Default: `caddy`
162 | - Type: string
163 | - **Auto Shutdown Containers on Exit**
164 | - Description: Automatically stop Docker containers on application exit
165 | - Default: `true`
166 | - Type: boolean
167 | - **Use Docker Status Bar**
168 | - Description: Show Docker container status in UI
169 | - Default: `false`
170 | - Type: boolean
--------------------------------------------------------------------------------
/src/FreeScribe.client/UI/MainWindow.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | MainWindow.py
4 |
5 | Main window module for the FreeScribe client application.
6 |
7 | This module contains the MainWindow class which is responsible for managing the main window of the application.
8 | It includes methods to start and stop Docker containers for LLM and Whisper services.
9 |
10 | This software is released under the AGPL-3.0 license
11 | Copyright (c) 2023-2024 Braedon Hendy
12 |
13 | Further updates and packaging added in 2024 through the ClinicianFOCUS initiative,
14 | a collaboration with Dr. Braedon Hendy and Conestoga College Institute of Applied
15 | Learning and Technology as part of the CNERG+ applied research project,
16 | Unburdening Primary Healthcare: An Open-Source AI Clinician Partner Platform".
17 | Prof. Michael Yingbull (PI), Dr. Braedon Hendy (Partner),
18 | and Research Students - Software Developer Alex Simko, Pemba Sherpa (F24), and Naitik Patel.
19 |
20 | """
21 |
22 | from ContainerManager import ContainerManager, ContainerState
23 | import tkinter as tk
24 |
25 | class MainWindow:
26 | """
27 | Main window class for the FreeScribe client application.
28 |
29 | This class initializes the main window and provides methods to manage Docker containers for LLM and Whisper services.
30 | """
31 |
32 | def __init__(self, settings):
33 | """
34 | Initialize the main window of the application.
35 | """
36 | self.container_manager = ContainerManager()
37 | self.settings = settings
38 |
39 | def start_LLM_container(self, widget_name, app_settings):
40 | """
41 | Start the LLM container.
42 |
43 | :param widget_name: The name of the widget to update with the container status.
44 | :type widget_name: str
45 | :param app_settings: The application settings containing container names.
46 | :type app_settings: ApplicationSettings
47 | """
48 | try:
49 | self.container_manager.set_status_icon_color(widget_name, self.container_manager.start_container(app_settings.editable_settings["LLM Container Name"]))
50 | self.container_manager.set_status_icon_color(widget_name, self.container_manager.start_container(app_settings.editable_settings["LLM Caddy Container Name"]))
51 | self.container_manager.set_status_icon_color(widget_name, self.container_manager.start_container(app_settings.editable_settings["LLM Authentication Container Name"]))
52 | except Exception as e:
53 | tk.messagebox.showerror("Error", f"An error occurred while starting the LLM container: {e}")
54 |
55 | def stop_LLM_container(self, widget_name, app_settings):
56 | """
57 | Stop the LLM container.
58 |
59 | :param widget_name: The name of the widget to update with the container status.
60 | :type widget_name: str
61 | :param app_settings: The application settings containing container names.
62 | :type app_settings: ApplicationSettings
63 | """
64 | try:
65 | self.container_manager.set_status_icon_color(widget_name, self.container_manager.stop_container(app_settings.editable_settings["LLM Container Name"]))
66 | self.container_manager.set_status_icon_color(widget_name, self.container_manager.stop_container(app_settings.editable_settings["LLM Caddy Container Name"]))
67 | self.container_manager.set_status_icon_color(widget_name, self.container_manager.stop_container(app_settings.editable_settings["LLM Authentication Container Name"]))
68 | except Exception as e:
69 | tk.messagebox.showerror("Error", f"An error occurred while stopping the LLM container: {e}")
70 |
71 | def start_whisper_container(self, widget_name, app_settings):
72 | """
73 | Start the Whisper container.
74 |
75 | :param widget_name: The name of the widget to update with the container status.
76 | :type widget_name: str
77 | :param app_settings: The application settings containing container names.
78 | :type app_settings: ApplicationSettings
79 | """
80 | try:
81 | self.container_manager.set_status_icon_color(widget_name, self.container_manager.start_container(app_settings.editable_settings["Whisper Container Name"]))
82 | self.container_manager.set_status_icon_color(widget_name, self.container_manager.start_container(app_settings.editable_settings["Whisper Caddy Container Name"]))
83 | except Exception as e:
84 | tk.messagebox.showerror("Error", f"An error occurred while starting the Whisper container: {e}")
85 |
86 | def stop_whisper_container(self, widget_name, app_settings):
87 | """
88 | Stop the Whisper container.
89 |
90 | :param widget_name: The name of the widget to update with the container status.
91 | :type widget_name: str
92 | :param app_settings: The application settings containing container names.
93 | :type app_settings: ApplicationSettings
94 | """
95 | try:
96 | self.container_manager.set_status_icon_color(widget_name, self.container_manager.stop_container(app_settings.editable_settings["Whisper Container Name"]))
97 | self.container_manager.set_status_icon_color(widget_name, self.container_manager.stop_container(app_settings.editable_settings["Whisper Caddy Container Name"]))
98 | except Exception as e:
99 | tk.messagebox.showerror("Error", f"An error occurred while stopping the Whisper container: {e}")
100 |
101 | def check_llm_containers(self):
102 | """
103 | Check the status of the LLM containers.
104 | """
105 | status_check = all([
106 | self.container_manager.check_container_status(self.settings.editable_settings["LLM Container Name"]),
107 | self.container_manager.check_container_status(self.settings.editable_settings["LLM Caddy Container Name"]),
108 | self.container_manager.check_container_status(self.settings.editable_settings["LLM Authentication Container Name"])
109 | ])
110 | return ContainerState.CONTAINER_STARTED if status_check else ContainerState.CONTAINER_STOPPED
111 |
112 | def check_whisper_containers(self):
113 | """
114 | Check the status of the Whisper containers.
115 | """
116 | status_check = all([
117 | self.container_manager.check_container_status(self.settings.editable_settings["Whisper Container Name"]),
118 | self.container_manager.check_container_status(self.settings.editable_settings["Whisper Caddy Container Name"])
119 | ])
120 |
121 | return ContainerState.CONTAINER_STARTED if status_check else ContainerState.CONTAINER_STOPPED
--------------------------------------------------------------------------------
/src/FreeScribe.client/ContainerManager.py:
--------------------------------------------------------------------------------
1 | """
2 | This software is released under the AGPL-3.0 license
3 | Copyright (c) 2023-2024 Braedon Hendy
4 |
5 | Further updates and packaging added in 2024 through the ClinicianFOCUS initiative,
6 | a collaboration with Dr. Braedon Hendy and Conestoga College Institute of Applied
7 | Learning and Technology as part of the CNERG+ applied research project,
8 | Unburdening Primary Healthcare: An Open-Source AI Clinician Partner Platform".
9 | Prof. Michael Yingbull (PI), Dr. Braedon Hendy (Partner),
10 | and Research Students - Software Developer Alex Simko, Pemba Sherpa (F24), and Naitik Patel.
11 | """
12 |
13 | from enum import Enum
14 | import docker
15 | import asyncio
16 | import time
17 |
18 | class ContainerState(Enum):
19 | CONTAINER_STOPPED = "ContainerStopped"
20 | CONTAINER_STARTED = "ContainerStarted"
21 |
22 | class ContainerManager:
23 | """
24 | Manages Docker containers by starting and stopping them.
25 |
26 | This class provides methods to interact with Docker containers,
27 | including starting, stopping, and checking their status.
28 |
29 | Attributes:
30 | client (docker.DockerClient): The Docker client used to interact with containers.
31 | """
32 |
33 | def __init__(self):
34 | """
35 | Initialize the ContainerManager with a Docker client.
36 | """
37 | self.client = None
38 |
39 | try:
40 | self.client = docker.from_env()
41 | except docker.errors.DockerException as e:
42 | self.client = None
43 |
44 | def start_container(self, container_name):
45 | """
46 | Start a Docker container by its name.
47 |
48 | :param container_name: The name of the container to start.
49 | :type container_name: str
50 | :raises docker.errors.NotFound: If the specified container is not found.
51 | :raises docker.errors.APIError: If an error occurs while starting the container.
52 | """
53 | try:
54 | container = self.client.containers.get(container_name)
55 | container.start()
56 | return ContainerState.CONTAINER_STARTED
57 | except docker.errors.NotFound as e:
58 | raise docker.errors.NotFound(f"Container {container_name} not found.") from e
59 | except docker.errors.APIError as e:
60 | raise docker.errors.APIError(f"An error occurred while starting the container: {e}") from e
61 |
62 | def update_container_status_icon(self, dot, container_name):
63 | """Update the status icon for a Docker container based on its current state.
64 |
65 | This method checks the current status of a specified Docker container and updates
66 | the visual indicator (dot) to reflect that status using appropriate colors.
67 | The method only executes if there is an active Docker client connection.
68 |
69 | Parameters
70 | ----------
71 | dot : QWidget
72 | The widget representing the status indicator dot that will be updated
73 | with the appropriate color based on container status.
74 | container_name : str
75 | The name of the Docker container to check the status for.
76 |
77 | Notes
78 | -----
79 | This method requires:
80 | - An active Docker client connection through container_manager
81 | - A valid container name that exists in Docker
82 |
83 | The status colors are managed by the container_manager's set_status_icon_color
84 | method and typically follow conventions like:
85 | - Green for running
86 | - Red for stopped/failed
87 | - Yellow for transitional states
88 |
89 | Examples
90 | --------
91 | >>> status_dot = QWidget()
92 | >>> self.update_container_status_icon(status_dot, "mysql-container")
93 | """
94 | if self.client is not None:
95 | status = self.check_container_status(container_name)
96 | self.set_status_icon_color(dot, ContainerState.CONTAINER_STARTED if status else ContainerState.CONTAINER_STOPPED)
97 |
98 | def stop_container(self, container_name):
99 | """
100 | Stop a Docker container by its name.
101 |
102 | :param container_name: The name of the container to stop.
103 | :type container_name: str
104 | :raises docker.errors.NotFound: If the specified container is not found.
105 | :raises docker.errors.APIError: If an error occurs while stopping the container.
106 | """
107 | try:
108 | container = self.client.containers.get(container_name)
109 | container.stop()
110 | print(f"Container {container_name} stopped successfully.")
111 | return ContainerState.CONTAINER_STOPPED
112 | except docker.errors.NotFound as e:
113 | raise docker.errors.NotFound(f"Container {container_name} not found.") from e
114 | except docker.errors.APIError as e:
115 | raise docker.errors.APIError(f"An error occurred while stopping the container: {e}") from e
116 |
117 | def check_container_status(self, container_name):
118 | """
119 | Check the status of a Docker container by its name.
120 |
121 | :param container_name: The name of the container to check.
122 | :type container_name: str
123 | :return: True if the container is running, False otherwise.
124 | :rtype: bool
125 | :raises docker.errors.NotFound: If the specified container is not found.
126 | :raises docker.errors.APIError: If an error occurs while checking the container status.
127 | """
128 | try:
129 | container = self.client.containers.get(container_name)
130 | status = container.status
131 |
132 | return status == "running"
133 |
134 | except docker.errors.NotFound:
135 | print(f"Container {container_name} not found.")
136 | return False
137 | except docker.errors.APIError as e:
138 | print(f"An error occurred while checking the container status: {e}")
139 | return False
140 | except Exception as e:
141 | print(f"An error occurred while checking the container status: {e}")
142 | return False
143 |
144 | def set_status_icon_color(self, widget, status: ContainerState):
145 | """
146 | Set the color of the status icon based on the status of the container.
147 |
148 | :param widget: The widget representing the status icon.
149 | :type widget: tkinter.Widget
150 | :param status: The status of the container.
151 | :type status: ContainerState
152 | """
153 | if status not in ContainerState:
154 | raise ValueError(f"Invalid container state: {status}")
155 |
156 | if status == ContainerState.CONTAINER_STARTED:
157 | widget.config(fg='green')
158 | elif status == ContainerState.CONTAINER_STOPPED:
159 | widget.config(fg='red')
160 |
161 | def check_docker_availability(self):
162 | """
163 | Check if the Docker client is available.
164 |
165 | :return: True if the Docker client is available, False otherwise.
166 | :rtype: bool
167 | """
168 | try:
169 | self.client = docker.from_env()
170 | except docker.errors.DockerException as e:
171 | self.client = None
172 |
173 | return self.client is not None
--------------------------------------------------------------------------------
/src/FreeScribe.client/UI/Widgets/MicrophoneSelector.py:
--------------------------------------------------------------------------------
1 | import tkinter as tk
2 | from tkinter import ttk
3 | import pyaudio
4 |
5 | class MicrophoneState:
6 | SELECTED_MICROPHONE_INDEX = None
7 | SELECTED_MICROPHONE_NAME = None
8 |
9 | @staticmethod
10 | def load_microphone_from_settings(app_settings):
11 | """
12 | Load the microphone settings from the application settings.
13 |
14 | Parameters
15 | ----------
16 | app_settings : dict
17 | Application settings including editable settings.
18 |
19 | Returns
20 | -------
21 | str
22 | The name of the currently selected microphone.
23 | """
24 | p = pyaudio.PyAudio()
25 |
26 | if "Current Mic" in app_settings.editable_settings:
27 | MicrophoneState.SELECTED_MICROPHONE_NAME = app_settings.editable_settings["Current Mic"]
28 | for i in range(p.get_device_count()):
29 | device_info = p.get_device_info_by_index(i)
30 | if device_info['name'] == MicrophoneState.SELECTED_MICROPHONE_NAME:
31 | MicrophoneState.SELECTED_MICROPHONE_INDEX = device_info["index"]
32 | break
33 | else:
34 | MicrophoneState.SELECTED_MICROPHONE_INDEX = 0
35 | MicrophoneState.SELECTED_MICROPHONE_NAME = p.get_device_info_by_index(0)['name']
36 |
37 | class MicrophoneSelector:
38 | """
39 | A class to manage microphone selection using a Tkinter UI.
40 |
41 | Attributes
42 | ----------
43 | PYAUDIO : pyaudio.PyAudio
44 | A PyAudio instance used to interact with audio devices.
45 | SELECTED_MICROPHONE_INDEX : int
46 | The index of the currently selected microphone.
47 | SELECTED_MICROPHONE_NAME : str
48 | The name of the currently selected microphone.
49 | """
50 |
51 | def __init__(self, root, row, column, app_settings):
52 | """
53 | Initialize the MicrophoneSelector class.
54 |
55 | Parameters
56 | ----------
57 | root : tk.Tk
58 | The root Tkinter instance.
59 | row : int
60 | The row position in the Tkinter grid for UI elements.
61 | column : int
62 | The column position in the Tkinter grid for UI elements.
63 | app_settings : dict
64 | Application settings including editable settings.
65 | """
66 | self.root = root
67 | self.settings = app_settings
68 |
69 | self.pyaudio = pyaudio.PyAudio()
70 |
71 | self.selected_index = None
72 | self.selected_name = None
73 |
74 | # Create UI elements
75 | self.label = tk.Label(root, text="Select a Microphone:")
76 | self.label.grid(row=row, column=0, pady=5, sticky="w")
77 |
78 | self.dropdown = ttk.Combobox(root, state="readonly", width=15)
79 | self.dropdown.grid(row=row, pady=5, column=1)
80 |
81 | # Populate microphones in the dropdown
82 | self.update_microphones()
83 |
84 | # Bind selection event
85 | self.dropdown.bind("<>", self.on_mic_selected)
86 |
87 | def update_microphones(self):
88 | """
89 | Update the microphone dropdown with available devices and manage selection.
90 | """
91 | # Initialize microphone mapping and list of names
92 | self.mic_mapping = {}
93 | selected_index = -1
94 |
95 | # Populate the microphone mapping with devices having input channels
96 | for i in range(self.pyaudio.get_device_count()):
97 | device_info = self.pyaudio.get_device_info_by_index(i)
98 |
99 | if device_info['maxInputChannels'] > 0:
100 | name = device_info['name']
101 |
102 | if name not in self.mic_mapping: # Avoid duplicates
103 | self.mic_mapping[name] = {
104 | "index": device_info['index'],
105 | "channels": device_info['maxInputChannels'],
106 | "defaultSampleRate": device_info['defaultSampleRate']
107 | }
108 |
109 | # Match the current mic setting, if applicable
110 | if name == self.settings.editable_settings.get("Current Mic") and selected_index == -1:
111 | selected_index = device_info['index']
112 |
113 | # Update the dropdown menu with the microphone names
114 | mic_names = list(self.mic_mapping.keys())
115 | self.dropdown['values'] = mic_names
116 |
117 | if selected_index != -1:
118 | # Set the dropdown and selected microphone to the current mic
119 | self.dropdown.set(self.settings.editable_settings["Current Mic"])
120 | self.update_selected_microphone(selected_index)
121 | elif mic_names:
122 | # Default to the first available microphone if none is selected
123 | first_mic_name = mic_names[0]
124 | self.dropdown.set(first_mic_name)
125 | self.update_selected_microphone(self.mic_mapping[first_mic_name]['index'])
126 | else:
127 | # Handle the case where no microphones are available
128 | self.dropdown.set("No microphones available")
129 | self.update_selected_microphone(-1)
130 |
131 | def on_mic_selected(self, event):
132 | """
133 | Handle the event when a microphone is selected from the dropdown.
134 |
135 | Parameters
136 | ----------
137 | event : tk.Event
138 | The event object containing information about the selection.
139 | """
140 | selected_name = self.dropdown.get()
141 | if selected_name in self.mic_mapping:
142 | selected_index = self.mic_mapping[selected_name]['index']
143 | self.update_selected_microphone(selected_index)
144 |
145 | def update_selected_microphone(self, selected_index):
146 | """
147 | Update the selected microphone index and name.
148 |
149 | Parameters
150 | ----------
151 | selected_index : int
152 | The index of the selected microphone.
153 | """
154 | if selected_index >= 0:
155 | try:
156 | selected_mic = self.pyaudio.get_device_info_by_index(selected_index)
157 | MicrophoneState.SELECTED_MICROPHONE_INDEX = selected_mic["index"]
158 | MicrophoneState.SELECTED_MICROPHONE_NAME = selected_mic["name"]
159 | self.selected_index = selected_mic["index"]
160 | self.selected_name = selected_mic["name"]
161 | except OSError:
162 | # Handle cases where the selected index is invalid
163 | MicrophoneState.SELECTED_MICROPHONE_INDEX = None
164 | MicrophoneState.SELECTED_MICROPHONE_NAME = None
165 | self.selected_index = None
166 | self.selected_name = None
167 | else:
168 | MicrophoneState.SELECTED_MICROPHONE_INDEX = None
169 | MicrophoneState.SELECTED_MICROPHONE_NAME = None
170 | self.selected_index = None
171 | self.selected_name = None
172 |
173 | def close(self):
174 | """
175 | Close the Tkinter root window.
176 | """
177 | self.root.destroy()
178 |
179 | def get(self):
180 | """
181 | Get the name of the currently selected microphone.
182 |
183 | Returns
184 | -------
185 | str
186 | The name of the currently selected microphone.
187 | """
188 | return MicrophoneState.SELECTED_MICROPHONE_NAME
189 |
--------------------------------------------------------------------------------
/scripts/assets/Gemma_License.txt:
--------------------------------------------------------------------------------
1 | Section 1: DEFINITIONS
2 | 1.1 Definitions
3 | (a) "Agreement" or "Gemma Terms of Use" means these terms and conditions that govern the use, reproduction, Distribution or modification of the Gemma Services and any terms and conditions incorporated by reference.
4 |
5 | (b) "Distribution" or "Distribute" means any transmission, publication, or other sharing of Gemma or Model Derivatives to a third party, including by providing or making Gemma or its functionality available as a hosted service via API, web access, or any other electronic or remote means ("Hosted Service").
6 |
7 | (c) "Gemma" means the set of machine learning language models, trained model weights and parameters identified at ai.google.dev/gemma, regardless of the source that you obtained it from.
8 |
9 | (d) "Google" means Google LLC.
10 |
11 | (e) "Model Derivatives" means all (i) modifications to Gemma, (ii) works based on Gemma, or (iii) any other machine learning model which is created by transfer of patterns of the weights, parameters, operations, or Output of Gemma, to that model in order to cause that model to perform similarly to Gemma, including distillation methods that use intermediate data representations or methods based on the generation of synthetic data Outputs by Gemma for training that model. For clarity, Outputs are not deemed Model Derivatives.
12 |
13 | (f) "Output" means the information content output of Gemma or a Model Derivative that results from operating or otherwise using Gemma or the Model Derivative, including via a Hosted Service.
14 |
15 | 1.2
16 | As used in this Agreement, "including" means "including without limitation".
17 |
18 | Section 2: ELIGIBILITY AND USAGE
19 | 2.1 Eligibility
20 | You represent and warrant that you have the legal capacity to enter into this Agreement (including being of sufficient age of consent). If you are accessing or using any of the Gemma Services for or on behalf of a legal entity, (a) you are entering into this Agreement on behalf of yourself and that legal entity, (b) you represent and warrant that you have the authority to act on behalf of and bind that entity to this Agreement and (c) references to "you" or "your" in the remainder of this Agreement refers to both you (as an individual) and that entity.
21 |
22 | 2.2 Use
23 | You may use, reproduce, modify, Distribute, perform or display any of the Gemma Services only in accordance with the terms of this Agreement, and must not violate (or encourage or permit anyone else to violate) any term of this Agreement.
24 |
25 | Section 3: DISTRIBUTION AND RESTRICTIONS
26 | 3.1 Distribution and Redistribution
27 | You may reproduce or Distribute copies of Gemma or Model Derivatives if you meet all of the following conditions:
28 |
29 | You must include the use restrictions referenced in Section 3.2 as an enforceable provision in any agreement (e.g., license agreement, terms of use, etc.) governing the use and/or distribution of Gemma or Model Derivatives and you must provide notice to subsequent users you Distribute to that Gemma or Model Derivatives are subject to the use restrictions in Section 3.2.
30 | You must provide all third party recipients of Gemma or Model Derivatives a copy of this Agreement.
31 | You must cause any modified files to carry prominent notices stating that you modified the files.
32 | All Distributions (other than through a Hosted Service) must be accompanied by a "Notice" text file that contains the following notice: "Gemma is provided under and subject to the Gemma Terms of Use found at ai.google.dev/gemma/terms".
33 | You may add your own intellectual property statement to your modifications and, except as set forth in this Section, may provide additional or different terms and conditions for use, reproduction, or Distribution of your modifications, or for any such Model Derivatives as a whole, provided your use, reproduction, modification, Distribution, performance, and display of Gemma otherwise complies with the terms and conditions of this Agreement. Any additional or different terms and conditions you impose must not conflict with the terms of this Agreement.
34 |
35 | 3.2 Use Restrictions
36 | You must not use any of the Gemma Services:
37 |
38 | for the restricted uses set forth in the Gemma Prohibited Use Policy at ai.google.dev/gemma/prohibited_use_policy ("Prohibited Use Policy"), which is hereby incorporated by reference into this Agreement; or
39 | in violation of applicable laws and regulations.
40 | To the maximum extent permitted by law, Google reserves the right to restrict (remotely or otherwise) usage of any of the Gemma Services that Google reasonably believes are in violation of this Agreement.
41 |
42 | 3.3 Generated Output
43 | Google claims no rights in Outputs you generate using Gemma. You and your users are solely responsible for Outputs and their subsequent uses.
44 |
45 | Section 4: ADDITIONAL PROVISIONS
46 | 4.1 Updates
47 | Google may update Gemma from time to time.
48 |
49 | 4.2 Trademarks
50 | Nothing in this Agreement grants you any rights to use Google's trademarks, trade names, logos or to otherwise suggest endorsement or misrepresent the relationship between you and Google. Google reserves any rights not expressly granted herein.
51 |
52 | 4.3 DISCLAIMER OF WARRANTY
53 | UNLESS REQUIRED BY APPLICABLE LAW, THE GEMMA SERVICES, AND OUTPUTS, ARE PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. YOU ARE SOLELY RESPONSIBLE FOR DETERMINING THE APPROPRIATENESS OF USING, REPRODUCING, MODIFYING, PERFORMING, DISPLAYING OR DISTRIBUTING ANY OF THE GEMMA SERVICES OR OUTPUTS AND ASSUME ANY AND ALL RISKS ASSOCIATED WITH YOUR USE OR DISTRIBUTION OF ANY OF THE GEMMA SERVICES OR OUTPUTS AND YOUR EXERCISE OF RIGHTS AND PERMISSIONS UNDER THIS AGREEMENT.
54 |
55 | 4.4 LIMITATION OF LIABILITY
56 | TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT AND UNDER NO LEGAL THEORY, WHETHER IN TORT (INCLUDING NEGLIGENCE), PRODUCT LIABILITY, CONTRACT, OR OTHERWISE, UNLESS REQUIRED BY APPLICABLE LAW, SHALL GOOGLE OR ITS AFFILIATES BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, EXEMPLARY, CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR LOST PROFITS OF ANY KIND ARISING FROM THIS AGREEMENT OR RELATED TO, ANY OF THE GEMMA SERVICES OR OUTPUTS EVEN IF GOOGLE OR ITS AFFILIATES HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
57 |
58 | 4.5 Term, Termination, and Survival
59 | The term of this Agreement will commence upon your acceptance of this Agreement (including acceptance by your use, modification, or Distribution, reproduction, performance or display of any portion or element of the Gemma Services) and will continue in full force and effect until terminated in accordance with the terms of this Agreement. Google may terminate this Agreement if you are in breach of any term of this Agreement. Upon termination of this Agreement, you must delete and cease use and Distribution of all copies of Gemma and Model Derivatives in your possession or control. Sections 1, 2.1, 3.3, 4.2 to 4.9 shall survive the termination of this Agreement.
60 |
61 | 4.6 Governing Law and Jurisdiction
62 | This Agreement will be governed by the laws of the State of California without regard to choice of law principles. The UN Convention on Contracts for the International Sale of Goods does not apply to this Agreement. The state and federal courts of Santa Clara County, California shall have exclusive jurisdiction of any dispute arising out of this Agreement.
63 |
64 | 4.7 Severability
65 | If any provision of this Agreement is held to be invalid, illegal or unenforceable, the remaining provisions shall be unaffected thereby and remain valid as if such provision had not been set forth herein.
66 |
67 | 4.8 Entire Agreement
68 | This Agreement states all the terms agreed between the parties and supersedes all other agreements between the parties as of the date of acceptance relating to its subject matter.
69 |
70 | 4.9 No Waiver
71 | Google will not be treated as having waived any rights by not exercising (or delaying the exercise of) any rights under this Agreement.
--------------------------------------------------------------------------------
/src/FreeScribe.client/UI/Widgets/AudioMeter.py:
--------------------------------------------------------------------------------
1 | """
2 | src/FreeScribe.client/UI/Widgets/AudioMeter.py
3 |
4 | This software is released under the AGPL-3.0 license
5 | Copyright (c) 2023-2024 Braedon Hendy
6 |
7 | Further updates and packaging added in 2024 through the ClinicianFOCUS initiative,
8 | a collaboration with Dr. Braedon Hendy and Conestoga College Institute of Applied
9 | Learning and Technology as part of the CNERG+ applied research project,
10 | Unburdening Primary Healthcare: An Open-Source AI Clinician Partner Platform".
11 | Prof. Michael Yingbull (PI), Dr. Braedon Hendy (Partner),
12 | and Research Students - Software Developer Alex Simko, Pemba Sherpa (F24), and Naitik Patel.
13 |
14 | """
15 |
16 | import struct
17 | import tkinter as tk
18 | from tkinter import ttk
19 | import pyaudio
20 | import numpy as np
21 | from threading import Thread
22 | from UI.Widgets.MicrophoneSelector import MicrophoneState
23 |
24 | class AudioMeter(tk.Frame):
25 | """
26 | A Tkinter widget that displays an audio level meter.
27 |
28 | This widget captures audio input and displays the audio level in real-time.
29 | It includes a threshold slider to adjust the sensitivity of the meter.
30 |
31 | :param master: The parent widget.
32 | :type master: tkinter.Widget
33 | :param width: The width of the widget.
34 | :type width: int
35 | :param height: The height of the widget.
36 | :type height: int
37 | :param threshold: The initial threshold value for the audio meter.
38 | :type threshold: int
39 | """
40 | def __init__(self, master=None, width=400, height=100, threshold=750):
41 | """
42 | Initialize the AudioMeter widget.
43 |
44 | :param master: The parent widget.
45 | :type master: tkinter.Widget
46 | :param width: The width of the widget.
47 | :type width: int
48 | :param height: The height of the widget.
49 | :type height: int
50 | :param threshold: The initial threshold value for the audio meter.
51 | :type threshold: int
52 | """
53 | super().__init__(master)
54 | self.master = master
55 | self.width = width
56 | self.height = height
57 | self.running = False
58 | self.threshold = threshold
59 | self.destroyed = False # Add flag to track widget destruction
60 | self.setup_audio()
61 | self.create_widgets()
62 |
63 | # Bind the cleanup method to widget destruction
64 | self.bind('', self.cleanup)
65 |
66 | def cleanup(self, event=None):
67 | """
68 | Clean up resources when the widget is destroyed.
69 |
70 | This method stops the audio stream and terminates the PyAudio instance.
71 |
72 | :param event: The event that triggered the cleanup (default is None).
73 | :type event: tkinter.Event
74 | """
75 |
76 | if self.destroyed:
77 | return
78 |
79 | self.destroyed = True
80 | self.running = False
81 |
82 | # Stop audio first
83 | if hasattr(self, 'stream') and self.stream:
84 | self.stream.stop_stream()
85 | self.stream.close()
86 | if hasattr(self, 'p') and self.p:
87 | self.p.terminate()
88 |
89 | # Then wait for thread
90 | if hasattr(self, 'monitoring_thread') and self.monitoring_thread:
91 | self.monitoring_thread.join(timeout=1.0)
92 |
93 | def destroy(self):
94 | """
95 | Override the destroy method to ensure cleanup.
96 |
97 | This method calls the cleanup method before destroying the widget.
98 | """
99 | self.cleanup()
100 | super().destroy()
101 |
102 | def setup_audio(self):
103 | """
104 | Set up the audio parameters for capturing audio input.
105 |
106 | This method initializes the PyAudio instance and sets the audio format,
107 | number of channels, sample rate, and chunk size.
108 | """
109 | # Adjusted CHUNK size for 16kHz to maintain similar update rate
110 | self.CHUNK = 512 # Reduced from 1024 to maintain similar time resolution
111 | self.FORMAT = pyaudio.paInt16
112 | self.CHANNELS = 1
113 | self.RATE = 16000 # Changed from 44100 to 16000
114 | self.p = pyaudio.PyAudio()
115 |
116 | def create_widgets(self):
117 | """
118 | Create the UI elements for the audio meter.
119 |
120 | This method creates the slider for adjusting the threshold, the canvas
121 | for displaying the audio level, and the threshold line indicator.
122 | """
123 | # Create frame for slider
124 | self.slider_frame = tk.Frame(self)
125 | self.slider_frame.pack(fill='x')
126 |
127 |
128 | # Add threshold slider - adjusted range for int16 audio values
129 | self.threshold_slider = tk.Scale(
130 | self.slider_frame,
131 | from_=100,
132 | to=32767, # Max value for 16-bit audio
133 | orient='horizontal',
134 | command=self.update_threshold,
135 | length=self.width
136 | )
137 |
138 | self.threshold_slider.set(self.threshold) # Set default threshold
139 | self.threshold_slider.pack(side='left', fill='x', expand=True, padx=0)
140 |
141 | # Make the canvas shorter since we only need enough height for the bar
142 | self.canvas = tk.Canvas(
143 | self,
144 | width=self.width,
145 | height=30,
146 | borderwidth=0,
147 | highlightthickness=0
148 | )
149 | self.canvas.pack(expand=True, fill='both', padx=0, pady=0)
150 |
151 | # Create horizontal level meter rectangle
152 | self.level_meter = self.canvas.create_rectangle(
153 | 0, 5,
154 | 0, 25,
155 | fill='green'
156 | )
157 |
158 | # Create threshold line indicator
159 | self.threshold_line = self.canvas.create_line(
160 | 0, 0,
161 | 0, 30,
162 | fill='red',
163 | width=2
164 | )
165 |
166 | self.toggle_monitoring()
167 |
168 | def update_threshold(self, value):
169 | """
170 | Update the threshold value and the visual indicator.
171 |
172 | This method updates the threshold value based on the slider position
173 | and adjusts the position of the threshold line on the canvas.
174 |
175 | :param value: The new threshold value from the slider.
176 | :type value: str
177 | """
178 | self.threshold = float(value)
179 |
180 | # Update threshold line position
181 | # Scale the threshold value to canvas width
182 | scaled_position = (float(value) / 32767) * self.width
183 | self.canvas.coords(
184 | self.threshold_line,
185 | scaled_position, 0,
186 | scaled_position, 30
187 | )
188 |
189 | def toggle_monitoring(self):
190 | """
191 | Start or stop the audio monitoring.
192 |
193 | This method starts or stops the audio stream and the monitoring thread
194 | based on the current state of the widget.
195 | """
196 | if not self.running:
197 | self.running = True
198 |
199 | try:
200 | self.stream = self.p.open(
201 | format=self.FORMAT,
202 | channels=1,
203 | rate=self.RATE,
204 | input=True,
205 | input_device_index=MicrophoneState.SELECTED_MICROPHONE_INDEX,
206 | frames_per_buffer=self.CHUNK,
207 | )
208 | except (OSError, IOError) as e:
209 | tk.messagebox.showerror("Error", f"Please check your microphone settings under the speech2text settings tab. Error opening audio stream: {e}")
210 |
211 | self.monitoring_thread = Thread(target=self.update_meter)
212 | self.monitoring_thread.start()
213 | else:
214 | self.running = False
215 | self.stream.stop_stream()
216 | self.stream.close()
217 |
218 | def update_meter(self):
219 | """
220 | Continuously update the audio meter.
221 |
222 | This method reads audio data from the stream, calculates the maximum
223 | audio level, and updates the meter display on the main thread.
224 | """
225 | while self.running and not self.destroyed: # Check destroyed flag
226 | try:
227 | data = self.stream.read(self.CHUNK, exception_on_overflow=False)
228 | audio_data = struct.unpack(f'{self.CHUNK}h', data)
229 | max_value = max(abs(np.array(audio_data)))
230 | level = min(self.width, int((max_value / 32767) * self.width))
231 |
232 | # Only schedule update if not destroyed
233 | if not self.destroyed:
234 | self.master.after(0, self.update_meter_display, level)
235 | except Exception as e:
236 | print(f"Error in audio monitoring: {e}")
237 | break
238 |
239 | def update_meter_display(self, level):
240 | """
241 | Update the meter display on the canvas.
242 |
243 | This method updates the position and color of the audio level meter
244 | based on the current audio level.
245 |
246 | :param level: The current audio level to display.
247 | :type level: int
248 | """
249 | if not self.destroyed and self.winfo_exists():
250 | try:
251 | self.canvas.coords(
252 | self.level_meter,
253 | 0, 5,
254 | level, 25
255 | )
256 |
257 | # Color logic
258 | if level < 120:
259 | color = 'green'
260 | elif level < 250:
261 | color = 'yellow'
262 | else:
263 | color = 'red'
264 | self.canvas.itemconfig(self.level_meter, fill=color)
265 | except tk.TclError:
266 | # Widget was destroyed during update
267 | self.cleanup()
268 |
--------------------------------------------------------------------------------
/src/FreeScribe.client/Model.py:
--------------------------------------------------------------------------------
1 | from llama_cpp import Llama
2 | import os
3 | from typing import Optional, Dict, Any
4 | import threading
5 | from UI.LoadingWindow import LoadingWindow
6 | import tkinter.messagebox as messagebox
7 |
8 | class Model:
9 | """
10 | Model class for handling GPU-accelerated text generation using the Llama library.
11 |
12 | This class provides an interface to initialize a language model with specific configurations
13 | for GPU acceleration, generate responses based on a text prompt, and retrieve GPU settings.
14 | The class is configured to support multi-GPU setups and custom configurations for batch size,
15 | context window, and sampling settings.
16 |
17 | Attributes:
18 | model: Instance of the Llama model configured with specified GPU and context parameters.
19 | config: Dictionary containing the GPU and model configuration.
20 |
21 | Methods:
22 | generate_response: Generates a text response based on an input prompt using
23 | the specified sampling parameters.
24 | get_gpu_info: Returns the current GPU configuration and batch size details.
25 | """
26 | def __init__(
27 | self,
28 | model_path: str,
29 | chat_template: str = None,
30 | context_size: int = 4096,
31 | gpu_layers: int = -1, # -1 means load all layers to GPU
32 | main_gpu: int = 0, # Primary GPU device index
33 | tensor_split: Optional[list] = None, # For multi-GPU setup
34 | n_batch: int = 512, # Batch size for inference
35 | n_threads: Optional[int] = None, # CPU threads when needed
36 | seed: int = 1337
37 | ):
38 | """
39 | Initializes the GGUF model with GPU acceleration.
40 |
41 | Args:
42 | model_path: Path to the model file
43 | context_size: Size of the context window
44 | gpu_layers: Number of layers to offload to GPU (-1 for all)
45 | main_gpu: Main GPU device index
46 | tensor_split: List of GPU memory splits for multi-GPU setup
47 | n_batch: Batch size for inference
48 | n_threads: Number of CPU threads
49 | seed: Random seed for reproducibility
50 | """
51 | try:
52 | # Set environment variables for GPU
53 | os.environ["CUDA_VISIBLE_DEVICES"] = str(main_gpu)
54 |
55 | # Initialize model with GPU settings
56 | self.model = Llama(
57 | model_path=model_path,
58 | n_ctx=context_size,
59 | n_gpu_layers=gpu_layers,
60 | n_batch=n_batch,
61 | n_threads=n_threads or os.cpu_count(),
62 | seed=seed,
63 | tensor_split=tensor_split,
64 | chat_format=chat_template,
65 | )
66 |
67 | # Store configuration
68 | self.config = {
69 | "gpu_layers": gpu_layers,
70 | "main_gpu": main_gpu,
71 | "context_size": context_size,
72 | "n_batch": n_batch
73 | }
74 | except Exception as e:
75 | self.model = None
76 | raise e
77 |
78 | def generate_response(
79 | self,
80 | prompt: str,
81 | max_tokens: int = 50,
82 | temperature: float = 0.1,
83 | top_p: float = 0.95,
84 | repeat_penalty: float = 1.1
85 | ) -> str:
86 | """
87 | Generates a response using GPU-accelerated inference.
88 |
89 | Args:
90 | prompt: Input text prompt
91 | max_tokens: Maximum number of tokens to generate
92 | temperature: Sampling temperature (higher = more random)
93 | top_p: Top-p sampling threshold
94 | repeat_penalty: Penalty for repeating tokens
95 |
96 | Returns:
97 | Generated text response
98 | """
99 | try:
100 | # Generate response using the model
101 |
102 | # Message template for chat completion
103 | messages = [
104 | {"role": "user",
105 | "content": prompt}
106 | ]
107 |
108 | response = self.model.create_chat_completion(
109 | messages,
110 | max_tokens=max_tokens,
111 | temperature=temperature,
112 | top_p=top_p,
113 | repeat_penalty=repeat_penalty,
114 | )
115 |
116 | # reset the model tokens
117 | self.model.reset()
118 | return response["choices"][0]["message"]["content"]
119 |
120 | except Exception as e:
121 | print(f"GPU inference error ({e.__class__.__name__}): {str(e)}")
122 | return f"({e.__class__.__name__}): {str(e)}"
123 |
124 |
125 | def get_gpu_info(self) -> Dict[str, Any]:
126 | """
127 | Returns information about the current GPU configuration.
128 | """
129 | return {
130 | "gpu_layers": self.config["gpu_layers"],
131 | "main_gpu": self.config["main_gpu"],
132 | "batch_size": self.config["n_batch"],
133 | "context_size": self.config["context_size"]
134 | }
135 |
136 | def close(self):
137 | """
138 | Unloads the model from GPU memory.
139 | """
140 | self.model.close()
141 | self.model = None
142 |
143 | def __del__(self):
144 | """Cleanup GPU memory on deletion"""
145 | if self.model is not None:
146 | self.model.close()
147 | self.model = None
148 |
149 | class ModelManager:
150 | """
151 | Manages the lifecycle of a local LLM model including setup and unloading operations.
152 |
153 | This class provides static methods to handle model initialization, loading, and cleanup
154 | using the llama.cpp Python bindings. It supports different model architectures and
155 | quantization levels.
156 |
157 | Attributes:
158 | local_model (Llama): Static reference to the loaded model instance. None if no model is loaded.
159 | """
160 | local_model = None
161 |
162 | @staticmethod
163 | def setup_model(app_settings, root):
164 | """
165 | Initialize and load the LLM model based on application settings.
166 |
167 | Creates a loading window and starts model loading in a separate thread to prevent
168 | UI freezing. Automatically checks thread status and closes the loading window
169 | when complete.
170 |
171 | Args:
172 | app_settings: Application settings object containing model preferences
173 | root: Tkinter root window for creating the loading dialog
174 |
175 | Raises:
176 | ValueError: If the specified model file cannot be loaded
177 |
178 | Note:
179 | The method uses threading to avoid blocking the UI while loading the model.
180 | GPU layers are set to -1 for CUDA architecture and 0 for CPU.
181 | """
182 | loading_window = LoadingWindow(root, "Loading Model", "Loading Model. Please wait")
183 |
184 | # unload before loading new model
185 | if ModelManager.local_model is not None:
186 | ModelManager.unload_model()
187 |
188 | def load_model():
189 | """
190 | Internal function to handle the actual model loading process.
191 |
192 | Determines the model file based on settings and initializes the Llama instance
193 | with appropriate parameters.
194 | """
195 | gpu_layers = 0
196 |
197 | if app_settings.editable_settings["Architecture"] == "CUDA (Nvidia GPU)":
198 | gpu_layers = -1
199 |
200 | model_to_use = "gemma-2-2b-it-Q8_0.gguf"
201 |
202 | model_path = f"./models/{model_to_use}"
203 | try:
204 | ModelManager.local_model = Model(model_path,
205 | context_size=4096,
206 | gpu_layers=gpu_layers,
207 | main_gpu=0,
208 | n_batch=512,
209 | n_threads=None,
210 | seed=1337)
211 | except Exception as e:
212 | # model doesnt exist
213 | #TODO: Logo to system log
214 | messagebox.showerror("Model Error", f"Model failed to load. Please ensure you have a valid model selected in the settings. Currently trying to load: {os.path.abspath(model_path)}. Error received ({e.__class__.__name__}): {str(e)}")
215 | ModelManager.local_model = None
216 |
217 | thread = threading.Thread(target=load_model)
218 | thread.start()
219 |
220 | def check_thread_status(thread, loading_window, root):
221 | """
222 | Recursive function to check the status of the model loading thread.
223 |
224 | Args:
225 | thread: The thread to monitor
226 | loading_window: LoadingWindow instance to close when complete
227 | root: Tkinter root window for scheduling checks
228 | """
229 | if thread.is_alive():
230 | root.after(500, lambda: check_thread_status(thread, loading_window, root))
231 | else:
232 | loading_window.destroy()
233 |
234 | root.after(500, lambda: check_thread_status(thread, loading_window, root))
235 |
236 |
237 | @staticmethod
238 | def start_model_threaded(settings, root_window):
239 | """
240 | Start the model in a separate thread.
241 |
242 | :param settings: Configuration settings for the model
243 | :type settings: dict
244 | :param root_window: The main application window reference
245 | :type root_window: tkinter.Tk
246 | :return: The created thread instance
247 | :rtype: threading.Thread
248 |
249 | This method creates and starts a new thread that runs the model's start
250 | function with the provided settings and root window reference. The model
251 | is accessed through ModelManager's local_model attribute.
252 | """
253 | thread = threading.Thread(target=ModelManager.setup_model, args=(settings, root_window))
254 | thread.start()
255 | return thread
256 |
257 | @staticmethod
258 | def unload_model():
259 | """
260 | Safely unload and cleanup the currently loaded model.
261 |
262 | Closes the model if it exists and sets the local_model reference to None.
263 | This method should be called before loading a new model or shutting down
264 | the application.
265 | """
266 | if ModelManager.local_model is not None:
267 | ModelManager.local_model.model.close()
268 | del ModelManager.local_model
269 | ModelManager.local_model = None
270 |
--------------------------------------------------------------------------------
/src/FreeScribe.client/UI/MainWindowUI.py:
--------------------------------------------------------------------------------
1 | import tkinter as tk
2 | from tkinter import ttk
3 | import UI.MainWindow as mw
4 | from UI.SettingsWindow import FeatureToggle
5 | from UI.SettingsWindowUI import SettingsWindowUI
6 | from UI.MarkdownWindow import MarkdownWindow
7 | from utils.file_utils import get_file_path
8 | from UI.DebugWindow import DebugPrintWindow
9 |
10 | DOCKER_CONTAINER_CHECK_INTERVAL = 10000 # Interval in milliseconds to check the Docker container status
11 | DOCKER_DESKTOP_CHECK_INTERVAL = 10000 # Interval in milliseconds to check the Docker Desktop status
12 |
13 | class MainWindowUI:
14 | """
15 | This class handles the user interface (UI) for the main application window,
16 | including the Docker status bar for managing the LLM and Whisper containers.
17 |
18 | :param root: The root Tkinter window.
19 | :param settings: The application settings passed to control the containers' behavior.
20 | """
21 |
22 | def __init__(self, root, settings):
23 | """
24 | Initialize the MainWindowUI class.
25 |
26 | :param root: The Tkinter root window.
27 | :param settings: The application settings used to control container states.
28 | """
29 | self.root = root # Tkinter root window
30 | self.docker_status_bar = None # Docker status bar frame
31 | self.is_status_bar_enabled = False # Flag to indicate if the Docker status bar is enabled
32 | self.app_settings = settings # Application settings
33 | self.logic = mw.MainWindow(self.app_settings) # Logic to control the container behavior
34 | self.scribe_template = None
35 | self.setting_window = SettingsWindowUI(self.app_settings, self, self.root) # Settings window
36 | self.root.iconbitmap(get_file_path('assets','logo.ico'))
37 |
38 | self.current_docker_status_check_id = None # ID for the current Docker status check
39 | self.current_container_status_check_id = None # ID for the current container status check
40 |
41 | def load_main_window(self):
42 | """
43 | Load the main window of the application.
44 | This method initializes the main window components, including the menu bar.
45 | """
46 | self._bring_to_focus()
47 | self._create_menu_bar()
48 | if (self.setting_window.settings.editable_settings['Show Welcome Message']):
49 | self._show_welcome_message()
50 |
51 | def _bring_to_focus(self):
52 | """
53 | Bring the main window to focus.
54 | """
55 | self.root.lift() # Lift the window to the top
56 | self.root.attributes('-topmost', True) # Set the window to be always on top
57 | self.root.focus_force() # Force focus on the window
58 | self.root.after_idle(self.root.attributes, '-topmost', False) # Reset the always on top attribute after idle
59 |
60 |
61 | def update_aiscribe_texts(self, event):
62 | if self.scribe_template is not None:
63 | selected_option = self.scribe_template.get()
64 | if selected_option in self.app_settings.scribe_template_mapping:
65 | self.app_settings.AISCRIBE, self.app_settings.AISCRIBE2 = self.app_settings.scribe_template_mapping[selected_option]
66 |
67 | def create_docker_status_bar(self):
68 | """
69 | Create a Docker status bar to display the status of the LLM and Whisper containers.
70 | Adds start and stop buttons for each container.
71 | """
72 | # if not feature do not do this
73 | if FeatureToggle.DOCKER_STATUS_BAR is not True:
74 | return
75 |
76 | if self.docker_status_bar is not None:
77 | return
78 |
79 | # Create the frame for the Docker status bar, placed at the bottom of the window
80 | self.docker_status_bar = tk.Frame(self.root, bd=1, relief=tk.SUNKEN)
81 | self.docker_status_bar.grid(row=4, column=0, columnspan=14, sticky='nsew')
82 |
83 | # Add a label to indicate Docker container status section
84 | self.docker_status = tk.Label(self.docker_status_bar, text="Docker Container Status:", padx=10)
85 | self.docker_status.pack(side=tk.LEFT)
86 |
87 | # Add LLM container status label
88 | llm_status = tk.Label(self.docker_status_bar, text="LLM Container Status:", padx=10)
89 | llm_status.pack(side=tk.LEFT)
90 |
91 | # Add status dot for LLM (default: red)
92 | llm_dot = tk.Label(self.docker_status_bar, text='●', fg='red')
93 | self.logic.container_manager.update_container_status_icon(llm_dot, self.app_settings.editable_settings["LLM Container Name"])
94 | llm_dot.pack(side=tk.LEFT)
95 |
96 | # Add Whisper server status label
97 | whisper_status = tk.Label(self.docker_status_bar, text="Whisper Server Status:", padx=10)
98 | whisper_status.pack(side=tk.LEFT)
99 |
100 | # Add status dot for Whisper (default: red)
101 | whisper_dot = tk.Label(self.docker_status_bar, text='●', fg='red')
102 | self.logic.container_manager.update_container_status_icon(whisper_dot, self.app_settings.editable_settings["Whisper Container Name"])
103 | whisper_dot.pack(side=tk.LEFT)
104 |
105 | # Start button for Whisper container with a command to invoke the start method from logic
106 | start_whisper_button = tk.Button(self.docker_status_bar, text="Start Whisper", command=lambda: self.logic.start_whisper_container(whisper_dot, self.app_settings))
107 | start_whisper_button.pack(side=tk.RIGHT)
108 |
109 | # Start button for LLM container with a command to invoke the start method from logic
110 | start_llm_button = tk.Button(self.docker_status_bar, text="Start LLM", command=lambda: self.logic.start_LLM_container(llm_dot, self.app_settings))
111 | start_llm_button.pack(side=tk.RIGHT)
112 |
113 | # Stop button for Whisper container with a command to invoke the stop method from logic
114 | stop_whisper_button = tk.Button(self.docker_status_bar, text="Stop Whisper", command=lambda: self.logic.stop_whisper_container(whisper_dot, self.app_settings))
115 | stop_whisper_button.pack(side=tk.RIGHT)
116 |
117 | # Stop button for LLM container with a command to invoke the stop method from logic
118 | stop_llm_button = tk.Button(self.docker_status_bar, text="Stop LLM", command=lambda: self.logic.stop_LLM_container(llm_dot, self.app_settings))
119 | stop_llm_button.pack(side=tk.RIGHT)
120 |
121 | self.is_status_bar_enabled = True
122 | self._background_availbility_docker_check()
123 | self._background_check_container_status(llm_dot, whisper_dot)
124 |
125 | def disable_docker_ui(self):
126 | """
127 | Disable the Docker status bar UI elements.
128 | """
129 |
130 | if FeatureToggle.DOCKER_STATUS_BAR is not True:
131 | return
132 |
133 |
134 | self.is_status_bar_enabled = False
135 | self.docker_status.config(text="(Docker not found)")
136 | if self.docker_status_bar is not None:
137 | for child in self.docker_status_bar.winfo_children():
138 | child.configure(state='disabled')
139 |
140 | def enable_docker_ui(self):
141 | """
142 | Enable the Docker status bar UI elements.
143 | """
144 |
145 | if FeatureToggle.DOCKER_STATUS_BAR is not True:
146 | return
147 |
148 | self.is_status_bar_enabled = True
149 | self.docker_status.config(text="Docker Container Status: ")
150 | if self.docker_status_bar is not None:
151 | for child in self.docker_status_bar.winfo_children():
152 | child.configure(state='normal')
153 |
154 | def destroy_docker_status_bar(self):
155 | """
156 | Destroy the Docker status bar if it exists.
157 | """
158 | if FeatureToggle.DOCKER_STATUS_BAR is not True:
159 | return
160 |
161 | if self.docker_status_bar is not None:
162 | self.docker_status_bar.destroy()
163 | self.docker_status_bar = None
164 |
165 | # cancel the check loop as the bar no longer exists and it is waster resources.
166 | if self.current_docker_status_check_id is not None:
167 | self.root.after_cancel(self.current_docker_status_check_id)
168 | self.current_docker_status_check_id = None
169 |
170 | if self.current_container_status_check_id is not None:
171 | self.root.after_cancel(self.current_container_status_check_id)
172 | self.current_container_status_check_id = None
173 |
174 | def toggle_menu_bar(self, enable: bool):
175 | """
176 | Enable or disable the menu bar.
177 |
178 | :param enable: True to enable the menu bar, False to disable it.
179 | :type enable: bool
180 | """
181 | if enable:
182 | self._create_menu_bar()
183 | else:
184 | self._destroy_menu_bar()
185 |
186 | def _create_menu_bar(self):
187 | """
188 | Private method to create menu bar.
189 | Create a menu bar with a Help menu.
190 | This method sets up the menu bar at the top of the main window and adds a Help menu with an About option.
191 | """
192 | self.menu_bar = tk.Menu(self.root)
193 | self.root.config(menu=self.menu_bar)
194 | self._create_settings_menu()
195 | self._create_help_menu()
196 |
197 | def _destroy_menu_bar(self):
198 | """
199 | Private method to destroy the menu bar.
200 | Destroy the menu bar if it exists.
201 | """
202 | if self.menu_bar is not None:
203 | self.menu_bar.destroy()
204 | self.menu_bar = None
205 | self._destroy_settings_menu()
206 | self._destroy_help_menu()
207 |
208 | def _create_settings_menu(self):
209 | # Add Settings menu
210 | setting_menu = tk.Menu(self.menu_bar, tearoff=0)
211 | self.menu_bar.add_cascade(label="Settings", menu=setting_menu)
212 | setting_menu.add_command(label="Settings", command=self.setting_window.open_settings_window)
213 |
214 | def _destroy_settings_menu(self):
215 | """
216 | Private method to destroy the Settings menu.
217 | Destroy the Settings menu if it exists.
218 | """
219 | if self.menu_bar is not None:
220 | setting_menu = self.menu_bar.nametowidget('Settings')
221 | if setting_menu is not None:
222 | setting_menu.destroy()
223 |
224 | def _create_help_menu(self):
225 | # Add Help menu
226 | help_menu = tk.Menu(self.menu_bar, tearoff=0)
227 | self.menu_bar.add_cascade(label="Help", menu=help_menu)
228 | help_menu.add_command(label="Debug Window", command=lambda: DebugPrintWindow(self.root))
229 | help_menu.add_command(label="About", command=lambda: self._show_md_content(get_file_path('markdown','help','about.md'), 'About'))
230 |
231 | def _destroy_help_menu(self):
232 | """
233 | Private method to destroy the Help menu.
234 | Destroy the Help menu if it exists.
235 | """
236 | if self.menu_bar is not None:
237 | help_menu = self.menu_bar.nametowidget('Help')
238 | if help_menu is not None:
239 | help_menu.destroy()
240 |
241 | def disable_settings_menu(self):
242 | """
243 | Disable the Settings menu.
244 | """
245 | if self.menu_bar is not None:
246 | self.menu_bar.entryconfig("Settings", state="disabled") # Disables the entire Settings menu
247 |
248 | def enable_settings_menu(self):
249 | """
250 | Enable the Settings menu.
251 | """
252 | if self.menu_bar is not None:
253 | self.menu_bar.entryconfig("Settings", state="normal")
254 |
255 | def _show_md_content(self, file_path: str, title: str, show_checkbox: bool = False):
256 | """
257 | Private method to display help/about information.
258 | Display help information in a message box.
259 | This method shows a message box with information about the application when the About option is selected from the Help menu.
260 | """
261 |
262 | # Callback function called when the window is closed
263 | def on_close(checkbox_state):
264 | self.setting_window.settings.editable_settings['Show Welcome Message'] = not checkbox_state
265 | self.setting_window.settings.save_settings_to_file()
266 |
267 | # Create a MarkdownWindow to display the content
268 | MarkdownWindow(self.root, title, file_path,
269 | callback=on_close if show_checkbox else None)
270 |
271 |
272 | def _on_help_window_close(self, help_window, dont_show_again: tk.BooleanVar):
273 | """
274 | Private method to handle the closing of the help window.
275 | Updates the 'Show Welcome Message' setting based on the checkbox state.
276 | """
277 | self.setting_window.settings.editable_settings['Show Welcome Message'] = not dont_show_again.get()
278 | self.setting_window.settings.save_settings_to_file()
279 | help_window.destroy()
280 |
281 | def _show_welcome_message(self):
282 | """
283 | Private method to display a welcome message.
284 | Display a welcome message when the application is launched.
285 | This method shows a welcome message in a message box when the application is launched.
286 | """
287 | self._show_md_content(get_file_path('markdown','welcome.md'), 'Welcome', True)
288 |
289 | def create_scribe_template(self, row=3, column=4, columnspan=3, pady=10, padx=10, sticky='nsew'):
290 | """
291 | Create a template for the Scribe application.
292 | """
293 | self.scribe_template = ttk.Combobox(self.root, values=self.app_settings.scribe_template_values, width=35, state="readonly")
294 | self.scribe_template.current(0)
295 | self.scribe_template.bind("<>", self.app_settings.scribe_template_values)
296 | self.scribe_template.grid(row=row, column=column, columnspan=columnspan, pady=pady, padx=padx, sticky=sticky)
297 |
298 | def destroy_scribe_template(self):
299 | """
300 | Destroy the Scribe template if it exists.
301 | """
302 | if self.scribe_template is not None:
303 | self.scribe_template.destroy()
304 | self.scribe_template = None
305 |
306 | def _background_availbility_docker_check(self):
307 | """
308 | Check if the Docker client is available in the background.
309 |
310 | This method is intended to be run in a separate thread to periodically
311 | check the availability of the Docker client.
312 | """
313 | print("Checking Docker availability in the background...")
314 | if self.logic.container_manager.check_docker_availability():
315 | # Enable the Docker status bar UI elements if not enabled
316 | if not self.is_status_bar_enabled:
317 | self.enable_docker_ui()
318 | print("Docker client is available.")
319 | else:
320 | # Disable the Docker status bar UI elements if not disabled
321 | if self.is_status_bar_enabled:
322 | self.disable_docker_ui()
323 |
324 | print("Docker client is not available.")
325 |
326 | self.current_docker_status_check_id = self.root.after(DOCKER_DESKTOP_CHECK_INTERVAL, self._background_availbility_docker_check)
327 |
328 | def _background_check_container_status(self, llm_dot, whisper_dot):
329 | """
330 | Check the status of Docker containers in the background.
331 |
332 | This method is intended to be run in a separate thread to periodically
333 | check the status of the LLM and Whisper containers.
334 | """
335 | if self.is_status_bar_enabled:
336 | self.logic.container_manager.set_status_icon_color(llm_dot, self.logic.check_llm_containers())
337 | self.logic.container_manager.set_status_icon_color(whisper_dot, self.logic.check_whisper_containers())
338 | self.current_container_status_check_id = self.root.after(DOCKER_CONTAINER_CHECK_INTERVAL, self._background_check_container_status, llm_dot, whisper_dot)
339 |
340 |
341 |
342 |
--------------------------------------------------------------------------------
/scripts/install.nsi:
--------------------------------------------------------------------------------
1 | !include "MUI2.nsh"
2 | !include "LogicLib.nsh"
3 | !include "FileFunc.nsh"
4 | !include "WordFunc.nsh"
5 |
6 | ; Define the name of the installer
7 | OutFile "..\dist\FreeScribeInstaller.exe"
8 |
9 | ; Define the default installation directory to AppData
10 | InstallDir "$PROGRAMFILES\FreeScribe"
11 |
12 | ; Define the name of the installer
13 | Name "FreeScribe"
14 |
15 | ; Define the version of the installer
16 | VIProductVersion "0.0.0.1"
17 | VIAddVersionKey "ProductName" "FreeScribe"
18 | VIAddVersionKey "FileVersion" "0.0.0.1"
19 | VIAddVersionKey "LegalCopyright" "Copyright (c) 2023-2024 Braedon Hendy"
20 | VIAddVersionKey "FileDescription" "FreeScribe Installer"
21 |
22 | ; Define the logo image
23 | !define MUI_ICON ./assets/logo.ico
24 | !define MIN_CUDA_DRIVER_VERSION 527.41 ; The nvidia graphic driver that is compatiable with Cuda 12.1
25 |
26 | ; Variables for checkboxes
27 | Var /GLOBAL CPU_RADIO
28 | Var /GLOBAL NVIDIA_RADIO
29 | Var /GLOBAL SELECTED_OPTION
30 | Var /GLOBAL REMOVE_CONFIG_CHECKBOX
31 | Var /GLOBAL REMOVE_CONFIG
32 |
33 | Function Check_For_Old_Version_In_App_Data
34 | ; Check if the old version exists in AppData
35 | IfFileExists "$APPDATA\FreeScribe\freescribe-client.exe" 0 OldVersionDoesNotExist
36 | ; Open Dialog to ask user if they want to uninstall the old version
37 | MessageBox MB_YESNO|MB_ICONQUESTION "An old version of FreeScribe has been detected. Would you like to uninstall it?" IDYES UninstallOldVersion IDNO OldVersionDoesNotExist
38 | UninstallOldVersion:
39 | ; Remove the contents/folders of the old version
40 | RMDir /r "$APPDATA\FreeScribe\presets"
41 | RMDir /r "$APPDATA\FreeScribe\_internal"
42 | RMDir /r "$APPDATA\FreeScribe\models"
43 |
44 | ; Remove the old version executable
45 | Delete "$APPDATA\FreeScribe\freescribe-client.exe"
46 |
47 | ; Remove the uninstaller entry from the Control Panel
48 | Delete "$APPDATA\FreeScribe\uninstall.exe"
49 |
50 | ; Remove the start menu shortcut
51 | Delete "$SMPROGRAMS\FreeScribe\FreeScribe.lnk"
52 | RMDir "$SMPROGRAMS\FreeScribe"
53 |
54 | ; Show message when uninstallation is complete
55 | MessageBox MB_OK "FreeScribe has been successfully uninstalled."
56 | OldVersionDoesNotExist:
57 | FunctionEnd
58 |
59 |
60 | ; Function to create a custom page with CPU/NVIDIA options
61 | Function ARCHITECTURE_SELECT
62 | Call Check_For_Old_Version_In_App_Data
63 | !insertmacro MUI_HEADER_TEXT "Architecture Selection" "Choose your preferred installation architecture based on your hardware"
64 |
65 | nsDialogs::Create 1018
66 | Pop $0
67 |
68 | ${If} $0 == error
69 | Abort
70 | ${EndIf}
71 |
72 | ; Main instruction text for architecture selection
73 | ${NSD_CreateLabel} 0 0 100% 12u "Choose your preferred installation architecture based on your hardware:"
74 | Pop $0
75 |
76 | ; Radio button for CPU
77 | ${NSD_CreateRadioButton} 10 15u 100% 10u "CPU"
78 | Pop $CPU_RADIO
79 | ${NSD_Check} $CPU_RADIO
80 | StrCpy $SELECTED_OPTION "CPU"
81 |
82 | ; CPU explanation text (grey with padding)
83 | ${NSD_CreateLabel} 20 25u 100% 20u "Recommended for most users. Runs on any modern processor and provides good performance for general use."
84 | Pop $0
85 | SetCtlColors $0 808080 transparent
86 |
87 | ; Radio button for NVIDIA
88 | ${NSD_CreateRadioButton} 10 55u 100% 10u "NVIDIA"
89 | Pop $NVIDIA_RADIO
90 |
91 | ; NVIDIA explanation text (grey with padding)
92 | ${NSD_CreateLabel} 20 65u 100% 30u "Choose this option if you have an NVIDIA GPU. Provides accelerated performance. Only select if you have a Nvidia GPU installed."
93 | Pop $0
94 | SetCtlColors $0 808080 transparent
95 |
96 | ; Bottom padding (10u of space)
97 | ${NSD_CreateLabel} 0 95u 100% 10u ""
98 | Pop $0
99 |
100 | ${NSD_OnClick} $CPU_RADIO OnRadioClick
101 | ${NSD_OnClick} $NVIDIA_RADIO OnRadioClick
102 |
103 | nsDialogs::Show
104 | FunctionEnd
105 |
106 | Function ARCHITECTURE_SELECT_LEAVE
107 | ${If} $SELECTED_OPTION == "NVIDIA"
108 | Call CheckNvidiaDrivers
109 | ${EndIf}
110 | FunctionEnd
111 |
112 | ; Callback function for radio button clicks
113 | Function OnRadioClick
114 | Pop $0 ; Get the handle of the clicked control
115 |
116 | ${If} $0 == $CPU_RADIO
117 | StrCpy $SELECTED_OPTION "CPU"
118 | ${ElseIf} $0 == $NVIDIA_RADIO
119 | StrCpy $SELECTED_OPTION "NVIDIA"
120 | ${EndIf}
121 | FunctionEnd
122 |
123 | ; Function to show message box on finish
124 | Function .onInstSuccess
125 | ; Check if silent, if is silent skip message box prompt
126 | IfSilent +2
127 | MessageBox MB_OK "Installation completed successfully! Please note upon first launch start time may be slow. Please wait for the program to open!"
128 | FunctionEnd
129 |
130 | Function un.onInit
131 | CheckIfFreeScribeIsRunning:
132 | nsExec::ExecToStack 'cmd /c tasklist /FI "IMAGENAME eq freescribe-client.exe" /NH | find /I "freescribe-client.exe" > nul'
133 | Pop $0 ; Return value
134 |
135 | ; Check if the process is running
136 | ${If} $0 == 0
137 | MessageBox MB_RETRYCANCEL "FreeScribe is currently running. Please close the application and try again." IDRETRY CheckIfFreeScribeIsRunning IDCANCEL abort
138 | abort:
139 | Abort
140 | ${EndIf}
141 | FunctionEnd
142 | ; Checks on installer start
143 | Function .onInit
144 | CheckIfFreeScribeIsRunning:
145 | nsExec::ExecToStack 'cmd /c tasklist /FI "IMAGENAME eq freescribe-client.exe" /NH | find /I "freescribe-client.exe" > nul'
146 | Pop $0 ; Return value
147 |
148 | ; Check if the process is running
149 | ${If} $0 == 0
150 | MessageBox MB_RETRYCANCEL "FreeScribe is currently running. Please close the application and try again." IDRETRY CheckIfFreeScribeIsRunning IDCANCEL abort
151 | abort:
152 | Abort
153 | ${EndIf}
154 |
155 | IfSilent SILENT_MODE NOT_SILENT_MODE
156 |
157 | SILENT_MODE:
158 | ${GetParameters} $R0
159 | ; Check for custom parameters
160 | ${GetOptions} $R0 "/ARCH=" $R1
161 | ${If} $R1 != ""
162 | StrCpy $SELECTED_OPTION $R1
163 | ${EndIf}
164 |
165 | NOT_SILENT_MODE:
166 | FunctionEnd
167 |
168 | Function CleanUninstall
169 | ; Remove the contents/folders of the old version
170 | RMDir /r "$INSTDIR\presets"
171 | RMDir /r "$INSTDIR\_internal"
172 |
173 | ; Remove the old version executable
174 | Delete "$INSTDIR\freescribe-client.exe"
175 |
176 | ; Remove the uninstaller entry from the Control Panel
177 | Delete "$INSTDIR\uninstall.exe"
178 |
179 | ; Remove the start menu shortcut
180 | Delete "$SMPROGRAMS\FreeScribe\FreeScribe.lnk"
181 | RMDir "$SMPROGRAMS\FreeScribe"
182 | FunctionEnd
183 |
184 | Function CheckForOldConfig
185 | ; Check if the old version exists in AppData
186 | IfFileExists "$APPDATA\FreeScribe\settings.txt" 0 End
187 | ; Open Dialog to ask user if they want to uninstall the old version
188 | MessageBox MB_YESNO|MB_ICONQUESTION "An old configuration file has been detected. We recommend removing it to prevent conflict with new versions. Would you like to remove it?" IDYES RemoveOldConfig IDNO End
189 | RemoveOldConfig:
190 | ClearErrors
191 | ; Remove the old version executable
192 | RMDir /r "$APPDATA\FreeScribe"
193 | ${If} ${Errors}
194 | MessageBox MB_RETRYCANCEL "Unable to remove old configuration. Please close any applications using these files and try again." IDRETRY RemoveOldConfig IDCANCEL ConfigFilesFailed
195 | ${EndIf}
196 | Goto End
197 | ConfigFilesFailed:
198 | MessageBox MB_OK|MB_ICONEXCLAMATION "Old configuration files could not be removed. Proceeding with installation."
199 | End:
200 | FunctionEnd
201 |
202 | ; Define the section of the installer
203 | Section "MainSection" SEC01
204 | Call CleanUninstall
205 | Call CheckForOldConfig
206 | ; Set output path to the installation directory
207 | SetOutPath "$INSTDIR"
208 |
209 | ${If} $SELECTED_OPTION == "CPU"
210 | ; Add files to the installer
211 | File /r "..\dist\freescribe-client-cpu\freescribe-client-cpu.exe"
212 | Rename "$INSTDIR\freescribe-client-cpu.exe" "$INSTDIR\freescribe-client.exe"
213 | File /r "..\dist\freescribe-client-cpu\_internal"
214 | ${EndIf}
215 |
216 | ${If} $SELECTED_OPTION == "NVIDIA"
217 | ; Add files to the installer
218 | File /r "..\dist\freescribe-client-nvidia\freescribe-client-nvidia.exe"
219 | Rename "$INSTDIR\freescribe-client-nvidia.exe" "$INSTDIR\freescribe-client.exe"
220 | File /r "..\dist\freescribe-client-nvidia\_internal"
221 | ${EndIf}
222 |
223 |
224 | ; add presets
225 | CreateDirectory "$INSTDIR\presets"
226 | SetOutPath "$INSTDIR\presets"
227 | File /r "..\src\FreeScribe.client\presets\*"
228 |
229 | SetOutPath "$INSTDIR"
230 |
231 | ; Create a start menu shortcut
232 | CreateDirectory "$SMPROGRAMS\FreeScribe"
233 | CreateShortcut "$SMPROGRAMS\FreeScribe\FreeScribe.lnk" "$INSTDIR\freescribe-client.exe"
234 |
235 | ; Create an uninstaller
236 | WriteUninstaller "$INSTDIR\Uninstall.exe"
237 | SectionEnd
238 |
239 | Section "GGUF Installs" GGUF_INSTALLS
240 | AddSize 2800000 ; Add the size in kilobytes for the models
241 |
242 | CreateDirectory "$INSTDIR\models"
243 | SetOutPath "$INSTDIR\models"
244 |
245 | ; Copy the license
246 | File ".\assets\gemma_license.txt"
247 |
248 | ; Check if the file already exists
249 | IfFileExists "$INSTDIR\models\gemma-2-2b-it-Q8_0.gguf" 0 +2
250 | Goto SkipDownload
251 |
252 | ; Install the gemma 2 q8
253 | inetc::get /TIMEOUT=30000 "https://huggingface.co/lmstudio-community/gemma-2-2b-it-GGUF/resolve/main/gemma-2-2b-it-Q8_0.gguf?download=true" "$INSTDIR\models\gemma-2-2b-it-Q8_0.gguf" /END
254 |
255 | SkipDownload:
256 | SetOutPath "$INSTDIR"
257 |
258 | SectionEnd
259 |
260 | ; Define the uninstaller section
261 | Section "Uninstall"
262 | ; Remove the installation directory and all its contents
263 | RMDir /r "$INSTDIR"
264 |
265 | ; Remove the start menu shortcut
266 | Delete "$SMPROGRAMS\FreeScribe\FreeScribe.lnk"
267 | RMDir "$SMPROGRAMS\FreeScribe"
268 |
269 | ; Remove the uninstaller entry from the Control Panel
270 | Delete "$INSTDIR\Uninstall.exe"
271 |
272 | RemoveConfigFiles:
273 | ; Remove configuration files if the checkbox is selected
274 | ${If} $REMOVE_CONFIG == ${BST_CHECKED}
275 | ClearErrors
276 | RMDir /r "$APPDATA\FreeScribe"
277 | ${If} ${Errors}
278 | MessageBox MB_RETRYCANCEL "Unable to remove old configuration. Please close any applications using these files and try again." IDRETRY RemoveConfigFiles IDCANCEL ConfigFilesFailed
279 | ${EndIf}
280 | ${EndIf}
281 |
282 | ; Show message when uninstallation is complete
283 | MessageBox MB_OK "FreeScribe has been successfully uninstalled."
284 | Goto EndUninstall
285 |
286 | ConfigFilesFailed:
287 | MessageBox MB_OK|MB_ICONEXCLAMATION "FreeScribe has been successfully uninstalled, but the configuration files could not be removed. Please close any applications using these files and try again."
288 | EndUninstall:
289 | SectionEnd
290 |
291 | # Variables for checkboxes
292 | Var DesktopShortcutCheckbox
293 | Var StartMenuCheckbox
294 | Var RunAppCheckbox
295 |
296 | Function CustomizeFinishPage
297 | !insertmacro MUI_HEADER_TEXT "Installation Complete" "Please select your preferences and close the installer."
298 |
299 | nsDialogs::Create 1018
300 | Pop $0
301 |
302 | ${If} $0 == error
303 | Abort
304 | ${EndIf}
305 |
306 | # Run App Checkbox
307 | ${NSD_CreateCheckbox} 10u 10u 100% 12u "Run FreeScribe after installation"
308 | Pop $RunAppCheckbox
309 | ${NSD_SetState} $RunAppCheckbox ${BST_CHECKED}
310 |
311 | # Desktop Shortcut Checkbox
312 | ${NSD_CreateCheckbox} 10u 30u 100% 12u "Create Desktop Shortcut"
313 | Pop $DesktopShortcutCheckbox
314 | ${NSD_SetState} $DesktopShortcutCheckbox ${BST_CHECKED}
315 |
316 | # Start Menu Checkbox
317 | ${NSD_CreateCheckbox} 10u 50u 100% 12u "Add to Start Menu"
318 | Pop $StartMenuCheckbox
319 | ${NSD_SetState} $StartMenuCheckbox ${BST_CHECKED}
320 |
321 | nsDialogs::Show
322 | FunctionEnd
323 |
324 | Function RunApp
325 | ${NSD_GetState} $RunAppCheckbox $0
326 | ${If} $0 == ${BST_CHECKED}
327 | Exec '"$INSTDIR\freescribe-client.exe"'
328 | ${EndIf}
329 |
330 | # Check Desktop Shortcut
331 | ${NSD_GetState} $DesktopShortcutCheckbox $0
332 | StrCmp $0 ${BST_CHECKED} +2
333 | Goto SkipDesktopShortcut
334 | CreateShortcut "$DESKTOP\FreeScribe.lnk" "$INSTDIR\freescribe-client.exe"
335 | SkipDesktopShortcut:
336 |
337 | # Check Start Menu
338 | ${NSD_GetState} $StartMenuCheckbox $0
339 | StrCmp $0 ${BST_CHECKED} +2
340 | Goto SkipStartMenu
341 | CreateDirectory "$SMPROGRAMS\FreeScribe"
342 | CreateShortcut "$SMPROGRAMS\FreeScribe\FreeScribe.lnk" "$INSTDIR\freescribe-client.exe"
343 | SkipStartMenu:
344 | FunctionEnd
345 |
346 | ; Function to execute when leaving the InstallFiles page
347 | ; Goes to the next page after the installation is complete
348 | Function InsfilesPageLeave
349 | SetAutoClose true
350 | FunctionEnd
351 |
352 | Function CheckNvidiaDrivers
353 | Var /GLOBAL DriverVersion
354 |
355 | ; Try to read from the registry
356 | SetRegView 64
357 | ReadRegStr $DriverVersion HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{B2FE1952-0186-46C3-BAEC-A80AA35AC5B8}_Display.Driver" "DisplayVersion"
358 |
359 | ${If} $DriverVersion == ""
360 | ; Fallback to 32-bit registry view
361 | SetRegView 32
362 | ReadRegStr $DriverVersion HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{B2FE1952-0186-46C3-BAEC-A80AA35AC5B8}_Display.Driver" "DisplayVersion"
363 | ${EndIf}
364 |
365 | ; No nvidia drivers detected - show error message
366 | ${If} $DriverVersion == ""
367 | MessageBox MB_OK "No valid Nvidia device deteced (Drivers Missing). This program relys on a Nvidia GPU to run. Functionality is not guaranteed without a Nvidia GPU."
368 | Goto driver_check_end
369 | ${EndIf}
370 | ; Push the version number to the stack
371 | Push $DriverVersion
372 | ; Push min driver version
373 | Push ${MIN_CUDA_DRIVER_VERSION}
374 |
375 | Call CompareVersions
376 |
377 | Pop $0 ; Get the return value
378 |
379 | ${If} $0 == 1
380 | MessageBox MB_OK "Your NVIDIA driver version ($DriverVersion) is older than the minimum required version (${MIN_CUDA_DRIVER_VERSION}). Please update at https://www.nvidia.com/en-us/drivers/. Then contiune with the installation."
381 | Abort
382 | ${EndIf}
383 | driver_check_end:
384 | FunctionEnd
385 |
386 | ;------------------------------------------------------------------------------
387 | ; Function: CompareVersions
388 | ; Purpose: Compares two version numbers in format "X.Y" (e.g., "1.0", "2.3")
389 | ;
390 | ; Parameters:
391 | ; Stack 1 (bottom): First version string to compare
392 | ; Stack 0 (top): Second version string to compare
393 | ;
394 | ; Returns:
395 | ; 0: Versions are equal
396 | ; 1: First version is less than second version
397 | ; 2: First version is greater than second version
398 | ;
399 | ; Example:
400 | ; Push "1.0" ; First version
401 | ; Push "2.0" ; Second version
402 | ; Call CompareVersions
403 | ; Pop $R0 ; $R0 will contain 1 (1.0 < 2.0)
404 | ;------------------------------------------------------------------------------
405 | Function CompareVersions
406 | Exch $R0 ; Get second version from stack into $R0
407 | Exch
408 | Exch $R1 ; Get first version from stack into $R1
409 | Push $R2
410 | Push $R3
411 | Push $R4
412 | Push $R5
413 |
414 | ; Split version strings into major and minor numbers
415 | ${WordFind} $R1 "." "+1" $R2 ; Extract major number from first version
416 | ${WordFind} $R1 "." "+2" $R3 ; Extract minor number from first version
417 | ${WordFind} $R0 "." "+1" $R4 ; Extract major number from second version
418 | ${WordFind} $R0 "." "+2" $R5 ; Extract minor number from second version
419 |
420 | ; Convert to comparable numbers:
421 | ; Multiply major version by 1000 to handle minor version properly
422 | IntOp $R2 $R2 * 1000 ; Convert first version major number
423 | IntOp $R4 $R4 * 1000 ; Convert second version major number
424 |
425 | ; Add minor numbers to create complete comparable values
426 | IntOp $R2 $R2 + $R3 ; First version complete number
427 | IntOp $R4 $R4 + $R5 ; Second version complete number
428 |
429 | ; Compare versions and set return value
430 | ${If} $R2 < $R4 ; If first version is less than second
431 | StrCpy $R0 1
432 | ${ElseIf} $R2 > $R4 ; If first version is greater than second
433 | StrCpy $R0 2
434 | ${Else} ; If versions are equal
435 | StrCpy $R0 0
436 | ${EndIf}
437 |
438 | ; Restore registers from stack
439 | Pop $R5
440 | Pop $R4
441 | Pop $R3
442 | Pop $R2
443 | Pop $R1
444 | Exch $R0 ; Put return value on stack
445 | FunctionEnd
446 |
447 | Function un.CreateRemoveConfigFilesPage
448 | !insertmacro MUI_HEADER_TEXT "Remove Configuration Files" "Do you want to remove the configuration files (e.g., settings)?"
449 |
450 | nsDialogs::Create 1018
451 | Pop $0
452 |
453 | ${If} $0 == error
454 | Abort
455 | ${EndIf}
456 |
457 | ${NSD_CreateCheckbox} 0 20u 100% 12u "Remove configuration files"
458 | Pop $REMOVE_CONFIG_CHECKBOX
459 | ${NSD_SetState} $REMOVE_CONFIG_CHECKBOX ${BST_CHECKED}
460 |
461 | nsDialogs::Show
462 | FunctionEnd
463 |
464 | Function un.RemoveConfigFilesPageLeave
465 | ${NSD_GetState} $REMOVE_CONFIG_CHECKBOX $REMOVE_CONFIG
466 | FunctionEnd
467 |
468 | ; Define installer pages
469 | !insertmacro MUI_PAGE_LICENSE ".\assets\License.txt"
470 | Page Custom ARCHITECTURE_SELECT ARCHITECTURE_SELECT_LEAVE
471 | !insertmacro MUI_PAGE_DIRECTORY
472 | !define MUI_PAGE_CUSTOMFUNCTION_LEAVE InsfilesPageLeave
473 | !insertmacro MUI_PAGE_INSTFILES
474 | Page Custom CustomizeFinishPage RunApp
475 |
476 | ; Define the uninstaller pages
477 | !insertmacro MUI_UNPAGE_CONFIRM
478 | UninstPage custom un.CreateRemoveConfigFilesPage un.RemoveConfigFilesPageLeave
479 | !insertmacro MUI_UNPAGE_INSTFILES
480 | !insertmacro MUI_UNPAGE_FINISH
481 |
482 | ; Define the languages
483 | !insertmacro MUI_LANGUAGE English
484 |
--------------------------------------------------------------------------------
/src/FreeScribe.client/clientfasterwhisper.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2023 Braedon Hendy
2 | # This software is released under the GNU General Public License v3.0
3 |
4 | import tkinter as tk
5 | from tkinter import scrolledtext, ttk, filedialog
6 | import requests
7 | import pyperclip
8 | import wave
9 | import threading
10 | import numpy as np
11 | import base64
12 | import json
13 | import pyaudio
14 | import tkinter.messagebox as messagebox
15 | import datetime
16 | import functools
17 | import os
18 | from faster_whisper import WhisperModel
19 | from openai import OpenAI
20 | import scrubadub
21 |
22 | # Add these near the top of your script
23 | editable_settings = {
24 | "use_story": False,
25 | "use_memory": False,
26 | "use_authors_note": False,
27 | "use_world_info": False,
28 | "max_context_length": 2048,
29 | "max_length": 360,
30 | "rep_pen": 1.1,
31 | "rep_pen_range": 2048,
32 | "rep_pen_slope": 0.7,
33 | "temperature": 0,
34 | "tfs": 0.97,
35 | "top_a": 0.8,
36 | "top_k": 30,
37 | "top_p": 0.4,
38 | "typical": 0.19,
39 | "sampler_order": [6, 0, 1, 3, 4, 2, 5],
40 | "singleline": False,
41 | "frmttriminc": False,
42 | "frmtrmblln": False,
43 | "Local Whisper": False,
44 | "Whisper Model": "medium.en",
45 | "GPT Model": "gpt-4"
46 | }
47 |
48 | # Function to build the full URL from IP
49 | def build_url(ip, port):
50 | return f"http://{ip}:{port}"
51 |
52 | # Function to save settings to a file
53 | def save_settings_to_file(koboldcpp_ip, whisperaudio_ip, openai_api_key):
54 | settings = {
55 | "koboldcpp_ip": koboldcpp_ip,
56 | "whisperaudio_ip": whisperaudio_ip,
57 | "openai_api_key": openai_api_key,
58 | "editable_settings": editable_settings
59 | }
60 | with open('settings.txt', 'w') as file:
61 | json.dump(settings, file)
62 |
63 | def load_settings_from_file():
64 | try:
65 | with open('settings.txt', 'r') as file:
66 | try:
67 | settings = json.load(file)
68 | except json.JSONDecodeError:
69 | return "192.168.1.195", "192.168.1.195", "None"
70 |
71 | koboldcpp_ip = settings.get("koboldcpp_ip", "192.168.1.195")
72 | whisperaudio_ip = settings.get("whisperaudio_ip", "192.168.1.195")
73 | openai_api_key = settings.get("openai_api_key", "NONE")
74 | loaded_editable_settings = settings.get("editable_settings", {})
75 | for key, value in loaded_editable_settings.items():
76 | if key in editable_settings:
77 | editable_settings[key] = value
78 | return koboldcpp_ip, whisperaudio_ip, openai_api_key
79 | except FileNotFoundError:
80 | # Return default values if file not found
81 | return "192.168.1.195", "192.168.1.195", "None"
82 |
83 | def load_aiscribe_from_file():
84 | try:
85 | with open('aiscribe.txt', 'r') as f:
86 | content = f.read().strip()
87 | return content if content else None
88 | except FileNotFoundError:
89 | return None
90 |
91 | def load_aiscribe2_from_file():
92 | try:
93 | with open('aiscribe2.txt', 'r') as f:
94 | content = f.read().strip()
95 | return content if content else None
96 | except FileNotFoundError:
97 | return None
98 |
99 | # Load settings at the start
100 | KOBOLDCPP_IP, WHISPERAUDIO_IP, OPENAI_API_KEY = load_settings_from_file()
101 | KOBOLDCPP = build_url(KOBOLDCPP_IP, "5001")
102 | WHISPERAUDIO = build_url(WHISPERAUDIO_IP, "8000/whisperaudio")
103 | response_history = []
104 | current_view = "full"
105 |
106 |
107 | # Other constants and global variables
108 | username = "user"
109 | botname = "Assistant"
110 | num_lines_to_keep = 20
111 | DEFAULT_AISCRIBE = "AI, please transform the following conversation into a concise SOAP note. Do not invent or assume any medical data, vital signs, or lab values. Base the note strictly on the information provided in the conversation. Ensure that the SOAP note is structured appropriately with Subjective, Objective, Assessment, and Plan sections. Here's the conversation:"
112 | DEFAULT_AISCRIBE2 = "Remember, the Subjective section should reflect the patient's perspective and complaints as mentioned in the conversation. The Objective section should only include observable or measurable data from the conversation. The Assessment should be a summary of your understanding and potential diagnoses, considering the conversation's content. The Plan should outline the proposed management or follow-up required, strictly based on the dialogue provided"
113 | AISCRIBE = load_aiscribe_from_file() or DEFAULT_AISCRIBE
114 | AISCRIBE2 = load_aiscribe2_from_file() or DEFAULT_AISCRIBE2
115 | uploaded_file_path = None
116 |
117 | # Function to get prompt for KoboldAI Generation
118 | def get_prompt(formatted_message):
119 | # Check and parse 'sampler_order' if it's a string
120 | sampler_order = editable_settings["sampler_order"]
121 | if isinstance(sampler_order, str):
122 | sampler_order = json.loads(sampler_order)
123 | return {
124 | "prompt": f"{formatted_message}\n",
125 | "use_story": editable_settings["use_story"],
126 | "use_memory": editable_settings["use_memory"],
127 | "use_authors_note": editable_settings["use_authors_note"],
128 | "use_world_info": editable_settings["use_world_info"],
129 | "max_context_length": int(editable_settings["max_context_length"]),
130 | "max_length": int(editable_settings["max_length"]),
131 | "rep_pen": float(editable_settings["rep_pen"]),
132 | "rep_pen_range": int(editable_settings["rep_pen_range"]),
133 | "rep_pen_slope": float(editable_settings["rep_pen_slope"]),
134 | "temperature": float(editable_settings["temperature"]),
135 | "tfs": float(editable_settings["tfs"]),
136 | "top_a": float(editable_settings["top_a"]),
137 | "top_k": int(editable_settings["top_k"]),
138 | "top_p": float(editable_settings["top_p"]),
139 | "typical": float(editable_settings["typical"]),
140 | "sampler_order": sampler_order,
141 | "singleline": editable_settings["singleline"],
142 | "frmttriminc": editable_settings["frmttriminc"],
143 | "frmtrmblln": editable_settings["frmtrmblln"]
144 | }
145 |
146 | def threaded_handle_message(formatted_message):
147 | thread = threading.Thread(target=handle_message, args=(formatted_message,))
148 | thread.start()
149 |
150 | def threaded_send_audio_to_server():
151 | thread = threading.Thread(target=send_audio_to_server)
152 | thread.start()
153 |
154 | def handle_message(formatted_message):
155 | if gpt_button.cget("bg") == "red":
156 | show_edit_transcription_popup(formatted_message)
157 | else:
158 | prompt = get_prompt(formatted_message)
159 | response = requests.post(f"{KOBOLDCPP}/api/v1/generate", json=prompt)
160 | if response.status_code == 200:
161 | results = response.json()['results']
162 | response_text = results[0]['text']
163 | response_text = response_text.replace(" ", " ").strip()
164 | update_gui_with_response(response_text)
165 |
166 | def send_and_receive():
167 | global use_aiscribe
168 | user_message = user_input.get("1.0", tk.END).strip()
169 | clear_response_display()
170 | if use_aiscribe:
171 | formatted_message = f'{AISCRIBE} [{user_message}] {AISCRIBE2}'
172 | else:
173 | formatted_message = user_message
174 | threaded_handle_message(formatted_message)
175 |
176 | def clear_response_display():
177 | response_display.configure(state='normal')
178 | response_display.delete("1.0", tk.END)
179 | response_display.configure(state='disabled')
180 |
181 | def update_gui_with_response(response_text):
182 | global response_history
183 | timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
184 | response_history.insert(0, (timestamp, response_text))
185 |
186 | # Update the timestamp listbox
187 | timestamp_listbox.delete(0, tk.END)
188 | for time, _ in response_history:
189 | timestamp_listbox.insert(tk.END, time)
190 |
191 | response_display.configure(state='normal')
192 | response_display.insert(tk.END, f"{response_text}\n")
193 | response_display.configure(state='disabled')
194 | pyperclip.copy(response_text)
195 | stop_flashing()
196 |
197 | def show_response(event):
198 | selection = event.widget.curselection()
199 | if selection:
200 | index = selection[0]
201 | response_text = response_history[index][1]
202 | response_display.configure(state='normal')
203 | response_display.delete('1.0', tk.END)
204 | response_display.insert('1.0', response_text)
205 | response_display.configure(state='disabled')
206 | pyperclip.copy(response_text)
207 |
208 | def send_text_to_chatgpt(edited_text):
209 | api_key = OPENAI_API_KEY
210 | headers = {
211 | "Authorization": f"Bearer {api_key}",
212 | "Content-Type": "application/json",
213 | }
214 | payload = {
215 | "model": editable_settings["GPT Model"].strip(),
216 | "messages": [
217 | {"role": "user", "content": edited_text}
218 | ],
219 | }
220 |
221 | response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, data=json.dumps(payload))
222 |
223 | if response.status_code == 200:
224 | response_data = response.json()
225 | response_text = (response_data['choices'][0]['message']['content'])
226 | update_gui_with_response(response_text)
227 |
228 |
229 | def show_edit_transcription_popup(formatted_message):
230 | popup = tk.Toplevel(root)
231 | popup.title("Scrub PHI Prior to GPT")
232 | text_area = scrolledtext.ScrolledText(popup, height=20, width=80)
233 | text_area.pack(padx=10, pady=10)
234 | cleaned_message = scrubadub.clean(formatted_message)
235 | text_area.insert(tk.END, cleaned_message)
236 |
237 | def on_proceed():
238 | edited_text = text_area.get("1.0", tk.END).strip()
239 | popup.destroy()
240 | send_text_to_chatgpt(edited_text)
241 |
242 | proceed_button = tk.Button(popup, text="Proceed", command=on_proceed)
243 | proceed_button.pack(side=tk.RIGHT, padx=10, pady=10)
244 |
245 | # Cancel button
246 | cancel_button = tk.Button(popup, text="Cancel", command=popup.destroy)
247 | cancel_button.pack(side=tk.LEFT, padx=10, pady=10)
248 |
249 | # Global variable to control recording state
250 | is_recording = False
251 | audio_data = []
252 | frames = []
253 | is_paused = False
254 | use_aiscribe = True
255 | is_gpt_button_active = False
256 |
257 | # Global variables for PyAudio
258 | CHUNK = 1024
259 | FORMAT = pyaudio.paInt16
260 | CHANNELS = 1
261 | RATE = 16000
262 | RECORD_SECONDS = 5
263 |
264 | p = pyaudio.PyAudio() # Creating an instance of PyAudio
265 |
266 | def toggle_pause():
267 | global is_paused
268 | is_paused = not is_paused
269 |
270 | if is_paused:
271 | pause_button.config(text="Resume", bg="red")
272 | else:
273 | pause_button.config(text="Pause", bg="SystemButtonFace")
274 |
275 | def record_audio():
276 | global is_paused, frames
277 | stream = p.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK)
278 |
279 | while is_recording:
280 | if not is_paused:
281 | data = stream.read(CHUNK)
282 | frames.append(data)
283 |
284 | stream.stop_stream()
285 | stream.close()
286 |
287 | # Function to save audio and send to server
288 | def save_audio():
289 | global frames
290 | if frames:
291 | with wave.open('recording.wav', 'wb') as wf:
292 | wf.setnchannels(CHANNELS)
293 | wf.setsampwidth(p.get_sample_size(FORMAT))
294 | wf.setframerate(RATE)
295 | wf.writeframes(b''.join(frames))
296 | frames = [] # Clear recorded data
297 | threaded_send_audio_to_server()
298 |
299 | def toggle_recording():
300 | global is_recording, recording_thread
301 | if not is_recording:
302 | # Clear the user_input and response_display textboxes
303 | # This code runs only when the recording is about to start
304 | user_input.delete("1.0", tk.END)
305 | response_display.configure(state='normal')
306 | response_display.delete("1.0", tk.END)
307 | response_display.configure(state='disabled')
308 | is_recording = True
309 | recording_thread = threading.Thread(target=record_audio)
310 | recording_thread.start()
311 | mic_button.config(bg="red", text="Microphone ON")
312 | start_flashing()
313 | else:
314 | is_recording = False
315 | if recording_thread.is_alive():
316 | recording_thread.join() # Ensure the recording thread is terminated
317 | save_audio()
318 | mic_button.config(bg="SystemButtonFace", text="Microphone OFF")
319 |
320 | def clear_all_text_fields():
321 | user_input.configure(state='normal')
322 | user_input.delete("1.0", tk.END)
323 | stop_flashing()
324 | response_display.configure(state='normal')
325 | response_display.delete("1.0", tk.END)
326 | response_display.configure(state='disabled')
327 |
328 | def toggle_gpt_button():
329 | global is_gpt_button_active
330 | if is_gpt_button_active:
331 | gpt_button.config(bg="SystemButtonFace", text="GPT OFF")
332 | is_gpt_button_active = False
333 | else:
334 | gpt_button.config(bg="red", text="GPT ON")
335 | is_gpt_button_active = True
336 |
337 | def toggle_aiscribe():
338 | global use_aiscribe
339 | use_aiscribe = not use_aiscribe
340 | toggle_button.config(text="AISCRIBE ON" if use_aiscribe else "AISCRIBE OFF")
341 |
342 | def save_settings(koboldcpp_ip, whisperaudio_ip, openai_api_key, aiscribe_text, aiscribe2_text, settings_window):
343 | global KOBOLDCPP, WHISPERAUDIO, KOBOLDCPP_IP, WHISPERAUDIO_IP, OPENAI_API_KEY, editable_settings, AISCRIBE, AISCRIBE2
344 | KOBOLDCPP_IP = koboldcpp_ip
345 | WHISPERAUDIO_IP = whisperaudio_ip
346 | OPENAI_API_KEY = openai_api_key
347 | KOBOLDCPP = build_url(KOBOLDCPP_IP, "5001")
348 | WHISPERAUDIO = build_url(WHISPERAUDIO_IP, "8000/whisperaudio")
349 | for setting, entry in editable_settings_entries.items():
350 | value = entry.get()
351 | if setting in ["max_context_length", "max_length", "rep_pen_range", "top_k"]:
352 | value = int(value)
353 | # Add similar conditions for other data types
354 | editable_settings[setting] = value
355 | save_settings_to_file(KOBOLDCPP_IP, WHISPERAUDIO_IP, OPENAI_API_KEY) # Save to file
356 | AISCRIBE = aiscribe_text
357 | AISCRIBE2 = aiscribe2_text
358 | with open('aiscribe.txt', 'w') as f:
359 | f.write(AISCRIBE)
360 | with open('aiscribe2.txt', 'w') as f:
361 | f.write(AISCRIBE2)
362 |
363 | settings_window.destroy()
364 |
365 | # New dictionary for entry widgets
366 | editable_settings_entries = {}
367 |
368 | def open_settings_window():
369 | settings_window = tk.Toplevel(root)
370 | settings_window.title("Settings")
371 |
372 | # KOBOLDCPP IP input
373 | tk.Label(settings_window, text="KOBOLDCPP IP:").grid(row=0, column=0)
374 | koboldcpp_ip_entry = tk.Entry(settings_window, width=50)
375 | koboldcpp_ip_entry.insert(0, KOBOLDCPP_IP)
376 | koboldcpp_ip_entry.grid(row=0, column=1)
377 |
378 | # WHISPERAUDIO IP input
379 | tk.Label(settings_window, text="WHISPERAUDIO IP:").grid(row=1, column=0)
380 | whisperaudio_ip_entry = tk.Entry(settings_window, width=50)
381 | whisperaudio_ip_entry.insert(0, WHISPERAUDIO_IP)
382 | whisperaudio_ip_entry.grid(row=1, column=1)
383 |
384 | tk.Label(settings_window, text="OpenAI API Key:").grid(row=2, column=0)
385 | openai_api_key_entry = tk.Entry(settings_window, width=50)
386 | openai_api_key_entry.insert(0, OPENAI_API_KEY)
387 | openai_api_key_entry.grid(row=2, column=1)
388 |
389 | row_index = 3
390 | for setting, value in editable_settings.items():
391 | tk.Label(settings_window, text=f"{setting}:").grid(row=row_index, column=0, sticky='nw')
392 | entry = tk.Entry(settings_window, width=50)
393 | entry.insert(0, str(value))
394 | entry.grid(row=row_index, column=1, sticky='nw')
395 | editable_settings_entries[setting] = entry
396 | row_index += 1
397 |
398 | # AISCRIBE text box
399 | tk.Label(settings_window, text="Context Before Conversation").grid(row=0, column=2, sticky='nw', padx=(10,0))
400 | aiscribe_textbox = tk.Text(settings_window, width=50, height=15)
401 | aiscribe_textbox.insert('1.0', AISCRIBE)
402 | aiscribe_textbox.grid(row=1, column=2, rowspan=10, sticky='nw', padx=(10,0))
403 |
404 | # AISCRIBE2 text box
405 | tk.Label(settings_window, text="Context After Conversation").grid(row=11, column=2, sticky='nw', padx=(10,0))
406 | aiscribe2_textbox = tk.Text(settings_window, width=50, height=15)
407 | aiscribe2_textbox.insert('1.0', AISCRIBE2)
408 | aiscribe2_textbox.grid(row=12, column=2, rowspan=10, sticky='nw', padx=(10,0))
409 |
410 | # Save, Close, and Default buttons under the left column
411 | save_button = tk.Button(settings_window, text="Save", width=15, command=lambda: save_settings(koboldcpp_ip_entry.get(), whisperaudio_ip_entry.get(), openai_api_key_entry.get(), aiscribe_textbox.get("1.0", tk.END), aiscribe2_textbox.get("1.0", tk.END), settings_window))
412 | save_button.grid(row=row_index, column=0, padx=5, pady=5)
413 |
414 | close_button = tk.Button(settings_window, text="Close", width=15, command=settings_window.destroy)
415 | close_button.grid(row=row_index + 1, column=0, padx=5, pady=5)
416 |
417 | default_button = tk.Button(settings_window, text="Default", width=15, command=clear_settings_file)
418 | default_button.grid(row=row_index + 2, column=0, padx=5, pady=5)
419 |
420 | def upload_file():
421 | global uploaded_file_path
422 | file_path = filedialog.askopenfilename(filetypes=(("Audio files", "*.wav *.mp3"),))
423 | if file_path:
424 | uploaded_file_path = file_path
425 | threaded_send_audio_to_server() # Add this line to process the file immediately
426 | start_flashing()
427 |
428 | def send_audio_to_server():
429 | global uploaded_file_path
430 | if editable_settings["Local Whisper"] == "True":
431 | print("Using Local Whisper for transcription.")
432 | # model = WhisperModel(model_size, device="cuda", compute_type="float16")
433 | # model = WhisperModel(model_size, device="cuda", compute_type="int8_float16")
434 | model = WhisperModel(editable_settings["Whisper Model"].strip(), device="cpu", compute_type="int8")
435 | file_to_send = uploaded_file_path if uploaded_file_path else 'recording.wav'
436 | uploaded_file_path = None
437 | segments, info = model.transcribe(file_to_send, beam_size=5)
438 | print("Detected language '%s' with probability %f" % (info.language, info.language_probability))
439 | transcribed_text = "".join(segment.text for segment in segments)
440 | user_input.delete("1.0", tk.END)
441 | user_input.insert(tk.END, transcribed_text)
442 | send_and_receive()
443 | else:
444 | print("Using Remote Whisper for transcription.")
445 | if uploaded_file_path:
446 | file_to_send = uploaded_file_path
447 | uploaded_file_path = None
448 | else:
449 | file_to_send = 'recording.wav'
450 | with open(file_to_send, 'rb') as f:
451 | files = {'audio': f}
452 | response = requests.post(WHISPERAUDIO, files=files)
453 | if response.status_code == 200:
454 | transcribed_text = response.json()['text']
455 | user_input.delete("1.0", tk.END)
456 | user_input.insert(tk.END, transcribed_text)
457 | send_and_receive()
458 |
459 | is_flashing = False
460 |
461 | def start_flashing():
462 | global is_flashing
463 | is_flashing = True
464 | flash_circle()
465 |
466 | def stop_flashing():
467 | global is_flashing
468 | is_flashing = False
469 | blinking_circle_canvas.itemconfig(circle, fill='white') # Reset to default color
470 |
471 | def flash_circle():
472 | if is_flashing:
473 | current_color = blinking_circle_canvas.itemcget(circle, 'fill')
474 | new_color = 'blue' if current_color != 'blue' else 'black'
475 | blinking_circle_canvas.itemconfig(circle, fill=new_color)
476 | root.after(1000, flash_circle) # Adjust the flashing speed as needed
477 |
478 | def send_and_flash():
479 | start_flashing()
480 | send_and_receive()
481 |
482 | def clear_settings_file():
483 | try:
484 | open('settings.txt', 'w').close() # This opens the files and immediately closes it, clearing its contents.
485 | open('aiscribe.txt', 'w').close()
486 | open('aiscribe2.txt', 'w').close()
487 | messagebox.showinfo("Settings Reset", "Settings have been reset. Please restart.")
488 | print("Settings file cleared.")
489 | except Exception as e:
490 | print(f"Error clearing settings files: {e}")
491 |
492 | def toggle_view():
493 | global current_view
494 | if current_view == "full":
495 | user_input.grid_remove()
496 | send_button.grid_remove()
497 | clear_button.grid_remove()
498 | toggle_button.grid_remove()
499 | gpt_button.grid_remove()
500 | settings_button.grid_remove()
501 | upload_button.grid_remove()
502 | response_display.grid_remove()
503 | timestamp_listbox.grid_remove()
504 | mic_button.config(width=10, height=1)
505 | pause_button.config(width=10, height=1)
506 | switch_view_button.config(width=10, height=1)
507 | mic_button.grid(row=0, column=0, pady=5)
508 | pause_button.grid(row=0, column=1, pady=5)
509 | switch_view_button.grid(row=0, column=2, pady=5)
510 | blinking_circle_canvas.grid(row=0, column=3, pady=5)
511 | root.attributes('-topmost', True)
512 | current_view = "minimal"
513 | else:
514 | mic_button.config(width=15, height=2)
515 | pause_button.config(width=15, height=2)
516 | switch_view_button.config(width=15, height=2)
517 | user_input.grid()
518 | send_button.grid()
519 | clear_button.grid()
520 | toggle_button.grid()
521 | gpt_button.grid()
522 | settings_button.grid()
523 | upload_button.grid()
524 | response_display.grid()
525 | timestamp_listbox.grid()
526 | mic_button.grid(row=1, column=0, pady=5)
527 | pause_button.grid(row=1, column=2, pady=5)
528 | switch_view_button.grid(row=1, column=8, pady=5)
529 | blinking_circle_canvas.grid(row=1, column=9, pady=5)
530 | root.attributes('-topmost', False)
531 | current_view = "full"
532 |
533 | # GUI Setup
534 | root = tk.Tk()
535 | root.title("AI Medical Scribe")
536 |
537 | user_input = scrolledtext.ScrolledText(root, height=15)
538 | user_input.grid(row=0, column=0, columnspan=10, padx=5, pady=5)
539 |
540 | mic_button = tk.Button(root, text="Microphone OFF", command=toggle_recording, height=2, width=15)
541 | mic_button.grid(row=1, column=0, pady=5)
542 |
543 | send_button = tk.Button(root, text="Send", command=send_and_flash, height=2, width=15)
544 | send_button.grid(row=1, column=1, pady=5)
545 |
546 | pause_button = tk.Button(root, text="Pause", command=toggle_pause, height=2, width=15)
547 | pause_button.grid(row=1, column=2, pady=5)
548 |
549 | clear_button = tk.Button(root, text="Clear", command=clear_all_text_fields, height=2, width=15)
550 | clear_button.grid(row=1, column=3, pady=5)
551 |
552 | toggle_button = tk.Button(root, text="AISCRIBE ON", command=toggle_aiscribe, height=2, width=15)
553 | toggle_button.grid(row=1, column=4, pady=5)
554 |
555 | gpt_button = tk.Button(root, text="GPT OFF", command=toggle_gpt_button, height=2, width=15)
556 | gpt_button.grid(row=1, column=5, pady=5)
557 |
558 | settings_button = tk.Button(root, text="Settings", command=open_settings_window, height=2, width=15)
559 | settings_button.grid(row=1, column=6, pady=5)
560 |
561 | upload_button = tk.Button(root, text="Upload File", command=upload_file, height=2, width=15)
562 | upload_button.grid(row=1, column=7, pady=5)
563 |
564 | switch_view_button = tk.Button(root, text="Switch View", command=toggle_view, height=2, width=15)
565 | switch_view_button.grid(row=1, column=8, pady=5)
566 |
567 | blinking_circle_canvas = tk.Canvas(root, width=20, height=20)
568 | blinking_circle_canvas.grid(row=1, column=9, pady=5)
569 | circle = blinking_circle_canvas.create_oval(5, 5, 15, 15, fill='white')
570 |
571 | response_display = scrolledtext.ScrolledText(root, height=15, state='disabled')
572 | response_display.grid(row=2, column=0, columnspan=10, padx=5, pady=5)
573 |
574 | timestamp_listbox = tk.Listbox(root, height=30)
575 | timestamp_listbox.grid(row=0, column=10, rowspan=3, padx=5, pady=5)
576 | timestamp_listbox.bind('<>', show_response)
577 |
578 | # Bind Alt+P to send_and_receive function
579 | root.bind('', lambda event: pause_button.invoke())
580 |
581 | # Bind Alt+R to toggle_recording function
582 | root.bind('', lambda event: mic_button.invoke())
583 |
584 | root.mainloop()
585 |
586 | p.terminate()
587 |
--------------------------------------------------------------------------------
/src/FreeScribe.client/UI/SettingsWindow.py:
--------------------------------------------------------------------------------
1 | """
2 | application_settings.py
3 |
4 | This software is released under the AGPL-3.0 license
5 | Copyright (c) 2023-2024 Braedon Hendy
6 |
7 | Further updates and packaging added in 2024 through the ClinicianFOCUS initiative,
8 | a collaboration with Dr. Braedon Hendy and Conestoga College Institute of Applied
9 | Learning and Technology as part of the CNERG+ applied research project,
10 | Unburdening Primary Healthcare: An Open-Source AI Clinician Partner Platform".
11 | Prof. Michael Yingbull (PI), Dr. Braedon Hendy (Partner),
12 | and Research Students - Software Developer Alex Simko, Pemba Sherpa (F24), and Naitik Patel.
13 |
14 | This module contains the ApplicationSettings class, which manages the settings for an
15 | application that involves audio processing and external API interactions, including
16 | WhisperAudio, and OpenAI services.
17 |
18 | """
19 |
20 | import json
21 | import os
22 | import tkinter as tk
23 | from tkinter import ttk, messagebox
24 | import requests
25 | import numpy as np
26 | from utils.file_utils import get_resource_path, get_file_path
27 | from Model import ModelManager
28 | import threading
29 | from UI.Widgets.MicrophoneSelector import MicrophoneState
30 | from utils.ip_utils import is_valid_url
31 | from enum import Enum
32 |
33 | class SettingsKeys(Enum):
34 | LOCAL_WHISPER = "Built-in Speech2Text"
35 | WHISPER_ENDPOINT = "Speech2Text (Whisper) Endpoint"
36 | WHISPER_SERVER_API_KEY = "Speech2Text (Whisper) API Key"
37 |
38 |
39 | class FeatureToggle:
40 | DOCKER_SETTINGS_TAB = False
41 | DOCKER_STATUS_BAR = False
42 |
43 | class SettingsWindow():
44 | """
45 | Manages application settings related to audio processing and external API services.
46 |
47 | Attributes
48 | ----------
49 | OPENAI_API_KEY : str
50 | The API key for OpenAI integration.
51 | AISCRIBE : str
52 | Placeholder for the first AI Scribe settings.
53 | AISCRIBE2 : str
54 | Placeholder for the second AI Scribe settings.
55 | # API_STYLE : str FUTURE FEATURE REVISION
56 | # The API style to be used (default is 'OpenAI'). FUTURE FEATURE
57 |
58 | editable_settings : dict
59 | A dictionary containing user-editable settings such as model parameters, audio
60 | settings, and real-time processing configurations.
61 |
62 | Methods
63 | -------
64 | load_settings_from_file():
65 | Loads settings from a JSON file and updates the internal state.
66 | save_settings_to_file():
67 | Saves the current settings to a JSON file.
68 | save_settings(openai_api_key, aiscribe_text, aiscribe2_text,
69 | settings_window, preset):
70 | Saves the current settings, including API keys, IP addresses, and user-defined parameters.
71 | load_aiscribe_from_file():
72 | Loads the first AI Scribe text from a file.
73 | load_aiscribe2_from_file():
74 | Loads the second AI Scribe text from a file.
75 | clear_settings_file(settings_window):
76 | Clears the content of settings files and closes the settings window.
77 | """
78 |
79 | CPU_INSTALL_FILE = "CPU_INSTALL.txt"
80 | NVIDIA_INSTALL_FILE = "NVIDIA_INSTALL.txt"
81 | STATE_FILES_DIR = "install_state"
82 |
83 | def __init__(self):
84 | """Initializes the ApplicationSettings with default values."""
85 |
86 |
87 | self.OPENAI_API_KEY = "None"
88 | # self.API_STYLE = "OpenAI" # FUTURE FEATURE REVISION
89 | self.main_window = None
90 | self.scribe_template_values = []
91 | self.scribe_template_mapping = {}
92 |
93 |
94 | self.general_settings = [
95 | "Show Welcome Message",
96 | "Show Scrub PHI"
97 | ]
98 |
99 | self.whisper_settings = [
100 | "BlankSpace", # Represents the SettingsKeys.LOCAL_WHISPER.value checkbox that is manually placed
101 | "Real Time",
102 | "BlankSpace", # Represents the model dropdown that is manually placed
103 | SettingsKeys.WHISPER_ENDPOINT.value,
104 | SettingsKeys.WHISPER_SERVER_API_KEY.value,
105 | "S2T Server Self-Signed Certificates",
106 | ]
107 |
108 | self.llm_settings = [
109 | "Model Endpoint",
110 | "AI Server Self-Signed Certificates",
111 | ]
112 |
113 | self.adv_ai_settings = [
114 | "use_story",
115 | "use_memory",
116 | "use_authors_note",
117 | "use_world_info",
118 | "Use best_of",
119 | "best_of",
120 | "max_context_length",
121 | "max_length",
122 | "rep_pen",
123 | "rep_pen_range",
124 | "rep_pen_slope",
125 | "temperature",
126 | "tfs",
127 | "top_a",
128 | "top_k",
129 | "top_p",
130 | "typical",
131 | "sampler_order",
132 | "singleline",
133 | "frmttriminc",
134 | "frmtrmblln",
135 | ]
136 |
137 | self.adv_whisper_settings = [
138 | "Real Time Audio Length",
139 | ]
140 |
141 |
142 | self.adv_general_settings = [
143 | "Enable Scribe Template",
144 | ]
145 |
146 | self.editable_settings = {
147 | "Model": "gpt-4",
148 | "Model Endpoint": "https://api.openai.com/v1/",
149 | "Use Local LLM": True,
150 | "Architecture": "CPU",
151 | "use_story": False,
152 | "use_memory": False,
153 | "use_authors_note": False,
154 | "use_world_info": False,
155 | "max_context_length": 5000,
156 | "max_length": 400,
157 | "rep_pen": 1.1,
158 | "rep_pen_range": 5000,
159 | "rep_pen_slope": 0.7,
160 | "temperature": 0.1,
161 | "tfs": 0.97,
162 | "top_a": 0.8,
163 | "top_k": 30,
164 | "top_p": 0.4,
165 | "typical": 0.19,
166 | "sampler_order": "[6, 0, 1, 3, 4, 2, 5]",
167 | "singleline": False,
168 | "frmttriminc": False,
169 | "frmtrmblln": False,
170 | "best_of": 2,
171 | "Use best_of": False,
172 | SettingsKeys.LOCAL_WHISPER.value: True,
173 | SettingsKeys.WHISPER_ENDPOINT.value: "https://localhost:2224/whisperaudio",
174 | SettingsKeys.WHISPER_SERVER_API_KEY.value: "",
175 | "Whisper Model": "small.en",
176 | "Current Mic": "None",
177 | "Real Time": True,
178 | "Real Time Audio Length": 5,
179 | "Real Time Silence Length": 1,
180 | "Silence cut-off": 0.035,
181 | "LLM Container Name": "ollama",
182 | "LLM Caddy Container Name": "caddy-ollama",
183 | "LLM Authentication Container Name": "authentication-ollama",
184 | "Whisper Container Name": "speech-container",
185 | "Whisper Caddy Container Name": "caddy",
186 | "Auto Shutdown Containers on Exit": True,
187 | "Use Docker Status Bar": False,
188 | "Preset": "Custom",
189 | "Show Welcome Message": True,
190 | "Enable Scribe Template": False,
191 | "Use Pre-Processing": True,
192 | "Use Post-Processing": False, # Disabled for now causes unexcepted behaviour
193 | "AI Server Self-Signed Certificates": False,
194 | "S2T Server Self-Signed Certificates": False,
195 | "Pre-Processing": "Please break down the conversation into a list of facts. Take the conversation and transform it to a easy to read list:\n\n",
196 | "Post-Processing": "\n\nUsing the provided list of facts, review the SOAP note for accuracy. Verify that all details align with the information provided in the list of facts and ensure consistency throughout. Update or adjust the SOAP note as necessary to reflect the listed facts without offering opinions or subjective commentary. Ensure that the revised note excludes a \"Notes\" section and does not include a header for the SOAP note. Provide the revised note after making any necessary corrections.",
197 | "Show Scrub PHI": False,
198 | }
199 |
200 | self.docker_settings = [
201 | "LLM Container Name",
202 | "LLM Caddy Container Name",
203 | "LLM Authentication Container Name",
204 | "Whisper Container Name",
205 | "Whisper Caddy Container Name",
206 | "Auto Shutdown Containers on Exit",
207 | "Use Docker Status Bar",
208 | ]
209 |
210 | self.editable_settings_entries = {}
211 |
212 | self.load_settings_from_file()
213 | self.AISCRIBE = self.load_aiscribe_from_file() or "AI, please transform the following conversation into a concise SOAP note. Do not assume any medical data, vital signs, or lab values. Base the note strictly on the information provided in the conversation. Ensure that the SOAP note is structured appropriately with Subjective, Objective, Assessment, and Plan sections. Strictly extract facts from the conversation. Here's the conversation:"
214 | self.AISCRIBE2 = self.load_aiscribe2_from_file() or "Remember, the Subjective section should reflect the patient's perspective and complaints as mentioned in the conversation. The Objective section should only include observable or measurable data from the conversation. The Assessment should be a summary of your understanding and potential diagnoses, considering the conversation's content. The Plan should outline the proposed management, strictly based on the dialogue provided. Do not add any information that did not occur and do not make assumptions. Strictly extract facts from the conversation."
215 |
216 | self.get_dropdown_values_and_mapping()
217 | self._create_settings_and_aiscribe_if_not_exist()
218 |
219 | MicrophoneState.load_microphone_from_settings(self)
220 |
221 | def get_dropdown_values_and_mapping(self):
222 | """
223 | Reads the 'options.txt' file to populate dropdown values and their mappings.
224 |
225 | This function attempts to read a file named 'options.txt' to extract templates
226 | that consist of three lines: a title, aiscribe, and aiscribe2. These templates
227 | are then used to populate the dropdown values and their corresponding mappings.
228 | If the file is not found, default values are used instead.
229 |
230 | :raises FileNotFoundError: If 'options.txt' is not found, a message is printed
231 | and default values are used.
232 | """
233 | self.scribe_template_values = []
234 | self.scribe_template_mapping = {}
235 | try:
236 | with open('options.txt', 'r') as file:
237 | content = file.read().strip()
238 | templates = content.split('\n\n')
239 | for template in templates:
240 | lines = template.split('\n')
241 | if len(lines) == 3:
242 | title, aiscribe, aiscribe2 = lines
243 | self.scribe_template_values.append(title)
244 | self.scribe_template_mapping[title] = (aiscribe, aiscribe2)
245 | except FileNotFoundError:
246 | print("options.txt not found, using default values.")
247 | # Fallback default options if file not found
248 | self.scribe_template_values = ["Settings Template"]
249 | self.scribe_template_mapping["Settings Template"] = (self.AISCRIBE, self.AISCRIBE2)
250 |
251 | def load_settings_from_file(self, filename='settings.txt'):
252 | """
253 | Loads settings from a JSON file.
254 |
255 | The settings are read from 'settings.txt'. If the file does not exist or cannot be parsed,
256 | default settings will be used. The method updates the instance attributes with loaded values.
257 |
258 | Returns:
259 | tuple: A tuple containing the IPs, ports, SSL settings, and API key.
260 | """
261 | try:
262 | with open(get_resource_path(filename), 'r') as file:
263 | try:
264 | settings = json.load(file)
265 | except json.JSONDecodeError:
266 | print("Error loading settings file. Using default settings.")
267 | return self.OPENAI_API_KEY
268 |
269 | self.OPENAI_API_KEY = settings.get("openai_api_key", self.OPENAI_API_KEY)
270 | # self.API_STYLE = settings.get("api_style", self.API_STYLE) # FUTURE FEATURE REVISION
271 | loaded_editable_settings = settings.get("editable_settings", {})
272 | for key, value in loaded_editable_settings.items():
273 | if key in self.editable_settings:
274 | self.editable_settings[key] = value
275 |
276 | if self.editable_settings["Use Docker Status Bar"] and self.main_window is not None:
277 | self.main_window.create_docker_status_bar()
278 |
279 | if self.editable_settings["Enable Scribe Template"] and self.main_window is not None:
280 | self.main_window.create_scribe_template()
281 |
282 |
283 | return self.OPENAI_API_KEY
284 | except FileNotFoundError:
285 | print("Settings file not found. Using default settings.")
286 | return self.OPENAI_API_KEY
287 |
288 | def save_settings_to_file(self):
289 | """
290 | Saves the current settings to a JSON file.
291 |
292 | The settings are written to 'settings.txt'. This includes all application settings
293 | such as IP addresses, ports, SSL settings, and editable settings.
294 |
295 | Returns:
296 | None
297 | """
298 | settings = {
299 | "openai_api_key": self.OPENAI_API_KEY,
300 | "editable_settings": self.editable_settings
301 | # "api_style": self.API_STYLE # FUTURE FEATURE REVISION
302 | }
303 | with open(get_resource_path('settings.txt'), 'w') as file:
304 | json.dump(settings, file)
305 |
306 | def save_settings(self, openai_api_key, aiscribe_text, aiscribe2_text, settings_window,
307 | silence_cutoff):
308 | """
309 | Save the current settings, including IP addresses, API keys, and user-defined parameters.
310 |
311 | This method writes the AI Scribe text to separate text files and updates the internal state
312 | of the Settings instance.
313 |
314 | :param str openai_api_key: The OpenAI API key for authentication.
315 | :param str aiscribe_text: The text for the first AI Scribe.
316 | :param str aiscribe2_text: The text for the second AI Scribe.
317 | :param tk.Toplevel settings_window: The settings window instance to be destroyed after saving.
318 | """
319 | self.OPENAI_API_KEY = openai_api_key
320 | # self.API_STYLE = api_style
321 |
322 | self.editable_settings["Silence cut-off"] = silence_cutoff
323 |
324 | for setting, entry in self.editable_settings_entries.items():
325 | value = entry.get()
326 | if setting in ["max_context_length", "max_length", "rep_pen_range", "top_k"]:
327 | value = int(value)
328 | self.editable_settings[setting] = value
329 |
330 | self.save_settings_to_file()
331 |
332 | self.AISCRIBE = aiscribe_text
333 | self.AISCRIBE2 = aiscribe2_text
334 |
335 | with open(get_resource_path('aiscribe.txt'), 'w') as f:
336 | f.write(self.AISCRIBE)
337 | with open(get_resource_path('aiscribe2.txt'), 'w') as f:
338 | f.write(self.AISCRIBE2)
339 |
340 | def load_aiscribe_from_file(self):
341 | """
342 | Load the AI Scribe text from a file.
343 |
344 | :returns: The AI Scribe text, or None if the file does not exist or is empty.
345 | :rtype: str or None
346 | """
347 | try:
348 | with open(get_resource_path('aiscribe.txt'), 'r') as f:
349 | return f.read()
350 | except FileNotFoundError:
351 | return None
352 |
353 | def load_aiscribe2_from_file(self):
354 | """
355 | Load the second AI Scribe text from a file.
356 |
357 | :returns: The second AI Scribe text, or None if the file does not exist or is empty.
358 | :rtype: str or None
359 | """
360 | try:
361 | with open(get_resource_path('aiscribe2.txt'), 'r') as f:
362 | return f.read()
363 | except FileNotFoundError:
364 | return None
365 |
366 |
367 | def clear_settings_file(self, settings_window):
368 | """
369 | Clears the content of settings files and closes the settings window.
370 |
371 | This method attempts to open and clear the contents of three text files:
372 | `settings.txt`, `aiscribe.txt`, and `aiscribe2.txt`. After clearing the
373 | files, it displays a message box to notify the user that the settings
374 | have been reset and closes the `settings_window`. If an error occurs
375 | during this process, the exception will be caught and printed.
376 |
377 | :param settings_window: The settings window object to be closed after resetting.
378 | :type settings_window: tkinter.Toplevel or similar
379 | :raises Exception: If there is an issue with file handling or window destruction.
380 |
381 | Example usage:
382 |
383 | """
384 | try:
385 | # Open the files and immediately close them to clear their contents.
386 | open(get_resource_path('settings.txt'), 'w').close()
387 | open(get_resource_path('aiscribe.txt'), 'w').close()
388 | open(get_resource_path('aiscribe2.txt'), 'w').close()
389 |
390 | # Display a message box informing the user of successful reset.
391 | messagebox.showinfo("Settings Reset", "Settings have been reset. Please restart.")
392 | print("Settings file cleared.")
393 |
394 | # Close the settings window.
395 | settings_window.destroy()
396 | except Exception as e:
397 | # Print any exception that occurs during file handling or window destruction.
398 | print(f"Error clearing settings files: {e}")
399 |
400 | def get_available_models(self,endpoint=None):
401 | """
402 | Returns a list of available models for the user to choose from.
403 |
404 | This method returns a list of available models that can be used with the AI Scribe
405 | service. The list includes the default model, `gpt-4`, as well as any other models
406 | that may be added in the future.
407 |
408 | Returns:
409 | list: A list of available models for the user to choose from.
410 | """
411 |
412 | headers = {
413 | "Authorization": f"Bearer {self.OPENAI_API_KEY}",
414 | "X-API-Key": self.OPENAI_API_KEY
415 | }
416 |
417 | endpoint = endpoint or self.editable_settings_entries["Model Endpoint"].get()
418 |
419 | # url validate the endpoint
420 | if not is_valid_url(endpoint):
421 | print("Invalid LLM Endpoint")
422 | return ["Invalid LLM Endpoint", "Custom"]
423 |
424 | try:
425 | verify = not self.editable_settings["AI Server Self-Signed Certificates"]
426 | response = requests.get(endpoint + "/models", headers=headers, timeout=1.0, verify=verify)
427 | response.raise_for_status() # Raise an error for bad responses
428 | models = response.json().get("data", []) # Extract the 'data' field
429 |
430 | if not models:
431 | return ["No models available", "Custom"]
432 |
433 | available_models = [model["id"] for model in models]
434 | available_models.append("Custom")
435 | return available_models
436 | except requests.RequestException as e:
437 | # messagebox.showerror("Error", f"Failed to fetch models: {e}. Please ensure your OpenAI API key is correct.")
438 | print(e)
439 | return ["Failed to load models", "Custom"]
440 |
441 | def update_models_dropdown(self, dropdown, endpoint=None):
442 | """
443 | Updates the models dropdown with the available models.
444 |
445 | This method fetches the available models from the AI Scribe service and updates
446 | the dropdown widget in the settings window with the new list of models.
447 | """
448 | if self.editable_settings_entries["Use Local LLM"].get():
449 | dropdown["values"] = ["gemma-2-2b-it-Q8_0.gguf"]
450 | dropdown.set("gemma-2-2b-it-Q8_0.gguf")
451 | else:
452 | dropdown["values"] = ["Loading models...", "Custom"]
453 | dropdown.set("Loading models...")
454 | models = self.get_available_models(endpoint=endpoint)
455 | dropdown["values"] = models
456 | if self.editable_settings["Model"] in models:
457 | dropdown.set(self.editable_settings["Model"])
458 | else:
459 | dropdown.set(models[0])
460 |
461 |
462 | def load_settings_preset(self, preset_name, settings_class):
463 | """
464 | Load a settings preset from a file.
465 |
466 | This method loads a settings preset from a JSON file with the given name.
467 | The settings are then applied to the application settings.
468 |
469 | Parameters:
470 | preset_name (str): The name of the settings preset to load.
471 |
472 | Returns:
473 | None
474 | """
475 | self.editable_settings["Preset"] = preset_name
476 |
477 | if preset_name != "Custom":
478 | # load the settigns from the json preset file
479 | self.load_settings_from_file("presets/" + preset_name + ".json")
480 |
481 | self.editable_settings["Preset"] = preset_name
482 | #close the settings window
483 | settings_class.close_window()
484 |
485 | # save the settings to the file
486 | self.save_settings_to_file()
487 |
488 | if preset_name != "Local AI":
489 | messagebox.showinfo("Settings Preset", "Settings preset loaded successfully. Closing settings window. Please re-open and set respective API keys.")
490 |
491 | # Unload ai model if switching
492 | # already has safety check in unload to check if model exist.
493 | ModelManager.unload_model()
494 | else: # if is local ai
495 | # load the models here
496 | ModelManager.start_model_threaded(self, self.main_window.root)
497 | else:
498 | messagebox.showinfo("Custom Settings", "To use custom settings then please fill in the values and save them.")
499 |
500 | def set_main_window(self, window):
501 | """
502 | Set the main window instance for the settings.
503 |
504 | This method sets the main window instance for the settings class, allowing
505 | the settings to interact with the main window when necessary.
506 |
507 | Parameters:
508 | window (MainWindow): The main window instance to set.
509 | """
510 | self.main_window = window
511 |
512 | def load_or_unload_model(self, old_model, new_model, old_use_local_llm, new_use_local_llm, old_architecture, new_architecture):
513 | # Check if old model and new model are different if they are reload and make sure new model is checked.
514 | if old_model != new_model and new_use_local_llm == 1:
515 | ModelManager.unload_model()
516 | ModelManager.start_model_threaded(self, self.main_window.root)
517 |
518 | # Load the model if check box is now selected
519 | if old_use_local_llm == 0 and new_use_local_llm == 1:
520 | ModelManager.start_model_threaded(self, self.main_window.root)
521 |
522 | # Check if Local LLM was on and if turned off unload model.abs
523 | if old_use_local_llm == 1 and new_use_local_llm == 0:
524 | ModelManager.unload_model()
525 |
526 | if old_architecture != new_architecture and new_use_local_llm == 1:
527 | ModelManager.unload_model()
528 | ModelManager.start_model_threaded(self, self.main_window.root)
529 |
530 | def _create_settings_and_aiscribe_if_not_exist(self):
531 | if not os.path.exists(get_resource_path('settings.txt')):
532 | print("Settings file not found. Creating default settings file.")
533 | self.save_settings_to_file()
534 | if not os.path.exists(get_resource_path('aiscribe.txt')):
535 | print("AIScribe file not found. Creating default AIScribe file.")
536 | with open(get_resource_path('aiscribe.txt'), 'w') as f:
537 | f.write(self.AISCRIBE)
538 | if not os.path.exists(get_resource_path('aiscribe2.txt')):
539 | print("AIScribe2 file not found. Creating default AIScribe2 file.")
540 | with open(get_resource_path('aiscribe2.txt'), 'w') as f:
541 | f.write(self.AISCRIBE2)
542 |
543 | def get_available_architectures(self):
544 | """
545 | Returns a list of available architectures for the user to choose from.
546 |
547 | Based on the install state files in _internal folder
548 |
549 | Files must be named CPU_INSTALL or NVIDIA_INSTALL
550 |
551 | Returns:
552 | list: A list of available architectures for the user to choose from.
553 | """
554 | architectures = ["CPU"] # CPU is always available as fallback
555 |
556 | # Check for NVIDIA support
557 | if os.path.isfile(get_file_path(self.STATE_FILES_DIR, self.NVIDIA_INSTALL_FILE)):
558 | architectures.append("CUDA (Nvidia GPU)")
559 |
560 | return architectures
561 |
--------------------------------------------------------------------------------