Spaces:
Sleeping
Sleeping
๊ฐ๋ฏผ๊ท commited on
Commit ยท
f0a4b63
0
Parent(s):
Initial commit for Hugging Face
Browse files- .dockerignore +80 -0
- .gitattributes +8 -0
- .github/workflows/fly-deploy.yml +18 -0
- .gitignore +78 -0
- Dockerfile +22 -0
- README.md +87 -0
- data/final_recipe_data.csv +3 -0
- data/stats.pkl +3 -0
- docs/logic_explanation.md +72 -0
- fly.toml +32 -0
- models/d2v.model +3 -0
- models/d2v.model.dv.vectors.npy +3 -0
- models/w2v.model +3 -0
- requirements.txt +12 -0
- src/api.py +156 -0
- src/app.py +494 -0
- src/logic.py +639 -0
.dockerignore
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# flyctl launch added from .gitignore
|
| 2 |
+
# Byte-compiled / optimized / DLL files
|
| 3 |
+
**/__pycache__
|
| 4 |
+
**/*.py[cod]
|
| 5 |
+
**/*$py.class
|
| 6 |
+
|
| 7 |
+
# C extensions
|
| 8 |
+
**/*.so
|
| 9 |
+
|
| 10 |
+
# Distribution / packaging
|
| 11 |
+
**/.Python
|
| 12 |
+
**/build
|
| 13 |
+
**/develop-eggs
|
| 14 |
+
**/dist
|
| 15 |
+
**/downloads
|
| 16 |
+
**/eggs
|
| 17 |
+
**/.eggs
|
| 18 |
+
**/lib
|
| 19 |
+
**/lib64
|
| 20 |
+
**/parts
|
| 21 |
+
**/sdist
|
| 22 |
+
**/var
|
| 23 |
+
**/wheels
|
| 24 |
+
**/*.egg-info
|
| 25 |
+
**/.installed.cfg
|
| 26 |
+
**/*.egg
|
| 27 |
+
|
| 28 |
+
# PyInstaller
|
| 29 |
+
**/*.manifest
|
| 30 |
+
**/*.spec
|
| 31 |
+
|
| 32 |
+
# Installer logs
|
| 33 |
+
**/pip-log.txt
|
| 34 |
+
**/pip-delete-this-directory.txt
|
| 35 |
+
|
| 36 |
+
# Unit test / coverage reports
|
| 37 |
+
**/htmlcov
|
| 38 |
+
**/.tox
|
| 39 |
+
**/.coverage
|
| 40 |
+
**/.coverage.*
|
| 41 |
+
**/.cache
|
| 42 |
+
**/nosetests.xml
|
| 43 |
+
**/coverage.xml
|
| 44 |
+
**/*.cover
|
| 45 |
+
**/.hypothesis
|
| 46 |
+
**/.pytest_cache
|
| 47 |
+
|
| 48 |
+
# Translations
|
| 49 |
+
**/*.mo
|
| 50 |
+
**/*.pot
|
| 51 |
+
|
| 52 |
+
# Jupyter Notebook
|
| 53 |
+
**/.ipynb_checkpoints
|
| 54 |
+
|
| 55 |
+
# pyenv
|
| 56 |
+
**/.python-version
|
| 57 |
+
|
| 58 |
+
# Environments
|
| 59 |
+
**/.env
|
| 60 |
+
**/.venv
|
| 61 |
+
**/env
|
| 62 |
+
**/venv
|
| 63 |
+
**/ENV
|
| 64 |
+
**/env.bak
|
| 65 |
+
**/venv.bak
|
| 66 |
+
|
| 67 |
+
# IDE
|
| 68 |
+
**/.idea
|
| 69 |
+
**/.vscode
|
| 70 |
+
**/*.swp
|
| 71 |
+
**/*.swo
|
| 72 |
+
**/*~
|
| 73 |
+
|
| 74 |
+
# OS
|
| 75 |
+
**/.DS_Store
|
| 76 |
+
**/Thumbs.db
|
| 77 |
+
|
| 78 |
+
# Local settings
|
| 79 |
+
**/.streamlit/secrets.toml
|
| 80 |
+
fly.toml
|
.gitattributes
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
models/d2v.model filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
models/d2v.model.dv.vectors.npy filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
models/w2v.model filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.csv filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
src/font.ttf filter=lfs diff=lfs merge=lfs -text
|
.github/workflows/fly-deploy.yml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/
|
| 2 |
+
|
| 3 |
+
name: Fly Deploy
|
| 4 |
+
on:
|
| 5 |
+
push:
|
| 6 |
+
branches:
|
| 7 |
+
- main
|
| 8 |
+
jobs:
|
| 9 |
+
deploy:
|
| 10 |
+
name: Deploy app
|
| 11 |
+
runs-on: ubuntu-latest
|
| 12 |
+
concurrency: deploy-group # optional: ensure only one action runs at a time
|
| 13 |
+
steps:
|
| 14 |
+
- uses: actions/checkout@v4
|
| 15 |
+
- uses: superfly/flyctl-actions/setup-flyctl@master
|
| 16 |
+
- run: flyctl deploy --remote-only
|
| 17 |
+
env:
|
| 18 |
+
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
|
.gitignore
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
*.egg-info/
|
| 24 |
+
.installed.cfg
|
| 25 |
+
*.egg
|
| 26 |
+
|
| 27 |
+
# PyInstaller
|
| 28 |
+
*.manifest
|
| 29 |
+
*.spec
|
| 30 |
+
|
| 31 |
+
# Installer logs
|
| 32 |
+
pip-log.txt
|
| 33 |
+
pip-delete-this-directory.txt
|
| 34 |
+
|
| 35 |
+
# Unit test / coverage reports
|
| 36 |
+
htmlcov/
|
| 37 |
+
.tox/
|
| 38 |
+
.coverage
|
| 39 |
+
.coverage.*
|
| 40 |
+
.cache
|
| 41 |
+
nosetests.xml
|
| 42 |
+
coverage.xml
|
| 43 |
+
*.cover
|
| 44 |
+
.hypothesis/
|
| 45 |
+
.pytest_cache/
|
| 46 |
+
|
| 47 |
+
# Translations
|
| 48 |
+
*.mo
|
| 49 |
+
*.pot
|
| 50 |
+
|
| 51 |
+
# Jupyter Notebook
|
| 52 |
+
.ipynb_checkpoints
|
| 53 |
+
|
| 54 |
+
# pyenv
|
| 55 |
+
.python-version
|
| 56 |
+
|
| 57 |
+
# Environments
|
| 58 |
+
.env
|
| 59 |
+
.venv
|
| 60 |
+
env/
|
| 61 |
+
venv/
|
| 62 |
+
ENV/
|
| 63 |
+
env.bak/
|
| 64 |
+
venv.bak/
|
| 65 |
+
|
| 66 |
+
# IDE
|
| 67 |
+
.idea/
|
| 68 |
+
.vscode/
|
| 69 |
+
*.swp
|
| 70 |
+
*.swo
|
| 71 |
+
*~
|
| 72 |
+
|
| 73 |
+
# OS
|
| 74 |
+
.DS_Store
|
| 75 |
+
Thumbs.db
|
| 76 |
+
|
| 77 |
+
# Local settings
|
| 78 |
+
.streamlit/secrets.toml
|
Dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.9-slim
|
| 2 |
+
|
| 3 |
+
# ๋ณด์ ๋ฐ ๊ถํ ์ค์ ์ ์ํ ์ ์ ์์ฑ (Hugging Face ๊ถ์ฅ ์ฌํญ)
|
| 4 |
+
RUN useradd -m -u 1000 user
|
| 5 |
+
USER user
|
| 6 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 7 |
+
|
| 8 |
+
WORKDIR /app
|
| 9 |
+
|
| 10 |
+
# ์์กด์ฑ ์ค์น
|
| 11 |
+
COPY --chown=user requirements.txt requirements.txt
|
| 12 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 13 |
+
pip install --no-cache-dir -r requirements.txt
|
| 14 |
+
|
| 15 |
+
# ์ฝ๋ ๋ณต์ฌ (๋ชจ๋ธ ํ์ผ ํฌํจ)
|
| 16 |
+
COPY --chown=user . .
|
| 17 |
+
|
| 18 |
+
# ํฌํธ ์ค์ (Hugging Face๋ 7860 ํฌํธ๋ฅผ ์ฌ์ฉํฉ๋๋ค)
|
| 19 |
+
EXPOSE 7860
|
| 20 |
+
|
| 21 |
+
# ์คํ ๋ช
๋ น์ด
|
| 22 |
+
CMD ["uvicorn", "src.api:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ๐ณ K-Recipe2Vec
|
| 2 |
+
|
| 3 |
+
AI ๊ธฐ๋ฐ ํ์ ์์ฌ๋ฃ ๋์ฒด ์ถ์ฒ ์๋น์ค
|
| 4 |
+
|
| 5 |
+
## ๐ Overview
|
| 6 |
+
|
| 7 |
+
ํ๊ตญ ์๋ฆฌ์์ ์์ฌ๋ฃ๋ฅผ ๋์ฒดํ ์ ์๋ ์ฌ๋ฃ๋ฅผ AI๊ฐ ์ถ์ฒํด์ฃผ๋ ์น ์๋น์ค์
๋๋ค.
|
| 8 |
+
Doc2Vec๊ณผ Word2Vec ๋ชจ๋ธ์ ํ์ฉํ์ฌ ์์ฌ๋ฃ ๊ฐ์ ์๋ฏธ์ ์ ์ฌ๋๋ฅผ ๋ถ์ํฉ๋๋ค.
|
| 9 |
+
|
| 10 |
+
## ๐ Demo
|
| 11 |
+
|
| 12 |
+
๐ **[Live Demo](https://korea-recipe-ai.streamlit.app/)** - Streamlit Cloud ๋ฐฐํฌ
|
| 13 |
+
|
| 14 |
+
## โจ Features
|
| 15 |
+
|
| 16 |
+
- **๐ฅฌ ์์ฌ๋ฃ ๋์ฒด ์ถ์ฒ**: ์๋ ์ฌ๋ฃ์ ๋ํ ์ ์ฌ ์ฌ๋ฃ ์ถ์ฒ
|
| 17 |
+
- **๐ 3D ์๊ฐํ**: PCA ๊ธฐ๋ฐ ์ฌ๋ฃ ๋ฒกํฐ ๊ณต๊ฐ ์๊ฐํ
|
| 18 |
+
- **๐ฐ ๊ฐ๊ฒฉ ์ ๋ณด**: ์ฌ๋ฃ๋ณ ๊ฐ๊ฒฉ ์ ๋ณด ์ ๊ณต
|
| 19 |
+
- **โ๏ธ ์๋ํด๋ผ์ฐ๋**: ์ถ์ฒ ์ฌ๋ฃ ์๊ฐํ
|
| 20 |
+
|
| 21 |
+
## ๐ ๏ธ Tech Stack
|
| 22 |
+
|
| 23 |
+
| Category | Technologies |
|
| 24 |
+
|----------|-------------|
|
| 25 |
+
| **Frontend** | Streamlit |
|
| 26 |
+
| **ML Models** | Gensim (Doc2Vec, Word2Vec) |
|
| 27 |
+
| **Data Processing** | Pandas, NumPy |
|
| 28 |
+
| **Visualization** | Plotly, Matplotlib, WordCloud |
|
| 29 |
+
| **Database** | Supabase |
|
| 30 |
+
| **Deployment** | Streamlit Cloud |
|
| 31 |
+
|
| 32 |
+
## ๐ Project Structure
|
| 33 |
+
|
| 34 |
+
```
|
| 35 |
+
k-recipe2vec/
|
| 36 |
+
โโโ app.py # Main Streamlit application
|
| 37 |
+
โโโ logic.py # Core recommendation logic (if exists)
|
| 38 |
+
โโโ requirements.txt # Python dependencies
|
| 39 |
+
โโโ d2v.model # Doc2Vec trained model
|
| 40 |
+
โโโ w2v.model # Word2Vec trained model
|
| 41 |
+
โโโ price_rank.csv # Price data
|
| 42 |
+
โโโ stats.pkl # Preprocessed statistics
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
## ๐ Getting Started
|
| 46 |
+
|
| 47 |
+
### Prerequisites
|
| 48 |
+
|
| 49 |
+
- Python 3.8+
|
| 50 |
+
- pip
|
| 51 |
+
|
| 52 |
+
### Installation
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
# Clone the repository
|
| 56 |
+
git clone https://github.com/nneans/k-recipe2vec.git
|
| 57 |
+
cd k-recipe2vec
|
| 58 |
+
|
| 59 |
+
# Install dependencies
|
| 60 |
+
pip install -r requirements.txt
|
| 61 |
+
|
| 62 |
+
# Run the app
|
| 63 |
+
streamlit run src/app.py
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
## ๐ Model Information
|
| 67 |
+
|
| 68 |
+
### Doc2Vec Model
|
| 69 |
+
- ํ๊ตญ ๋ ์ํผ ๋ฐ์ดํฐ ๊ธฐ๋ฐ ํ์ต
|
| 70 |
+
- ๋ ์ํผ ๋จ์ ๋ฌธ์ ์๋ฒ ๋ฉ
|
| 71 |
+
|
| 72 |
+
### Word2Vec Model
|
| 73 |
+
- ์์ฌ๋ฃ ๊ฐ ์๋ฏธ์ ์ ์ฌ๋ ํ์ต
|
| 74 |
+
- ๋์ฒด ๊ฐ๋ฅํ ์ฌ๋ฃ ์ถ์ฒ์ ํ์ฉ
|
| 75 |
+
|
| 76 |
+
## ๐ค Contributing
|
| 77 |
+
|
| 78 |
+
๋ฒ๊ทธ ๋ฆฌํฌํธ, ๊ธฐ๋ฅ ์ ์, PR ํ์ํฉ๋๋ค!
|
| 79 |
+
|
| 80 |
+
## ๐ License
|
| 81 |
+
|
| 82 |
+
MIT License
|
| 83 |
+
|
| 84 |
+
## ๐ค Author
|
| 85 |
+
|
| 86 |
+
**Mingyun Kang**
|
| 87 |
+
- GitHub: [@nneans](https://github.com/nneans)
|
data/final_recipe_data.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0f6379eeed75a6d8b2f44a8a3e713c0b3bb3f1af58d0ed92ee0b8a2523cf9e65
|
| 3 |
+
size 31853277
|
data/stats.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e2564ef8229041aadab57e0d4c371bb6ba781ceb0815390909fbc45851571304
|
| 3 |
+
size 14211436
|
docs/logic_explanation.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
### AI ์ถ์ฒ ๋ก์ง ์์ธ ํด๋ถ
|
| 2 |
+
|
| 3 |
+
์ด ์์คํ
์ 12๋ง์ฌ ๊ฐ์ ํ์ ๋ ์ํผ ๋ฐ์ดํฐ๋ฅผ ํ์ตํ AI๊ฐ ์ฌ๋ฃ์ ์๋ฏธ์ ๋ฌธ๋งฅ์ ๋ถ์ํฉ๋๋ค. ๋จ์ํ ์ด๋ฆ์ด ๋น์ทํ ์ฌ๋ฃ๊ฐ ์๋, **'์ง๊ธ ์ด ์๋ฆฌ์ ๊ฐ์ฅ ์ ์ด์ธ๋ฆฌ๋'** ์ต์ ์ ๋์์ ์ฐพ์๋ด๋ ๊ณผ์ ์
๋๋ค.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
#### ๐ก AI๋ ์ฌ๋ฃ๋ฅผ ์ด๋ป๊ฒ ์ดํดํ ๊น์? (3์ฐจ์ ๋ฒกํฐ ๊ณต๊ฐ ์์)
|
| 8 |
+
AI๋ ๋ชจ๋ ์์ฌ๋ฃ๋ฅผ ๊ฑฐ๋ํ 3์ฐจ์ ๊ณต๊ฐ(์ค์ ๋ก๋ ์๋ฐฑ ์ฐจ์) ์์ **'์ขํ(๋ฒกํฐ)'**๋ก ์ธ์ํฉ๋๋ค.
|
| 9 |
+
* **์ ์ฌ๋๊ฐ ๋๋ค๋ ๋ป:** ์ด ๊ณต๊ฐ์์ ๋ ์ฌ๋ฃ์ ์ขํ๊ฐ ์๋ก **๊ฐ๊น์ด ์์น**์ ๋ชจ์ฌ ์๊ฑฐ๋, ์์ ์์ ๋ป์ด๋๊ฐ๋ **ํ์ดํ์ ๋ฐฉํฅ์ด ๋น์ทํ๋ค**๋ ์๋ฏธ์
๋๋ค.
|
| 10 |
+
|
| 11 |
+
์ ๊ทธ๋ฆผ์ฒ๋ผ '๋ผ์ง๊ณ ๊ธฐ'์ '์๊ณ ๊ธฐ'๋ '์ก๋ฅ'๋ผ๋ ๋น์ทํ ์ฑ์ง์ ๊ฐ์ ธ ๊ณต๊ฐ์์์ ๊ฐ๊น์ด ์์น์ ๋ชจ์ฌ ์์ต๋๋ค. ๋ฐ๋ฉด, '์ฌ๊ณผ'๋ ์ฑ์ง์ด ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ ๋ฉ๋ฆฌ ๋จ์ด์ ธ ์์ต๋๋ค. AI๋ ์ด **'๊ฑฐ๋ฆฌ์ ๋ฐฉํฅ์ ๋ฉ๊ณผ ๊ฐ๊น์'**์ ๊ณ์ฐํ์ฌ ์ถ์ฒ์ ํ์ฉํฉ๋๋ค.
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
#### 1๋จ๊ณ. ์๋ฏธ ํ์
(Word2Vec): "์น๊ตฌ๋ฅผ ๋ณด๋ฉด ๋๋ฅผ ์ ์ ์์ด"
|
| 16 |
+
* **ํต์ฌ ์๋ฆฌ:** AI๋ ์ฌ๋ฃ์ ๋ง์ด๋ ์๊ฐ์ ์ง์ ์์ง ๋ชปํฉ๋๋ค. ๋์ **'ํจ๊ป ์์ฃผ ์ฐ์ด๋ ์ฃผ๋ณ ์ฌ๋ฃ(๋ฌธ๋งฅ)'**๊ฐ ๋น์ทํ ์๋ก ์ ์ฌํ ์ญํ ์ ํ๋ ์ฌ๋ฃ๋ก ํ์ตํฉ๋๋ค.
|
| 17 |
+
* **์์ (ํ๊ฒ ์ฌ๋ฃ: ๋ผ์ง๊ณ ๊ธฐ)**
|
| 18 |
+
* **๋ผ์ง๊ณ ๊ธฐ**์ ์น๊ตฌ๋ค: [๊ฐ์ฅ, ๋ง๋, ์ํ, ๊ณ ์ถ์ฅ, ๊น์น, ๋ณถ๊ธฐ]
|
| 19 |
+
* ๐ฅฉ **์๊ณ ๊ธฐ (์ ์ฌ๋ 0.85):** [๊ฐ์ฅ, ๋ง๋, ์ํ, ์ฐธ๊ธฐ๋ฆ, ๋ถ๊ณ ๊ธฐ] โ ๊ฒน์น๋ ์น๊ตฌ๊ฐ ๋งค์ฐ ๋ง์ (๋น์ทํ ์ฌ๋ฃ!)
|
| 20 |
+
* ๐ **๊ณ ๋ฑ์ด (์ ์ฌ๋ 0.45):** [๊ฐ์ฅ, ๋ง๋, **๋ฌด, ์๊ฐ, ๋น๋ฆฐ๋ด**] โ ์ผ๋ถ ๊ฒน์น์ง๋ง, ๋ค๋ฅธ ์น๊ตฌ๋ค๋ ๋ง์ (์กฐ๊ธ ๋ค๋ฅธ ์ฌ๋ฃ)
|
| 21 |
+
* ๐ **์ฌ๊ณผ (์ ์ฌ๋ 0.10):** [์คํ, **๋ง์๋ค์ฆ, ์๋ฌ๋, ์์นจ**] โ ๊ฒน์น๋ ์น๊ตฌ๊ฐ ๊ฑฐ์ ์์ (์์ ํ ๋ค๋ฅธ ์ฌ๋ฃ)
|
| 22 |
+
|
| 23 |
+
#### 2๋จ๊ณ. ๋ฌธ๋งฅ ์ดํด (Doc2Vec): "๊ฐ์ ์กฐ๋ฆฌ๋ฒ ์ํฉ์์์ ๊ถํฉ ํ์
"
|
| 24 |
+
* **ํต์ฌ (์ฝ๋ ๊ตฌํ ๋ด์ฉ):** ๋จ์ํ '์ด ์ฌ๋ฃ๊ฐ ์๋ฆฌ์ ์ด์ธ๋ฆฌ๋?'๋ฅผ ๋ณด๋ ๊ฒ์ด ์๋๋ผ, **'ํ์ฌ์ ์กฐ๋ฆฌ๋ฐฉ๋ฒ(์: ๋์ด๊ธฐ, ๋ณถ๊ธฐ)'๊ณผ ๋์ผํ ์ํฉ์์** ์ ์ด์ธ๋ฆฌ๋์ง๋ฅผ ํ๋จํฉ๋๋ค.
|
| 25 |
+
* **์๋ ์๋ฆฌ (Ver.1 DB ๋ชจ๋ ๊ธฐ์ค):**
|
| 26 |
+
1. ํ์ฌ ํ๊ฒ ์๋ฆฌ์ **'์กฐ๋ฆฌ๋ฐฉ๋ฒ'(์: ๋์ด๊ธฐ)**์ ํ์ธํฉ๋๋ค.
|
| 27 |
+
2. ํ๋ณด ์ฌ๋ฃ๊ฐ ์ฌ์ฉ๋ ์๋ง์ ๋ ์ํผ ์ค, **๊ฐ์ ์กฐ๋ฆฌ๋ฐฉ๋ฒ('๋์ด๊ธฐ')์ด ์ฌ์ฉ๋ ๋ ์ํผ๋ค๋ง ๊ณจ๋ผ๋
๋๋ค.**
|
| 28 |
+
3. ๊ณจ๋ผ๋ธ ๋ ์ํผ๋ค์ ์ขํ๊ฐ ํ์ฌ ํ๊ฒ ์๋ฆฌ์ ์ขํ์ ์ผ๋ง๋ ๊ฐ๊น์ด์ง ๋น๊ตํฉ๋๋ค.
|
| 29 |
+
* **์ ์ด๋ ๊ฒ ํ๋์?** ๊ฐ์ ์ฌ๋ฃ๋ผ๋ '๋ณถ์ ๋'์ '๋์ผ ๋'์ ์ญํ ์ด ๋ค๋ฅด๊ธฐ ๋๋ฌธ์
๋๋ค. ์กฐ๋ฆฌ๋ฒ ์กฐ๊ฑด์ ๊ฑธ์ด ๋ ์ ํํ ๋ฌธ๋งฅ ํ์
์ ํฉ๋๋ค.
|
| 30 |
+
|
| 31 |
+
#### 3๋จ๊ณ. ํต๊ณ์ ์ ํฉ์ฑ (Ver.1 DB ๋ชจ๋ ์ ์ฉ): "๋ฐ์ดํฐ ๊ฒ์ฆ (Lift)"
|
| 32 |
+
* **์ญํ :** ์ค์ ๋ฐ์ดํฐ์์ ํด๋น ์ฌ๋ฃ๊ฐ ํน์ **์กฐ๋ฆฌ๋ฒ**์ด๋ **์๋ฆฌ ์นดํ
๊ณ ๋ฆฌ**์ '์ ๋
' ๋ง์ด ์ฐ์ด๋์ง ๊ฒ์ฆํฉ๋๋ค. (์ฌ๊ธฐ์ ์นดํ
๊ณ ๋ฆฌ ์ ๋ณด๋ ํจ๊ป ํ์ฉ๋ฉ๋๋ค.)
|
| 33 |
+
* **ํต์ฌ ๊ฐ๋
(Lift, ํฅ์๋):** ํ๊ท ์ ์ธ ์ฌ์ฉ ํ๋ฅ ๋๋น, ํน์ ์กฐ๊ฑด์์ ์ฌ์ฉ ํ๋ฅ ์ด ์ผ๋ง๋ ๋์์ง๋์ง๋ฅผ ๋ด
๋๋ค. ๊ธฐ์ค๊ฐ์ **1**์
๋๋ค.
|
| 34 |
+
* **ํ๋จ ๊ธฐ์ค:**
|
| 35 |
+
* **Lift > 1 (์ถ์ฒ):** ํ๊ท ๋ณด๋ค ์ด ์กฐ๊ฑด์์ ๋ ์์ฃผ ์ฐ์ (๊ถํฉ์ด ์ข์)
|
| 36 |
+
* **Lift โ 1 (๋ณดํต):** ํ๊ท ์ ์ธ ์์ค์ผ๋ก ์ฐ์
|
| 37 |
+
* **Lift < 1 (๋น์ถ์ฒ):** ํ๊ท ๋ณด๋ค ์ด ์กฐ๊ฑด์์ ๋ ์ฐ์ (๊ถํฉ์ด ์ ์ข์)
|
| 38 |
+
* **์์ (์กฐ๋ฆฌ๋ฒ: ๋์ด๊ธฐ):** ๋๋ถ(Lift **> 1**, ๋์ผ ๋ ํ์), ์์ถ(Lift **< 1**, ๋์ผ ๋ ์ ์)
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
#### ๐ ์ถ์ฒ ์๊ณ ๋ฆฌ์ฆ ์ฌํ: ์ด๋ป๊ฒ ์ต์ ์ ์ฌ๋ฃ๋ฅผ ์ฐพ์๋ผ๊น?
|
| 43 |
+
|
| 44 |
+
**1. ๋จ์ผ ์ฌ๋ฃ ๋์ฒด (Best N ์ฐพ๊ธฐ)**
|
| 45 |
+
* ์ 1~3๋จ๊ณ ์ ์์ ๊ฐ์ค์น๋ฅผ ์ ์ฉํ **์ต์ข
์ข
ํฉ ์ ์**๋ฅผ ๊ณ์ฐํ๊ณ , ์ ์๊ฐ ๊ฐ์ฅ ๋์ ์์๋๋ก ์์ N๊ฐ์ ์ฌ๋ฃ๋ฅผ ์ถ์ฒํฉ๋๋ค.
|
| 46 |
+
|
| 47 |
+
**2. ๋ค์ค ์ฌ๋ฃ ๋์ฒด (์ต์ ์กฐํฉ ์ฐพ๊ธฐ - ๋น ์์น)**
|
| 48 |
+
* ์ฌ๋ฌ ์ฌ๋ฃ๋ฅผ ๋์์ ๋ฐ๊ฟ ๋๋ ๊ฒฝ์ฐ์ ์๊ฐ ํญ๋ฐ์ ์ผ๋ก ๋์ด๋ฉ๋๋ค. ์ด๋ ํจ์จ์ ์ธ ํ์์ ์ํด **'๋น ์์น(Beam Search)'**๋ฅผ ์ฌ์ฉํฉ๋๋ค.
|
| 49 |
+
* **์๋ ์๋ฆฌ (๋งค ๋จ๊ณ๋ง๋ค):**
|
| 50 |
+
1. ํ์ฌ๊น์ง ๊ตฌ์ฑ๋ ์กฐํฉ์ ์๋ก์ด ์ฌ๋ฃ ํ๋ณด๋ฅผ ํ๋์ฉ ์ถ๊ฐํด๋ด
๋๋ค.
|
| 51 |
+
2. ์๋ก์ด ์กฐํฉ์ ์ ์๋ฅผ ๊ณ์ฐํฉ๋๋ค. (์ ์ = ํ์ฌ๊น์ง์ ์ ์ + ์ ์ฌ๋ฃ์ AI ์ ์)
|
| 52 |
+
3. ๋ชจ๋ ํ๋ณด ์กฐํฉ ์ค **์ ์๊ฐ ๊ฐ์ฅ ๋์ ์์ K๊ฐ(Beam Width)์ ์กฐํฉ๋ง ๋จ๊ธฐ๊ณ ** ๋๋จธ์ง๋ ๋ฒ๋ฆฝ๋๋ค.
|
| 53 |
+
4. ์ด ๊ณผ์ ์ ๋ชฉํํ ์ฌ๋ฃ ์๋งํผ ๋ฐ๋ณตํ์ฌ ์ต์ข
์ ์ผ๋ก ๊ฐ์ฅ ์ข์ ์กฐํฉ์ ์ฐพ์๋
๋๋ค.
|
| 54 |
+
> **๐ก ๋น์ :** ์ด๋์ด ์ฒ์์์ ๋ณด๋ฌผ์ ์ฐพ์ ๋, ์ฌ๋ฌ ๊ฐ๋ ๊ธธ ์ค ๊ฐ์ฅ ๋ฐ์ ๋น์ด ๋น์ถ๋ ๊ธธ 3๊ณณ(K=3)๋ง ๊ณจ๋ผ์ ๊ณ์ ๋ฐ๋ผ๊ฐ๋ ๊ฒ๊ณผ ๊ฐ์ต๋๋ค.
|
| 55 |
+
|
| 56 |
+
---
|
| 57 |
+
|
| 58 |
+
#### ๐ ์ต์ข
์ข
ํฉ ์ ์ ๊ณ์ฐ ์์ (๊ฐ์ค์น ์ ์ฉ)
|
| 59 |
+
**(์๋๋ฆฌ์ค: ๊น์น์ฐ๊ฐ(๋์ด๊ธฐ, ๊ตญ/ํ)์์ '๋ผ์ง๊ณ ๊ธฐ' ๋์ '์ฐธ์น์บ' ์ถ์ฒ ์)**
|
| 60 |
+
|
| 61 |
+
* **1. ์๋ฏธ ์ ์:** 0.70 ร ๊ฐ์ค์น 5.0 = **3.50**
|
| 62 |
+
* **2. ๋ฌธ๋งฅ ์ ์:** 0.95 ร ๊ฐ์ค์น 1.0 = **0.95** (๊ฐ์ '๋์ด๊ธฐ' ์๋ฆฌ๋ค๊ณผ ๋น๊ต)
|
| 63 |
+
* **3. ์กฐ๋ฆฌ ํต๊ณ:** 0.90 ร ๊ฐ์ค์น 1.0 = **0.90** ('๋์ด๊ธฐ' ๋ฐ์ดํฐ ๊ฒ์ฆ)
|
| 64 |
+
* **4. ๋ถ๋ฅ ํต๊ณ:** 0.85 ร ๊ฐ์ค์น 1.0 = **0.85** ('๊ตญ/ํ' ๋ฐ์ดํฐ ๊ฒ์ฆ)
|
| 65 |
+
|
| 66 |
+
๐ **์ด์ :** 3.50 + 0.95 + 0.90 + 0.85 = **6.20** / (์ด ๊ฐ์ค์น 8.0) = **์ต์ข
์ ํฉ๋ 77.5%**
|
| 67 |
+
|
| 68 |
+
---
|
| 69 |
+
|
| 70 |
+
#### ๐ฐ ์์ ์๊ฐ ๋ณ๋ (๋ณ๋ ๊ณ์ฐ)
|
| 71 |
+
AI ์ ์์ ๋ณ๊ฐ๋ก ์ ๊ณต๋๋ ์ฐธ๊ณ ์ ๋ณด์
๋๋ค. ์ค์๊ฐ ์ ํํ ์์ธ๊ฐ ์๋, **์ฌ์ ์ ์ ์๋ ์ฌ๋ฃ๋ณ ์๋์ ๊ฐ๊ฒฉ ๋ฑ๊ธ(1~5๋ฑ๊ธ)**์ ๊ธฐ์ค์ผ๋ก ๊ณ์ฐ๋ฉ๋๋ค.
|
| 72 |
+
* **์:** ๋ผ์ง๊ณ ๊ธฐ(4๋ฑ๊ธ) โก๏ธ ๋๋ถ(2๋ฑ๊ธ) ๋์ฒด ์ `4 - 2 = +2` (๐ข ์ด +2๋จ๊ณ ์ ๊ฐ ์์)
|
fly.toml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# fly.toml app configuration file generated for k-recipe2vec
|
| 2 |
+
#
|
| 3 |
+
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
|
| 4 |
+
#
|
| 5 |
+
|
| 6 |
+
app = 'k-recipe2vec'
|
| 7 |
+
primary_region = 'sjc'
|
| 8 |
+
|
| 9 |
+
[build]
|
| 10 |
+
|
| 11 |
+
[http_service]
|
| 12 |
+
internal_port = 8080
|
| 13 |
+
force_https = true
|
| 14 |
+
auto_stop_machines = 'stop'
|
| 15 |
+
auto_start_machines = true
|
| 16 |
+
min_machines_running = 1
|
| 17 |
+
processes = ['app']
|
| 18 |
+
|
| 19 |
+
[[vm]]
|
| 20 |
+
memory = '512mb'
|
| 21 |
+
cpus = 1
|
| 22 |
+
cpu_kind = 'shared'
|
| 23 |
+
|
| 24 |
+
swap_size_mb = 512
|
| 25 |
+
|
| 26 |
+
[[services.http_checks]]
|
| 27 |
+
interval = "10s"
|
| 28 |
+
timeout = "5s"
|
| 29 |
+
grace_period = "600s" # ์ค์: ์ฑ ์์ ๋๊ธฐ ์๊ฐ์ 10๋ถ์ผ๋ก ์ค์
|
| 30 |
+
restart_limit = 0
|
| 31 |
+
method = "get"
|
| 32 |
+
path = "/"
|
models/d2v.model
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b5e9a9c8c6bc7acaa809206c065679f4535bd521da269e528cca50914276875b
|
| 3 |
+
size 10299708
|
models/d2v.model.dv.vectors.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d3b053c724d19a192980f81be997c4a5750588cff4ae50b5e86cdf985bafdf47
|
| 3 |
+
size 52294128
|
models/w2v.model
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:29715dd37205c6f72615f4dbd5cdf59ddd2a757aec82959ba1aa1f6c630496c2
|
| 3 |
+
size 4529859
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit
|
| 2 |
+
pandas
|
| 3 |
+
numpy
|
| 4 |
+
gensim
|
| 5 |
+
scikit-learn
|
| 6 |
+
matplotlib
|
| 7 |
+
supabase
|
| 8 |
+
wordcloud
|
| 9 |
+
plotly
|
| 10 |
+
|
| 11 |
+
fastapi
|
| 12 |
+
uvicorn
|
src/api.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
from typing import List, Optional
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import numpy as np
|
| 9 |
+
import threading
|
| 10 |
+
|
| 11 |
+
# ๊ฐ์ ํด๋์ logic.py ์ํฌํธ๋ฅผ ์ํด ๊ฒฝ๋ก ์ถ๊ฐ
|
| 12 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 13 |
+
|
| 14 |
+
import logic
|
| 15 |
+
|
| 16 |
+
app = FastAPI(title="K-Recipe2Vec API")
|
| 17 |
+
|
| 18 |
+
# CORS ์ค์
|
| 19 |
+
app.add_middleware(
|
| 20 |
+
CORSMiddleware,
|
| 21 |
+
allow_origins=["*"],
|
| 22 |
+
allow_credentials=True,
|
| 23 |
+
allow_methods=["*"],
|
| 24 |
+
allow_headers=["*"],
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# --- Request Models ---
|
| 28 |
+
class IngredientRequest(BaseModel):
|
| 29 |
+
recipe_id: int
|
| 30 |
+
target: List[str]
|
| 31 |
+
stopwords: List[str] = []
|
| 32 |
+
w_w2v: float = 0.5
|
| 33 |
+
w_d2v: float = 0.5
|
| 34 |
+
w_method: float = 0.0
|
| 35 |
+
w_cat: float = 0.0
|
| 36 |
+
|
| 37 |
+
class CustomContextRequest(BaseModel):
|
| 38 |
+
context_ings: List[str]
|
| 39 |
+
target: List[str]
|
| 40 |
+
stopwords: List[str] = []
|
| 41 |
+
w_w2v: float = 0.5
|
| 42 |
+
w_d2v: float = 0.5
|
| 43 |
+
excluded: List[str] = []
|
| 44 |
+
|
| 45 |
+
# --- Startup ---
|
| 46 |
+
@app.on_event("startup")
|
| 47 |
+
def startup_event():
|
| 48 |
+
# ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋์์ ๋ชจ๋ธ ๋ก๋ฉ ์์ (์ฑ ์์์ ๋ง์ง ์์)
|
| 49 |
+
threading.Thread(target=logic.load_resources).start()
|
| 50 |
+
|
| 51 |
+
# --- Endpoints ---
|
| 52 |
+
|
| 53 |
+
@app.get("/")
|
| 54 |
+
def health_check():
|
| 55 |
+
# ๋ชจ๋ธ ๋ก๋ฉ ์ํ ํ์ธ
|
| 56 |
+
status = "loading" if logic.df is None else "ok"
|
| 57 |
+
return {"status": status, "service": "K-Recipe2Vec API"}
|
| 58 |
+
|
| 59 |
+
@app.get("/recipes/search")
|
| 60 |
+
def search_recipes(q: str):
|
| 61 |
+
"""์๋ฆฌ๋ช
์ผ๋ก ๋ ์ํผ ๊ฒ์"""
|
| 62 |
+
logic.ensure_initialized() # ๋ก๋ฉ ๋๊ธฐ
|
| 63 |
+
|
| 64 |
+
if not q: return []
|
| 65 |
+
try:
|
| 66 |
+
mask = logic.df['์๋ฆฌ๋ช
'].str.contains(q, case=False, na=False)
|
| 67 |
+
results = logic.df.loc[mask, ['๋ ์ํผ์ผ๋ จ๋ฒํธ', '์๋ฆฌ๋ช
', '์ฌ๋ฃํ ํฐ']].head(20)
|
| 68 |
+
|
| 69 |
+
output = []
|
| 70 |
+
for _, row in results.iterrows():
|
| 71 |
+
output.append({
|
| 72 |
+
"id": int(row['๋ ์ํผ์ผ๋ จ๋ฒํธ']),
|
| 73 |
+
"name": row['์๋ฆฌ๋ช
'],
|
| 74 |
+
"ingredients": row['์ฌ๋ฃํ ํฐ']
|
| 75 |
+
})
|
| 76 |
+
return output
|
| 77 |
+
except Exception as e:
|
| 78 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 79 |
+
|
| 80 |
+
@app.get("/recipes/{recipe_id}")
|
| 81 |
+
def get_recipe_detail(recipe_id: int):
|
| 82 |
+
"""๋ ์ํผ ์์ธ ์ ๋ณด ์กฐํ"""
|
| 83 |
+
logic.ensure_initialized() # ๋ก๋ฉ ๋๊ธฐ
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
row = logic.df[logic.df['๋ ์ํผ์ผ๋ จ๋ฒํธ'] == recipe_id]
|
| 87 |
+
if row.empty:
|
| 88 |
+
raise HTTPException(status_code=404, detail="Recipe not found")
|
| 89 |
+
|
| 90 |
+
row = row.iloc[0]
|
| 91 |
+
return {
|
| 92 |
+
"id": int(row['๋ ์ํผ์ผ๋ จ๋ฒํธ']),
|
| 93 |
+
"name": row['์๋ฆฌ๋ช
'],
|
| 94 |
+
"method": row['์๋ฆฌ๋ฐฉ๋ฒ๋ณ๋ช
'],
|
| 95 |
+
"category": row['์๋ฆฌ์ข
๋ฅ๋ณ๋ช
_์ธ๋ถํ'],
|
| 96 |
+
"ingredients": row['์ฌ๋ฃํ ํฐ']
|
| 97 |
+
}
|
| 98 |
+
except Exception as e:
|
| 99 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 100 |
+
|
| 101 |
+
@app.post("/recommend/db/single")
|
| 102 |
+
def recommend_db_single(req: IngredientRequest):
|
| 103 |
+
"""DB ๋ ์ํผ ๊ธฐ๋ฐ ๋จ์ผ ์ฌ๋ฃ ๋์ฒด"""
|
| 104 |
+
logic.ensure_initialized() # ๋ก๋ฉ ๋๊ธฐ
|
| 105 |
+
|
| 106 |
+
if not req.target:
|
| 107 |
+
raise HTTPException(status_code=400, detail="Target ingredient required")
|
| 108 |
+
try:
|
| 109 |
+
df = logic.substitute_single(
|
| 110 |
+
req.recipe_id, req.target[0], req.stopwords,
|
| 111 |
+
req.w_w2v, req.w_d2v, req.w_method, req.w_cat
|
| 112 |
+
)
|
| 113 |
+
if df.empty: return []
|
| 114 |
+
df = df.replace([np.inf, -np.inf], 0).fillna(0)
|
| 115 |
+
return df.to_dict(orient="records")
|
| 116 |
+
except Exception as e:
|
| 117 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 118 |
+
|
| 119 |
+
@app.post("/recommend/db/multi")
|
| 120 |
+
def recommend_db_multi(req: IngredientRequest):
|
| 121 |
+
"""DB ๋ ์ํผ ๊ธฐ๋ฐ ๋ค์ค ์ฌ๋ฃ ๋์ฒด"""
|
| 122 |
+
logic.ensure_initialized() # ๋ก๋ฉ ๋๊ธฐ
|
| 123 |
+
|
| 124 |
+
try:
|
| 125 |
+
results = logic.substitute_multi(
|
| 126 |
+
req.recipe_id, req.target, req.stopwords,
|
| 127 |
+
req.w_w2v, req.w_d2v, req.w_method, req.w_cat
|
| 128 |
+
)
|
| 129 |
+
formatted = []
|
| 130 |
+
for subs, score, saving in results:
|
| 131 |
+
formatted.append({
|
| 132 |
+
"substitutes": subs,
|
| 133 |
+
"score": float(score),
|
| 134 |
+
"saving_score": int(saving)
|
| 135 |
+
})
|
| 136 |
+
return formatted
|
| 137 |
+
except Exception as e:
|
| 138 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 139 |
+
|
| 140 |
+
@app.post("/recommend/custom/single")
|
| 141 |
+
def recommend_custom_single(req: CustomContextRequest):
|
| 142 |
+
"""์ฌ์ฉ์ ์ ์ ์ฌ๋ฃ ๊ธฐ๋ฐ ๋จ์ผ ๋์ฒด"""
|
| 143 |
+
logic.ensure_initialized() # ๋ก๋ฉ ๋๊ธฐ
|
| 144 |
+
|
| 145 |
+
if not req.target:
|
| 146 |
+
raise HTTPException(status_code=400, detail="Target ingredient required")
|
| 147 |
+
try:
|
| 148 |
+
df = logic.substitute_single_custom(
|
| 149 |
+
req.target[0], req.context_ings, req.stopwords,
|
| 150 |
+
req.w_w2v, req.w_d2v, excluded_ings=req.excluded
|
| 151 |
+
)
|
| 152 |
+
if df.empty: return []
|
| 153 |
+
df = df.replace([np.inf, -np.inf], 0).fillna(0)
|
| 154 |
+
return df.to_dict(orient="records")
|
| 155 |
+
except Exception as e:
|
| 156 |
+
raise HTTPException(status_code=500, detail=str(e))
|
src/app.py
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import logic
|
| 5 |
+
import os
|
| 6 |
+
from datetime import datetime, timedelta, timezone
|
| 7 |
+
from wordcloud import WordCloud
|
| 8 |
+
import matplotlib.pyplot as plt
|
| 9 |
+
# [NEW] 3D ์๊ฐํ๋ฅผ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
|
| 10 |
+
import plotly.express as px
|
| 11 |
+
from sklearn.decomposition import PCA
|
| 12 |
+
import numpy as np
|
| 13 |
+
|
| 14 |
+
# -------------------------------------------------------------------------
|
| 15 |
+
# 1. ํ์ด์ง ๊ธฐ๋ณธ ์ค์ & ์ธ์
์ํ ์ด๊ธฐํ
|
| 16 |
+
# -------------------------------------------------------------------------
|
| 17 |
+
st.set_page_config(page_title="AI ํ์ ์ฌ๋ฃ ์ถ์ฒ", layout="wide")
|
| 18 |
+
st.title("๐ณ AI ์์ฌ๋ฃ ๋์ฒด ์ถ์ฒ ๋์๋ณด๋")
|
| 19 |
+
|
| 20 |
+
# [NEW] ํฌํธํด๋ฆฌ์ค ์คํ์ผ ์ ์ฉ (Nanum Pen Script & Theme)
|
| 21 |
+
st.markdown("""
|
| 22 |
+
<style>
|
| 23 |
+
@import url('https://fonts.googleapis.com/css2?family=Nanum+Pen+Script&display=swap');
|
| 24 |
+
|
| 25 |
+
html, body, [class*="css"] {
|
| 26 |
+
font-family: 'Nanum Pen Script', cursive !important;
|
| 27 |
+
font-size: 1.25rem;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/* ์ ๋ชฉ ํฐํธ ํฌ๊ธฐ ํค์ฐ๊ธฐ */
|
| 31 |
+
h1 { font-size: 3.5rem !important; color: #1e293b !important; }
|
| 32 |
+
h2 { font-size: 2.8rem !important; color: #334155 !important; }
|
| 33 |
+
h3 { font-size: 2.2rem !important; color: #475569 !important; }
|
| 34 |
+
|
| 35 |
+
/* ๋ฐฐ๊ฒฝ ๊ทธ๋ผ๋ฐ์ด์
(ํฌํธํด๋ฆฌ์ค์ ์ ์ฌํ๊ฒ) */
|
| 36 |
+
.stApp {
|
| 37 |
+
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%) !important;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* ์ค๋ช
๋ฐ์ค ์คํ์ผ */
|
| 41 |
+
div[data-testid="stMarkdownContainer"] > div {
|
| 42 |
+
font-family: 'Nanum Pen Script', cursive !important;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/* ๋ฒํผ ์คํ์ผ */
|
| 46 |
+
.stButton > button {
|
| 47 |
+
background: #3b82f6 !important;
|
| 48 |
+
color: white !important;
|
| 49 |
+
border-radius: 12px !important;
|
| 50 |
+
border: none !important;
|
| 51 |
+
font-family: 'Nanum Pen Script', cursive !important;
|
| 52 |
+
font-size: 1.4rem !important;
|
| 53 |
+
padding: 0.5rem 1rem !important;
|
| 54 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
| 55 |
+
transition: all 0.2s;
|
| 56 |
+
}
|
| 57 |
+
.stButton > button:hover {
|
| 58 |
+
transform: scale(1.05);
|
| 59 |
+
opacity: 0.9;
|
| 60 |
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/* ์
๋ ฅ ํ๋ ์คํ์ผ */
|
| 64 |
+
.stTextInput > div > div > input, .stTextArea > div > div > textarea {
|
| 65 |
+
background-color: rgba(255, 255, 255, 0.9) !important;
|
| 66 |
+
border-radius: 10px !important;
|
| 67 |
+
border: 1px solid #cbd5e1 !important;
|
| 68 |
+
font-family: 'Nanum Pen Script', cursive !important;
|
| 69 |
+
font-size: 1.2rem !important;
|
| 70 |
+
color: #1e293b !important;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/* Expander ์คํ์ผ */
|
| 74 |
+
.streamlit-expanderHeader {
|
| 75 |
+
font-family: 'Nanum Pen Script', cursive !important;
|
| 76 |
+
font-size: 1.3rem !important;
|
| 77 |
+
background-color: rgba(255, 255, 255, 0.5) !important;
|
| 78 |
+
border-radius: 8px !important;
|
| 79 |
+
}
|
| 80 |
+
</style>
|
| 81 |
+
""", unsafe_allow_html=True)
|
| 82 |
+
|
| 83 |
+
if 'voted_logs' not in st.session_state: st.session_state['voted_logs'] = set()
|
| 84 |
+
if "stopword_input_field" not in st.session_state: st.session_state["stopword_input_field"] = ""
|
| 85 |
+
if "board_nick_input" not in st.session_state: st.session_state["board_nick_input"] = ""
|
| 86 |
+
if "board_msg_input" not in st.session_state: st.session_state["board_msg_input"] = ""
|
| 87 |
+
if "feedback_input_field" not in st.session_state: st.session_state["feedback_input_field"] = ""
|
| 88 |
+
|
| 89 |
+
# -------------------------------------------------------------------------
|
| 90 |
+
# 2. ํฌํผ ํจ์ ๋ฐ ๋ค์ด์ผ๋ก๊ทธ
|
| 91 |
+
# -------------------------------------------------------------------------
|
| 92 |
+
def format_saving(score, is_multi=False):
|
| 93 |
+
prefix = "์ด " if is_multi else ""
|
| 94 |
+
if score > 0: return f"๐ข {prefix}+{score}๋จ๊ณ (์ ๊ฐ)"
|
| 95 |
+
elif score < 0: return f"๐ด {prefix}{score}๋จ๊ณ (๋น์)"
|
| 96 |
+
else: return "โช ๋์ผ ์์ค"
|
| 97 |
+
|
| 98 |
+
@st.dialog("๐ง AI ์ถ์ฒ ์๊ณ ๋ฆฌ์ฆ ์๋ ์๋ฆฌ ์์ธ", width="large")
|
| 99 |
+
def show_logic_dialog():
|
| 100 |
+
if os.path.exists("flowchart.png"):
|
| 101 |
+
st.image("flowchart.png", use_container_width=True)
|
| 102 |
+
try:
|
| 103 |
+
with open("docs/logic_explanation.md", "r", encoding="utf-8") as f:
|
| 104 |
+
markdown_text = f.read()
|
| 105 |
+
st.markdown("---")
|
| 106 |
+
st.markdown(markdown_text)
|
| 107 |
+
except:
|
| 108 |
+
st.error("์ค๋ช
ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค.")
|
| 109 |
+
|
| 110 |
+
@st.dialog("โ๏ธ ๊ฒ์ ํธ๋ ๋ ์๋ํด๋ผ์ฐ๋", width="large")
|
| 111 |
+
def show_wordcloud_dialog(timeframe_text, text_data):
|
| 112 |
+
st.subheader(f"{timeframe_text} ๋ง์ด ๊ฒ์๋ ํ๊ฒ ์ฌ๋ฃ")
|
| 113 |
+
if not text_data:
|
| 114 |
+
st.info("๋ฐ์ดํฐ๊ฐ ์ถฉ๋ถํ์ง ์์ต๋๋ค.")
|
| 115 |
+
return
|
| 116 |
+
font_path = "src/font.ttf" if os.path.exists("src/font.ttf") else None
|
| 117 |
+
try:
|
| 118 |
+
wordcloud = WordCloud(font_path=font_path, width=800, height=400, background_color='white', colormap='viridis', random_state=42).generate(text_data)
|
| 119 |
+
fig, ax = plt.subplots(figsize=(10, 5))
|
| 120 |
+
ax.imshow(wordcloud, interpolation='bilinear'); ax.axis('off')
|
| 121 |
+
st.pyplot(fig)
|
| 122 |
+
if not font_path: st.caption("โ ๏ธ ํ๊ธ ํฐํธ ํ์ผ์ด ์์ด ๊ธ์๊ฐ ๊นจ์ง ์ ์์ต๋๋ค.")
|
| 123 |
+
except Exception as e: st.error(f"์ค๋ฅ ๋ฐ์: {e}")
|
| 124 |
+
|
| 125 |
+
# [NEW] 3D ๋ฒกํฐ ๊ณต๊ฐ ์๊ฐํ ํ์
|
| 126 |
+
@st.dialog("๐ ์ฌ๋ฃ ๋ฒกํฐ ๊ณต๊ฐ (3D Visualization)", width="large")
|
| 127 |
+
def show_3d_space_dialog():
|
| 128 |
+
st.caption("AI๊ฐ ํ์ตํ ์ฌ๋ฃ๋ค์ ๊ด๊ณ๋ฅผ 3์ฐจ์ ๊ณต๊ฐ์์ ํ์ธํด๋ณด์ธ์. (์์ 300๊ฐ ์ฌ๋ฃ)")
|
| 129 |
+
|
| 130 |
+
try:
|
| 131 |
+
# logic.py์์ ๋ก๋๋ Word2Vec ๋ชจ๋ธ ๊ฐ์ ธ์ค๊ธฐ
|
| 132 |
+
model = logic.w2v_model
|
| 133 |
+
|
| 134 |
+
# ๋น๋์ ์์ 300๊ฐ ๋จ์ด ์ถ์ถ
|
| 135 |
+
words = model.wv.index_to_key[:300]
|
| 136 |
+
vectors = np.array([model.wv[word] for word in words])
|
| 137 |
+
|
| 138 |
+
# PCA๋ก 100์ฐจ์ -> 3์ฐจ์ ์ถ์
|
| 139 |
+
pca = PCA(n_components=3)
|
| 140 |
+
projections = pca.fit_transform(vectors)
|
| 141 |
+
|
| 142 |
+
# ๋ฐ์ดํฐํ๋ ์ ์์ฑ
|
| 143 |
+
df_vis = pd.DataFrame(projections, columns=['x', 'y', 'z'])
|
| 144 |
+
df_vis['word'] = words
|
| 145 |
+
|
| 146 |
+
# Plotly 3D ์ฐ์ ๋ ๊ทธ๋ฆฌ๊ธฐ
|
| 147 |
+
fig = px.scatter_3d(
|
| 148 |
+
df_vis, x='x', y='y', z='z',
|
| 149 |
+
text='word',
|
| 150 |
+
hover_name='word',
|
| 151 |
+
color='z', # ๋์ด์ ๋ฐ๋ผ ์์ ๋ณํ
|
| 152 |
+
color_continuous_scale='Viridis'
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
fig.update_traces(
|
| 156 |
+
marker=dict(size=4, opacity=0.8),
|
| 157 |
+
textposition='top center',
|
| 158 |
+
textfont=dict(size=10, color='black') # ํ
์คํธ ์คํ์ผ
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
fig.update_layout(
|
| 162 |
+
height=600,
|
| 163 |
+
scene=dict(
|
| 164 |
+
xaxis=dict(showticklabels=False, title=''),
|
| 165 |
+
yaxis=dict(showticklabels=False, title=''),
|
| 166 |
+
zaxis=dict(showticklabels=False, title='')
|
| 167 |
+
),
|
| 168 |
+
margin=dict(l=0, r=0, b=0, t=0)
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 172 |
+
st.info("๐ก **ํ:** ๋ง์ฐ์ค๋ก ๋๋๊ทธํ์ฌ ํ์ ํ๊ฑฐ๋ ํ ๋ก ํ๋/์ถ์ํ ์ ์์ต๋๋ค. ๊ฐ๊น์ด ์๋ ์ฌ๋ฃ๋ค์ AI๊ฐ '๋น์ทํ ์ฑ์ง'๋ก ์ธ์ํ ๊ฒ์
๋๋ค.")
|
| 173 |
+
|
| 174 |
+
except Exception as e:
|
| 175 |
+
st.error(f"์๊ฐํ ์์ฑ ์คํจ: {e}")
|
| 176 |
+
|
| 177 |
+
# [CALLBACK] ํจ์๋ค
|
| 178 |
+
def handle_board_submission():
|
| 179 |
+
nick = st.session_state.get("board_nick_input", "")
|
| 180 |
+
msg = st.session_state.get("board_msg_input", "")
|
| 181 |
+
if nick and msg:
|
| 182 |
+
if logic.save_board_message(nick, msg):
|
| 183 |
+
st.toast("๊ฒ์๊ธ์ด ๋ฑ๋ก๋์์ต๋๋ค!", icon="โ
")
|
| 184 |
+
st.session_state["board_nick_input"] = ""
|
| 185 |
+
st.session_state["board_msg_input"] = ""
|
| 186 |
+
else: st.toast("๊ฒ์๊ธ ๋ฑ๋ก์ ์คํจํ์ต๋๋ค.", icon="โ")
|
| 187 |
+
else: st.toast("๋๋ค์๊ณผ ๋ด์ฉ์ ๋ชจ๋ ์
๋ ฅํด์ฃผ์ธ์.", icon="โ ๏ธ")
|
| 188 |
+
|
| 189 |
+
def handle_stopword_submission():
|
| 190 |
+
current_input = st.session_state.get("stopword_input_field", "")
|
| 191 |
+
if current_input:
|
| 192 |
+
is_success, msg = logic.save_stopwords_to_db(current_input)
|
| 193 |
+
if is_success:
|
| 194 |
+
st.toast(msg, icon="โ
")
|
| 195 |
+
st.session_state["stopword_input_field"] = ""
|
| 196 |
+
else: st.toast(msg, icon="โ")
|
| 197 |
+
else: st.toast("๋จ์ด๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.", icon="โ ๏ธ")
|
| 198 |
+
|
| 199 |
+
def handle_feedback_submission():
|
| 200 |
+
content = st.session_state.get("feedback_input_field", "")
|
| 201 |
+
if content:
|
| 202 |
+
if logic.save_feedback_to_db(content):
|
| 203 |
+
st.toast("์๊ฒฌ ๊ฐ์ฌํฉ๋๋ค!", icon="โ
")
|
| 204 |
+
st.balloons()
|
| 205 |
+
st.session_state["feedback_input_field"] = ""
|
| 206 |
+
else: st.toast("์ ์ก ์คํจ", icon="โ")
|
| 207 |
+
else: st.toast("๋ด์ฉ์ ์
๋ ฅํด์ฃผ์ธ์.", icon="โ ๏ธ")
|
| 208 |
+
|
| 209 |
+
# -------------------------------------------------------------------------
|
| 210 |
+
# 3. ์ฌ์ด๋๋ฐ UI
|
| 211 |
+
# -------------------------------------------------------------------------
|
| 212 |
+
with st.sidebar:
|
| 213 |
+
st.header("๐๏ธ ์ปจํธ๋กค ํจ๋")
|
| 214 |
+
selected_mode = st.radio("๋ชจ๋ ์ ํ", ["๐ Ver.1 ๊ธฐ์กด ๋ ์ํผ DB ๊ฒ์", "โจ Ver.2 ๋๋ง์ ์ฌ๋ฃ ์
๋ ฅ (์ปค์คํ
)"], index=0)
|
| 215 |
+
st.divider()
|
| 216 |
+
st.subheader("โ๏ธ ๊ฐ์ค์น ์ค์ ")
|
| 217 |
+
is_v1 = selected_mode == "๐ Ver.1 ๊ธฐ์กด ๋ ์ํผ DB ๊ฒ์"
|
| 218 |
+
w_w2v = st.slider("๋งยท์ฑ์ง (Word2Vec)", 0.0, 5.0, 5.0, 0.5)
|
| 219 |
+
w_d2v = st.slider("๋ฌธ๋งฅ (Doc2Vec)", 0.0, 5.0, 1.0, 0.5)
|
| 220 |
+
w_method = st.slider("์กฐ๋ฆฌ๋ฒ ํต๊ณ (Ver.1 ์ ์ฉ)", 0.0, 5.0, 1.0, 0.5, disabled=not is_v1)
|
| 221 |
+
w_cat = st.slider("์นดํ
๊ณ ๋ฆฌ ํต๊ณ (Ver.1 ์ ์ฉ)", 0.0, 5.0, 1.0, 0.5, disabled=not is_v1)
|
| 222 |
+
if not is_v1: st.caption("๐ก ์ปค์คํ
๋ชจ๋์์๋ ํต๊ณ ๊ฐ์ค์น๊ฐ ์ ์ฉ๋์ง ์์ต๋๋ค.")
|
| 223 |
+
|
| 224 |
+
excluded_ingredients = []
|
| 225 |
+
if not is_v1:
|
| 226 |
+
st.divider()
|
| 227 |
+
st.subheader("๐ซ ์ ์ธํ ์ฌ๋ฃ ์ค์ ")
|
| 228 |
+
all_ing_options = sorted(list(logic.all_ingredients_set))
|
| 229 |
+
excluded_ingredients = st.multiselect("์ ์ธํ ์ฌ๋ฃ ์ ํ", all_ing_options, placeholder="์: ๋
์ฝฉ, ์ค์ด")
|
| 230 |
+
|
| 231 |
+
st.divider()
|
| 232 |
+
# [NEW] 3D ์๊ฐํ ๋ฒํผ ์ถ๊ฐ
|
| 233 |
+
if st.button("๐ ์ฌ๋ฃ ์ฐ์ฃผ(3D) ํํํ๊ธฐ", use_container_width=True):
|
| 234 |
+
show_3d_space_dialog()
|
| 235 |
+
|
| 236 |
+
if st.button("๐ค ์ด๋ค ๊ณผ์ ์ ๊ฑฐ์ณ ์ฌ๋ฃ๊ฐ ์ถ์ฒ๋๋์?", use_container_width=True):
|
| 237 |
+
show_logic_dialog()
|
| 238 |
+
|
| 239 |
+
st.divider()
|
| 240 |
+
st.subheader("๐ ์ธ์ฌ์ดํธ ๋์๋ณด๋ (Beta)")
|
| 241 |
+
kst = timezone(timedelta(hours=9))
|
| 242 |
+
today_date_string = datetime.now(kst).strftime("%Y๋
%m์ %d์ผ")
|
| 243 |
+
stopwords_list = logic.load_global_stopwords()
|
| 244 |
+
|
| 245 |
+
tab_today, tab_all = st.tabs(["๐
์ค๋", "๐ ๋์ "])
|
| 246 |
+
|
| 247 |
+
wc_text_today = logic.get_wordcloud_text('today')
|
| 248 |
+
wc_text_all = logic.get_wordcloud_text('all')
|
| 249 |
+
today_count, today_dishes, today_targets = logic.get_usage_stats(timeframe='today')
|
| 250 |
+
all_count, all_dishes, all_targets = logic.get_usage_stats(timeframe='all')
|
| 251 |
+
|
| 252 |
+
with tab_today:
|
| 253 |
+
st.caption(f"๊ธฐ์ค์ผ: {today_date_string} (KST)")
|
| 254 |
+
col_m1, col_m2 = st.columns(2)
|
| 255 |
+
col_m1.metric("์ค๋ ์ฌ์ฉ๋", f"{today_count}๊ฑด")
|
| 256 |
+
col_m2.metric("๋์ ๋ถ์ฉ์ด", f"{len(stopwords_list)}๊ฐ")
|
| 257 |
+
if today_count > 0:
|
| 258 |
+
if st.button("โ๏ธ ์ค๋์ ์๋ํด๋ผ์ฐ๋", key="btn_wc_today", use_container_width=True):
|
| 259 |
+
show_wordcloud_dialog("์ค๋", wc_text_today)
|
| 260 |
+
st.caption("๐ฅ ์ค๋ ๋ง์ด ๋์ฒด๋ ์ฌ๋ฃ")
|
| 261 |
+
if not today_targets.empty: st.bar_chart(today_targets, color="#FF6B6B", height=200)
|
| 262 |
+
else: st.info("์ค๋์ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.")
|
| 263 |
+
|
| 264 |
+
with tab_all:
|
| 265 |
+
st.caption("์๋น์ค ์์ ์ดํ ์ ์ฒด ๋ฐ์ดํฐ")
|
| 266 |
+
col_a1, col_a2 = st.columns(2)
|
| 267 |
+
col_a1.metric("์ด ์ฌ์ฉ๋", f"{all_count}๊ฑด")
|
| 268 |
+
col_a2.metric("๋์ ๋ถ์ฉ์ด", f"{len(stopwords_list)}๊ฐ")
|
| 269 |
+
if all_count > 0:
|
| 270 |
+
if st.button("โ๏ธ ๋์ ์๋ํด๋ผ์ฐ๋", key="btn_wc_all", use_container_width=True):
|
| 271 |
+
show_wordcloud_dialog("๋์ ", wc_text_all)
|
| 272 |
+
st.caption("๐ฅ ์ญ๋ ๋ง์ด ๋์ฒด๋ ์ฌ๋ฃ")
|
| 273 |
+
if not all_targets.empty: st.bar_chart(all_targets, color="#FF6B6B", height=200)
|
| 274 |
+
else: st.info("๋์ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.")
|
| 275 |
+
|
| 276 |
+
with st.expander("๐ ์ ๊ณ ๋ ๋ถ์ฉ์ด ๋ชฉ๋ก ๋ณด๊ธฐ"):
|
| 277 |
+
if stopwords_list: st.dataframe(pd.DataFrame(stopwords_list, columns=["๋ถ์ฉ์ด"]), use_container_width=True, hide_index=True)
|
| 278 |
+
else: st.info("์ ๊ณ ๋ ๋ถ์ฉ์ด๊ฐ ์์ต๋๋ค.")
|
| 279 |
+
|
| 280 |
+
st.divider()
|
| 281 |
+
with st.expander("๐ฌ ์ต๋ช
๊ฒ์ํ (Beta)", expanded=True):
|
| 282 |
+
with st.form("board_form"):
|
| 283 |
+
st.text_input("๋๋ค์", placeholder="์ต๋ช
", key="board_nick_input")
|
| 284 |
+
st.text_area("๋ด์ฉ", placeholder="์์ ๋กญ๊ฒ ์๊ฒฌ์ ๋จ๊ฒจ์ฃผ์ธ์", height=80, key="board_msg_input")
|
| 285 |
+
st.form_submit_button("๋ฑ๋ก", on_click=handle_board_submission)
|
| 286 |
+
|
| 287 |
+
st.markdown("---")
|
| 288 |
+
messages = logic.get_board_messages()
|
| 289 |
+
if messages:
|
| 290 |
+
for m in messages:
|
| 291 |
+
st.markdown(f"**{m['nickname']}** <span style='color:grey; font-size:0.8em;'>({m['display_time']})</span>", unsafe_allow_html=True)
|
| 292 |
+
st.text(m['content'])
|
| 293 |
+
st.divider()
|
| 294 |
+
else: st.caption("์ฒซ ๋ฒ์งธ ๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!")
|
| 295 |
+
|
| 296 |
+
# -------------------------------------------------------------------------
|
| 297 |
+
# 4. ๋ฉ์ธ UI (๊ธฐ์กด๊ณผ ๋์ผ)
|
| 298 |
+
# -------------------------------------------------------------------------
|
| 299 |
+
col_main, _ = st.columns([0.9, 0.1])
|
| 300 |
+
with col_main:
|
| 301 |
+
if selected_mode == "๐ Ver.1 ๊ธฐ์กด ๋ ์ํผ DB ๊ฒ์":
|
| 302 |
+
st.markdown("""<div style="background-color: #f0f8ff; padding: 15px; border-radius: 10px; margin-bottom: 20px;"><h4 style="margin:0; color:#0066cc;">[Ver.1] ๋ ์ํผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๊ฒ์</h4><p style="margin:5px 0 0 0; font-size:14px;">ํ์ต๋ 12๋ง์ฌ ๊ฐ์ ๋ ์ํผ ์ค ํ๋๋ฅผ ์ ํํ์ฌ ๋ถ์ํฉ๋๋ค. ๋ชจ๋ ํต๊ณ ์ ์๊ฐ ํ์ฉ๋ฉ๋๋ค.</p></div>""", unsafe_allow_html=True)
|
| 303 |
+
search_keyword = st.text_input("๐ฝ๏ธ ์๋ฆฌ๋ช
๊ฒ์ (ํค์๋ ์
๋ ฅ ํ ์ํฐ)", placeholder="์: ๋์ฅ์ฐ๊ฐ")
|
| 304 |
+
final_dish_name = None
|
| 305 |
+
if search_keyword:
|
| 306 |
+
exact_match = logic.df[logic.df['์๋ฆฌ๋ช
'] == search_keyword]
|
| 307 |
+
exact_name = exact_match['์๋ฆฌ๋ช
'].iloc[0] if not exact_match.empty else None
|
| 308 |
+
candidates = logic.df[logic.df['์๋ฆฌ๋ช
'].str.contains(search_keyword, na=False, case=False)]
|
| 309 |
+
if exact_name: candidates = candidates[candidates['์๋ฆฌ๋ช
'] != exact_name]
|
| 310 |
+
candidate_names = sorted(candidates['์๋ฆฌ๋ช
'].unique().tolist())[:30]
|
| 311 |
+
options = []
|
| 312 |
+
if exact_name: options.append(exact_name)
|
| 313 |
+
options.extend(candidate_names)
|
| 314 |
+
if not options: st.warning(f"๐ '{search_keyword}'๊ฐ ํฌํจ๋ ์๋ฆฌ๋ช
์ ์ฐพ์ ์ ์์ต๋๋ค.")
|
| 315 |
+
else:
|
| 316 |
+
index_to_select = 0 if exact_name else None
|
| 317 |
+
label_msg = f"๐ '{search_keyword}' ๊ฒ์ ๊ฒฐ๊ณผ ({len(options)}๊ฐ)"
|
| 318 |
+
if exact_name: label_msg += " - ์ ํํ ์๋ฆฌ๋ช
์ด ๋ฐ๊ฒฌ๋์์ต๋๋ค!"
|
| 319 |
+
selected_option = st.selectbox(label_msg, options, index=index_to_select)
|
| 320 |
+
final_dish_name = selected_option
|
| 321 |
+
if final_dish_name:
|
| 322 |
+
st.success(f"โ
์ ํ๋ ์๋ฆฌ: **{final_dish_name}**")
|
| 323 |
+
cands = logic.df[logic.df['์๋ฆฌ๋ช
'] == final_dish_name]
|
| 324 |
+
cands = cands.head(10).reset_index(drop=True)
|
| 325 |
+
if cands.empty: st.error("โ ๋ ์ํผ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.")
|
| 326 |
+
else:
|
| 327 |
+
st.divider()
|
| 328 |
+
options = {}
|
| 329 |
+
for _, r in cands.iterrows():
|
| 330 |
+
preview = ', '.join(r['์ฌ๋ฃํ ํฐ'])
|
| 331 |
+
options[f"[{r['์๋ฆฌ๋ฐฉ๋ฒ๋ณ๋ช
']}] {r['์๋ฆฌ๋ช
']} (ID:{r['๋ ์ํผ์ผ๋ จ๋ฒํธ']}) - {preview}"] = r['๋ ์ํผ์ผ๋ จ๋ฒํธ']
|
| 332 |
+
selected_label = st.selectbox("๐ ๋ถ์ํ ๋ ์ํผ๋ฅผ ์ ํํ์ธ์", list(options.keys()))
|
| 333 |
+
recipe_id = options[selected_label]
|
| 334 |
+
c1, c2 = st.columns(2)
|
| 335 |
+
with c1: target_str = st.text_input("๐ฏ ๋ฐ๊ฟ ์ฌ๋ฃ", placeholder="๋ผ์ง๊ณ ๊ธฐ, ์ํ")
|
| 336 |
+
with c2: stop_str = st.text_input("๐ซ ์ ๊ฑฐํ ๋ฌธ๊ตฌ", placeholder="์ฝ๊ฐ, ์ํ์ฉ")
|
| 337 |
+
if target_str:
|
| 338 |
+
targets = [t.strip() for t in target_str.split(',') if t.strip()]
|
| 339 |
+
stops = [s.strip() for s in stop_str.split(',') if s.strip()]
|
| 340 |
+
current_recipe_row = logic.df[logic.df['๋ ์ํผ์ผ๋ จ๋ฒํธ'] == recipe_id].iloc[0]
|
| 341 |
+
recipe_ingredients = current_recipe_row['์ฌ๋ฃํ ํฐ']
|
| 342 |
+
invalid_targets = [t for t in targets if t not in recipe_ingredients]
|
| 343 |
+
if not targets: st.warning("ํ๊ฒ ์ฌ๋ฃ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.")
|
| 344 |
+
elif invalid_targets:
|
| 345 |
+
st.error(f"๐จ ๋ค์ ์ฌ๋ฃ๋ ์ ํํ ๋ ์ํผ์ ์์ต๋๋ค: {', '.join(invalid_targets)}")
|
| 346 |
+
st.info("๐ก ํ: ๋ ์ํผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์ ์๋ ์ฌ๋ฃ๋ช
์ ์ ํํ ์
๋ ฅํด์ฃผ์ธ์.")
|
| 347 |
+
else:
|
| 348 |
+
st.divider()
|
| 349 |
+
has_result = False
|
| 350 |
+
final_recs = []
|
| 351 |
+
if len(targets) == 1:
|
| 352 |
+
st.subheader("๐น ๋จ์ผ ์ฌ๋ฃ ๋์ฒด ์ถ์ฒ (DB ๊ธฐ๋ฐ)")
|
| 353 |
+
t = targets[0]
|
| 354 |
+
res = logic.substitute_single(recipe_id, t, stops, w_w2v, w_d2v, w_method, w_cat, topn=5)
|
| 355 |
+
st.markdown(f"**{t}** ๋์ฒด ๊ฒฐ๊ณผ")
|
| 356 |
+
if not res.empty:
|
| 357 |
+
has_result = True
|
| 358 |
+
final_recs = res['๋์ฒด์ฌ๋ฃ'].head(3).tolist()
|
| 359 |
+
d_df = res[['๋์ฒด์ฌ๋ฃ', '์ต์ข
์ ์', 'saving_score']].copy()
|
| 360 |
+
d_df['์์ ์๊ฐ๋ณ๋'] = d_df['saving_score'].apply(lambda x: format_saving(x))
|
| 361 |
+
d_df = d_df[['๋์ฒด์ฌ๋ฃ', '์ต์ข
์ ์', '์์ ์๊ฐ๋ณ๋']]
|
| 362 |
+
d_df.columns = ['์ถ์ฒ์ฌ๋ฃ', '์ ํฉ๋', '์์ ์๊ฐ๋ณ๋']
|
| 363 |
+
st.dataframe(d_df.style.format("{:.1%}", subset=['์ ํฉ๋']).background_gradient(cmap='Greens', subset=['์ ํฉ๋']), use_container_width=True, hide_index=True)
|
| 364 |
+
else: st.warning("๊ฒฐ๊ณผ ์์")
|
| 365 |
+
elif len(targets) > 1:
|
| 366 |
+
st.subheader("๐งฉ ์ต์ ์ ์ฌ๋ฃ ์กฐํฉ (DB ๊ธฐ๋ฐ ๋ค์ค ๋์ฒด)")
|
| 367 |
+
multi_res = logic.substitute_multi(recipe_id, targets, stops, w_w2v, w_d2v, w_method, w_cat)
|
| 368 |
+
if multi_res:
|
| 369 |
+
has_result = True
|
| 370 |
+
final_recs = [", ".join(subs) for subs, score, saving in multi_res]
|
| 371 |
+
m_df = pd.DataFrame([(f"{', '.join(subs)}", score, format_saving(saving, True)) for subs, score, saving in multi_res], columns=['์ถ์ฒ ์กฐํฉ', '์ข
ํฉ ์ ์', '์์ ์๊ฐ๋ณ๋ ํฉ๊ณ'])
|
| 372 |
+
st.dataframe(m_df.style.format("{:.1%}", subset=['์ข
ํฉ ์ ์']).background_gradient(cmap='Blues', subset=['์ข
ํฉ ์ ์']), use_container_width=True, hide_index=True)
|
| 373 |
+
else: st.info("์กฐํฉ์ ์ฐพ์ ์ ์์ต๋๋ค.")
|
| 374 |
+
if has_result:
|
| 375 |
+
current_state = f"DB_{final_dish_name}_{target_str}_{stop_str}_{w_w2v}_{w_d2v}_{w_method}_{w_cat}_{final_recs}"
|
| 376 |
+
if 'last_log' not in st.session_state: st.session_state['last_log'] = ""
|
| 377 |
+
if st.session_state['last_log'] != current_state:
|
| 378 |
+
log_id = logic.save_log_to_db(final_dish_name, target_str, stops, w_w2v, w_d2v, w_method, w_cat, rec_list=final_recs, is_custom=False)
|
| 379 |
+
st.session_state['current_log_id'] = log_id
|
| 380 |
+
st.session_state['last_log'] = current_state
|
| 381 |
+
if 'current_log_id' in st.session_state and st.session_state['current_log_id']:
|
| 382 |
+
cl_id = st.session_state['current_log_id']
|
| 383 |
+
is_voted = cl_id in st.session_state['voted_logs']
|
| 384 |
+
st.write(""); b1, b2, _ = st.columns([0.2, 0.2, 0.6])
|
| 385 |
+
if is_voted: b1.success("โ
ํ๊ฐ ์๋ฃ!"); b2.write("")
|
| 386 |
+
else:
|
| 387 |
+
b1.button("๐ ๋ง์กฑํด์", key="btn_sat_db", use_container_width=True, on_click=lambda: (logic.update_feedback_in_db(cl_id, "satisfy"), st.session_state['voted_logs'].add(cl_id), st.toast("๊ฐ์ฌํฉ๋๋ค!")))
|
| 388 |
+
b2.button("๐ ์์ฌ์์", key="btn_dis_db", use_container_width=True, on_click=lambda: (logic.update_feedback_in_db(cl_id, "dissatisfy"), st.session_state['voted_logs'].add(cl_id), st.toast("์๊ฒฌ ๊ฐ์ฌํฉ๋๋ค.")))
|
| 389 |
+
|
| 390 |
+
elif selected_mode == "โจ Ver.2 ๋๋ง์ ์ฌ๋ฃ ์
๋ ฅ (์ปค์คํ
)":
|
| 391 |
+
st.markdown("""<div style="background-color: #fff5f0; padding: 15px; border-radius: 10px; margin-bottom: 20px;"><h4 style="margin:0; color:#cc5500;">[Ver.2] ๋๋ง์ ์ฌ๋ฃ ๋ฆฌ์คํธ ์
๋ ฅ</h4><p style="margin:5px 0 0 0; font-size:14px;">๋์ฅ๊ณ ์ ์ฌ๋ฃ๋ค์ ์ง์ ์
๋ ฅํ์ธ์. ๋ฌธ๋งฅ์ ์ค์๊ฐ์ผ๋ก ๋ถ์ํ์ฌ ์ถ์ฒํฉ๋๋ค. (ํต๊ณ ์ ์ ์ ์ธ)</p></div>""", unsafe_allow_html=True)
|
| 392 |
+
st.markdown("##### ๐ท๏ธ ์๋ฆฌ๋ช
์
๋ ฅ (์ฐธ๊ณ ์ฉ)")
|
| 393 |
+
search_keyword_v2 = st.text_input("ํค์๋ ์
๋ ฅ ํ ์ํฐ (์: ๋ณถ์๋ฐฅ) - ์ ํ์ฌํญ", key="v2_search")
|
| 394 |
+
custom_dish_name = search_keyword_v2
|
| 395 |
+
if search_keyword_v2:
|
| 396 |
+
exact_match_v2 = logic.df[logic.df['์๋ฆฌ๋ช
'] == search_keyword_v2]
|
| 397 |
+
exact_name_v2 = exact_match_v2['์๋ฆฌ๋ช
'].iloc[0] if not exact_match_v2.empty else None
|
| 398 |
+
candidates_v2 = logic.df[logic.df['์๋ฆฌ๋ช
'].str.contains(search_keyword_v2, na=False, case=False)]
|
| 399 |
+
if exact_name_v2: candidates_v2 = candidates_v2[candidates_v2['์๋ฆฌ๋ช
'] != exact_name_v2]
|
| 400 |
+
candidate_names_v2 = sorted(candidates_v2['์๋ฆฌ๋ช
'].unique().tolist())[:30]
|
| 401 |
+
options_v2 = []
|
| 402 |
+
if exact_name_v2: options_v2.append(exact_name_v2)
|
| 403 |
+
options_v2.append("(์ง์ ์
๋ ฅํ ์ด๋ฆ ์ฌ์ฉ)")
|
| 404 |
+
options_v2.extend(candidate_names_v2)
|
| 405 |
+
if options_v2:
|
| 406 |
+
idx_v2 = 0 if exact_name_v2 else 0
|
| 407 |
+
label_v2 = f"๐ก ๊ด๋ จ ์๋ฆฌ๋ช
๋ฐ๊ฒฌ ({len(options_v2)-1}๊ฐ)"
|
| 408 |
+
if exact_name_v2: label_v2 += " - ์ ํํ ์๋ฆฌ๋ช
๋ฐ๊ฒฌ!"
|
| 409 |
+
sel_v2 = st.selectbox(label_v2, options_v2, index=idx_v2, key="v2_select")
|
| 410 |
+
if sel_v2 != "(์ง์ ์
๋ ฅํ ์ด๋ฆ ์ฌ์ฉ)": custom_dish_name = sel_v2
|
| 411 |
+
|
| 412 |
+
st.write("")
|
| 413 |
+
context_str = st.text_area("๐ ์ ์ฒด ์ฌ๋ฃ ๋ฆฌ์คํธ (์ผํ๋ก ๊ตฌ๋ถ)", placeholder="์: ๋ฐฅ, ๊ณ๋, ๋ํ, ๊ฐ์ฅ, ์ฐธ๊ธฐ๋ฆ", height=100, key="v2_context")
|
| 414 |
+
if context_str:
|
| 415 |
+
context_ings_list = [ing.strip() for ing in context_str.split(',') if ing.strip()]
|
| 416 |
+
if not context_ings_list: st.warning("์ฌ๋ฃ๋ฅผ ํ ๊ฐ ์ด์ ์
๋ ฅํด์ฃผ์ธ์.")
|
| 417 |
+
else:
|
| 418 |
+
st.caption(f"์ธ์๋ ์ฌ๋ฃ ({len(context_ings_list)}๊ฐ): {', '.join(context_ings_list)}")
|
| 419 |
+
c1_c, c2_c = st.columns(2)
|
| 420 |
+
with c1_c: target_str_c = st.text_input("๐ฏ ๋ฐ๊ฟ ์ฌ๋ฃ (์ ๋ฆฌ์คํธ ์ค)", placeholder="์: ๊ณ๋", key="v2_target")
|
| 421 |
+
with c2_c: stop_str_c = st.text_input("๐ซ ์ ๊ฑฐํ ๋ฌธ๊ตฌ (์์)", placeholder="์: ์ฝ๊ฐ", key="v2_stop")
|
| 422 |
+
if target_str_c:
|
| 423 |
+
targets_c = [t.strip() for t in target_str_c.split(',') if t.strip()]
|
| 424 |
+
stops_c = [s.strip() for s in stop_str_c.split(',') if s.strip()]
|
| 425 |
+
invalid_targets = [t for t in targets_c if t not in context_ings_list]
|
| 426 |
+
if invalid_targets: st.error(f"๐จ ๋ค์ ์ฌ๋ฃ๋ ์ ์ฒด ๋ฆฌ์คํธ์ ์์ต๋๋ค: {', '.join(invalid_targets)}")
|
| 427 |
+
elif not targets_c: st.warning("๋ฐ๊ฟ ์ฌ๋ฃ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.")
|
| 428 |
+
else:
|
| 429 |
+
st.divider()
|
| 430 |
+
has_result_c = False
|
| 431 |
+
final_recs_c = []
|
| 432 |
+
if len(targets_c) == 1:
|
| 433 |
+
st.subheader("๐น ๋จ์ผ ์ฌ๋ฃ ๋์ฒด ์ถ์ฒ (์ปค์คํ
)")
|
| 434 |
+
t_c = targets_c[0]
|
| 435 |
+
res_c = logic.substitute_single_custom(t_c, context_ings_list, stops_c, w_w2v, w_d2v, excluded_ings=excluded_ingredients, topn=5)
|
| 436 |
+
st.markdown(f"**{t_c}** ๋์ฒด ๊ฒฐ๊ณผ")
|
| 437 |
+
if not res_c.empty:
|
| 438 |
+
has_result_c = True
|
| 439 |
+
final_recs_c = res_c['๋์ฒด์ฌ๋ฃ'].head(3).tolist()
|
| 440 |
+
d_df_c = res_c[['๋์ฒด์ฌ๋ฃ', '์ต์ข
์ ์', 'saving_score']].copy()
|
| 441 |
+
d_df_c['์์ ์๊ฐ๋ณ๋'] = d_df_c['saving_score'].apply(lambda x: format_saving(x))
|
| 442 |
+
d_df_c = d_df_c[['๋์ฒด์ฌ๋ฃ', '์ต์ข
์ ์', '์์ ์๊ฐ๋ณ๋']]
|
| 443 |
+
d_df_c.columns = ['์ถ์ฒ์ฌ๋ฃ', '์ ํฉ๋', '์์ ์๊ฐ๋ณ๋']
|
| 444 |
+
st.dataframe(d_df_c.style.format("{:.1%}", subset=['์ ํฉ๋']).background_gradient(cmap='Greens', subset=['์ ํฉ๋']), use_container_width=True, hide_index=True)
|
| 445 |
+
else: st.warning("๊ฒฐ๊ณผ ์์")
|
| 446 |
+
elif len(targets_c) > 1:
|
| 447 |
+
st.subheader("๐งฉ ์ต์ ์ ์ฌ๋ฃ ์กฐํฉ (์ปค์คํ
๋ค์ค ๋์ฒด)")
|
| 448 |
+
multi_res_c = logic.substitute_multi_custom(targets_c, context_ings_list, stops_c, w_w2v, w_d2v, excluded_ings=excluded_ingredients)
|
| 449 |
+
if multi_res_c:
|
| 450 |
+
has_result_c = True
|
| 451 |
+
final_recs_c = [", ".join(subs) for subs, score, saving in multi_res_c]
|
| 452 |
+
m_df_c = pd.DataFrame([(f"{', '.join(subs)}", score, format_saving(saving, True)) for subs, score, saving in multi_res_c], columns=['์ถ์ฒ ์กฐํฉ', '์ข
ํฉ ์ ์', '์์ ์๊ฐ๋ณ๋ ํฉ๊ณ'])
|
| 453 |
+
st.dataframe(m_df_c.style.format("{:.1%}", subset=['์ข
ํฉ ์ ์']).background_gradient(cmap='Blues', subset=['์ข
ํฉ ์ ์']), use_container_width=True, hide_index=True)
|
| 454 |
+
else: st.info("์กฐํฉ์ ์ฐพ์ ์ ์์ต๋๋ค.")
|
| 455 |
+
if has_result_c:
|
| 456 |
+
current_state_c = f"Custom_{custom_dish_name}_{target_str_c}_{stop_str_c}_{w_w2v}_{w_d2v}_{final_recs_c}"
|
| 457 |
+
if 'last_log_c' not in st.session_state: st.session_state['last_log_c'] = ""
|
| 458 |
+
if st.session_state['last_log_c'] != current_state_c:
|
| 459 |
+
log_id_c = logic.save_log_to_db(custom_dish_name, target_str_c, stops_c, w_w2v, w_d2v, 0, 0, rec_list=final_recs_c, is_custom=True)
|
| 460 |
+
st.session_state['current_log_id_c'] = log_id_c
|
| 461 |
+
st.session_state['last_log_c'] = current_state_c
|
| 462 |
+
if 'current_log_id_c' in st.session_state and st.session_state['current_log_id_c']:
|
| 463 |
+
cl_id_c = st.session_state['current_log_id_c']
|
| 464 |
+
is_voted_c = cl_id_c in st.session_state['voted_logs']
|
| 465 |
+
st.write(""); b1_c, b2_c, _ = st.columns([0.2, 0.2, 0.6])
|
| 466 |
+
if is_voted_c: b1_c.success("โ
ํ๊ฐ ์๋ฃ!"); b2_c.write("")
|
| 467 |
+
else:
|
| 468 |
+
b1_c.button("๐ ๋ง์กฑํด์", key="btn_sat_custom", use_container_width=True, on_click=lambda: (logic.update_feedback_in_db(cl_id_c, "satisfy"), st.session_state['voted_logs'].add(cl_id_c), st.toast("๊ฐ์ฌํฉ๋๋ค!")))
|
| 469 |
+
b2_c.button("๐ ์์ฌ์์", key="btn_dis_custom", use_container_width=True, on_click=lambda: (logic.update_feedback_in_db(cl_id_c, "dissatisfy"), st.session_state['voted_logs'].add(cl_id_c), st.toast("์๊ฒฌ ๊ฐ์ฌํฉ๋๋ค.")))
|
| 470 |
+
else: st.info("๐ ์ ์ฒด ์ฌ๋ฃ ๋ฆฌ์คํธ๋ฅผ ๋จผ์ ์
๋ ฅํด์ฃผ์ธ์.")
|
| 471 |
+
|
| 472 |
+
# -------------------------------------------------------------------------
|
| 473 |
+
# 5. ํ๋จ ํผ๋๋ฐฑ ๋ฐ ๋ถ์ฉ์ด ์ ๊ณ ์์ญ
|
| 474 |
+
# -------------------------------------------------------------------------
|
| 475 |
+
st.divider()
|
| 476 |
+
col_feedback, col_stopword = st.columns(2)
|
| 477 |
+
|
| 478 |
+
with col_feedback:
|
| 479 |
+
st.subheader("๐ข ์๋น์ค ์๊ฒฌ ๋ณด๋ด๊ธฐ")
|
| 480 |
+
with st.form("feedback_form"):
|
| 481 |
+
text = st.text_area("๊ฐ์ ํ ์ ์ด๋ ๋ฒ๊ทธ๊ฐ ์๋ค๋ฉด ์๋ ค์ฃผ์ธ์!", height=100, key="feedback_input_field")
|
| 482 |
+
st.form_submit_button("์๊ฒฌ ๋ณด๋ด๊ธฐ", use_container_width=True, on_click=handle_feedback_submission)
|
| 483 |
+
|
| 484 |
+
with col_stopword:
|
| 485 |
+
st.subheader("๐ซ ๋ถ์ฉ์ด(์ด์ํ ๋จ์ด) ์ ๊ณ ํ๊ธฐ")
|
| 486 |
+
st.caption(
|
| 487 |
+
"์ถ์ฒ ๊ฒฐ๊ณผ์ ์ด์ํ ๋จ์ด๊ฐ ์๋์? ์ ๊ณ ํด์ฃผ์๋ฉด ๋ค์๋ถํฐ ์ ์ธ๋ฉ๋๋ค.",
|
| 488 |
+
help="ํ์ฌ ํ์ต ๋ฐ์ดํฐ์ ํฌํจ๋ ๋ถ์ฉ์ด๊ฐ ๋๋ฌด ๋ง์ ์ผ์ผ์ด ์์์
์ผ๋ก ์ฒ๋ฆฌํ๊ธฐ ์ด๋ ต์ต๋๋ค. ๐ฅ ์ฌ๋ฌ๋ถ์ ์ ๊ณ ๊ฐ ๋ชจ์ด๋ฉด ๋ฐ์ดํฐ์ ํ์ง์ด ๋์์ง๊ณ ์ถ์ฒ ๊ฒฐ๊ณผ๋ ๋ ์ ํํด์ง๋๋ค. ์์คํ ๊ธฐ์ฌ ๋ถํ๋๋ฆฝ๋๋ค! ๐"
|
| 489 |
+
)
|
| 490 |
+
st.info("๐ก Tip: '๊ฐ์ฅor์ง๊ฐ์ฅ' ๊ฐ์ ๊ฒฝ์ฐ 'or'๋ฅผ ์ ๊ณ ํ๋ฉด '๊ฐ์ฅ์ง๊ฐ์ฅ'์ผ๋ก ํฉ์ณ์ ธ ์ถ์ฒ์์ ์ ์ธ๋ฉ๋๋ค.")
|
| 491 |
+
|
| 492 |
+
with st.form("stopword_form"):
|
| 493 |
+
st.text_input("์ ๊ณ ํ ๋จ์ด ์
๋ ฅ (์ผํ๋ก ๊ตฌ๋ถ)", placeholder="์: ๋ฉดํฌ, ํฉ์์ด์ , ํ
๋ฐญ", key="stopword_input_field")
|
| 494 |
+
st.form_submit_button("์ ๊ณ ํ๊ธฐ", use_container_width=True, on_click=handle_stopword_submission)
|
src/logic.py
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# logic.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import numpy as np
|
| 5 |
+
from gensim.models import Word2Vec, Doc2Vec
|
| 6 |
+
from ast import literal_eval
|
| 7 |
+
import pickle
|
| 8 |
+
from datetime import datetime, timedelta, timezone
|
| 9 |
+
from supabase import create_client
|
| 10 |
+
import re
|
| 11 |
+
from collections import Counter
|
| 12 |
+
|
| 13 |
+
# ==========================================
|
| 14 |
+
# 0. ํ๊ฒฝ ์ค์ ๋ฐ ๊ท์น ์ ์
|
| 15 |
+
# ==========================================
|
| 16 |
+
PRICE_KEYWORD_RULES = [
|
| 17 |
+
(5, ['์๊ณ ๊ธฐ', 'ํ์ฐ', '์ฑ๋', '๋ฑ์ฌ', '์์ฌ', '๊ฐ๋น์ด', '์ ๋ณต', '์ฅ์ด']),
|
| 18 |
+
(4, ['๋ผ์ง', '์ผ๊ฒน', '๋ชฉ์ด', '์๋ค๋ฆฌ', '๋ท๋ค๋ฆฌ', '๊ฐ๋น', '์ค๋ฆฌ', '๋์ง', '์ค์ง์ด', '์์ฐ', '๋ช
๋']),
|
| 19 |
+
(3, ['๋ญ', '์นํจ', 'ํ', '์์์ง', '๋ฒ ์ด์ปจ', '์คํธ', '์ฐธ์น', '๋์', '์ด๋ฌต', '๋ง์ด', '๋ฒ์ฏ', '์น์ฆ']),
|
| 20 |
+
(2, ['๋๋ถ', '์๋๋ถ', '์ฝฉ๋๋ฌผ', '์์ฃผ', '๊น์น', '๋ฌด', '๊ฐ์', '๊ณ ๊ตฌ๋ง', '๋น๊ทผ', 'ํธ๋ฐ']),
|
| 21 |
+
(1, ['์ํ', '๋ํ', '์ชฝํ', '์คํ', '๋ง๋', '๊ณ ์ถ', '๋ฌผ', '์๊ธ', '์คํ', '๊ฐ์ฅ', '์์ค', '์๋
', '์ก์'])
|
| 22 |
+
]
|
| 23 |
+
PRICE_RULE_EXCEPTIONS = ['๋ผ์ง๊ฐ์', '๋ญ์์ฅํ', '์์ฐ์ ', '๋ฉธ์น์ก์ ', '๋ค์๋ค']
|
| 24 |
+
|
| 25 |
+
# ==========================================
|
| 26 |
+
# 1. Supabase DB ์ฐ๋ ๋ฐ ๋ฐ์ดํฐ ์ ์ฅ/๋ก๋
|
| 27 |
+
# ==========================================
|
| 28 |
+
@st.cache_resource
|
| 29 |
+
def init_supabase():
|
| 30 |
+
try:
|
| 31 |
+
url = None
|
| 32 |
+
key = None
|
| 33 |
+
|
| 34 |
+
# 1. Try Streamlit secrets
|
| 35 |
+
try:
|
| 36 |
+
if hasattr(st, "secrets") and "supabase" in st.secrets:
|
| 37 |
+
url = st.secrets["supabase"]["url"]
|
| 38 |
+
key = st.secrets["supabase"]["key"]
|
| 39 |
+
except:
|
| 40 |
+
pass
|
| 41 |
+
|
| 42 |
+
# 2. Add fallback to OS environment variables
|
| 43 |
+
if not url or not key:
|
| 44 |
+
url = os.environ.get("SUPABASE_URL")
|
| 45 |
+
key = os.environ.get("SUPABASE_KEY")
|
| 46 |
+
|
| 47 |
+
if not url or not key:
|
| 48 |
+
# ๋ก์ปฌ ๊ฐ๋ฐ ์ค secrets ์์ด ์คํ๋ ๊ฒฝ์ฐ๋ฅผ ๋๋นํด ์คํตํ๊ฑฐ๋ ์๋ฌ ์ฒ๋ฆฌ
|
| 49 |
+
# API ์๋ฒ์์๋ ํ์์ด๋ฏ๋ก ๋ก๊ทธ ๋จ๊น
|
| 50 |
+
return None
|
| 51 |
+
|
| 52 |
+
return create_client(url, key)
|
| 53 |
+
except Exception as e:
|
| 54 |
+
print(f"Supabase ์ฐ๊ฒฐ ๊ฒฝ๊ณ : {e}")
|
| 55 |
+
return None
|
| 56 |
+
|
| 57 |
+
def get_kst_now_iso():
|
| 58 |
+
kst_timezone = timezone(timedelta(hours=9))
|
| 59 |
+
now_kst = datetime.now(kst_timezone)
|
| 60 |
+
return now_kst.isoformat()
|
| 61 |
+
|
| 62 |
+
@st.cache_data(ttl=300)
|
| 63 |
+
def load_global_stopwords():
|
| 64 |
+
try:
|
| 65 |
+
supabase = init_supabase()
|
| 66 |
+
response = supabase.table("stopwords").select("word").order("created_at", desc=True).execute()
|
| 67 |
+
if response.data:
|
| 68 |
+
return [item['word'] for item in response.data]
|
| 69 |
+
return []
|
| 70 |
+
except Exception as e:
|
| 71 |
+
print(f"๋ถ์ฉ์ด ๋ก๋ ์คํจ: {e}")
|
| 72 |
+
return []
|
| 73 |
+
|
| 74 |
+
@st.cache_data(ttl=600)
|
| 75 |
+
def get_usage_stats(timeframe='today'):
|
| 76 |
+
try:
|
| 77 |
+
supabase = init_supabase()
|
| 78 |
+
query = supabase.table("usage_log").select("dish, target")
|
| 79 |
+
|
| 80 |
+
if timeframe == 'today':
|
| 81 |
+
kst = timezone(timedelta(hours=9))
|
| 82 |
+
now_kst = datetime.now(kst)
|
| 83 |
+
today_start = now_kst.replace(hour=0, minute=0, second=0, microsecond=0)
|
| 84 |
+
tomorrow_start = today_start + timedelta(days=1)
|
| 85 |
+
query = query.gte("created_at", today_start.isoformat()).lt("created_at", tomorrow_start.isoformat())
|
| 86 |
+
|
| 87 |
+
response = query.execute()
|
| 88 |
+
data = response.data
|
| 89 |
+
|
| 90 |
+
count = len(data)
|
| 91 |
+
top_dishes = pd.Series(dtype=int)
|
| 92 |
+
top_targets = pd.Series(dtype=int)
|
| 93 |
+
|
| 94 |
+
if count > 0:
|
| 95 |
+
df_log = pd.DataFrame(data)
|
| 96 |
+
df_log['clean_dish'] = df_log['dish'].astype(str).str.replace(r'\[Custom\]', '', regex=True).str.strip()
|
| 97 |
+
top_dishes = df_log[df_log['clean_dish'] != '']['clean_dish'].value_counts().head(5)
|
| 98 |
+
|
| 99 |
+
all_targets = []
|
| 100 |
+
for t in df_log['target']:
|
| 101 |
+
if t:
|
| 102 |
+
all_targets.extend([x.strip() for x in str(t).split(',') if x.strip()])
|
| 103 |
+
top_targets = pd.Series(all_targets).value_counts().head(5)
|
| 104 |
+
|
| 105 |
+
return count, top_dishes, top_targets
|
| 106 |
+
|
| 107 |
+
except Exception as e:
|
| 108 |
+
print(f"ํต๊ณ ๋ฐ์ดํฐ ๋ก๋ ์คํจ ({timeframe}): {e}")
|
| 109 |
+
return 0, pd.Series(dtype=int), pd.Series(dtype=int)
|
| 110 |
+
|
| 111 |
+
@st.cache_data(ttl=600)
|
| 112 |
+
def get_wordcloud_text(timeframe='today'):
|
| 113 |
+
try:
|
| 114 |
+
supabase = init_supabase()
|
| 115 |
+
query = supabase.table("usage_log").select("target")
|
| 116 |
+
if timeframe == 'today':
|
| 117 |
+
kst = timezone(timedelta(hours=9))
|
| 118 |
+
now_kst = datetime.now(kst)
|
| 119 |
+
today_start = now_kst.replace(hour=0, minute=0, second=0, microsecond=0)
|
| 120 |
+
tomorrow_start = today_start + timedelta(days=1)
|
| 121 |
+
query = query.gte("created_at", today_start.isoformat()).lt("created_at", tomorrow_start.isoformat())
|
| 122 |
+
|
| 123 |
+
response = query.execute()
|
| 124 |
+
data = response.data
|
| 125 |
+
all_targets = []
|
| 126 |
+
if data:
|
| 127 |
+
for item in data:
|
| 128 |
+
if item['target']:
|
| 129 |
+
all_targets.extend([x.strip() for x in str(item['target']).split(',') if x.strip()])
|
| 130 |
+
return " ".join(all_targets)
|
| 131 |
+
except Exception as e:
|
| 132 |
+
print(f"์๋ํด๋ผ์ฐ๋ ๋ฐ์ดํฐ ๋ก๋ ์คํจ: {e}")
|
| 133 |
+
return ""
|
| 134 |
+
|
| 135 |
+
def save_stopwords_to_db(words_string):
|
| 136 |
+
words = [w.strip() for w in words_string.split(',') if w.strip()]
|
| 137 |
+
if not words: return False, "์ ์ฅํ ๋จ์ด๊ฐ ์์ต๋๋ค."
|
| 138 |
+
supabase = init_supabase()
|
| 139 |
+
success_count, duplicate_count, fail_count = 0, 0, 0
|
| 140 |
+
for word in words:
|
| 141 |
+
try:
|
| 142 |
+
supabase.table("stopwords").insert({"word": word}).execute()
|
| 143 |
+
success_count += 1
|
| 144 |
+
except Exception as e:
|
| 145 |
+
if 'duplicate' in str(e).lower(): duplicate_count += 1
|
| 146 |
+
else: fail_count += 1
|
| 147 |
+
if success_count > 0: st.cache_data.clear()
|
| 148 |
+
msg_parts = []
|
| 149 |
+
if success_count > 0: msg_parts.append(f"โ
{success_count}๊ฐ ์ ์ฅ")
|
| 150 |
+
if duplicate_count > 0: msg_parts.append(f"โ ๏ธ {duplicate_count}๊ฐ ์ค๋ณต")
|
| 151 |
+
if fail_count > 0: msg_parts.append(f"โ {fail_count}๊ฐ ์คํจ")
|
| 152 |
+
return success_count > 0, ", ".join(msg_parts)
|
| 153 |
+
|
| 154 |
+
@st.cache_data(ttl=60)
|
| 155 |
+
def get_board_messages():
|
| 156 |
+
try:
|
| 157 |
+
supabase = init_supabase()
|
| 158 |
+
response = supabase.table("board").select("*").order("created_at", desc=True).limit(50).execute()
|
| 159 |
+
if response.data:
|
| 160 |
+
for item in response.data:
|
| 161 |
+
dt = datetime.fromisoformat(item['created_at'])
|
| 162 |
+
dt_kst = dt + timedelta(hours=9)
|
| 163 |
+
item['display_time'] = dt_kst.strftime("%m/%d %H:%M")
|
| 164 |
+
return response.data
|
| 165 |
+
return []
|
| 166 |
+
except Exception as e:
|
| 167 |
+
print(f"๊ฒ์ํ ๋ก๋ ์คํจ: {e}")
|
| 168 |
+
return []
|
| 169 |
+
|
| 170 |
+
def save_board_message(nickname, content):
|
| 171 |
+
if not nickname or not content: return False
|
| 172 |
+
try:
|
| 173 |
+
supabase = init_supabase()
|
| 174 |
+
supabase.table("board").insert({"nickname": nickname, "content": content}).execute()
|
| 175 |
+
st.cache_data.clear()
|
| 176 |
+
return True
|
| 177 |
+
except Exception as e:
|
| 178 |
+
print(f"๊ฒ์ํ ์ ์ฅ ์คํจ: {e}")
|
| 179 |
+
return False
|
| 180 |
+
|
| 181 |
+
def save_feedback_to_db(feedback_text):
|
| 182 |
+
try:
|
| 183 |
+
supabase = init_supabase()
|
| 184 |
+
supabase.table("feedback").insert({"content": feedback_text, "created_at": get_kst_now_iso()}).execute()
|
| 185 |
+
return True
|
| 186 |
+
except Exception as e:
|
| 187 |
+
print(f"ํผ๋๋ฐฑ ์ ์ฅ ์๋ฌ: {e}")
|
| 188 |
+
return False
|
| 189 |
+
|
| 190 |
+
def save_log_to_db(dish, target, stops, w1, w2, w3, w4, rec_list=None, is_custom=False):
|
| 191 |
+
try:
|
| 192 |
+
supabase = init_supabase()
|
| 193 |
+
r1 = rec_list[0] if rec_list and len(rec_list) > 0 else None
|
| 194 |
+
r2 = rec_list[1] if rec_list and len(rec_list) > 1 else None
|
| 195 |
+
r3 = rec_list[2] if rec_list and len(rec_list) > 2 else None
|
| 196 |
+
dish_name_to_save = f"[Custom] {dish}" if is_custom else dish
|
| 197 |
+
data = {
|
| 198 |
+
"dish": dish_name_to_save, "target": target, "stops": ", ".join(stops) if stops else "์์",
|
| 199 |
+
"w_w2v": w1, "w_d2v": w2, "w_method": w3, "w_cat": w4, "rec_1": r1, "rec_2": r2, "rec_3": r3,
|
| 200 |
+
"created_at": get_kst_now_iso()
|
| 201 |
+
}
|
| 202 |
+
response = supabase.table("usage_log").insert(data).execute()
|
| 203 |
+
if response.data: return response.data[0]['id']
|
| 204 |
+
return None
|
| 205 |
+
except Exception as e:
|
| 206 |
+
print(f"๋ก๊ทธ ์ ์ฅ ์๋ฌ: {e}")
|
| 207 |
+
return None
|
| 208 |
+
|
| 209 |
+
def update_feedback_in_db(log_id, status):
|
| 210 |
+
try:
|
| 211 |
+
supabase = init_supabase()
|
| 212 |
+
if log_id:
|
| 213 |
+
supabase.table("usage_log").update({"satisfaction": status}).eq("id", log_id).execute()
|
| 214 |
+
return True
|
| 215 |
+
return False
|
| 216 |
+
except Exception as e:
|
| 217 |
+
print(f"๋ง์กฑ๋ ์
๋ฐ์ดํธ ์๋ฌ: {e}")
|
| 218 |
+
return False
|
| 219 |
+
|
| 220 |
+
# ==========================================
|
| 221 |
+
# 2. ๋ฐ์ดํฐ ๋ฐ ๋ชจ๋ธ ๋ก๋
|
| 222 |
+
# ==========================================
|
| 223 |
+
# ==========================================
|
| 224 |
+
# 2. ๋ฐ์ดํฐ ๋ฐ ๋ชจ๋ธ ๋ก๋ (Lazy Loading ์ ์ฉ)
|
| 225 |
+
# ==========================================
|
| 226 |
+
w2v_model = None
|
| 227 |
+
d2v_model = None
|
| 228 |
+
df = None
|
| 229 |
+
stats = None
|
| 230 |
+
price_map = {}
|
| 231 |
+
global_stopwords_set = set()
|
| 232 |
+
all_ingredients_set = set()
|
| 233 |
+
method_map = {}
|
| 234 |
+
recipes_by_ingredient = {}
|
| 235 |
+
ing_method_counts = {}
|
| 236 |
+
ing_cat_counts = {}
|
| 237 |
+
total_method_counts = {}
|
| 238 |
+
total_cat_counts = {}
|
| 239 |
+
TOTAL_RECIPES = 0
|
| 240 |
+
|
| 241 |
+
def load_resources():
|
| 242 |
+
global w2v_model, d2v_model, df, stats, price_map, global_stopwords_set, all_ingredients_set
|
| 243 |
+
global method_map, recipes_by_ingredient, ing_method_counts, ing_cat_counts, total_method_counts, total_cat_counts, TOTAL_RECIPES
|
| 244 |
+
|
| 245 |
+
print("Loading resources... (This may take a while)")
|
| 246 |
+
# mmap='r' ์ต์
์ผ๋ก ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ต์ํ (๋์คํฌ์์ ์ง์ ์ฝ์)
|
| 247 |
+
w2v_model = Word2Vec.load("models/w2v.model", mmap='r')
|
| 248 |
+
d2v_model = Doc2Vec.load("models/d2v.model", mmap='r')
|
| 249 |
+
df_temp = pd.read_csv("data/final_recipe_data.csv")
|
| 250 |
+
df_temp['์ฌ๋ฃํ ํฐ'] = df_temp['์ฌ๋ฃํ ํฐ'].apply(literal_eval)
|
| 251 |
+
df = df_temp # Assign to global
|
| 252 |
+
|
| 253 |
+
with open("data/stats.pkl", "rb") as f:
|
| 254 |
+
stats = pickle.load(f)
|
| 255 |
+
|
| 256 |
+
try:
|
| 257 |
+
price_df = pd.read_csv("data/price_rank.csv", encoding='utf-8-sig')
|
| 258 |
+
price_df.columns = price_df.columns.str.strip()
|
| 259 |
+
price_map = dict(zip(price_df['ingredient'], price_df['rank']))
|
| 260 |
+
except:
|
| 261 |
+
price_map = {}
|
| 262 |
+
|
| 263 |
+
global_stopwords_set = set()
|
| 264 |
+
all_ingredients_set = set()
|
| 265 |
+
for ings in df['์ฌ๋ฃํ ํฐ']:
|
| 266 |
+
all_ingredients_set.update(ings)
|
| 267 |
+
|
| 268 |
+
# Stats unpacking
|
| 269 |
+
method_map = stats["method_map"]
|
| 270 |
+
recipes_by_ingredient = stats["recipes_by_ingredient"]
|
| 271 |
+
ing_method_counts = stats["ing_method_counts"]
|
| 272 |
+
ing_cat_counts = stats["ing_cat_counts"]
|
| 273 |
+
total_method_counts = stats["total_method_counts"]
|
| 274 |
+
total_cat_counts = stats["total_cat_counts"]
|
| 275 |
+
TOTAL_RECIPES = stats["TOTAL_RECIPES"]
|
| 276 |
+
print("Resources loaded successfully!")
|
| 277 |
+
|
| 278 |
+
def ensure_initialized():
|
| 279 |
+
if df is None:
|
| 280 |
+
load_resources()
|
| 281 |
+
|
| 282 |
+
# ๊ธฐ์กด ์ฆ์ ์คํ ์ฝ๋ ์ ๊ฑฐ
|
| 283 |
+
# w2v_model, d2v_model, ... = load_resources()
|
| 284 |
+
|
| 285 |
+
# ==========================================
|
| 286 |
+
# 3. ํต์ฌ ๊ณ์ฐ ๋ก์ง
|
| 287 |
+
# ==========================================
|
| 288 |
+
def cos_sim(vec_a, vec_b):
|
| 289 |
+
norm = (np.linalg.norm(vec_a) * np.linalg.norm(vec_b) + 1e-9)
|
| 290 |
+
return max(0.0, float(np.dot(vec_a, vec_b) / norm))
|
| 291 |
+
|
| 292 |
+
def get_stat_score(ingredient, target_key, ing_count_dict, total_count_dict, total_n, min_count=5):
|
| 293 |
+
cnts = ing_count_dict.get(ingredient)
|
| 294 |
+
if not cnts: return 0.0
|
| 295 |
+
ing_target_count = cnts[target_key]
|
| 296 |
+
ing_total_count = sum(cnts.values())
|
| 297 |
+
if ing_total_count < min_count: return 0.0
|
| 298 |
+
prob_ing_context = ing_target_count / ing_total_count
|
| 299 |
+
baseline_prob = total_count_dict[target_key] / total_n
|
| 300 |
+
if baseline_prob == 0: return 0.0
|
| 301 |
+
return prob_ing_context / baseline_prob
|
| 302 |
+
|
| 303 |
+
def get_estimated_price_rank(ing_name, price_map):
|
| 304 |
+
if ing_name in price_map: return price_map[ing_name]
|
| 305 |
+
if any(exp in ing_name for exp in PRICE_RULE_EXCEPTIONS): return 3
|
| 306 |
+
for rank, keywords in PRICE_KEYWORD_RULES:
|
| 307 |
+
if any(kw in ing_name for kw in keywords): return rank
|
| 308 |
+
return 3
|
| 309 |
+
|
| 310 |
+
# ==========================================
|
| 311 |
+
# 4. ๋์ฒด ์ถ์ฒ ์๊ณ ๋ฆฌ์ฆ (DB ๊ธฐ๋ฐ)
|
| 312 |
+
# ==========================================
|
| 313 |
+
def substitute_single(recipe_id, target_ing, user_stopwords, w_w2v, w_d2v, w_method, w_cat, topn=10):
|
| 314 |
+
row = df[df['๋ ์ํผ์ผ๋ จ๋ฒํธ'] == recipe_id].iloc[0]
|
| 315 |
+
current_method = row['์๋ฆฌ๋ฐฉ๋ฒ๋ณ๋ช
']
|
| 316 |
+
current_cat = row['์๋ฆฌ์ข
๋ฅ๋ณ๋ช
_์ธ๋ถํ']
|
| 317 |
+
context_ings = row['์ฌ๋ฃํ ํฐ']
|
| 318 |
+
tag = f"recipe_{recipe_id}"
|
| 319 |
+
if target_ing not in w2v_model.wv: return pd.DataFrame()
|
| 320 |
+
total_weight = w_w2v + w_d2v + w_method + w_cat
|
| 321 |
+
if total_weight == 0: total_weight = 1.0
|
| 322 |
+
vec_recipe = None
|
| 323 |
+
if w_d2v > 0 and tag in d2v_model.dv: vec_recipe = d2v_model.dv[tag]
|
| 324 |
+
target_rank = get_estimated_price_rank(target_ing, price_map)
|
| 325 |
+
candidates_raw = w2v_model.wv.most_similar(target_ing, topn=50)
|
| 326 |
+
temp_results = []
|
| 327 |
+
seen_candidates = set()
|
| 328 |
+
|
| 329 |
+
# [์์ ] ์ค์๊ฐ ๋ก๋
|
| 330 |
+
global_stopwords_set = set(load_global_stopwords())
|
| 331 |
+
final_stopwords = set(user_stopwords) | global_stopwords_set
|
| 332 |
+
|
| 333 |
+
for cand, score_w2v in candidates_raw:
|
| 334 |
+
clean_cand = cand
|
| 335 |
+
if final_stopwords:
|
| 336 |
+
for stop in final_stopwords: clean_cand = clean_cand.replace(stop, "")
|
| 337 |
+
clean_cand = clean_cand.strip()
|
| 338 |
+
|
| 339 |
+
if not clean_cand: continue
|
| 340 |
+
if clean_cand in final_stopwords: continue
|
| 341 |
+
|
| 342 |
+
if clean_cand in context_ings: continue
|
| 343 |
+
if clean_cand == target_ing: continue
|
| 344 |
+
if clean_cand not in w2v_model.wv: continue
|
| 345 |
+
if clean_cand in seen_candidates: continue
|
| 346 |
+
seen_candidates.add(clean_cand)
|
| 347 |
+
real_score_w2v = w2v_model.wv.similarity(target_ing, clean_cand)
|
| 348 |
+
s_w2v = max(0.0, real_score_w2v)
|
| 349 |
+
if s_w2v < 0.35: continue
|
| 350 |
+
s_d2v = 0.0
|
| 351 |
+
if w_d2v > 0 and vec_recipe is not None:
|
| 352 |
+
rid_list = recipes_by_ingredient.get(clean_cand, [])
|
| 353 |
+
same_method_ids = [r for r in rid_list if method_map.get(r) == current_method]
|
| 354 |
+
if len(same_method_ids) > 20:
|
| 355 |
+
np.random.seed(42)
|
| 356 |
+
same_method_ids = np.random.choice(same_method_ids, 20, replace=False)
|
| 357 |
+
if same_method_ids is not None and len(same_method_ids) > 0:
|
| 358 |
+
sims = []
|
| 359 |
+
for r in same_method_ids:
|
| 360 |
+
rt = f"recipe_{r}"
|
| 361 |
+
if rt in d2v_model.dv: sims.append(cos_sim(vec_recipe, d2v_model.dv[rt]))
|
| 362 |
+
if sims: s_d2v = np.mean(sims)
|
| 363 |
+
s_method = 0.0 if w_method <= 0 else get_stat_score(clean_cand, current_method, ing_method_counts, total_method_counts, TOTAL_RECIPES)
|
| 364 |
+
s_cat = 0.0 if w_cat <= 0 else get_stat_score(clean_cand, current_cat, ing_cat_counts, total_cat_counts, TOTAL_RECIPES)
|
| 365 |
+
cand_rank = get_estimated_price_rank(clean_cand, price_map)
|
| 366 |
+
saving_score = target_rank - cand_rank
|
| 367 |
+
temp_results.append({"๋์ฒด์ฌ๋ฃ": clean_cand, "raw_W2V": s_w2v, "raw_D2V": s_d2v, "raw_Method": s_method, "raw_Category": s_cat, "saving_score": saving_score})
|
| 368 |
+
if not temp_results: return pd.DataFrame()
|
| 369 |
+
df_res = pd.DataFrame(temp_results)
|
| 370 |
+
cols = ["raw_W2V", "raw_D2V", "raw_Method", "raw_Category"]
|
| 371 |
+
norm_cols = ["W2V", "D2V", "Method", "Category"]
|
| 372 |
+
for raw_col, norm_col in zip(cols, norm_cols):
|
| 373 |
+
min_val = df_res[raw_col].min()
|
| 374 |
+
max_val = df_res[raw_col].max()
|
| 375 |
+
if max_val - min_val == 0: df_res[norm_col] = 0.5
|
| 376 |
+
else: df_res[norm_col] = (df_res[raw_col] - min_val) / (max_val - min_val)
|
| 377 |
+
df_res["์ต์ข
์ ์"] = ((df_res["W2V"]*w_w2v) + (df_res["D2V"]*w_d2v) + (df_res["Method"]*w_method) + (df_res["Category"]*w_cat)) / total_weight
|
| 378 |
+
return df_res.sort_values("์ต์ข
์ ์", ascending=False).head(topn).reset_index(drop=True)
|
| 379 |
+
|
| 380 |
+
def substitute_multi(recipe_id, targets, user_stopwords, w_w2v, w_d2v, w_method, w_cat, beam_width=3, result_topn=3):
|
| 381 |
+
row = df[df['๋ ์ํผ์ผ๋ จ๋ฒํธ'] == recipe_id].iloc[0]
|
| 382 |
+
current_method = row['์๋ฆฌ๋ฐฉ๋ฒ๋ณ๋ช
']
|
| 383 |
+
current_cat = row['์๋ฆฌ์ข
๋ฅ๋ณ๋ช
_์ธ๋ถํ']
|
| 384 |
+
initial_context = row['์ฌ๋ฃํ ํฐ']
|
| 385 |
+
tag = f"recipe_{recipe_id}"
|
| 386 |
+
vec_recipe = None
|
| 387 |
+
if w_d2v > 0 and tag in d2v_model.dv: vec_recipe = d2v_model.dv[tag]
|
| 388 |
+
total_weight = w_w2v + w_d2v + w_method + w_cat
|
| 389 |
+
if total_weight == 0: total_weight = 1.0
|
| 390 |
+
target_ranks_sum = 0
|
| 391 |
+
for t in targets: target_ranks_sum += get_estimated_price_rank(t, price_map)
|
| 392 |
+
|
| 393 |
+
# [์์ ] ์ค์๊ฐ ๋ก๋
|
| 394 |
+
global_stopwords_set = set(load_global_stopwords())
|
| 395 |
+
final_stopwords = set(user_stopwords) | global_stopwords_set
|
| 396 |
+
|
| 397 |
+
beam = [(0.0, [], initial_context)]
|
| 398 |
+
for target_ing in targets:
|
| 399 |
+
next_beam = []
|
| 400 |
+
if target_ing not in w2v_model.wv:
|
| 401 |
+
for score, subs, ctx in beam: next_beam.append((score, subs + [target_ing], ctx))
|
| 402 |
+
beam = next_beam
|
| 403 |
+
continue
|
| 404 |
+
for path_score, path_subs, path_ctx in beam:
|
| 405 |
+
current_ctx_ing = [x for x in path_ctx if x != target_ing]
|
| 406 |
+
candidates = w2v_model.wv.most_similar(target_ing, topn=30)
|
| 407 |
+
temp_candidates = []
|
| 408 |
+
seen_candidates = set()
|
| 409 |
+
for cand, _ in candidates:
|
| 410 |
+
clean_cand = cand
|
| 411 |
+
if final_stopwords:
|
| 412 |
+
for stop in final_stopwords: clean_cand = clean_cand.replace(stop, "")
|
| 413 |
+
clean_cand = clean_cand.strip()
|
| 414 |
+
|
| 415 |
+
if not clean_cand: continue
|
| 416 |
+
if clean_cand in final_stopwords: continue
|
| 417 |
+
|
| 418 |
+
if clean_cand in current_ctx_ing or clean_cand in path_subs: continue
|
| 419 |
+
if clean_cand == target_ing: continue
|
| 420 |
+
if clean_cand not in w2v_model.wv: continue
|
| 421 |
+
if clean_cand in seen_candidates: continue
|
| 422 |
+
seen_candidates.add(clean_cand)
|
| 423 |
+
sim_orig = w2v_model.wv.similarity(target_ing, clean_cand)
|
| 424 |
+
sim_orig = max(0.0, sim_orig)
|
| 425 |
+
if sim_orig < 0.3: continue
|
| 426 |
+
harmony_scores = [w2v_model.wv.similarity(clean_cand, c) for c in current_ctx_ing if c in w2v_model.wv]
|
| 427 |
+
sim_harmony = np.mean(harmony_scores) if harmony_scores else 0.0
|
| 428 |
+
s_w2v = 0.5 * sim_orig + 0.5 * max(0.0, sim_harmony)
|
| 429 |
+
s_d2v = 0.0
|
| 430 |
+
if vec_recipe is not None:
|
| 431 |
+
rid_list = recipes_by_ingredient.get(clean_cand, [])
|
| 432 |
+
same_method_ids = [r for r in rid_list if method_map.get(r) == current_method]
|
| 433 |
+
if len(same_method_ids) > 10:
|
| 434 |
+
np.random.seed(42)
|
| 435 |
+
same_method_ids = np.random.choice(same_method_ids, 10, replace=False)
|
| 436 |
+
if same_method_ids is not None and len(same_method_ids) > 0:
|
| 437 |
+
sims = []
|
| 438 |
+
for r in same_method_ids:
|
| 439 |
+
rt = f"recipe_{r}"
|
| 440 |
+
if rt in d2v_model.dv: sims.append(cos_sim(vec_recipe, d2v_model.dv[rt]))
|
| 441 |
+
if sims: s_d2v = np.mean(sims)
|
| 442 |
+
s_method = 0.0 if w_method <= 0 else get_stat_score(clean_cand, current_method, ing_method_counts, total_method_counts, TOTAL_RECIPES)
|
| 443 |
+
s_cat = 0.0 if w_cat <= 0 else get_stat_score(clean_cand, current_cat, ing_cat_counts, total_cat_counts, TOTAL_RECIPES)
|
| 444 |
+
temp_candidates.append({"cand": clean_cand, "raw_w2v": s_w2v, "raw_d2v": s_d2v, "raw_method": s_method, "raw_cat": s_cat})
|
| 445 |
+
if not temp_candidates: continue
|
| 446 |
+
df_temp = pd.DataFrame(temp_candidates)
|
| 447 |
+
cols = ["raw_w2v", "raw_d2v", "raw_method", "raw_cat"]
|
| 448 |
+
for col in cols:
|
| 449 |
+
min_val = df_temp[col].min()
|
| 450 |
+
max_val = df_temp[col].max()
|
| 451 |
+
if max_val - min_val == 0: df_temp[col + "_norm"] = 0.5
|
| 452 |
+
else: df_temp[col + "_norm"] = (df_temp[col] - min_val) / (max_val - min_val)
|
| 453 |
+
for _, r in df_temp.iterrows():
|
| 454 |
+
weighted_sum = ((r["raw_w2v_norm"]*w_w2v) + (r["raw_d2v_norm"]*w_d2v) + (r["raw_method_norm"]*w_method) + (r["raw_cat_norm"]*w_cat)) / total_weight
|
| 455 |
+
new_total_score = path_score + weighted_sum
|
| 456 |
+
new_subs = path_subs + [r["cand"]]
|
| 457 |
+
new_ctx = current_ctx_ing + [r["cand"]]
|
| 458 |
+
next_beam.append((new_total_score, new_subs, new_ctx))
|
| 459 |
+
next_beam.sort(key=lambda x: x[0], reverse=True)
|
| 460 |
+
beam = next_beam[:beam_width]
|
| 461 |
+
final_results = []
|
| 462 |
+
for score, subs, _ in beam:
|
| 463 |
+
avg_score = score / len(targets) if targets else 0.0
|
| 464 |
+
cand_ranks_sum = 0
|
| 465 |
+
for sub_ing in subs: cand_ranks_sum += get_estimated_price_rank(sub_ing, price_map)
|
| 466 |
+
total_saving_score = target_ranks_sum - cand_ranks_sum
|
| 467 |
+
final_results.append((subs, avg_score, total_saving_score))
|
| 468 |
+
return final_results[:result_topn]
|
| 469 |
+
|
| 470 |
+
# ==========================================
|
| 471 |
+
# 5. ์ปค์คํ
์
๋ ฅ ๊ธฐ๋ฐ ๋์ฒด ์๊ณ ๋ฆฌ์ฆ (์์ ๋จ)
|
| 472 |
+
# ==========================================
|
| 473 |
+
def substitute_single_custom(target_ing, context_ings_list, user_stopwords, w_w2v, w_d2v, excluded_ings=None, topn=10):
|
| 474 |
+
if target_ing not in w2v_model.wv: return pd.DataFrame()
|
| 475 |
+
total_weight = w_w2v + w_d2v
|
| 476 |
+
if total_weight == 0: total_weight = 1.0
|
| 477 |
+
vec_custom_context = None
|
| 478 |
+
if w_d2v > 0:
|
| 479 |
+
valid_context = [word for word in context_ings_list if word in d2v_model.wv]
|
| 480 |
+
if valid_context: vec_custom_context = d2v_model.infer_vector(valid_context)
|
| 481 |
+
target_rank = get_estimated_price_rank(target_ing, price_map)
|
| 482 |
+
candidates_raw = w2v_model.wv.most_similar(target_ing, topn=50)
|
| 483 |
+
temp_results = []
|
| 484 |
+
seen_candidates = set()
|
| 485 |
+
|
| 486 |
+
# [์์ ] ์ค์๊ฐ ๋ก๋
|
| 487 |
+
global_stopwords_set = set(load_global_stopwords())
|
| 488 |
+
final_stopwords = set(user_stopwords) | global_stopwords_set
|
| 489 |
+
excluded_set = set(excluded_ings) if excluded_ings else set()
|
| 490 |
+
|
| 491 |
+
for cand, score_w2v in candidates_raw:
|
| 492 |
+
clean_cand = cand
|
| 493 |
+
if final_stopwords:
|
| 494 |
+
for stop in final_stopwords: clean_cand = clean_cand.replace(stop, "")
|
| 495 |
+
clean_cand = clean_cand.strip()
|
| 496 |
+
|
| 497 |
+
if not clean_cand: continue
|
| 498 |
+
if clean_cand in final_stopwords: continue
|
| 499 |
+
if clean_cand in excluded_set: continue
|
| 500 |
+
|
| 501 |
+
if clean_cand in context_ings_list: continue
|
| 502 |
+
if clean_cand == target_ing: continue
|
| 503 |
+
if clean_cand not in w2v_model.wv: continue
|
| 504 |
+
if clean_cand in seen_candidates: continue
|
| 505 |
+
seen_candidates.add(clean_cand)
|
| 506 |
+
real_score_w2v = w2v_model.wv.similarity(target_ing, clean_cand)
|
| 507 |
+
s_w2v = max(0.0, real_score_w2v)
|
| 508 |
+
if s_w2v < 0.35: continue
|
| 509 |
+
s_d2v = 0.0
|
| 510 |
+
if w_d2v > 0 and vec_custom_context is not None:
|
| 511 |
+
rid_list = recipes_by_ingredient.get(clean_cand, [])
|
| 512 |
+
if len(rid_list) > 20:
|
| 513 |
+
np.random.seed(42)
|
| 514 |
+
rid_list = np.random.choice(rid_list, 20, replace=False)
|
| 515 |
+
if rid_list is not None and len(rid_list) > 0:
|
| 516 |
+
sims = []
|
| 517 |
+
for r in rid_list:
|
| 518 |
+
rt = f"recipe_{r}"
|
| 519 |
+
if rt in d2v_model.dv: sims.append(cos_sim(vec_custom_context, d2v_model.dv[rt]))
|
| 520 |
+
if sims: s_d2v = np.mean(sims)
|
| 521 |
+
s_method, s_cat = 0.0, 0.0
|
| 522 |
+
cand_rank = get_estimated_price_rank(clean_cand, price_map)
|
| 523 |
+
saving_score = target_rank - cand_rank
|
| 524 |
+
temp_results.append({"๋์ฒด์ฌ๋ฃ": clean_cand, "raw_W2V": s_w2v, "raw_D2V": s_d2v, "raw_Method": s_method, "raw_Category": s_cat, "saving_score": saving_score})
|
| 525 |
+
if not temp_results: return pd.DataFrame()
|
| 526 |
+
df_res = pd.DataFrame(temp_results)
|
| 527 |
+
cols = ["raw_W2V", "raw_D2V"]
|
| 528 |
+
norm_cols = ["W2V", "D2V"]
|
| 529 |
+
for raw_col, norm_col in zip(cols, norm_cols):
|
| 530 |
+
min_val = df_res[raw_col].min()
|
| 531 |
+
max_val = df_res[raw_col].max()
|
| 532 |
+
if max_val - min_val == 0: df_res[norm_col] = 0.5
|
| 533 |
+
else: df_res[norm_col] = (df_res[raw_col] - min_val) / (max_val - min_val)
|
| 534 |
+
df_res["์ต์ข
์ ์"] = ((df_res["W2V"]*w_w2v) + (df_res["D2V"]*w_d2v)) / total_weight
|
| 535 |
+
return df_res.sort_values("์ต์ข
์ ์", ascending=False).head(topn).reset_index(drop=True)
|
| 536 |
+
|
| 537 |
+
def substitute_multi_custom(targets, context_ings_list, user_stopwords, w_w2v, w_d2v, excluded_ings=None, beam_width=3, result_topn=3):
|
| 538 |
+
total_weight = w_w2v + w_d2v
|
| 539 |
+
if total_weight == 0: total_weight = 1.0
|
| 540 |
+
vec_custom_context = None
|
| 541 |
+
if w_d2v > 0:
|
| 542 |
+
valid_context = [word for word in context_ings_list if word in d2v_model.wv]
|
| 543 |
+
if valid_context: vec_custom_context = d2v_model.infer_vector(valid_context)
|
| 544 |
+
target_ranks_sum = 0
|
| 545 |
+
for t in targets: target_ranks_sum += get_estimated_price_rank(t, price_map)
|
| 546 |
+
|
| 547 |
+
# [์์ ] ์ค์๊ฐ ๋ก๋
|
| 548 |
+
global_stopwords_set = set(load_global_stopwords())
|
| 549 |
+
final_stopwords = set(user_stopwords) | global_stopwords_set
|
| 550 |
+
excluded_set = set(excluded_ings) if excluded_ings else set()
|
| 551 |
+
|
| 552 |
+
beam = [(0.0, [], context_ings_list)]
|
| 553 |
+
for target_ing in targets:
|
| 554 |
+
next_beam = []
|
| 555 |
+
if target_ing not in w2v_model.wv:
|
| 556 |
+
for score, subs, ctx in beam: next_beam.append((score, subs + [target_ing], ctx))
|
| 557 |
+
beam = next_beam
|
| 558 |
+
continue
|
| 559 |
+
for path_score, path_subs, path_ctx in beam:
|
| 560 |
+
current_ctx_ing = [x for x in path_ctx if x != target_ing]
|
| 561 |
+
candidates = w2v_model.wv.most_similar(target_ing, topn=30)
|
| 562 |
+
temp_candidates = []
|
| 563 |
+
seen_candidates = set()
|
| 564 |
+
for cand, _ in candidates:
|
| 565 |
+
clean_cand = cand
|
| 566 |
+
if final_stopwords:
|
| 567 |
+
for stop in final_stopwords: clean_cand = clean_cand.replace(stop, "")
|
| 568 |
+
clean_cand = clean_cand.strip()
|
| 569 |
+
|
| 570 |
+
if not clean_cand: continue
|
| 571 |
+
if clean_cand in final_stopwords: continue
|
| 572 |
+
if clean_cand in excluded_set: continue
|
| 573 |
+
|
| 574 |
+
if clean_cand in current_ctx_ing or clean_cand in path_subs: continue
|
| 575 |
+
if clean_cand == target_ing: continue
|
| 576 |
+
if clean_cand not in w2v_model.wv: continue
|
| 577 |
+
if clean_cand in seen_candidates: continue
|
| 578 |
+
seen_candidates.add(clean_cand)
|
| 579 |
+
sim_orig = w2v_model.wv.similarity(target_ing, clean_cand)
|
| 580 |
+
sim_orig = max(0.0, sim_orig)
|
| 581 |
+
if sim_orig < 0.3: continue
|
| 582 |
+
harmony_scores = [w2v_model.wv.similarity(clean_cand, c) for c in current_ctx_ing if c in w2v_model.wv]
|
| 583 |
+
sim_harmony = np.mean(harmony_scores) if harmony_scores else 0.0
|
| 584 |
+
s_w2v = 0.5 * sim_orig + 0.5 * max(0.0, sim_harmony)
|
| 585 |
+
s_d2v = 0.0
|
| 586 |
+
if w_d2v > 0:
|
| 587 |
+
valid_path_ctx = [word for word in current_ctx_ing if word in d2v_model.wv]
|
| 588 |
+
if valid_path_ctx:
|
| 589 |
+
vec_path_context = d2v_model.infer_vector(valid_path_ctx)
|
| 590 |
+
rid_list = recipes_by_ingredient.get(clean_cand, [])
|
| 591 |
+
if len(rid_list) > 10:
|
| 592 |
+
np.random.seed(42)
|
| 593 |
+
rid_list = np.random.choice(rid_list, 10, replace=False)
|
| 594 |
+
if rid_list is not None and len(rid_list) > 0:
|
| 595 |
+
sims = []
|
| 596 |
+
for r in rid_list:
|
| 597 |
+
rt = f"recipe_{r}"
|
| 598 |
+
if rt in d2v_model.dv: sims.append(cos_sim(vec_path_context, d2v_model.dv[rt]))
|
| 599 |
+
if sims: s_d2v = np.mean(sims)
|
| 600 |
+
s_method, s_cat = 0.0, 0.0
|
| 601 |
+
temp_candidates.append({"cand": clean_cand, "raw_w2v": s_w2v, "raw_d2v": s_d2v})
|
| 602 |
+
if not temp_candidates: continue
|
| 603 |
+
df_temp = pd.DataFrame(temp_candidates)
|
| 604 |
+
cols = ["raw_w2v", "raw_d2v"]
|
| 605 |
+
for col in cols:
|
| 606 |
+
min_val = df_temp[col].min()
|
| 607 |
+
max_val = df_temp[col].max()
|
| 608 |
+
if max_val - min_val == 0: df_temp[col + "_norm"] = 0.5
|
| 609 |
+
else: df_temp[col + "_norm"] = (df_temp[col] - min_val) / (max_val - min_val)
|
| 610 |
+
for _, r in df_temp.iterrows():
|
| 611 |
+
weighted_sum = ((r["raw_w2v_norm"]*w_w2v) + (r["raw_d2v_norm"]*w_d2v)) / total_weight
|
| 612 |
+
new_total_score = path_score + weighted_sum
|
| 613 |
+
new_subs = path_subs + [r["cand"]]
|
| 614 |
+
new_ctx = current_ctx_ing + [r["cand"]]
|
| 615 |
+
next_beam.append((new_total_score, new_subs, new_ctx))
|
| 616 |
+
next_beam.sort(key=lambda x: x[0], reverse=True)
|
| 617 |
+
beam = next_beam[:beam_width]
|
| 618 |
+
final_results = []
|
| 619 |
+
for score, subs, _ in beam:
|
| 620 |
+
avg_score = score / len(targets) if targets else 0.0
|
| 621 |
+
cand_ranks_sum = 0
|
| 622 |
+
for sub_ing in subs: cand_ranks_sum += get_estimated_price_rank(sub_ing, price_map)
|
| 623 |
+
total_saving_score = target_ranks_sum - cand_ranks_sum
|
| 624 |
+
final_results.append((subs, avg_score, total_saving_score))
|
| 625 |
+
return final_results[:result_topn]
|
| 626 |
+
|
| 627 |
+
# ==========================================
|
| 628 |
+
# 6. ์ฌ๋ฃ ํค์๋ ๊ธฐ๋ฐ ๋ ์ํผ ๊ฒ์ (๊ธฐ์กด๊ณผ ๋์ผ)
|
| 629 |
+
# ==========================================
|
| 630 |
+
def find_recipes_by_ingredient_keyword(keyword, topn=5):
|
| 631 |
+
keyword = keyword.strip()
|
| 632 |
+
if not keyword: return []
|
| 633 |
+
matched_dishes = set()
|
| 634 |
+
for _, row in df.iterrows():
|
| 635 |
+
for ing in row['์ฌ๋ฃํ ํฐ']:
|
| 636 |
+
if keyword in ing:
|
| 637 |
+
matched_dishes.add(row['์๋ฆฌ๋ช
'])
|
| 638 |
+
break
|
| 639 |
+
return list(matched_dishes)[:topn]
|