Merge branch 'main' into frontend_AI_communication

This commit is contained in:
Krzysztof Rudnicki 2024-05-28 05:06:53 +02:00
commit 52e509404e
40 changed files with 15098 additions and 20 deletions

View File

@ -1,25 +1,15 @@
# Use an official Python runtime as a parent image FROM node:latest
FROM python:3.10-slim
# Set the working directory to /app # Create app directory
WORKDIR /app WORKDIR /usr/src/app
# Install necessary OS packages # Install app dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy the Python script, requirements, and constants.ini file into the container at /app # If you also need http-server globally
COPY connector/Include/frontend_AI_connector.py /app/ RUN npm install -g http-server
COPY connector/Include/requirements.txt /app/
COPY connector/Include/init_scripts/constants.ini /app/init_scripts/
# Modify requirements.txt to use psycopg2-binary # Bundle app source
RUN sed -i 's/psycopg2==2.9.9/psycopg2-binary==2.9.9/' requirements.txt COPY . .
# Install any needed packages specified in requirements.txt EXPOSE 8080
RUN pip install --no-cache-dir -r requirements.txt CMD ["http-server", "-p 8080"]
# Run frontend_AI_connector.py when the container launches
CMD ["python", "./frontend_AI_connector.py"]

25
Dockerfile_backend Normal file
View File

@ -0,0 +1,25 @@
# Use an official Python runtime as a parent image
FROM python:3.10-slim
# Set the working directory to /app
WORKDIR /app
# Install necessary OS packages
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy the Python script, requirements, and constants.ini file into the container at /app
COPY connector/Include/frontend_AI_connector.py /app/
COPY connector/Include/requirements.txt /app/
COPY connector/Include/init_scripts/constants.ini /app/init_scripts/
# Modify requirements.txt to use psycopg2-binary
RUN sed -i 's/psycopg2==2.9.9/psycopg2-binary==2.9.9/' requirements.txt
# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Run frontend_AI_connector.py when the container launches
CMD ["python", "./frontend_AI_connector.py"]

Binary file not shown.

View File

View File

@ -0,0 +1,12 @@
1. Receive data from database
2. Calculate data:
a) User Count
b) Movies Count
c) Rating Count
d) Total data size
e) "Hot" movies -> movies that received most ratings during last
week
f) System logs (keeps tracks of all messages exchanged by all services)
1. timestamp (when was a message send)
2. Message raw data
3. Send requests to frondend upon request

View File

View File

View File

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,25 @@
One script upon being ran should deploy the entire solution to some
cloud service
It should:
1. Build everything
2. Connect to cloud service (Azure?)
3. Send the data
After it will be run website should be accessible under some address
(cloud service should provide this address?)
Decide:
What cloud service? (Azure?) Requirements:
a. Free (https://github.com/cloudcommunity/Cloud-Free-Tier-Comparison)
b. Popular
AWS:
+ Most popular
+ "Always" free
+ AWS CDK available
Azure
-Microsoft
Google Cloud
+We have google accounts anyway
What technology for script (Ansible?)

View File

@ -0,0 +1,12 @@
Monitoring service
Monitoring service should keep track of all communication
send between all services
Store logs with:
1. timestamp (when was a message send)
2. Message raw data
To decide:
What to use for Monitoring service?
Maybe cloud service will provide us with functioning one?
Maybe deployment tool already has one?

19
frontend/.eslintrc.cjs Normal file
View File

@ -0,0 +1,19 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'prettier',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
};

24
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,3 @@
{
"singleQuote": true
}

19
frontend/Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM node:18-alpine AS build-stage
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
FROM nginx:stable-alpine
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

13
frontend/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Movie Recommendation</title>
</head>
<body>
<div id="root" class="flex min-h-dvh flex-col bg-gray-800 text-white"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4887
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
frontend/package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "movie-recommendation-client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"firebase": "^10.11.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-firebase-hooks": "^5.1.1",
"react-loader-spinner": "^6.1.6",
"react-router-dom": "^6.23.1"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"tailwindcss": "^3.4.3",
"typescript": "^5.2.2",
"vite": "^5.2.0"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

