Fix Image Search and Improve Desktop App

### Fix Image Search
  - Do not use XMP metadata by default for image search
    - It seems to be buggy currently. The returned results do not make sense with XMP metadata enabled

### Fix Image Search using Desktop App
  - Fix configuring Image Search via Desktop GUI
    - Set `input-directories`, instead of unused `input-files` for `content-type.image` in `khoj.yml`
  - Fix running Image Search via Desktop apps. 
    - Previously the transformers wasn't getting packaged into the app by pyinstaller
    - This is required by image search to run. So the desktop apps would fail to start when image search was enabled
    - Resolves #68
  - Append selected files, directories via "Add" button in Desktop GUI
    - This allows selecting multiple files, directories using Desktop GUI
    - Previously selecting multiple image directories had to be entered manually

### Improve Desktop App
  - Show Splash Screen to Desktop on App Initialization
    - The app takes a while to load during first run 
    - A splash screen signals that app is loading and not being unresponsive
    - Note: _Pyinstaller only supports splash screens on Windows, Linux. Not on Macs._
  - Add Khoj icon to the Windows, Linux app. Windows expects a `.ico` icon type
  - Only exclude `libtorch_{cuda, cpu, python}` on Linux machine
    - Seems those libraries are being used on Mac (and maybe Windows). 
    - Linux is where the app size benefits from removing these is maximum anyway
  - Fix PyInstaller Warnings on App Start
    - The warning show up as annoying error popups on Windows
This commit is contained in:
Debanjum
2022-08-19 17:37:09 +00:00
committed by GitHub
11 changed files with 107 additions and 43 deletions

View File

@@ -35,7 +35,7 @@ jobs:
shell: bash
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
sudo apt install libegl1 libxcb-xinerama0 -y
sudo apt install libegl1 libxcb-xinerama0 python3-tk -y
fi
python -m pip install --upgrade pip
pip install pyinstaller

101
Khoj.spec
View File

