Spaces:
Sleeping
Sleeping
github-actions commited on
Commit ·
9826f0b
0
Parent(s):
Deploy to Spaces with Xet
Browse files- .gitattributes +2 -0
- .github/workflows/hf_sync.yml +59 -0
- .github/workflows/run_model_tests.yml +44 -0
- .github/workflows/run_tests.yml +39 -0
- .gitignore +211 -0
- .python-version +1 -0
- LICENSE +21 -0
- README.md +99 -0
- anime.db +3 -0
- app.py +51 -0
- backend.py +100 -0
- constants.py +15 -0
- create_db.py +94 -0
- data/.gitkeep +0 -0
- genrelist.txt +16 -0
- main.py +6 -0
- pyproject.toml +14 -0
- requirements.txt +85 -0
- retrieval_utils.py +78 -0
- static/css/gradiomain.css +240 -0
- static/css/theme.css +35 -0
- static/images/background.png +3 -0
- static/images/sidebar.jpg +3 -0
- test_detect_genre.py +11 -0
- tests/test_chat_models.py +48 -0
- tests/test_retrieval_utils.py +163 -0
- uv.lock +0 -0
.gitattributes
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
static/images/* filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
anime.db filter=lfs diff=lfs merge=lfs -text
|
.github/workflows/hf_sync.yml
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Sync to HuggingFace Spaces
|
| 2 |
+
on:
|
| 3 |
+
workflow_run:
|
| 4 |
+
workflows: ["Run All Unit Tests", "Run Model Tests on Production Branch"]
|
| 5 |
+
types:
|
| 6 |
+
- completed
|
| 7 |
+
branches: [ "production" ]
|
| 8 |
+
workflow_dispatch:
|
| 9 |
+
|
| 10 |
+
jobs:
|
| 11 |
+
sync-to-hub:
|
| 12 |
+
runs-on: ubuntu-latest
|
| 13 |
+
|
| 14 |
+
# Only run if the triggering workflows succeeded or when we run it manually from GH Actions UI
|
| 15 |
+
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
|
| 16 |
+
|
| 17 |
+
steps:
|
| 18 |
+
- uses: actions/checkout@v3
|
| 19 |
+
with:
|
| 20 |
+
ref: production
|
| 21 |
+
fetch-depth: 0
|
| 22 |
+
lfs: true
|
| 23 |
+
- name: Install xet
|
| 24 |
+
run: |
|
| 25 |
+
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/huggingface/xet-core/refs/heads/main/git_xet/install.sh | sh
|
| 26 |
+
echo "$HOME/.xet/bin" >> $GITHUB_PATH
|
| 27 |
+
- name: Set git user for xet security
|
| 28 |
+
run: |
|
| 29 |
+
git config user.name "github-actions"
|
| 30 |
+
git config user.email "actions@github.com"
|
| 31 |
+
- name: Convert binaries to xet and Push to HuggingFace
|
| 32 |
+
env:
|
| 33 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 34 |
+
run: |
|
| 35 |
+
# 1. Install Xet
|
| 36 |
+
git xet install
|
| 37 |
+
|
| 38 |
+
# 2. Create a fresh, empty branch for deployment
|
| 39 |
+
# This disconnects from previous history, removing the "binary in history" error
|
| 40 |
+
git checkout --orphan deployment-branch
|
| 41 |
+
|
| 42 |
+
# 3. Unstage everything so we can re-add them properly with Xet
|
| 43 |
+
git reset
|
| 44 |
+
|
| 45 |
+
# 4. Configure Xet tracking
|
| 46 |
+
# We do this *before* adding files so Xet handles them correctly
|
| 47 |
+
git xet track "anime.db" "static/images/*"
|
| 48 |
+
|
| 49 |
+
# 5. Add all files
|
| 50 |
+
# Since we are in a fresh branch, this adds everything currently on disk
|
| 51 |
+
git add .
|
| 52 |
+
|
| 53 |
+
# 6. Commit and Force Push
|
| 54 |
+
git config user.name "github-actions"
|
| 55 |
+
git config user.email "actions@github.com"
|
| 56 |
+
git commit -m "Deploy to Spaces with Xet"
|
| 57 |
+
|
| 58 |
+
# Force push to 'main' on Hugging Face
|
| 59 |
+
git push --force https://MLDevOps:$HF_TOKEN@huggingface.co/spaces/MLDevOps/CS553_CaseStudy1 HEAD:main
|
.github/workflows/run_model_tests.yml
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Run Model Tests on Production Branch
|
| 2 |
+
|
| 3 |
+
# Run on every push and pull request
|
| 4 |
+
on:
|
| 5 |
+
push:
|
| 6 |
+
branches: [ "production" ]
|
| 7 |
+
workflow_dispatch:
|
| 8 |
+
|
| 9 |
+
# Run the following Jobs (just 1)
|
| 10 |
+
jobs:
|
| 11 |
+
|
| 12 |
+
# Run the "run_test" job
|
| 13 |
+
run_test:
|
| 14 |
+
|
| 15 |
+
# It should run on latest ubuntu OS
|
| 16 |
+
runs-on: ubuntu-latest
|
| 17 |
+
|
| 18 |
+
# It should use the following steps
|
| 19 |
+
steps:
|
| 20 |
+
|
| 21 |
+
# Checkout the current repository
|
| 22 |
+
- uses: actions/checkout@v3
|
| 23 |
+
with:
|
| 24 |
+
ref: production
|
| 25 |
+
fetch-depth: 0
|
| 26 |
+
lfs: true
|
| 27 |
+
|
| 28 |
+
# Install Python with 3.12 version
|
| 29 |
+
- uses: actions/setup-python@v5
|
| 30 |
+
with:
|
| 31 |
+
python-version: "3.12"
|
| 32 |
+
|
| 33 |
+
# Install all required python libraries
|
| 34 |
+
- name: Install Required Libraries
|
| 35 |
+
run: |
|
| 36 |
+
pip install -r requirements.txt
|
| 37 |
+
|
| 38 |
+
# Run all the unit tests quietly
|
| 39 |
+
- name: Run Unit Test for Chat Models (Quietly)
|
| 40 |
+
env:
|
| 41 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 42 |
+
PYTHONPATH: ${{ github.workspace }}
|
| 43 |
+
run: |
|
| 44 |
+
pytest -q tests/test_chat_models.py
|
.github/workflows/run_tests.yml
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Run All Unit Tests
|
| 2 |
+
|
| 3 |
+
# Run on every push and pull request
|
| 4 |
+
on:
|
| 5 |
+
push:
|
| 6 |
+
workflow_dispatch:
|
| 7 |
+
|
| 8 |
+
# Run the following Jobs (just 1)
|
| 9 |
+
jobs:
|
| 10 |
+
|
| 11 |
+
# Run the "run_test" job
|
| 12 |
+
run_test:
|
| 13 |
+
|
| 14 |
+
# It should run on latest ubuntu OS
|
| 15 |
+
runs-on: ubuntu-latest
|
| 16 |
+
|
| 17 |
+
# It should use the following steps
|
| 18 |
+
steps:
|
| 19 |
+
|
| 20 |
+
# Checkout the current repository
|
| 21 |
+
- uses: actions/checkout@v3
|
| 22 |
+
|
| 23 |
+
# Install Python with 3.12 version
|
| 24 |
+
- uses: actions/setup-python@v5
|
| 25 |
+
with:
|
| 26 |
+
python-version: "3.12"
|
| 27 |
+
|
| 28 |
+
# Install all required python libraries
|
| 29 |
+
- name: Install Required Libraries
|
| 30 |
+
run: |
|
| 31 |
+
pip install -r requirements.txt
|
| 32 |
+
|
| 33 |
+
# Run all the unit tests quietly
|
| 34 |
+
- name: Run all Unit Tests (Quietly)
|
| 35 |
+
env:
|
| 36 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 37 |
+
PYTHONPATH: ${{ github.workspace }}
|
| 38 |
+
run: |
|
| 39 |
+
pytest -q --ignore=tests/test_chat_models.py
|
.gitignore
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Ignore CSV files
|
| 2 |
+
*.csv
|
| 3 |
+
|
| 4 |
+
# Byte-compiled / optimized / DLL files
|
| 5 |
+
__pycache__/
|
| 6 |
+
*.py[codz]
|
| 7 |
+
*$py.class
|
| 8 |
+
|
| 9 |
+
# C extensions
|
| 10 |
+
*.so
|
| 11 |
+
|
| 12 |
+
# Distribution / packaging
|
| 13 |
+
.Python
|
| 14 |
+
build/
|
| 15 |
+
develop-eggs/
|
| 16 |
+
dist/
|
| 17 |
+
downloads/
|
| 18 |
+
eggs/
|
| 19 |
+
.eggs/
|
| 20 |
+
lib/
|
| 21 |
+
lib64/
|
| 22 |
+
parts/
|
| 23 |
+
sdist/
|
| 24 |
+
var/
|
| 25 |
+
wheels/
|
| 26 |
+
share/python-wheels/
|
| 27 |
+
*.egg-info/
|
| 28 |
+
.installed.cfg
|
| 29 |
+
*.egg
|
| 30 |
+
MANIFEST
|
| 31 |
+
|
| 32 |
+
# PyInstaller
|
| 33 |
+
# Usually these files are written by a python script from a template
|
| 34 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 35 |
+
*.manifest
|
| 36 |
+
*.spec
|
| 37 |
+
|
| 38 |
+
# Installer logs
|
| 39 |
+
pip-log.txt
|
| 40 |
+
pip-delete-this-directory.txt
|
| 41 |
+
|
| 42 |
+
# Unit test / coverage reports
|
| 43 |
+
htmlcov/
|
| 44 |
+
.tox/
|
| 45 |
+
.nox/
|
| 46 |
+
.coverage
|
| 47 |
+
.coverage.*
|
| 48 |
+
.cache
|
| 49 |
+
nosetests.xml
|
| 50 |
+
coverage.xml
|
| 51 |
+
*.cover
|
| 52 |
+
*.py.cover
|
| 53 |
+
.hypothesis/
|
| 54 |
+
.pytest_cache/
|
| 55 |
+
cover/
|
| 56 |
+
|
| 57 |
+
# Translations
|
| 58 |
+
*.mo
|
| 59 |
+
*.pot
|
| 60 |
+
|
| 61 |
+
# Django stuff:
|
| 62 |
+
*.log
|
| 63 |
+
local_settings.py
|
| 64 |
+
db.sqlite3
|
| 65 |
+
db.sqlite3-journal
|
| 66 |
+
|
| 67 |
+
# Flask stuff:
|
| 68 |
+
instance/
|
| 69 |
+
.webassets-cache
|
| 70 |
+
|
| 71 |
+
# Scrapy stuff:
|
| 72 |
+
.scrapy
|
| 73 |
+
|
| 74 |
+
# Sphinx documentation
|
| 75 |
+
docs/_build/
|
| 76 |
+
|
| 77 |
+
# PyBuilder
|
| 78 |
+
.pybuilder/
|
| 79 |
+
target/
|
| 80 |
+
|
| 81 |
+
# Jupyter Notebook
|
| 82 |
+
.ipynb_checkpoints
|
| 83 |
+
|
| 84 |
+
# IPython
|
| 85 |
+
profile_default/
|
| 86 |
+
ipython_config.py
|
| 87 |
+
|
| 88 |
+
# pyenv
|
| 89 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 90 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 91 |
+
# .python-version
|
| 92 |
+
|
| 93 |
+
# pipenv
|
| 94 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 95 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 96 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 97 |
+
# install all needed dependencies.
|
| 98 |
+
#Pipfile.lock
|
| 99 |
+
|
| 100 |
+
# UV
|
| 101 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
| 102 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 103 |
+
# commonly ignored for libraries.
|
| 104 |
+
#uv.lock
|
| 105 |
+
|
| 106 |
+
# poetry
|
| 107 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 108 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 109 |
+
# commonly ignored for libraries.
|
| 110 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 111 |
+
#poetry.lock
|
| 112 |
+
#poetry.toml
|
| 113 |
+
|
| 114 |
+
# pdm
|
| 115 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 116 |
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
| 117 |
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
| 118 |
+
#pdm.lock
|
| 119 |
+
#pdm.toml
|
| 120 |
+
.pdm-python
|
| 121 |
+
.pdm-build/
|
| 122 |
+
|
| 123 |
+
# pixi
|
| 124 |
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
| 125 |
+
#pixi.lock
|
| 126 |
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
| 127 |
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
| 128 |
+
.pixi
|
| 129 |
+
|
| 130 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 131 |
+
__pypackages__/
|
| 132 |
+
|
| 133 |
+
# Celery stuff
|
| 134 |
+
celerybeat-schedule
|
| 135 |
+
celerybeat.pid
|
| 136 |
+
|
| 137 |
+
# SageMath parsed files
|
| 138 |
+
*.sage.py
|
| 139 |
+
|
| 140 |
+
# Environments
|
| 141 |
+
.env
|
| 142 |
+
.envrc
|
| 143 |
+
.venv
|
| 144 |
+
env/
|
| 145 |
+
venv/
|
| 146 |
+
ENV/
|
| 147 |
+
env.bak/
|
| 148 |
+
venv.bak/
|
| 149 |
+
|
| 150 |
+
# Spyder project settings
|
| 151 |
+
.spyderproject
|
| 152 |
+
.spyproject
|
| 153 |
+
|
| 154 |
+
# Rope project settings
|
| 155 |
+
.ropeproject
|
| 156 |
+
|
| 157 |
+
# mkdocs documentation
|
| 158 |
+
/site
|
| 159 |
+
|
| 160 |
+
# mypy
|
| 161 |
+
.mypy_cache/
|
| 162 |
+
.dmypy.json
|
| 163 |
+
dmypy.json
|
| 164 |
+
|
| 165 |
+
# Pyre type checker
|
| 166 |
+
.pyre/
|
| 167 |
+
|
| 168 |
+
# pytype static type analyzer
|
| 169 |
+
.pytype/
|
| 170 |
+
|
| 171 |
+
# Cython debug symbols
|
| 172 |
+
cython_debug/
|
| 173 |
+
|
| 174 |
+
# PyCharm
|
| 175 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 176 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 177 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 178 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 179 |
+
.idea/
|
| 180 |
+
.venv/
|
| 181 |
+
|
| 182 |
+
# Abstra
|
| 183 |
+
# Abstra is an AI-powered process automation framework.
|
| 184 |
+
# Ignore directories containing user credentials, local state, and settings.
|
| 185 |
+
# Learn more at https://abstra.io/docs
|
| 186 |
+
.abstra/
|
| 187 |
+
|
| 188 |
+
# Visual Studio Code
|
| 189 |
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
| 190 |
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
| 191 |
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
| 192 |
+
# you could uncomment the following to ignore the entire vscode folder
|
| 193 |
+
# .vscode/
|
| 194 |
+
|
| 195 |
+
# Ruff stuff:
|
| 196 |
+
.ruff_cache/
|
| 197 |
+
|
| 198 |
+
# PyPI configuration file
|
| 199 |
+
.pypirc
|
| 200 |
+
|
| 201 |
+
# Cursor
|
| 202 |
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
| 203 |
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
| 204 |
+
# refer to https://docs.cursor.com/context/ignore-files
|
| 205 |
+
.cursorignore
|
| 206 |
+
.cursorindexingignore
|
| 207 |
+
|
| 208 |
+
# Marimo
|
| 209 |
+
marimo/_static/
|
| 210 |
+
marimo/_lsp/
|
| 211 |
+
__marimo__/
|
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.12
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 ShafathZ
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: CS553 CaseStudy1
|
| 3 |
+
emoji: 💬
|
| 4 |
+
colorFrom: yellow
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 6.5.1
|
| 8 |
+
python_version: 3.12.3
|
| 9 |
+
app_file: app.py
|
| 10 |
+
pinned: false
|
| 11 |
+
hf_oauth: true
|
| 12 |
+
hf_oauth_scopes:
|
| 13 |
+
- inference-api
|
| 14 |
+
license: mit
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
An Anime Recommendation chatbot using [Gradio](https://gradio.app), [`huggingface_hub`](https://huggingface.co/docs/huggingface_hub/v0.22.2/en/index), and the [Hugging Face Inference API](https://huggingface.co/docs/api-inference/index).
|
| 18 |
+
|
| 19 |
+
## Models Used by our Chatbot
|
| 20 |
+
| Type of Model | Model Name (Hugging Face Path) |
|
| 21 |
+
|---------------|--------------------------------|
|
| 22 |
+
| Local Model | `Qwen/Qwen3-0.6B` |
|
| 23 |
+
| Inference Client Model| `openai/gpt-oss-20b` |
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
## Working with UV (Ultra-Violet)
|
| 27 |
+
### Install UV
|
| 28 |
+
Please download `uv` (Ultra-Violet) for Python Project Dependency Management: https://docs.astral.sh/uv/getting-started/installation/#installation-methods
|
| 29 |
+
|
| 30 |
+
### Initializing a uv virtual env
|
| 31 |
+
Run following commands by navigating to the project directory:
|
| 32 |
+
```bash
|
| 33 |
+
cd /path/to/your/project
|
| 34 |
+
uv sync
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
### Activating the virtual env
|
| 38 |
+
In the same project directory, execute the following (if virtual env is not already active):
|
| 39 |
+
```bash
|
| 40 |
+
source .venv/bin/activate
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
### Adding any Libraries / Dependencies
|
| 44 |
+
To add any new dependencies (libraries):
|
| 45 |
+
```bash
|
| 46 |
+
uv add <library_name>
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## Working with HuggingFace Spaces Locally
|
| 50 |
+
### Install Gradio with oAuth
|
| 51 |
+
Run the following command in your Python environment:
|
| 52 |
+
```bash
|
| 53 |
+
uv add "gradio[oauth]"
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
### Set up HuggingFace Token
|
| 57 |
+
1. Go to your HuggingFace profile at: https://huggingface.co/settings/tokens
|
| 58 |
+
2. Generate a new token for your HuggingFace Space at `Create New Token` -> `Fine-grained`.
|
| 59 |
+
3. Under `Repository permissions` section, search for the repo: "spaces/MLDevOps/CS553_CaseStudy1" and select it
|
| 60 |
+
4. Check the box for "Write access to contents/settings of selected repos" and click "Create Token" at the bottom.
|
| 61 |
+
5. Copy and Paste the generated token into a `.env` file in the root directory of your local copy of CS553_CaseStudy1 repo:
|
| 62 |
+
```
|
| 63 |
+
HF_TOKEN=XXXXXXXXX
|
| 64 |
+
```
|
| 65 |
+
6. Login into HF:
|
| 66 |
+
```bash
|
| 67 |
+
hf auth login
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
### Running Gradio App on HuggingFace Spaces Locally
|
| 71 |
+
Run the following command:
|
| 72 |
+
```bash
|
| 73 |
+
python app.py
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
It will spit out logs indicating the url to open in browser:
|
| 77 |
+
```
|
| 78 |
+
...
|
| 79 |
+
* Running on local URL: http://127.0.0.1:7860
|
| 80 |
+
...
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### Debugging Gradio Issue
|
| 84 |
+
In app.py, the line:
|
| 85 |
+
```python
|
| 86 |
+
chatbot = gr.ChatInterface(
|
| 87 |
+
respond,
|
| 88 |
+
type="messages",
|
| 89 |
+
...
|
| 90 |
+
)
|
| 91 |
+
```
|
| 92 |
+
might need to be changed to remove the type line as follows due to a deprecation issue on HuggingFace Spaces:
|
| 93 |
+
```python
|
| 94 |
+
chatbot = gr.ChatInterface(
|
| 95 |
+
respond,
|
| 96 |
+
...
|
| 97 |
+
)
|
| 98 |
+
```
|
| 99 |
+
With this, run the program and it should work locally on localhost server!
|
anime.db
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f9f2c79bc2be7d84d3089640ea010361c4aea5eb4e580e17148f60e68337ced4
|
| 3 |
+
size 1294336
|
app.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import backend
|
| 4 |
+
from constants import *
|
| 5 |
+
|
| 6 |
+
theme_css = Path("static/css/theme.css").read_text() if Path("static/css/theme.css").exists() else ""
|
| 7 |
+
main_css = Path("static/css/gradiomain.css").read_text()
|
| 8 |
+
CSS = theme_css + "\n\n" + main_css
|
| 9 |
+
|
| 10 |
+
# Load static directory
|
| 11 |
+
gr.set_static_paths(paths=[Path.cwd().absolute()/"static"])
|
| 12 |
+
|
| 13 |
+
# Adapter function between frontend and backend. Returns a generator yielding backend results.
|
| 14 |
+
def respond(
|
| 15 |
+
message,
|
| 16 |
+
history: list[dict[str, str]],
|
| 17 |
+
use_local_model,
|
| 18 |
+
hf_token: gr.OAuthToken,
|
| 19 |
+
):
|
| 20 |
+
for r in backend.process_user_query(SYSTEM_PROMPT, history, message, use_local_model, MAX_TOKENS, TEMPERATURE, TOP_P, hf_token.token):
|
| 21 |
+
yield r
|
| 22 |
+
|
| 23 |
+
with gr.Blocks() as homepage:
|
| 24 |
+
gr.Markdown(
|
| 25 |
+
"""
|
| 26 |
+
# Ani<span style="font-size: 2rem;">ℤ</span>enith
|
| 27 |
+
An AI designed to give recommendations of the best anime options based on your preferences! Has knowledge of a full database of anime!
|
| 28 |
+
""",
|
| 29 |
+
elem_classes=["page-header"]
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
with gr.Sidebar():
|
| 33 |
+
gr.LoginButton()
|
| 34 |
+
|
| 35 |
+
local_model = gr.Checkbox(
|
| 36 |
+
label="Use Local Model?",
|
| 37 |
+
value=False,
|
| 38 |
+
elem_classes=["toggle-button"]
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# Main chatbot interface
|
| 42 |
+
chatbot = gr.ChatInterface(
|
| 43 |
+
respond,
|
| 44 |
+
additional_inputs=[
|
| 45 |
+
local_model,
|
| 46 |
+
],
|
| 47 |
+
)
|
| 48 |
+
chatbot.chatbot.elem_classes = ["custom-chatbot"]
|
| 49 |
+
|
| 50 |
+
if __name__ == "__main__":
|
| 51 |
+
homepage.launch(css=CSS)
|
backend.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
from huggingface_hub import InferenceClient
|
| 3 |
+
from transformers import pipeline
|
| 4 |
+
from retrieval_utils import get_recommendations
|
| 5 |
+
|
| 6 |
+
genre_list = open("genrelist.txt", "r").read().splitlines()
|
| 7 |
+
|
| 8 |
+
def process_user_query(system_message: str, history: List[dict], user_message: str, use_local_model: bool, max_tokens: int, temperature: float, top_p: float, hf_token):
|
| 9 |
+
# 1. Retrieve genres from the user message using naive approach
|
| 10 |
+
genre_list = detect_genres(user_message)
|
| 11 |
+
|
| 12 |
+
# 2. Retrieve relevant results from DB if the genre_list is not empty
|
| 13 |
+
recommendations_string = ""
|
| 14 |
+
if len(genre_list) > 0:
|
| 15 |
+
recommendations_string = get_recommendations(genre_list)
|
| 16 |
+
|
| 17 |
+
# 3. Query the model
|
| 18 |
+
for result in query_model(system_message,
|
| 19 |
+
history,
|
| 20 |
+
user_message,
|
| 21 |
+
recommendations_string,
|
| 22 |
+
use_local_model,
|
| 23 |
+
max_tokens,
|
| 24 |
+
temperature,
|
| 25 |
+
top_p,
|
| 26 |
+
hf_token):
|
| 27 |
+
yield result
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def detect_genres(message: str) -> List[str]:
|
| 31 |
+
requested_genres = []
|
| 32 |
+
# Simple naive genre check by detecting if any of our system stored genres are within the user query
|
| 33 |
+
# TODO: Improve genre detection instead to use Retriever and RAG framework in the future
|
| 34 |
+
for genre in genre_list:
|
| 35 |
+
if message.__contains__(genre):
|
| 36 |
+
requested_genres.append(genre)
|
| 37 |
+
return requested_genres
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def query_model(
|
| 41 |
+
system_message: str,
|
| 42 |
+
history: List[dict],
|
| 43 |
+
user_message: str,
|
| 44 |
+
recommendations_string: str,
|
| 45 |
+
use_local_model: bool,
|
| 46 |
+
max_tokens: int, # TODO: Remove this and hardcode a value in constants.py
|
| 47 |
+
temperature: float, # TODO: Remove this and hardcode a value in constants.py
|
| 48 |
+
top_p: float, # TODO: Remove this and hardcode a value in constants.py
|
| 49 |
+
hf_token):
|
| 50 |
+
|
| 51 |
+
# Construct messages for the language model
|
| 52 |
+
# Start by adding system prompt
|
| 53 |
+
system_prompt = system_message
|
| 54 |
+
if recommendations_string:
|
| 55 |
+
system_prompt += "\nRECOMMENDATION JSON:" + f"\n{recommendations_string}"
|
| 56 |
+
messages = [{"role": "system", "content": system_prompt}]
|
| 57 |
+
|
| 58 |
+
# Add the rest of the history
|
| 59 |
+
messages.extend(history)
|
| 60 |
+
|
| 61 |
+
# Add the current user prompt
|
| 62 |
+
messages.append({"role": "user", "content": user_message})
|
| 63 |
+
|
| 64 |
+
# Determine which model to use (local or external)
|
| 65 |
+
if use_local_model:
|
| 66 |
+
# Local Model -- Uses pipeline from transformers library
|
| 67 |
+
pipeline_local_model = pipeline(task='text-generation',
|
| 68 |
+
model='Qwen/Qwen3-0.6B',
|
| 69 |
+
max_new_tokens=max_tokens,
|
| 70 |
+
temperature=temperature,
|
| 71 |
+
do_sample=False,
|
| 72 |
+
top_p=top_p
|
| 73 |
+
)
|
| 74 |
+
# Get the response from the local model
|
| 75 |
+
response = pipeline_local_model(messages)
|
| 76 |
+
|
| 77 |
+
# Parse the output and yield it
|
| 78 |
+
yield response[0]['generated_text'][-1]['content'].split('</think>')[-1].strip()
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
elif not use_local_model:
|
| 82 |
+
# Non-local Model -- Use InferenceClient
|
| 83 |
+
client = InferenceClient(
|
| 84 |
+
token=hf_token,
|
| 85 |
+
model="openai/gpt-oss-20b",
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
response = ""
|
| 89 |
+
for chunk in client.chat_completion(
|
| 90 |
+
messages=messages,
|
| 91 |
+
max_tokens=max_tokens,
|
| 92 |
+
stream=True,
|
| 93 |
+
temperature=temperature,
|
| 94 |
+
top_p=top_p,
|
| 95 |
+
):
|
| 96 |
+
if chunk.choices and chunk.choices[0].delta.content:
|
| 97 |
+
token = chunk.choices[0].delta.content
|
| 98 |
+
response += token
|
| 99 |
+
yield response
|
| 100 |
+
|
constants.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
SYSTEM_PROMPT = f"""
|
| 2 |
+
You are an expert on recommending Anime shows. Please use the RECOMMENDATIONS to answer the user's question.
|
| 3 |
+
The RECOMMENDATIONS is a JSON String that contains information of top Anime sorted in descending order by:
|
| 4 |
+
1. Number of Requested Genre Matches from the User
|
| 5 |
+
2. The Score of the Anime
|
| 6 |
+
|
| 7 |
+
If the RECOMMENDATIONS JSON String is not given:
|
| 8 |
+
1. Then answer the question like a Friendly Chatbot!
|
| 9 |
+
2. Do not reference anything about a RECOMMENDATION JSON
|
| 10 |
+
3. Ask the user to provide their favorite genre(s) for Anime Recommendations
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
MAX_TOKENS = 2048
|
| 14 |
+
TEMPERATURE = 0.7
|
| 15 |
+
TOP_P = 0.7
|
create_db.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import sqlite3
|
| 3 |
+
|
| 4 |
+
# Load the Anime Dataset as a Pandas DataFrame
|
| 5 |
+
df = pd.read_csv('data/anime-dataset-2023.csv')
|
| 6 |
+
|
| 7 |
+
# Separate the DataFrame into separate tables
|
| 8 |
+
# Genre Table
|
| 9 |
+
# Create a separate DataFrame using Genres column
|
| 10 |
+
genres_df = df['Genres'].str.split(', ').explode().str.strip().drop_duplicates().reset_index(drop=True)
|
| 11 |
+
genres_df = pd.DataFrame({'genre_id': range(1, len(genres_df) + 1), 'genre_name': genres_df})
|
| 12 |
+
|
| 13 |
+
# Exclude some Genres
|
| 14 |
+
excluded_genres = ['Hentai', 'UNKNOWN', 'Erotica', 'Ecchi']
|
| 15 |
+
genres_df = genres_df[~genres_df['genre_name'].isin(excluded_genres)]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# Anime Table
|
| 19 |
+
# Filter out animes that contain excluded genres
|
| 20 |
+
pattern = '|'.join(excluded_genres)
|
| 21 |
+
anime_df = df[~df['Genres'].str.contains(pattern, na=True)]
|
| 22 |
+
|
| 23 |
+
# Filter out animes without English Names
|
| 24 |
+
anime_df = anime_df[anime_df['English name'] != 'UNKNOWN']
|
| 25 |
+
|
| 26 |
+
# Filter out animes without Scores
|
| 27 |
+
anime_df = anime_df[anime_df['Score'] != 'UNKNOWN']
|
| 28 |
+
|
| 29 |
+
# Filter out animes without Synopsis
|
| 30 |
+
anime_df = anime_df[anime_df['Synopsis'] != 'No description available for this anime.']
|
| 31 |
+
|
| 32 |
+
# Sort by Score and Keep only Top 1000 Animes
|
| 33 |
+
anime_df = anime_df.sort_values(by='Score', ascending=False)
|
| 34 |
+
anime_df = anime_df.head(1000)
|
| 35 |
+
|
| 36 |
+
# Rename Columns
|
| 37 |
+
anime_df = anime_df.rename(columns={'English name': 'name', 'Score': 'score', 'Synopsis': 'synopsis'})
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# AnimeGenre Table
|
| 43 |
+
# Create a mapping of genre_name to genre_id
|
| 44 |
+
genre_mapping = genres_df.set_index('genre_name')['genre_id'].to_dict()
|
| 45 |
+
|
| 46 |
+
# Explode genres for filtered animes and map to genre_ids
|
| 47 |
+
anime_genre_df = anime_df[['anime_id', 'Genres']].copy()
|
| 48 |
+
anime_genre_df = anime_genre_df.assign(genre_name=anime_genre_df['Genres'].str.split(', ')).explode('genre_name')
|
| 49 |
+
anime_genre_df['genre_name'] = anime_genre_df['genre_name'].str.strip()
|
| 50 |
+
anime_genre_df['genre_id'] = anime_genre_df['genre_name'].map(genre_mapping)
|
| 51 |
+
anime_genre_df = anime_genre_df[['anime_id', 'genre_id']].dropna()
|
| 52 |
+
anime_genre_df['genre_id'] = anime_genre_df['genre_id'].astype(int)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# Final Clean Up
|
| 57 |
+
# Keep only anime_id, Name, Score and Synopsis Columns
|
| 58 |
+
anime_df = anime_df[['anime_id', 'name', 'score', 'synopsis']]
|
| 59 |
+
anime_df = anime_df.rename(columns={'anime_id': 'id'})
|
| 60 |
+
genres_df = genres_df.rename(columns={'genre_id': 'id'})
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
SCHEMA_SQL = '''
|
| 64 |
+
PRAGMA foreign_keys = ON;
|
| 65 |
+
|
| 66 |
+
CREATE TABLE IF NOT EXISTS Anime (
|
| 67 |
+
id INTEGER PRIMARY KEY,
|
| 68 |
+
name VARCHAR(50),
|
| 69 |
+
score FLOAT,
|
| 70 |
+
synopsis TEXT
|
| 71 |
+
);
|
| 72 |
+
|
| 73 |
+
CREATE TABLE IF NOT EXISTS Genre (
|
| 74 |
+
id INTEGER PRIMARY KEY,
|
| 75 |
+
genre_name VARCHAR(20)
|
| 76 |
+
);
|
| 77 |
+
|
| 78 |
+
CREATE TABLE IF NOT EXISTS AnimeGenre (
|
| 79 |
+
anime_id INTEGER NOT NULL,
|
| 80 |
+
genre_id INTEGER NOT NULL,
|
| 81 |
+
PRIMARY KEY (anime_id, genre_id),
|
| 82 |
+
FOREIGN KEY (anime_id) REFERENCES Anime(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
| 83 |
+
FOREIGN KEY (genre_id) REFERENCES Genre(id) ON DELETE CASCADE ON UPDATE CASCADE
|
| 84 |
+
);
|
| 85 |
+
|
| 86 |
+
CREATE INDEX IF NOT EXISTS idx_anime_id ON AnimeGenre(anime_id);
|
| 87 |
+
CREATE INDEX IF NOT EXISTS idx_genre_id ON AnimeGenre(genre_id);
|
| 88 |
+
'''
|
| 89 |
+
|
| 90 |
+
with sqlite3.connect('anime.db') as conn:
|
| 91 |
+
conn.executescript(SCHEMA_SQL)
|
| 92 |
+
anime_df.to_sql('Anime', conn, if_exists='delete_rows', index=False, method='multi')
|
| 93 |
+
genres_df.to_sql('Genre', conn, if_exists='delete_rows', index=False, method='multi')
|
| 94 |
+
anime_genre_df.to_sql('AnimeGenre', conn, if_exists='delete_rows', index=False, method='multi')
|
data/.gitkeep
ADDED
|
File without changes
|
genrelist.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Action
|
| 2 |
+
Award Winning
|
| 3 |
+
Sci-Fi
|
| 4 |
+
Adventure
|
| 5 |
+
Drama
|
| 6 |
+
Mystery
|
| 7 |
+
Supernatural
|
| 8 |
+
Fantasy
|
| 9 |
+
Sports
|
| 10 |
+
Comedy
|
| 11 |
+
Romance
|
| 12 |
+
Slice of Life
|
| 13 |
+
Suspense
|
| 14 |
+
Gourmet
|
| 15 |
+
Avant Garde
|
| 16 |
+
Horror
|
main.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def main():
|
| 2 |
+
print("Hello from cs553-casestudy1!")
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
main()
|
pyproject.toml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "cs553-casestudy1"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Add your description here"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.12"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"gradio[oauth]>=6.5.1",
|
| 9 |
+
"huggingface-hub>=1.3.5",
|
| 10 |
+
"pytest>=9.0.2",
|
| 11 |
+
"itsdangerous>=2.2.0",
|
| 12 |
+
"torch>=2.10.0",
|
| 13 |
+
"transformers>=5.0.0",
|
| 14 |
+
]
|
requirements.txt
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
aiofiles==24.1.0
|
| 2 |
+
annotated-doc==0.0.4
|
| 3 |
+
annotated-types==0.7.0
|
| 4 |
+
anyio==4.12.1
|
| 5 |
+
authlib==1.6.6
|
| 6 |
+
brotli==1.2.0
|
| 7 |
+
certifi==2026.1.4
|
| 8 |
+
cffi==2.0.0
|
| 9 |
+
click==8.3.1
|
| 10 |
+
cryptography==46.0.4
|
| 11 |
+
cuda-bindings==12.9.4
|
| 12 |
+
cuda-pathfinder==1.3.3
|
| 13 |
+
fastapi==0.128.0
|
| 14 |
+
ffmpy==1.0.0
|
| 15 |
+
filelock==3.20.3
|
| 16 |
+
fsspec==2026.1.0
|
| 17 |
+
gradio==6.5.1
|
| 18 |
+
gradio-client==2.0.3
|
| 19 |
+
groovy==0.1.2
|
| 20 |
+
h11==0.16.0
|
| 21 |
+
hf-xet==1.2.0
|
| 22 |
+
httpcore==1.0.9
|
| 23 |
+
httpx==0.28.1
|
| 24 |
+
huggingface-hub==1.3.7
|
| 25 |
+
idna==3.11
|
| 26 |
+
iniconfig==2.3.0
|
| 27 |
+
itsdangerous==2.2.0
|
| 28 |
+
jinja2==3.1.6
|
| 29 |
+
markdown-it-py==4.0.0
|
| 30 |
+
markupsafe==3.0.3
|
| 31 |
+
mdurl==0.1.2
|
| 32 |
+
mpmath==1.3.0
|
| 33 |
+
networkx==3.6.1
|
| 34 |
+
numpy==2.4.2
|
| 35 |
+
nvidia-cublas-cu12==12.8.4.1
|
| 36 |
+
nvidia-cuda-cupti-cu12==12.8.90
|
| 37 |
+
nvidia-cuda-nvrtc-cu12==12.8.93
|
| 38 |
+
nvidia-cuda-runtime-cu12==12.8.90
|
| 39 |
+
nvidia-cudnn-cu12==9.10.2.21
|
| 40 |
+
nvidia-cufft-cu12==11.3.3.83
|
| 41 |
+
nvidia-cufile-cu12==1.13.1.3
|
| 42 |
+
nvidia-curand-cu12==10.3.9.90
|
| 43 |
+
nvidia-cusolver-cu12==11.7.3.90
|
| 44 |
+
nvidia-cusparse-cu12==12.5.8.93
|
| 45 |
+
nvidia-cusparselt-cu12==0.7.1
|
| 46 |
+
nvidia-nccl-cu12==2.27.5
|
| 47 |
+
nvidia-nvjitlink-cu12==12.8.93
|
| 48 |
+
nvidia-nvshmem-cu12==3.4.5
|
| 49 |
+
nvidia-nvtx-cu12==12.8.90
|
| 50 |
+
orjson==3.11.7
|
| 51 |
+
packaging==26.0
|
| 52 |
+
pandas==3.0.0
|
| 53 |
+
pillow==12.1.0
|
| 54 |
+
pluggy==1.6.0
|
| 55 |
+
pycparser==3.0
|
| 56 |
+
pydantic==2.12.5
|
| 57 |
+
pydantic-core==2.41.5
|
| 58 |
+
pydub==0.25.1
|
| 59 |
+
pygments==2.19.2
|
| 60 |
+
pytest==9.0.2
|
| 61 |
+
python-dateutil==2.9.0.post0
|
| 62 |
+
python-multipart==0.0.22
|
| 63 |
+
pytz==2025.2
|
| 64 |
+
pyyaml==6.0.3
|
| 65 |
+
regex==2026.1.15
|
| 66 |
+
rich==14.3.2
|
| 67 |
+
safehttpx==0.1.7
|
| 68 |
+
safetensors==0.7.0
|
| 69 |
+
semantic-version==2.10.0
|
| 70 |
+
setuptools==80.10.2
|
| 71 |
+
shellingham==1.5.4
|
| 72 |
+
six==1.17.0
|
| 73 |
+
starlette==0.50.0
|
| 74 |
+
sympy==1.14.0
|
| 75 |
+
tokenizers==0.22.2
|
| 76 |
+
tomlkit==0.13.3
|
| 77 |
+
torch==2.10.0
|
| 78 |
+
tqdm==4.67.3
|
| 79 |
+
transformers==5.0.0
|
| 80 |
+
triton==3.6.0
|
| 81 |
+
typer==0.21.1
|
| 82 |
+
typer-slim==0.21.1
|
| 83 |
+
typing-extensions==4.15.0
|
| 84 |
+
typing-inspection==0.4.2
|
| 85 |
+
uvicorn==0.40.0
|
retrieval_utils.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
from typing import List, Tuple
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
# Constants
|
| 6 |
+
DB_PATH = "anime.db"
|
| 7 |
+
|
| 8 |
+
def get_recommendations(requested_genres: List[str], limit: int = 5) -> str:
|
| 9 |
+
# Establish Connection and Cursor
|
| 10 |
+
connection = sqlite3.connect(DB_PATH)
|
| 11 |
+
cursor = connection.cursor()
|
| 12 |
+
|
| 13 |
+
# Prepare placeholders for the SQL Query
|
| 14 |
+
placeholders = ', '.join(['?'] * len(requested_genres))
|
| 15 |
+
|
| 16 |
+
# Define SQL Query for the Weighted Ranking Logic
|
| 17 |
+
query = f"""
|
| 18 |
+
-- Project only Anime's name, score, and synopsis
|
| 19 |
+
SELECT
|
| 20 |
+
A.name,
|
| 21 |
+
A.score,
|
| 22 |
+
A.synopsis
|
| 23 |
+
|
| 24 |
+
-- From Anime Table
|
| 25 |
+
FROM Anime A
|
| 26 |
+
|
| 27 |
+
-- Join Anime Table with AnimeGenre on ids
|
| 28 |
+
JOIN AnimeGenre AG ON A.id = AG.anime_id
|
| 29 |
+
|
| 30 |
+
-- Join Genre Table with AnimeGenre on ids
|
| 31 |
+
JOIN Genre G ON AG.genre_id = G.id
|
| 32 |
+
|
| 33 |
+
-- Filter by only genre_name which belong in the list of requested genres
|
| 34 |
+
WHERE G.genre_name IN ({placeholders})
|
| 35 |
+
|
| 36 |
+
-- Group by Anime id
|
| 37 |
+
GROUP BY A.id
|
| 38 |
+
|
| 39 |
+
-- Primary Sort by Count of Matches in the requested_genres list
|
| 40 |
+
-- Secondary Sort by Anime's score
|
| 41 |
+
ORDER BY COUNT(G.id) DESC, A.score DESC
|
| 42 |
+
|
| 43 |
+
-- Return only the top 5 matches
|
| 44 |
+
LIMIT ?
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
# Execute the Query with the requested genres and the limit
|
| 48 |
+
cursor.execute(query, requested_genres + [limit])
|
| 49 |
+
|
| 50 |
+
# Gather the results
|
| 51 |
+
results = cursor.fetchall()
|
| 52 |
+
|
| 53 |
+
# Close the Connection
|
| 54 |
+
connection.close()
|
| 55 |
+
|
| 56 |
+
# Compose a JSON String from the results and return it
|
| 57 |
+
return jsonify_recommendations(results)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def jsonify_recommendations(recommendations: List[Tuple[str, float, str]]) -> str:
|
| 61 |
+
# Process each anime recommendations into a list of dicts (for easy JSON conversion)
|
| 62 |
+
list_of_dicts = []
|
| 63 |
+
for anime in recommendations:
|
| 64 |
+
list_of_dicts.append({
|
| 65 |
+
'name': anime[0],
|
| 66 |
+
'score': anime[1],
|
| 67 |
+
'description': anime[2]
|
| 68 |
+
})
|
| 69 |
+
|
| 70 |
+
# JSONify and return the list of dicts
|
| 71 |
+
return json.dumps(list_of_dicts, indent=4)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# Driver Code
|
| 75 |
+
if __name__ == '__main__':
|
| 76 |
+
requested_genres = ["Action", "Drama"]
|
| 77 |
+
recommendations = get_recommendations(requested_genres)
|
| 78 |
+
print(recommendations)
|
static/css/gradiomain.css
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ====== MAIN APP CONTAINER AND HEADER ====== */
|
| 2 |
+
.page-header {
|
| 3 |
+
text-align: center;
|
| 4 |
+
border-radius: 22px;
|
| 5 |
+
margin-bottom: 1rem;
|
| 6 |
+
margin-top: 0.25rem;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
.page-header h1 {
|
| 10 |
+
margin-bottom: 1.2rem;
|
| 11 |
+
margin-top: 2rem;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
.gradio-container {
|
| 15 |
+
background-image:
|
| 16 |
+
linear-gradient(rgba(12, 13, 35, 0.6), rgba(12, 13, 35, 0.8)),
|
| 17 |
+
url('/gradio_api/file=static/images/background.png');
|
| 18 |
+
background-size: cover;
|
| 19 |
+
background-position: center;
|
| 20 |
+
background-repeat: no-repeat;
|
| 21 |
+
background-attachment: fixed;
|
| 22 |
+
background-blend-mode: hard-light;
|
| 23 |
+
|
| 24 |
+
color: var(--text-main);
|
| 25 |
+
font-family: 'Inter', sans-serif;
|
| 26 |
+
min-height: 100vh;
|
| 27 |
+
padding: 20px;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/* ====== CHATBOT CONTAINER ====== */
|
| 31 |
+
.custom-chatbot,
|
| 32 |
+
[role="log"] {
|
| 33 |
+
background: var(--bg-glass-strong) !important;
|
| 34 |
+
border-radius: 22px !important;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/* Why is Gradio like this )): */
|
| 38 |
+
.block,
|
| 39 |
+
.form,
|
| 40 |
+
.gr-group {
|
| 41 |
+
background: var(--bg-glass-strong);
|
| 42 |
+
border-radius: 22px !important;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/* ====== CHAT MESSAGES ====== */
|
| 46 |
+
.bot,
|
| 47 |
+
.user {
|
| 48 |
+
padding: 16px 20px;
|
| 49 |
+
border-radius: 18px;
|
| 50 |
+
margin: 12px 0;
|
| 51 |
+
|
| 52 |
+
max-width: 70%;
|
| 53 |
+
min-width: fit-content;
|
| 54 |
+
display: inline-flex;
|
| 55 |
+
|
| 56 |
+
border: 1px solid var(--border-soft);
|
| 57 |
+
box-shadow: 0 4px 20px rgba(122, 162, 255, 0.2);
|
| 58 |
+
|
| 59 |
+
white-space: normal;
|
| 60 |
+
overflow-wrap: break-word;
|
| 61 |
+
word-break: normal;
|
| 62 |
+
box-sizing: border-box;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.bot {
|
| 66 |
+
background: var(--bot-message);
|
| 67 |
+
color: var(--text-main);
|
| 68 |
+
margin-right: auto;
|
| 69 |
+
margin-left: 0;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.user {
|
| 73 |
+
background: var(--user-message);
|
| 74 |
+
color: white;
|
| 75 |
+
margin-left: auto;
|
| 76 |
+
margin-right: 0;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/* ====== CHAT INPUT AREA ====== */
|
| 80 |
+
.gr-text-input,
|
| 81 |
+
[data-testid="textbox"] {
|
| 82 |
+
background: rgba(40, 24, 65, 0.6) !important;
|
| 83 |
+
border-radius: 18px !important;
|
| 84 |
+
border: 1px solid var(--border-soft) !important;
|
| 85 |
+
min-height: 60px !important;
|
| 86 |
+
height: auto !important;
|
| 87 |
+
margin-right: 12px !important;
|
| 88 |
+
box-shadow: 0 0 10px rgba(255, 124, 229, 0.1);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.gr-text-input:hover,
|
| 92 |
+
textarea:hover {
|
| 93 |
+
box-shadow: 0 0 15px var(--accent-glow) !important;
|
| 94 |
+
transition: box-shadow 0.3s ease !important;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.gr-text-input:focus,
|
| 98 |
+
textarea:focus {
|
| 99 |
+
box-shadow: 0 0 20px var(--accent-glow) !important;
|
| 100 |
+
outline: none !important;
|
| 101 |
+
transition: box-shadow 0.3s ease !important;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
textarea {
|
| 105 |
+
background: transparent !important;
|
| 106 |
+
color: var(--text-main) !important;
|
| 107 |
+
border: none !important;
|
| 108 |
+
border-radius: 16px !important;
|
| 109 |
+
padding: 16px 20px !important;
|
| 110 |
+
font-size: 16px !important;
|
| 111 |
+
line-height: 1.5 !important;
|
| 112 |
+
min-height: 60px !important;
|
| 113 |
+
height: auto !important;
|
| 114 |
+
resize: vertical !important;
|
| 115 |
+
overflow-y: auto !important;
|
| 116 |
+
max-height: 200px !important;
|
| 117 |
+
white-space: pre-wrap !important;
|
| 118 |
+
word-wrap: break-word !important;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
textarea::placeholder {
|
| 122 |
+
color: var(--text-muted) !important;
|
| 123 |
+
opacity: 0.75 !important;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
/* ====== BUTTON STUFF ====== */
|
| 127 |
+
button[data-testid="login-button"],
|
| 128 |
+
.submit-button {
|
| 129 |
+
background: var(--btn-bg);
|
| 130 |
+
border: none !important;
|
| 131 |
+
border-radius: 16px !important;
|
| 132 |
+
color: white !important;
|
| 133 |
+
font-weight: 600 !important;
|
| 134 |
+
padding: 16px 28px !important;
|
| 135 |
+
min-height: 60px !important;
|
| 136 |
+
box-shadow: none;
|
| 137 |
+
cursor: pointer !important;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.submit-button:hover,
|
| 141 |
+
.submit-button:focus-visible {
|
| 142 |
+
box-shadow: 0 0 18px var(--accent-glow);
|
| 143 |
+
transform: translateY(-1px);
|
| 144 |
+
filter: brightness(1.05);
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
/* Sidebar buttons */
|
| 148 |
+
.sidebar button {
|
| 149 |
+
background: linear-gradient(135deg, #ff7ce5, #7ac5ff) !important;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.sidebar button:hover {
|
| 153 |
+
background: linear-gradient(135deg, #ff9de0, #7ac5ff) !important;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
/* Toggle Buttons */
|
| 157 |
+
.block.toggle-button {
|
| 158 |
+
margin-left: auto !important;
|
| 159 |
+
margin-right: auto !important;
|
| 160 |
+
width: fit-content;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.toggle-button input[type="checkbox"] {
|
| 164 |
+
display: none;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.toggle-button label {
|
| 168 |
+
display: inline-flex;
|
| 169 |
+
align-items: center;
|
| 170 |
+
gap: 0.4rem;
|
| 171 |
+
|
| 172 |
+
padding: 0.65rem 1.2rem;
|
| 173 |
+
border-radius: 999px;
|
| 174 |
+
cursor: pointer;
|
| 175 |
+
|
| 176 |
+
font-weight: 600;
|
| 177 |
+
letter-spacing: 0.02em;
|
| 178 |
+
|
| 179 |
+
background: var(--btn-bg);
|
| 180 |
+
color: var(--btn-text);
|
| 181 |
+
border: 1px solid var(--btn-border);
|
| 182 |
+
|
| 183 |
+
transition:
|
| 184 |
+
background 0.25s ease,
|
| 185 |
+
box-shadow 0.25s ease,
|
| 186 |
+
transform 0.25s ease;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.toggle-button label:hover {
|
| 190 |
+
background: var(--btn-bg-hover);
|
| 191 |
+
box-shadow: 0 0 22px rgba(122, 197, 255, 0.7);
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.toggle-button label:has(input:checked) {
|
| 195 |
+
background: var(--btn-bg-active);
|
| 196 |
+
box-shadow: 0 0 28px rgba(122, 197, 255, 0.9);
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
/* Add the word ON in green text if enabled local model */
|
| 200 |
+
.toggle-button label:has(input:checked)::after {
|
| 201 |
+
content: " ON";
|
| 202 |
+
font-weight: 700;
|
| 203 |
+
color: #4cffc3;
|
| 204 |
+
margin-left: 0.3rem;
|
| 205 |
+
text-shadow: 0 0 6px rgba(76, 255, 195, 0.8);
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.toggle-button label:active {
|
| 209 |
+
transform: scale(0.96);
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
/* ====== SLIDERS ====== */
|
| 213 |
+
.custom-slider input[type="range"]::-webkit-slider-thumb {
|
| 214 |
+
background: var(--accent-secondary) !important;
|
| 215 |
+
width: 22px !important;
|
| 216 |
+
height: 22px !important;
|
| 217 |
+
border-radius: 50% !important;
|
| 218 |
+
border: 2px solid white !important;
|
| 219 |
+
box-shadow: 0 0 12px var(--accent-glow);
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.custom-slider input[type="range"]::-webkit-slider-runnable-track {
|
| 223 |
+
height: 8px;
|
| 224 |
+
border-radius: 8px;
|
| 225 |
+
background: linear-gradient(120deg, var(--accent-primary), var(--accent-secondary));
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
/* ====== SIDEBAR ====== */
|
| 229 |
+
.sidebar {
|
| 230 |
+
background-image:
|
| 231 |
+
linear-gradient(rgba(20, 24, 55, 0.4), rgba(20, 24, 55, 0.4)),
|
| 232 |
+
url('/gradio_api/file=static/images/sidebar.jpg');
|
| 233 |
+
background-size: auto, cover;
|
| 234 |
+
background-position: top left, center;
|
| 235 |
+
background-repeat: no-repeat, no-repeat;
|
| 236 |
+
|
| 237 |
+
border-right: 1px solid var(--border-soft) !important;
|
| 238 |
+
backdrop-filter: blur(15px);
|
| 239 |
+
padding: 20px !important;
|
| 240 |
+
}
|
static/css/theme.css
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* This page contains base theme colors for the app */
|
| 2 |
+
:root {
|
| 3 |
+
/* Base backgrounds */
|
| 4 |
+
--bg-main: #0c0d23;
|
| 5 |
+
--bg-glass: rgba(28, 20, 55, 0.55);
|
| 6 |
+
--bg-glass-strong: rgba(28, 20, 55, 0.85);
|
| 7 |
+
--bg-gradient: linear-gradient(135deg, #1b1b40, #3a1f5f);
|
| 8 |
+
|
| 9 |
+
/* Accents */
|
| 10 |
+
--accent-primary: #ff7ce5;
|
| 11 |
+
--accent-secondary: #7ac5ff;
|
| 12 |
+
--accent-glow: rgba(255, 124, 229, 0.5);
|
| 13 |
+
|
| 14 |
+
/* Borders and shadows */
|
| 15 |
+
--border-soft: rgba(255, 255, 255, 0.08);
|
| 16 |
+
--shadow-soft: 0 15px 35px rgba(255, 124, 229, 0.3);
|
| 17 |
+
|
| 18 |
+
/* Text */
|
| 19 |
+
--text-main: #f0f0ff;
|
| 20 |
+
--text-muted: #b9b6ff;
|
| 21 |
+
|
| 22 |
+
/* Chat messages */
|
| 23 |
+
--user-message: linear-gradient(135deg, #ff9de0, #7ac5ff);
|
| 24 |
+
--bot-message: linear-gradient(135deg, #ff9de0, #7ac5ff);
|
| 25 |
+
|
| 26 |
+
/* Buttons */
|
| 27 |
+
--btn-bg: linear-gradient(135deg, #7ac5ff, #9aa4ff);
|
| 28 |
+
--btn-bg-hover: linear-gradient(135deg, #8fd0ff, #aab3ff);
|
| 29 |
+
--btn-bg-active: linear-gradient(135deg, #6bb6f5, #8f98ff);
|
| 30 |
+
|
| 31 |
+
--btn-text: #0c0d23;
|
| 32 |
+
--btn-border: rgba(122, 197, 255, 0.45);
|
| 33 |
+
|
| 34 |
+
--btn-glow: 0 0 18px rgba(122, 197, 255, 0.55);
|
| 35 |
+
}
|
static/images/background.png
ADDED
|
Git LFS Details
|
static/images/sidebar.jpg
ADDED
|
Git LFS Details
|
test_detect_genre.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from backend import detect_genres
|
| 2 |
+
|
| 3 |
+
def test_one_genre():
|
| 4 |
+
detect_genres("I like Action"),["Action"]
|
| 5 |
+
|
| 6 |
+
def test_multiple_genres():
|
| 7 |
+
detect_genres("I like Action and Mystery"),["Action","Mystery"]
|
| 8 |
+
|
| 9 |
+
def test_no_genres():
|
| 10 |
+
detect_genres("I like Interesting shows"),[]
|
| 11 |
+
|
tests/test_chat_models.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
from backend import process_user_query
|
| 6 |
+
|
| 7 |
+
TEST_SYSTEM_MESSAGE = "You are a friendly chatbot."
|
| 8 |
+
TEST_USER_MESSAGE = "Hello!"
|
| 9 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 10 |
+
|
| 11 |
+
def test_HF_token_exists():
|
| 12 |
+
token = os.getenv("HF_TOKEN")
|
| 13 |
+
assert token is not None
|
| 14 |
+
assert len(token) > 1
|
| 15 |
+
|
| 16 |
+
def test_local_model_runs():
|
| 17 |
+
use_local_model = True
|
| 18 |
+
collected_result = ""
|
| 19 |
+
for result in process_user_query(system_message=TEST_SYSTEM_MESSAGE,
|
| 20 |
+
history=[],
|
| 21 |
+
user_message=TEST_USER_MESSAGE,
|
| 22 |
+
use_local_model=use_local_model,
|
| 23 |
+
max_tokens=100,
|
| 24 |
+
temperature=0.7,
|
| 25 |
+
top_p=0.7,
|
| 26 |
+
hf_token=HF_TOKEN):
|
| 27 |
+
collected_result = result
|
| 28 |
+
|
| 29 |
+
assert len(collected_result) > 0
|
| 30 |
+
|
| 31 |
+
def test_external_model_runs():
|
| 32 |
+
use_local_model = False
|
| 33 |
+
collected_result = ""
|
| 34 |
+
for result in process_user_query(system_message=TEST_SYSTEM_MESSAGE,
|
| 35 |
+
history=[],
|
| 36 |
+
user_message=TEST_USER_MESSAGE,
|
| 37 |
+
use_local_model=use_local_model,
|
| 38 |
+
max_tokens=100,
|
| 39 |
+
temperature=0.7,
|
| 40 |
+
top_p=0.7,
|
| 41 |
+
hf_token=HF_TOKEN):
|
| 42 |
+
collected_result = result
|
| 43 |
+
assert len(collected_result) > 0
|
| 44 |
+
|
| 45 |
+
if __name__ == "__main__":
|
| 46 |
+
pytest.main()
|
| 47 |
+
|
| 48 |
+
|
tests/test_retrieval_utils.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import sqlite3
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import pytest
|
| 5 |
+
import retrieval_utils
|
| 6 |
+
from retrieval_utils import get_recommendations
|
| 7 |
+
|
| 8 |
+
# Setup Test DB
|
| 9 |
+
def _setup_test_db(db_path: str):
|
| 10 |
+
# Setup the DB Connection
|
| 11 |
+
connection = sqlite3.connect(db_path)
|
| 12 |
+
cursor = connection.cursor()
|
| 13 |
+
|
| 14 |
+
# Create Test Anime Table
|
| 15 |
+
cursor.execute(
|
| 16 |
+
"""
|
| 17 |
+
CREATE TABLE Anime (
|
| 18 |
+
id INTEGER PRIMARY KEY,
|
| 19 |
+
name TEXT NOT NULL,
|
| 20 |
+
score REAL NOT NULL,
|
| 21 |
+
synopsis TEXT
|
| 22 |
+
);
|
| 23 |
+
"""
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# Create Test Genre Table
|
| 27 |
+
cursor.execute(
|
| 28 |
+
"""
|
| 29 |
+
CREATE TABLE Genre (
|
| 30 |
+
id INTEGER PRIMARY KEY,
|
| 31 |
+
genre_name TEXT NOT NULL
|
| 32 |
+
);
|
| 33 |
+
"""
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
# Create Test AnimeGenre Table
|
| 37 |
+
cursor.execute(
|
| 38 |
+
"""
|
| 39 |
+
CREATE TABLE AnimeGenre (
|
| 40 |
+
anime_id INTEGER NOT NULL,
|
| 41 |
+
genre_id INTEGER NOT NULL
|
| 42 |
+
);
|
| 43 |
+
"""
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
# Define new values to be inserted in the Anime Table
|
| 47 |
+
anime_rows = [
|
| 48 |
+
(1, "Alpha", 9.1, "Alpha synopsis"),
|
| 49 |
+
(2, "Beta", 8.7, "Beta synopsis"),
|
| 50 |
+
(3, "Gamma", 8.9, "Gamma synopsis"),
|
| 51 |
+
(4, "Delta", 7.5, "Delta synopsis"),
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
# Define new values to be inserted in the Genre Table
|
| 55 |
+
genre_rows = [
|
| 56 |
+
(1, "Action"),
|
| 57 |
+
(2, "Drama"),
|
| 58 |
+
(3, "Comedy"),
|
| 59 |
+
]
|
| 60 |
+
|
| 61 |
+
# Define new values to be inserted in the AnimeGenre Table
|
| 62 |
+
anime_genre_rows = [
|
| 63 |
+
(1, 1), (1, 2), # Alpha: Action, Drama (2 matches)
|
| 64 |
+
(2, 1), # Beta: Action (1 match)
|
| 65 |
+
(3, 2), # Gamma: Drama (1 match)
|
| 66 |
+
(4, 3), # Delta: Comedy (0 matches for Action/Drama)
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
# Insert into all Tables the defined new values above
|
| 70 |
+
cursor.executemany("INSERT INTO Anime VALUES (?, ?, ?, ?);", anime_rows)
|
| 71 |
+
cursor.executemany("INSERT INTO Genre VALUES (?, ?);", genre_rows)
|
| 72 |
+
cursor.executemany("INSERT INTO AnimeGenre VALUES (?, ?);", anime_genre_rows)
|
| 73 |
+
|
| 74 |
+
# Commit all the writes to the DB file
|
| 75 |
+
connection.commit()
|
| 76 |
+
|
| 77 |
+
# Close the cursor and the connection
|
| 78 |
+
cursor.close()
|
| 79 |
+
connection.close()
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def test_get_recommendations_orders_by_match_count_then_score(tmp_path: Path,
|
| 83 |
+
monkeypatch: pytest.MonkeyPatch):
|
| 84 |
+
# Setup Test Data and Mocks
|
| 85 |
+
# Construct a temporary path for the Test DB
|
| 86 |
+
db_path = tmp_path / "test.db"
|
| 87 |
+
|
| 88 |
+
# Setup the test db
|
| 89 |
+
_setup_test_db(str(db_path))
|
| 90 |
+
|
| 91 |
+
# Monkeypatch the DB_PATH variable of retrieval_utils file
|
| 92 |
+
monkeypatch.setattr(retrieval_utils, "DB_PATH", str(db_path))
|
| 93 |
+
|
| 94 |
+
# Execute the Method under Test
|
| 95 |
+
result_json = get_recommendations(["Action", "Drama"], limit=3)
|
| 96 |
+
result = json.loads(result_json)
|
| 97 |
+
|
| 98 |
+
# Assert on the results
|
| 99 |
+
assert [item["name"] for item in result] == ["Alpha", "Gamma", "Beta"]
|
| 100 |
+
assert result[0]["score"] == 9.1
|
| 101 |
+
assert "description" in result[0]
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def test_get_recommendations_respects_limit(tmp_path: Path,
|
| 105 |
+
monkeypatch: pytest.MonkeyPatch):
|
| 106 |
+
# Setup Test Data and Mocks
|
| 107 |
+
# Construct a temporary path for the Test DB
|
| 108 |
+
db_path = tmp_path / "test.db"
|
| 109 |
+
|
| 110 |
+
# Setup the test db
|
| 111 |
+
_setup_test_db(str(db_path))
|
| 112 |
+
|
| 113 |
+
# Monkeypatch the DB_PATH variable of retrieval_utils file
|
| 114 |
+
monkeypatch.setattr(retrieval_utils, "DB_PATH", str(db_path))
|
| 115 |
+
|
| 116 |
+
# Execute the Method under Test
|
| 117 |
+
result_json = get_recommendations(["Action", "Drama"], limit=1)
|
| 118 |
+
result = json.loads(result_json)
|
| 119 |
+
|
| 120 |
+
# Assert on the results
|
| 121 |
+
assert len(result) == 1
|
| 122 |
+
assert result[0]["name"] == "Alpha"
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def test_get_recommendations_single_genre(tmp_path: Path,
|
| 126 |
+
monkeypatch: pytest.MonkeyPatch):
|
| 127 |
+
# Setup Test Data and Mocks
|
| 128 |
+
# Construct a temporary path for the Test DB
|
| 129 |
+
db_path = tmp_path / "test.db"
|
| 130 |
+
|
| 131 |
+
# Setup the test db
|
| 132 |
+
_setup_test_db(db_path)
|
| 133 |
+
|
| 134 |
+
# Monkeypatch the DB_PATH variable of retrieval_utils file
|
| 135 |
+
monkeypatch.setattr(retrieval_utils, "DB_PATH", str(db_path))
|
| 136 |
+
|
| 137 |
+
# Execute the Method under Test
|
| 138 |
+
result_json = get_recommendations(["Drama"], limit=5)
|
| 139 |
+
result = json.loads(result_json)
|
| 140 |
+
|
| 141 |
+
# Assert on the results
|
| 142 |
+
assert [item["name"] for item in result] == ["Alpha", "Gamma"]
|
| 143 |
+
assert all("description" in item for item in result)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def test_get_recommendations_no_genre(tmp_path: Path,
|
| 147 |
+
monkeypatch: pytest.MonkeyPatch):
|
| 148 |
+
# Setup Test Data and Mocks
|
| 149 |
+
# Construct a temporary path for the Test DB
|
| 150 |
+
db_path = tmp_path / "test.db"
|
| 151 |
+
|
| 152 |
+
# Setup the test db
|
| 153 |
+
_setup_test_db(db_path)
|
| 154 |
+
|
| 155 |
+
# Monkeypatch the DB_PATH variable of retrieval_utils file
|
| 156 |
+
monkeypatch.setattr(retrieval_utils, "DB_PATH", str(db_path))
|
| 157 |
+
|
| 158 |
+
# Execute the Method under Test
|
| 159 |
+
result_json = get_recommendations([], limit=5)
|
| 160 |
+
result = json.loads(result_json)
|
| 161 |
+
|
| 162 |
+
# Assert on the results
|
| 163 |
+
assert len(result) == 0
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|