1
frontend/public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

33
frontend/src/App.tsx Normal file
View File

@ -0,0 +1,33 @@
import { Outlet, RouterProvider, createBrowserRouter } from 'react-router-dom';
import Navbar from './components/Navbar';
import Home from './pages/Home';
import Recommendations from './pages/Recommendations';
import Rate from './pages/Rate';
import Analytics from './pages/Analytics';
function App() {
const Layout = (
<>
<Navbar />
<Outlet />
</>
);
const router = createBrowserRouter([
{
path: '/',
element: Layout,
children: [
{ path: '/', element: <Home /> },
{ path: '/recommendations', element: <Recommendations /> },
{ path: '/rate', element: <Rate /> },
{ path: '/analytics', element: <Analytics /> },
{ path: '*', element: <code>404</code> },
],
},
]);
return <RouterProvider router={router} />;
}
export default App;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#fff" d="M44.59 4.21a63.28 63.28 0 004.33 120.9 67.6 67.6 0 0032.36.35 57.13 57.13 0 0025.9-13.46 57.44 57.44 0 0016-26.26 74.33 74.33 0 001.61-33.58H65.27v24.69h34.47a29.72 29.72 0 01-12.66 19.52 36.16 36.16 0 01-13.93 5.5 41.29 41.29 0 01-15.1 0A37.16 37.16 0 0144 95.74a39.3 39.3 0 01-14.5-19.42 38.31 38.31 0 010-24.63 39.25 39.25 0 019.18-14.91A37.17 37.17 0 0176.13 27a34.28 34.28 0 0113.64 8q5.83-5.8 11.64-11.63c2-2.09 4.18-4.08 6.15-6.22A61.22 61.22 0 0087.2 4.59a64 64 0 00-42.61-.38z"/><path fill="#e33629" d="M44.59 4.21a64 64 0 0142.61.37 61.22 61.22 0 0120.35 12.62c-2 2.14-4.11 4.14-6.15 6.22Q95.58 29.23 89.77 35a34.28 34.28 0 00-13.64-8 37.17 37.17 0 00-37.46 9.74 39.25 39.25 0 00-9.18 14.91L8.76 35.6A63.53 63.53 0 0144.59 4.21z"/><path fill="#f8bd00" d="M3.26 51.5a62.93 62.93 0 015.5-15.9l20.73 16.09a38.31 38.31 0 000 24.63q-10.36 8-20.73 16.08a63.33 63.33 0 01-5.5-40.9z"/><path fill="#587dbd" d="M65.27 52.15h59.52a74.33 74.33 0 01-1.61 33.58 57.44 57.44 0 01-16 26.26c-6.69-5.22-13.41-10.4-20.1-15.62a29.72 29.72 0 0012.66-19.54H65.27c-.01-8.22 0-16.45 0-24.68z"/><path fill="#319f43" d="M8.75 92.4q10.37-8 20.73-16.08A39.3 39.3 0 0044 95.74a37.16 37.16 0 0014.08 6.08 41.29 41.29 0 0015.1 0 36.16 36.16 0 0013.93-5.5c6.69 5.22 13.41 10.4 20.1 15.62a57.13 57.13 0 01-25.9 13.47 67.6 67.6 0 01-32.36-.35 63 63 0 01-23-11.59A63.73 63.73 0 018.75 92.4z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,55 @@
import { useAuthState, useSignOut } from 'react-firebase-hooks/auth';
import { Link, NavLink } from 'react-router-dom';
import { auth } from '../firebase';
function Navbar() {
const [user, userLoading] = useAuthState(auth);
const [signOut] = useSignOut(auth);
return (
<header className="px-6 py-4 flex justify-between items-center">
<Link to="/">
<h1 className="text-xl sm:text-2xl">Movie Recommendation</h1>
</Link>
{userLoading && <p>Loading user info</p>}
{!userLoading && !user && (
<p className="text-gray-300 text-lg">Logged out</p>
)}
{!!user && (
<div className="flex gap-8">
<nav>
<ul className="flex flex-col text-center gap-4 md:flex-row">
<li>
<NavLink
to="/recommendations"
className={({ isActive }) => (isActive ? 'font-medium' : '')}
>
Recommendations
</NavLink>
</li>
<li>
<NavLink
to="/rate"
className={({ isActive }) => (isActive ? 'font-medium' : '')}
>
Rate
</NavLink>
</li>
<li>
<NavLink
to="/analytics"
className={({ isActive }) => (isActive ? 'font-medium' : '')}
>
Analytics
</NavLink>
</li>
</ul>
</nav>
<button onClick={() => signOut()}>Log out</button>
</div>
)}
</header>
);
}
export default Navbar;