@@ -2,8 +2,12 @@
from os.path import join
from platform import system
from PyInstaller.utils.hooks import copy_metadata
import sysconfig
datas = [('src/interface/web', 'src/interface/web')]
datas = [
('src/interface/web', 'src/interface/web'),
(f'{sysconfig.get_paths()["purelib"]}/transformers', 'transformers')
]
datas += copy_metadata('tqdm')
datas += copy_metadata('regex')
datas += copy_metadata('requests')
@@ -12,10 +16,8 @@ datas += copy_metadata('filelock')
datas += copy_metadata('numpy')
datas += copy_metadata('tokenizers')
block_cipher = None
a = Analysis(
['src/main.py'],
pathex=[],
@@ -32,41 +34,78 @@ a = Analysis(
noarchive=False,
)
# Filter out unused, duplicate shared libs
extension = {'Windows': '.dll', 'Darwin': '.dylib', 'Linux': '.so'}[system()]
# Filter out unused and/or duplicate shared libs
torch_lib_paths = {
join('torch', 'lib', 'libtorch_cuda' + extension),
join('torch', 'lib', 'libtorch_cpu' + extension),
join('torch', 'lib', 'libtorch_python' + extension)
join('torch', 'lib', 'libtorch_cuda.so'),
join('torch', 'lib', 'libtorch_cpu.so'),
}
a.datas = [entry for entry in a.datas if not entry[0] in torch_lib_paths]
a.datas = [entry for entry in a.datas if not 'torch/_C.cp' in entry[0]]
a.datas = [entry for entry in a.datas if not 'torch/_dl.cp' in entry[0]]
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='Khoj',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch='x86_64',
codesign_identity=None,
entitlements_file=None,
icon='src/interface/web/assets/icons/favicon.icns',
)
if system() != 'Darwin':
# Add Splash screen to show on app launch
splash = Splash(
'src/interface/web/assets/icons/favicon-144x144.png',
binaries=a.binaries,
datas=a.datas,
text_pos=(10, 50),
text_size=12,
text_color='blue',
minify_script=True,
always_on_top=True
)
if system() == 'Darwin':
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
splash,
splash.binaries,
[],
name='Khoj',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch='x86_64',
codesign_identity=None,
entitlements_file=None,
icon='src/interface/web/assets/icons/favicon-144x144.ico',
)
else:
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='Khoj',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch='x86_64',
codesign_identity=None,
entitlements_file=None,
icon='src/interface/web/assets/icons/favicon.icns',
)
app = BUNDLE(
exe,
name='Khoj.app',

View File

@@ -22,7 +22,7 @@ content-type:
input-filter: # /path/to/images/*.jpg REQUIRED IF input-directories IS NOT SET
embeddings-file: "~/.khoj/content/image/image_embeddings.pt"
batch-size: 50
use-xmp-metadata: true
use-xmp-metadata: false
music:
input-files: # ["/path/to/music-file.org"] REQUIRED IF input-filter IS NOT SET OR

View File

@@ -29,7 +29,7 @@ class FileBrowser(QtWidgets.QWidget):
self.lineEdit.textChanged.connect(self.updateFieldHeight)
layout.addWidget(self.lineEdit)
self.button = QtWidgets.QPushButton('Select')
self.button = QtWidgets.QPushButton('Add')
self.button.clicked.connect(self.storeFilesSelectedInFileDialog)
layout.addWidget(self.button)
layout.addStretch()
@@ -47,7 +47,7 @@ class FileBrowser(QtWidgets.QWidget):
return 'Images (*.jp[e]g)'
def storeFilesSelectedInFileDialog(self):
filepaths = []
filepaths = self.getPaths()
if self.search_type == SearchType.Image:
filepaths.append(QtWidgets.QFileDialog.getExistingDirectory(self, caption='Choose Folder',
directory=self.dirpath))

View File

@@ -185,7 +185,10 @@ class MainWindow(QtWidgets.QMainWindow):
default_search_config = self.get_default_config(search_type = child.search_type)
self.new_config['content-type'][child.search_type.value] = merge_dicts(current_search_config, default_search_config)
elif isinstance(child, FileBrowser) and child.search_type in self.new_config['content-type']:
self.new_config['content-type'][child.search_type.value]['input-files'] = child.getPaths() if child.getPaths() != [] else None
if child.search_type.value == SearchType.Image:
self.new_config['content-type'][child.search_type.value]['input-directories'] = child.getPaths() if child.getPaths() != [] else None
else:
self.new_config['content-type'][child.search_type.value]['input-files'] = child.getPaths() if child.getPaths() != [] else None
def update_processor_settings(self):
"Update config with conversation settings from UI"

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

View File

@@ -1,7 +1,8 @@
# Standard Packages
from platform import system
import os
import signal
import sys
from platform import system
# External Packages
import uvicorn
@@ -26,6 +27,9 @@ app.include_router(router)
def run():
# Turn Tokenizers Parallelism Off. App does not support it.
os.environ["TOKENIZERS_PARALLELISM"] = 'false'
# Load config from CLI
state.cli_args = sys.argv[1:]
args = cli(state.cli_args)
@@ -67,6 +71,18 @@ def run():
# Start Application
server.start()
gui.aboutToQuit.connect(server.terminate)
# Close Splash Screen if still open
if system() != 'Darwin':
try:
import pyi_splash
# Update the text on the splash screen
pyi_splash.update_text("Khoj setup complete")
# Close Splash Screen
pyi_splash.close()
except:
pass
gui.exec()

View File

@@ -211,9 +211,9 @@ def collate_results(hits, image_names, output_directory, image_files_url, count=
# Add the image metadata to the results
results += [{
"entry": f'{image_files_url}/{target_image_name}',
"score": f"{hit['score']:.3f}",
"image_score": f"{hit['image_score']:.3f}",
"metadata_score": f"{hit['metadata_score']:.3f}",
"score": f"{hit['score']:.9f}",
"image_score": f"{hit['image_score']:.9f}",
"metadata_score": f"{hit['metadata_score']:.9f}",
}]
return results

View File

@@ -3,7 +3,7 @@ import argparse
import pathlib
# Internal Packages
from src.utils.helpers import get_absolute_path, resolve_absolute_path
from src.utils.helpers import resolve_absolute_path
from src.utils.yaml import parse_config_from_file

View File

@@ -30,7 +30,7 @@ default_config = {
'input-filter': None,
'embeddings-file': '~/.khoj/content/image/image_embeddings.pt',
'batch-size': 50,
'use-xmp-metadata': True
'use-xmp-metadata': False
},
'music': {
'input-files': None,

View File

@@ -1,5 +1,6 @@
# Standard Packages
import pathlib
import sys
from os.path import join
@@ -54,4 +55,9 @@ def load_model(model_name, model_dir, model_type):
if model_path is not None:
model.save(model_path)
return model
return model
def is_pyinstaller_app():
"Returns true if the app is running from Native GUI created by PyInstaller"
return getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS')