SamuelTheo commited on
Commit
aa3fc00
·
verified ·
1 Parent(s): 105c213

Upload 13 files

Browse files
Files changed (13) hide show
  1. .dockerignore +9 -0
  2. .env.example +1 -0
  3. .gitattributes +36 -35
  4. .gitignore +220 -0
  5. .python-version +1 -0
  6. Dockerfile +37 -0
  7. README.md +85 -10
  8. app.py +98 -0
  9. best_hyperparameters.json +8 -0
  10. cnn_fis_classifier.keras +3 -0
  11. inference.py +227 -0
  12. pyproject.toml +18 -0
  13. uv.lock +0 -0
.dockerignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ .env
2
+ .venv
3
+ __pycache__
4
+ *.py[cod]
5
+ .pytest_cache
6
+ .mypy_cache
7
+ .ruff_cache
8
+ .ipynb_checkpoints
9
+ Dockerfile
.env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ AI_ENGINE_ALLOWED_ORIGINS=http://localhost:3000
.gitattributes CHANGED
@@ -1,35 +1,36 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ cnn_fis_classifier.keras filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
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
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ *.lcov
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ cover/
54
+
55
+ # Translations
56
+ *.mo
57
+ *.pot
58
+
59
+ # Django stuff:
60
+ *.log
61
+ local_settings.py
62
+ db.sqlite3
63
+ db.sqlite3-journal
64
+
65
+ # Flask stuff:
66
+ instance/
67
+ .webassets-cache
68
+
69
+ # Scrapy stuff:
70
+ .scrapy
71
+
72
+ # Sphinx documentation
73
+ docs/_build/
74
+
75
+ # PyBuilder
76
+ .pybuilder/
77
+ target/
78
+
79
+ # Jupyter Notebook
80
+ .ipynb_checkpoints
81
+
82
+ # IPython
83
+ profile_default/
84
+ ipython_config.py
85
+
86
+ # pyenv
87
+ # For a library or package, you might want to ignore these files since the code is
88
+ # intended to run in multiple environments; otherwise, check them in:
89
+ # .python-version
90
+
91
+ # pipenv
92
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
94
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
95
+ # install all needed dependencies.
96
+ # Pipfile.lock
97
+
98
+ # UV
99
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
100
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
101
+ # commonly ignored for libraries.
102
+ # uv.lock
103
+
104
+ # poetry
105
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
106
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
107
+ # commonly ignored for libraries.
108
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
109
+ # poetry.lock
110
+ # poetry.toml
111
+
112
+ # pdm
113
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
114
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
115
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
116
+ # pdm.lock
117
+ # pdm.toml
118
+ .pdm-python
119
+ .pdm-build/
120
+
121
+ # pixi
122
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
123
+ # pixi.lock
124
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
125
+ # in the .venv directory. It is recommended not to include this directory in version control.
126
+ .pixi/*
127
+ !.pixi/config.toml
128
+
129
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
130
+ __pypackages__/
131
+
132
+ # Celery stuff
133
+ celerybeat-schedule*
134
+ celerybeat.pid
135
+
136
+ # Redis
137
+ *.rdb
138
+ *.aof
139
+ *.pid
140
+
141
+ # RabbitMQ
142
+ mnesia/
143
+ rabbitmq/
144
+ rabbitmq-data/
145
+
146
+ # ActiveMQ
147
+ activemq-data/
148
+
149
+ # SageMath parsed files
150
+ *.sage.py
151
+
152
+ # Environments
153
+ .env
154
+ .envrc
155
+ .venv
156
+ env/
157
+ venv/
158
+ ENV/
159
+ env.bak/
160
+ venv.bak/
161
+
162
+ # Spyder project settings
163
+ .spyderproject
164
+ .spyproject
165
+
166
+ # Rope project settings
167
+ .ropeproject
168
+
169
+ # mkdocs documentation
170
+ /site
171
+
172
+ # mypy
173
+ .mypy_cache/
174
+ .dmypy.json
175
+ dmypy.json
176
+
177
+ # Pyre type checker
178
+ .pyre/
179
+
180
+ # pytype static type analyzer
181
+ .pytype/
182
+
183
+ # Cython debug symbols
184
+ cython_debug/
185
+
186
+ # PyCharm
187
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
188
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
189
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
190
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
191
+ # .idea/
192
+
193
+ # Abstra
194
+ # Abstra is an AI-powered process automation framework.
195
+ # Ignore directories containing user credentials, local state, and settings.
196
+ # Learn more at https://abstra.io/docs
197
+ .abstra/
198
+
199
+ # Visual Studio Code
200
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
201
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
202
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
203
+ # you could uncomment the following to ignore the entire vscode folder
204
+ # .vscode/
205
+ # Temporary file for partial code execution
206
+ tempCodeRunnerFile.py
207
+
208
+ # Ruff stuff:
209
+ .ruff_cache/
210
+
211
+ # PyPI configuration file
212
+ .pypirc
213
+
214
+ # Marimo
215
+ marimo/_static/
216
+ marimo/_lsp/
217
+ __marimo__/
218
+
219
+ # Streamlit
220
+ .streamlit/secrets.toml
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.12
Dockerfile ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1.7
2
+
3
+ FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS runtime
4
+
5
+ ENV PYTHONUNBUFFERED=1 \
6
+ PYTHONDONTWRITEBYTECODE=1 \
7
+ TF_CPP_MIN_LOG_LEVEL=2 \
8
+ UV_COMPILE_BYTECODE=1 \
9
+ UV_LINK_MODE=copy
10
+
11
+ WORKDIR /app
12
+
13
+ # Runtime libraries needed by OpenCV/TensorFlow wheels on Debian slim.
14
+ RUN apt-get update \
15
+ && apt-get install -y --no-install-recommends \
16
+ libgl1 \
17
+ libglib2.0-0 \
18
+ libgomp1 \
19
+ && rm -rf /var/lib/apt/lists/*
20
+
21
+ COPY pyproject.toml uv.lock ./
22
+ RUN uv sync --frozen --no-dev
23
+
24
+ COPY main.py inference.py ./
25
+ COPY artifacts ./artifacts
26
+
27
+ RUN useradd --create-home --shell /usr/sbin/nologin appuser \
28
+ && chown -R appuser:appuser /app
29
+
30
+ USER appuser
31
+
32
+ EXPOSE 7860
33
+
34
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
35
+ CMD ./.venv/bin/python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:7860/health', timeout=5).read()" || exit 1
36
+
37
+ CMD ["./.venv/bin/uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860", "--proxy-headers"]
README.md CHANGED
@@ -1,10 +1,85 @@
1
- ---
2
- title: Soft Computing Ai
3
- emoji: 👁
4
- colorFrom: purple
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AI Engine
2
+
3
+ FastAPI inference service for the Soft Computing final project.
4
+
5
+ This service:
6
+
7
+ - accepts an image upload through an HTTP API,
8
+ - loads the trained Keras CNN model,
9
+ - preprocesses the image using the same `256x256` RGB normalization flow as the notebook,
10
+ - runs the CNN prediction,
11
+ - applies the same Fuzzy Inference System rules from the notebook, and
12
+ - returns the final label plus the raw class probabilities.
13
+
14
+ ## Model Path
15
+
16
+ The service reads the trained model from:
17
+
18
+ ```text
19
+ ai_engine/artifacts/cnn_fis_classifier.keras
20
+ ```
21
+
22
+ ## Install Dependencies
23
+
24
+ ```powershell
25
+ cd ai_engine
26
+ uv sync
27
+ ```
28
+
29
+ ## Run the API
30
+
31
+ ```powershell
32
+ cd ai_engine
33
+ uv run uvicorn main:app --reload --host 0.0.0.0 --port 8000
34
+ ```
35
+
36
+ The API will be available at:
37
+
38
+ ```text
39
+ http://127.0.0.1:8000
40
+ ```
41
+
42
+ ## Endpoints
43
+
44
+ ### `GET /health`
45
+
46
+ Returns service status plus model path information.
47
+
48
+ ### `POST /predict`
49
+
50
+ Accepts one multipart file field named `file`.
51
+
52
+ Example with PowerShell:
53
+
54
+ ```powershell
55
+ curl.exe -X POST "http://127.0.0.1:8000/predict" `
56
+ -F "file=@C:\path\to\image.jpg"
57
+ ```
58
+
59
+ Example response:
60
+
61
+ ```json
62
+ {
63
+ "label": "Positive",
64
+ "fis_score": 82.34,
65
+ "cnn_prediction_name": "Positive",
66
+ "probabilities": {
67
+ "negative": 0.07,
68
+ "neutral": 0.11,
69
+ "positive": 0.82
70
+ }
71
+ }
72
+ ```
73
+
74
+ ## CORS
75
+
76
+ By default, the API allows requests from:
77
+
78
+ - `http://localhost:3000`
79
+ - `http://127.0.0.1:3000`
80
+
81
+ To override that list:
82
+
83
+ ```powershell
84
+ $env:AI_ENGINE_ALLOWED_ORIGINS="http://localhost:3000,http://127.0.0.1:3000"
85
+ ```
app.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from contextlib import asynccontextmanager
5
+
6
+ import uvicorn
7
+ from fastapi import FastAPI, File, HTTPException, UploadFile
8
+ from fastapi.concurrency import run_in_threadpool
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from pydantic import BaseModel
11
+
12
+ from inference import get_model_path, load_model, predict_image_bytes
13
+
14
+
15
+ class ProbabilityResponse(BaseModel):
16
+ negative: float
17
+ neutral: float
18
+ positive: float
19
+
20
+
21
+ class PredictionResponse(BaseModel):
22
+ label: str
23
+ fis_score: float
24
+ cnn_prediction_name: str
25
+ probabilities: ProbabilityResponse
26
+
27
+
28
+ def get_allowed_origins() -> list[str]:
29
+ configured_origins = os.getenv("AI_ENGINE_ALLOWED_ORIGINS", "")
30
+ if configured_origins.strip():
31
+ return [origin.strip() for origin in configured_origins.split(",") if origin.strip()]
32
+
33
+ return [
34
+ "http://localhost:3000",
35
+ "http://127.0.0.1:3000",
36
+ ]
37
+
38
+
39
+ @asynccontextmanager
40
+ async def lifespan(_: FastAPI):
41
+ yield
42
+
43
+
44
+ app = FastAPI(
45
+ title="Soft Computing AI Engine",
46
+ description="FastAPI service for CNN + FIS image sentiment inference.",
47
+ version="0.1.0",
48
+ lifespan=lifespan,
49
+ )
50
+
51
+ app.add_middleware(
52
+ CORSMiddleware,
53
+ allow_origins=get_allowed_origins(),
54
+ allow_credentials=False,
55
+ allow_methods=["*"],
56
+ allow_headers=["*"],
57
+ )
58
+
59
+
60
+ @app.get("/health")
61
+ def health():
62
+ model_path = get_model_path()
63
+ return {
64
+ "status": "ok",
65
+ "model_exists": model_path.exists(),
66
+ "model_loaded": load_model.cache_info().currsize > 0,
67
+ "model_path": str(model_path),
68
+ }
69
+
70
+
71
+ @app.post("/predict", response_model=PredictionResponse)
72
+ async def predict(file: UploadFile = File(...)):
73
+ if not file.content_type or not file.content_type.startswith("image/"):
74
+ raise HTTPException(status_code=400, detail="Upload harus berupa file gambar.")
75
+
76
+ image_bytes = await file.read()
77
+ await file.close()
78
+
79
+ if not image_bytes:
80
+ raise HTTPException(status_code=400, detail="File gambar kosong.")
81
+
82
+ try:
83
+ prediction = await run_in_threadpool(predict_image_bytes, image_bytes)
84
+ except FileNotFoundError as error:
85
+ raise HTTPException(status_code=500, detail=str(error)) from error
86
+ except ValueError as error:
87
+ raise HTTPException(status_code=400, detail=str(error)) from error
88
+ except Exception as error:
89
+ raise HTTPException(
90
+ status_code=500,
91
+ detail=f"Prediksi gagal dijalankan: {error}",
92
+ ) from error
93
+
94
+ return PredictionResponse.model_validate(prediction)
95
+
96
+
97
+ if __name__ == "__main__":
98
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
best_hyperparameters.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "learning_rate": 0.003,
3
+ "filters_1": 96,
4
+ "filters_2": 256,
5
+ "dense_units": 64,
6
+ "dropout_rate": 0.5,
7
+ "batch_size": 16
8
+ }
cnn_fis_classifier.keras ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:38b5984a674788bba461685adf41cf58199ebb30d30706010a2d53c8c311deb4
3
+ size 10029074
inference.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import cv2
8
+ import numpy as np
9
+ import skfuzzy as fuzz
10
+ import tensorflow as tf
11
+ from skfuzzy import control as ctrl
12
+
13
+ IMAGE_SIZE = (256, 256)
14
+ LABEL_TOKENS = ("neg", "neutral", "pos")
15
+ LABEL_MAP = {label: index for index, label in enumerate(LABEL_TOKENS)}
16
+ LABEL_NAMES = {
17
+ LABEL_MAP["neg"]: "Negative",
18
+ LABEL_MAP["neutral"]: "Neutral",
19
+ LABEL_MAP["pos"]: "Positive",
20
+ }
21
+ NUM_CLASSES = len(LABEL_TOKENS)
22
+ MODEL_PATH = (
23
+ Path(__file__).resolve().parent / "artifacts" / "cnn_fis_classifier.keras"
24
+ )
25
+
26
+
27
+ def get_model_path() -> Path:
28
+ return MODEL_PATH.resolve()
29
+
30
+
31
+ @lru_cache(maxsize=1)
32
+ def load_model():
33
+ model_path = get_model_path()
34
+ if not model_path.exists():
35
+ raise FileNotFoundError(
36
+ f"Keras model tidak ditemukan di: {model_path}. "
37
+ "Pastikan file artefak ada di folder ai_engine/artifacts."
38
+ )
39
+
40
+ return tf.keras.models.load_model(model_path)
41
+
42
+
43
+ def preprocess_image_bytes(image_bytes: bytes) -> np.ndarray:
44
+ image_array = np.frombuffer(image_bytes, dtype=np.uint8)
45
+ image = cv2.imdecode(image_array, cv2.IMREAD_COLOR)
46
+ if image is None:
47
+ raise ValueError("Gambar tidak bisa dibaca. Pastikan file upload adalah gambar yang valid.")
48
+
49
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
50
+ image = cv2.resize(image, IMAGE_SIZE, interpolation=cv2.INTER_AREA)
51
+ image = image.astype(np.float32) / 255.0
52
+ return image
53
+
54
+
55
+ def normalize_probability_vector(model_output: np.ndarray) -> np.ndarray:
56
+ probabilities = np.asarray(model_output, dtype=np.float32).reshape(-1)
57
+
58
+ if probabilities.shape[0] == NUM_CLASSES:
59
+ return probabilities
60
+
61
+ if probabilities.shape[0] == 1:
62
+ # Current artifact is binary sigmoid: one score means positive confidence.
63
+ positive_probability = float(np.clip(probabilities[0], 0.0, 1.0))
64
+ return np.asarray(
65
+ [
66
+ 1.0 - positive_probability,
67
+ 0.0,
68
+ positive_probability,
69
+ ],
70
+ dtype=np.float32,
71
+ )
72
+
73
+ raise ValueError(
74
+ f"Expected either {NUM_CLASSES} class probabilities or 1 binary probability, "
75
+ f"received shape {probabilities.shape}."
76
+ )
77
+
78
+
79
+ def probability_dict_from_vector(probability_vector: np.ndarray) -> dict[str, float]:
80
+ probabilities = normalize_probability_vector(probability_vector)
81
+ if probabilities.shape[0] != NUM_CLASSES:
82
+ raise ValueError(
83
+ f"Expected {NUM_CLASSES} class probabilities, received shape {probabilities.shape}."
84
+ )
85
+
86
+ return {
87
+ "negative": float(probabilities[LABEL_MAP["neg"]]),
88
+ "neutral": float(probabilities[LABEL_MAP["neutral"]]),
89
+ "positive": float(probabilities[LABEL_MAP["pos"]]),
90
+ }
91
+
92
+
93
+ def build_fuzzy_system() -> ctrl.ControlSystemSimulation:
94
+ positive_confidence = ctrl.Antecedent(np.linspace(0, 1, 101), "positive_confidence")
95
+ neutral_confidence = ctrl.Antecedent(np.linspace(0, 1, 101), "neutral_confidence")
96
+ negative_confidence = ctrl.Antecedent(np.linspace(0, 1, 101), "negative_confidence")
97
+ final_decision = ctrl.Consequent(np.linspace(0, 100, 101), "final_decision")
98
+
99
+ for variable in (positive_confidence, neutral_confidence, negative_confidence):
100
+ variable["low"] = fuzz.trimf(variable.universe, [0.0, 0.0, 0.45])
101
+ variable["medium"] = fuzz.trimf(variable.universe, [0.2, 0.5, 0.8])
102
+ variable["high"] = fuzz.trimf(variable.universe, [0.55, 1.0, 1.0])
103
+
104
+ final_decision["negative"] = fuzz.trimf(final_decision.universe, [0, 0, 35])
105
+ final_decision["neutral"] = fuzz.trimf(final_decision.universe, [30, 50, 70])
106
+ final_decision["positive"] = fuzz.trimf(final_decision.universe, [65, 100, 100])
107
+
108
+ rules = [
109
+ ctrl.Rule(
110
+ positive_confidence["high"]
111
+ & neutral_confidence["low"]
112
+ & negative_confidence["low"],
113
+ final_decision["positive"],
114
+ ),
115
+ ctrl.Rule(
116
+ positive_confidence["high"]
117
+ & neutral_confidence["medium"]
118
+ & negative_confidence["low"],
119
+ final_decision["positive"],
120
+ ),
121
+ ctrl.Rule(
122
+ positive_confidence["medium"]
123
+ & neutral_confidence["low"]
124
+ & negative_confidence["low"],
125
+ final_decision["positive"],
126
+ ),
127
+ ctrl.Rule(
128
+ negative_confidence["high"]
129
+ & positive_confidence["low"]
130
+ & neutral_confidence["low"],
131
+ final_decision["negative"],
132
+ ),
133
+ ctrl.Rule(
134
+ negative_confidence["high"]
135
+ & positive_confidence["low"]
136
+ & neutral_confidence["medium"],
137
+ final_decision["negative"],
138
+ ),
139
+ ctrl.Rule(
140
+ negative_confidence["medium"]
141
+ & positive_confidence["low"]
142
+ & neutral_confidence["low"],
143
+ final_decision["negative"],
144
+ ),
145
+ ctrl.Rule(
146
+ neutral_confidence["high"]
147
+ & positive_confidence["low"]
148
+ & negative_confidence["low"],
149
+ final_decision["neutral"],
150
+ ),
151
+ ctrl.Rule(
152
+ neutral_confidence["high"]
153
+ & positive_confidence["medium"]
154
+ & negative_confidence["low"],
155
+ final_decision["neutral"],
156
+ ),
157
+ ctrl.Rule(
158
+ neutral_confidence["high"]
159
+ & negative_confidence["medium"]
160
+ & positive_confidence["low"],
161
+ final_decision["neutral"],
162
+ ),
163
+ ctrl.Rule(
164
+ positive_confidence["medium"] & negative_confidence["medium"],
165
+ final_decision["neutral"],
166
+ ),
167
+ ctrl.Rule(
168
+ positive_confidence["medium"] & neutral_confidence["medium"],
169
+ final_decision["neutral"],
170
+ ),
171
+ ctrl.Rule(
172
+ negative_confidence["medium"] & neutral_confidence["medium"],
173
+ final_decision["neutral"],
174
+ ),
175
+ ctrl.Rule(
176
+ positive_confidence["high"] & negative_confidence["high"],
177
+ final_decision["neutral"],
178
+ ),
179
+ ctrl.Rule(
180
+ positive_confidence["low"]
181
+ & negative_confidence["low"]
182
+ & neutral_confidence["low"],
183
+ final_decision["neutral"],
184
+ ),
185
+ ]
186
+
187
+ control_system = ctrl.ControlSystem(rules)
188
+ return ctrl.ControlSystemSimulation(control_system)
189
+
190
+
191
+ def fuzzy_label_from_probabilities(probability_vector: np.ndarray) -> tuple[str, float]:
192
+ probabilities = probability_dict_from_vector(probability_vector)
193
+
194
+ fuzzy_system = build_fuzzy_system()
195
+ fuzzy_system.input["positive_confidence"] = probabilities["positive"]
196
+ fuzzy_system.input["neutral_confidence"] = probabilities["neutral"]
197
+ fuzzy_system.input["negative_confidence"] = probabilities["negative"]
198
+ fuzzy_system.compute()
199
+
200
+ decision_score = float(fuzzy_system.output["final_decision"])
201
+ if decision_score >= 65:
202
+ label = "Positive"
203
+ elif decision_score <= 35:
204
+ label = "Negative"
205
+ else:
206
+ label = "Neutral"
207
+
208
+ return label, decision_score
209
+
210
+
211
+ def predict_image_bytes(image_bytes: bytes) -> dict[str, Any]:
212
+ image = preprocess_image_bytes(image_bytes)
213
+ model = load_model()
214
+
215
+ batch = np.expand_dims(image, axis=0).astype(np.float32)
216
+ model_output = np.asarray(model.predict(batch, verbose=0)[0], dtype=np.float32)
217
+ probability_vector = normalize_probability_vector(model_output)
218
+ probabilities = probability_dict_from_vector(probability_vector)
219
+ fuzzy_label, fuzzy_score = fuzzy_label_from_probabilities(probability_vector)
220
+ cnn_prediction_name = LABEL_NAMES[int(np.argmax(probability_vector))]
221
+
222
+ return {
223
+ "label": fuzzy_label,
224
+ "fis_score": fuzzy_score,
225
+ "cnn_prediction_name": cnn_prediction_name,
226
+ "probabilities": probabilities,
227
+ }
pyproject.toml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "ai-engine"
3
+ version = "0.1.0"
4
+ description = "FastAPI inference service for the Soft Computing final project"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "fastapi>=0.116.0",
9
+ "numpy>=2.4.5",
10
+ "opencv-python>=4.13.0.92",
11
+ "python-multipart>=0.0.20",
12
+ "scikit-fuzzy>=0.5.0",
13
+ "tensorflow[and-cuda]>=2.21.0; sys_platform == 'linux'",
14
+ "tensorflow>=2.21.0; sys_platform == 'win32'",
15
+ "uvicorn[standard]>=0.35.0",
16
+ "scipy>=1.17.1",
17
+ "networkx>=3.6.1",
18
+ ]
uv.lock ADDED
The diff for this file is too large to render. See raw diff