View File

@ -0,0 +1,55 @@
import {
collection,
getCountFromServer,
query,
where,
} from 'firebase/firestore';
import { useEffect, useState } from 'react';
import { useAuthState } from 'react-firebase-hooks/auth';
import { Link } from 'react-router-dom';
import { auth, db } from '../firebase';
const ratingsRef = collection(db, 'ratings');
function Welcome() {
const [user] = useAuthState(auth);
const [ratingsCount, setRatingsCount] = useState<number | null>(null);
if (!user) throw new Error('No user');
useEffect(() => {
(async () => {
const ratingsQuery = query(ratingsRef, where('userId', '==', user.uid));
const snapshot = await getCountFromServer(ratingsQuery);
setRatingsCount(snapshot.data().count);
})();
}, [user.uid]);
return (
<main>
<p className="text-lg mb-8">Hello {user.displayName}!</p>
{ratingsCount === null && <p>Loading your data</p>}
{Number.isInteger(ratingsCount) &&
(ratingsCount === 0 ? (
<p>
<span>You don't have any ratings yet. Please </span>
<Link to="/rate" className="underline">
rate
</Link>
<span> your first movie.</span>
</p>
) : (
<p>
<span>Go to </span>
<Link to="/recommendations" className="underline">
recommendations
</Link>
<span> to get inspired.</span>
</p>
))}
</main>
);
}
export default Welcome;

18
frontend/src/firebase.ts Normal file
View File

@ -0,0 +1,18 @@
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
const firebaseConfig = {
apiKey: 'AIzaSyAF34ovW0qaDlV68sbpCjylz4TQ0he_XEM',
authDomain: 'movie-recommendation-2024.firebaseapp.com',
projectId: 'movie-recommendation-2024',
storageBucket: 'movie-recommendation-2024.appspot.com',
messagingSenderId: '777010585410',
appId: '1:777010585410:web:7db1436b72879d512032db',
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
export { auth, db };

3
frontend/src/index.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

10
frontend/src/main.tsx Normal file
View File

@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

View File

@ -0,0 +1,5 @@
function Analytics() {
return <div>Analytics</div>;
}
export default Analytics;

View File

@ -0,0 +1,35 @@
import { useAuthState, useSignInWithGoogle } from 'react-firebase-hooks/auth';
import { Triangle } from 'react-loader-spinner';
import Welcome from '../components/Welcome';
import { auth } from '../firebase';
import googleLogo from '../assets/google.svg';
function Home() {
const [user, userLoading] = useAuthState(auth);
const [signInWithGoogle] = useSignInWithGoogle(auth);
return (
<main className="flex flex-col items-center mt-16 mx-4 text-center">
<h2 className="text-2xl font-medium mb-8">
Find the perfect movie to watch tonight.
</h2>
{userLoading && <Triangle />}
{!userLoading && !user && (
<>
<p className="mt-4">But first you need to log in:</p>
<button
type="button"
onClick={() => signInWithGoogle()}
className="mt-10 flex items-center gap-2 border rounded-full py-2 px-4 border-gray-400 hover:bg-gray-700 duration-100"
>
<img src={googleLogo} alt="Google" className="size-8" />
<span>Log in with Google</span>
</button>
</>
)}
{!!user && <Welcome />}
</main>
);
}
export default Home;

View File

@ -0,0 +1,5 @@
function Rate() {
return <div>Rate</div>;
}
export default Rate;

View File

@ -0,0 +1,5 @@
function Recommendations() {
return <div>Recommendations</div>;
}
export default Recommendations;

1
frontend/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,9 @@
import type { Config } from 'tailwindcss';
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
} satisfies Config;

25
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

7
frontend/vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,119 @@
import pandas as pd
import numpy as np
from ast import literal_eval
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.metrics.pairwise import cosine_similarity
def get_director(x):
for i in x:
if i['job'] == 'Director':
return i['name']
return np.nan
def get_list(x):
if isinstance(x, list):
names = [i['name'] for i in x]
if len(names) > 3:
names = names[:3]
return names
return []
def clean_data(x):
if isinstance(x, list):
return [str.lower(i.replace(" ", "")) for i in x]
else:
if isinstance(x, str):
return str.lower(x.replace(" ", ""))
else:
return ''
def create_soup(x):
return ' '.join(x['keywords']) + ' ' + ' '.join(x['cast']) + ' ' + x['director'] + ' ' + ' '.join(x['genres'])
class MovieRecommender:
def __init__(self):
self.df = None
self.cosine_sim = None
def fit(self, credits_file, movies_file):
"""
Fittuje AI do przekazanych danych
:param credits_file: csv z creditsami
:param movies_file: csv z filmami
:return: Nic
"""
df1 = pd.read_csv(credits_file)
df2 = pd.read_csv(movies_file)
df1.columns = ['id', 'tittle', 'cast', 'crew']
df2 = df2.merge(df1, on='id')
df2['overview'] = df2['overview'].fillna('')
self.df = df2
features = ['cast', 'crew', 'keywords', 'genres']
for feature in features:
df2[feature] = df2[feature].apply(literal_eval)
df2['director'] = df2['crew'].apply(get_director)
features = ['cast', 'keywords', 'genres']
for feature in features:
df2[feature] = df2[feature].apply(get_list)
features = ['cast', 'keywords', 'director', 'genres']
for feature in features:
df2[feature] = df2[feature].apply(clean_data)
df2['soup'] = df2.apply(create_soup, axis=1)
count = CountVectorizer(stop_words='english')
count_matrix = count.fit_transform(df2['soup'])
self.cosine_sim = cosine_similarity(count_matrix, count_matrix)
self.df = df2.reset_index()
def _get_recommendations_one_input(self, movie_id):
"""
Tworzy rekomendacje, bazując na jednym filmie
:param movie_id: id filmu, dla którego ma zrobić rekomendację
:return: Zwraca listę [movie_ids, similarity_scores] gdzie oba argumenty np.array
"""
indices = pd.Series(self.df.index, index=self.df['id']).drop_duplicates()
idx = indices[movie_id]
sim_scores = list(enumerate(self.cosine_sim[idx]))
sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)
sim_scores = sim_scores[1:101]
movie_indices = [i[0] for i in sim_scores]
sim_scores = np.array([t[1] for t in sim_scores])
return [self.df['id'].iloc[movie_indices].values, sim_scores]
def get_recommendations(self, movie_ids: list) -> {}:
"""
Tworzy listę rekomendacji bazującą na id podanych filmów
:param movie_ids: id filmów, na podstawie których ma wybrać rekomendowane filmy
:return: Zwraca dicta {movie_id: similarity_scores}
"""
recommended_movies = {}
for movie_id in movie_ids:
recommended_ids, sim_scores = self._get_recommendations_one_input(movie_id)
for recommended_id, sim_score in zip(recommended_ids, sim_scores):
if recommended_id in movie_ids:
continue
if recommended_movies.get(recommended_id) is None:
recommended_movies[recommended_id] = sim_score / len(movie_ids)
else:
recommended_movies[recommended_id] += sim_score / len(movie_ids)
return recommended_movies
# Przykładowe użycie:
if __name__ == "__main__":
recommender = MovieRecommender()
recommender.fit('datasets/tmdb_5000_credits.csv', 'datasets/tmdb_5000_movies.csv')
recommendations = recommender.get_recommendations([49026, 155, 312113])
print(recommendations)