This commit is contained in:
telereview
2023-03-23 19:06:51 +01:00
120 changed files with 5753 additions and 3337 deletions

30
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,30 @@
{
"configurations": [
{
"name": "Docker Node.js Launch",
"type": "docker",
"request": "launch",
"preLaunchTask": "docker-run: debug",
"platform": "node",
"node": {
"package": "${workspaceFolder}/code/reviews_api/package.json",
"localRoot": "${workspaceFolder}/code/reviews_api"
}
},
{
"name": "Docker: Python - General",
"type": "docker",
"request": "launch",
"preLaunchTask": "docker-run: debug",
"python": {
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app"
}
],
"projectType": "general"
}
}
]
}

46
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,46 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "docker-build",
"label": "docker-build",
"platform": "node",
"dockerBuild": {
"dockerfile": "${workspaceFolder}/code/reviews_api/Dockerfile",
"context": "${workspaceFolder}/code/reviews_api",
"pull": true
},
"node": {
"package": "${workspaceFolder}/code/reviews_api/package.json"
}
},
{
"type": "docker-run",
"label": "docker-run: release",
"dependsOn": [
"docker-build"
],
"platform": "node",
"node": {
"package": "${workspaceFolder}/code/reviews_api/package.json"
}
},
{
"type": "docker-run",
"label": "docker-run: debug",
"dependsOn": [
"docker-build"
],
"dockerRun": {
"env": {
"DEBUG": "*",
"NODE_ENV": "development"
}
},
"node": {
"package": "${workspaceFolder}/code/reviews_api/package.json",
"enableDebugging": true
}
}
]
}

View File

@@ -1,23 +0,0 @@
Ceci est votre dépôt pour le projet PACT.
Vous **DEVEZ** modifier ce fichier (`README.md`) et créer tous les
répertoires et fichiers dont vous aurez besoin pour votre projet.
# Important, le rapport d'avancement
Le répertoire `rapport` contient un squelette pour votre rapporte d'avancement.
Ce répertoire **ne doit pas être renommé** ni le fichier `README.adoc` qu'il contient.
Le fichier `README.adoc` est le point d'entrée du rapport.
Il est rédigé en utilisant le langage [**AsciiDoc**](http://asciidoc.org/).
La syntaxe est supportée par GitLab qui le formatera correctement dans l'interface Web.
Le document final sera généré en utilisant l'outil [Asciidoctor](http://asciidoctor.org/) qui supporte les mêmes extensions que GitLab (pour les équations par exemple).
Un résumé de la syntaxe supportée est accessible [ici](http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/).
Vous pouvez éditer les différents fichiers en utilisant *votre éditeur de texte favori*.
Si vous n'en avez pas, vous pouvez, par exemple utiliser:
- [**Visual Studio Code**](https://code.visualstudio.com/) avec l'extension [AsciiDoc](https://marketplace.visualstudio.com/items?itemName=asciidoctor.asciidoctor-vscode) qui ajoute coloration syntaxique et rendu en temps réel.
- Ou bien sûr votre éditeur de text préféré **Sublim Text**, **Vim**, **Emacs**,…
- Une extension pour navigateur Web est aussi disponible pour visualiser le résultat ([**ici**](https://github.com/asciidoctor/asciidoctor-browser-extension)).

View File

@@ -0,0 +1,59 @@
import cv2
import mediapipe as mp
mp_drawing = mp.solutions.drawing_utils
mp_face_mesh = mp.solutions.face_mesh
drawing_spec = mp_drawing.DrawingSpec(thickness=1, circle_radius=1)
cap = cv2.VideoCapture(0)
with mp_face_mesh.FaceMesh(
max_num_faces=1,
refine_landmarks=True,
min_detection_confidence=0.5,
min_tracking_confidence=0.5) as face_mesh:
while cap.isOpened():
success, image = cap.read()
if not success:
print("Ignoring empty camera frame.")
continue
# Initialize the face mesh model
face_mesh = mp_face_mesh.FaceMesh(static_image_mode=False, max_num_faces=1, min_detection_confidence=0.5)
# Load the input image
# lecture de la vidéo
ret, frame = cap.read()
# conversion de l'image en RGB
image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Process the image and extract the landmarks
results = face_mesh.process(image)
if results.multi_face_landmarks:
landmarks = results.multi_face_landmarks[0]
# Define the landmark indices for the corners of the eyes and the tip of the nose
left_eye = [33, 133, 246, 161, 160, 159, 158, 157, 173, 133]
right_eye = [362, 263, 373, 380, 381, 382, 384, 385, 386, 362]
nose_tip = 4
# Calculate the distance between the eyes and the nose tip
left_eye_x = landmarks.landmark[left_eye[0]].x * image.shape[1]
right_eye_x = landmarks.landmark[right_eye[0]].x * image.shape[1]
nose_x = landmarks.landmark[nose_tip].x * image.shape[1]
eye_distance = abs(left_eye_x - right_eye_x)
nose_distance = abs(nose_x - (left_eye_x + right_eye_x) / 2)
# Determine the gender based on the eye and nose distances
if eye_distance > 1.5 * nose_distance:
gender = "Female"
else:
gender = "Male"
# Draw the landmarks on the image
cv2.putText(image, gender, (10, 50),cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
# affichage de la vidéo
cv2.imshow('Video', cv2.cvtColor(image, cv2.COLOR_RGB2BGR))
if cv2.waitKey(10) & 0xFF == ord('q'):
break
# libération de la caméra et des ressources
cap.release()
cv2.destroyAllWindows()

View File

@@ -0,0 +1,88 @@
import cv2
import numpy as np
import mediapipe as mp
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
mp_hands = mp.solutions.hands
def prodScalaire(V1,V2):
return V1[0]*V2[0]+V1[1]*V2[1]/(np.sqrt(V1[0]**2+V1[1]**2)*np.sqrt(V2[0]**2+V2[1]**2))
def reconnaissancePouce(handLandmarks):
etatDuPouce=["neutre","baissé","levé"]
i=0
j=0
for cpt in range (0,4):
V1=[handLandmarks[(4*cpt)+6][0]-handLandmarks[(4*cpt)+5][0],handLandmarks[(4*cpt)+6][1]-handLandmarks[(4*cpt)+5][1]]
V2=[handLandmarks[(4*cpt)+8][0]-handLandmarks[(4*cpt)+6][0],handLandmarks[(4*cpt)+8][1]-handLandmarks[(4*cpt)+6][1]]
j=np.dot(V1,V2)
if (j>0.005):
return etatDuPouce[0]
V1=[handLandmarks[4][0]-handLandmarks[1][0],handLandmarks[4][1]-handLandmarks[1][1]]
V2=[handLandmarks[2][0]-handLandmarks[1][0],handLandmarks[2][1]-handLandmarks[1][1]]
if((np.dot(V1,V2))>0 and (handLandmarks[4][1]>handLandmarks[2][1])):
i=1
elif(np.dot(V1,V2)>0 and handLandmarks[4][1]<handLandmarks[2][1]):
i=2
return etatDuPouce[i]
cap = cv2.VideoCapture(0)
with mp_hands.Hands(
model_complexity=0,
min_detection_confidence=0.5,
min_tracking_confidence=0.5) as hands:
while cap.isOpened():
success, image = cap.read()
if not success:
print("Ignoring empty camera frame.")
# If loading a video, use 'break' instead of 'continue'.
continue
# To improve performance, optionally mark the image as not writeable to
# pass by reference.
image.flags.writeable = False
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
results = hands.process(image)
# Draw the hand annotations on the image.
image.flags.writeable = True
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
if results.multi_hand_landmarks:
for hand_landmarks in results.multi_hand_landmarks:
mp_drawing.draw_landmarks(
image,
hand_landmarks,
mp_hands.HAND_CONNECTIONS,
mp_drawing_styles.get_default_hand_landmarks_style(),
mp_drawing_styles.get_default_hand_connections_style())
# Set variable to keep landmarks positions (x and y)
handLandmarks = []
if results.multi_hand_landmarks:
for hand_landmarks in results.multi_hand_landmarks:
# Fill list with x and y positions of each landmark
for landmarks in hand_landmarks.landmark:
handLandmarks.append([landmarks.x, landmarks.y])
cv2.putText(image, reconnaissancePouce(handLandmarks), (50, 450), cv2.FONT_HERSHEY_SIMPLEX, 3, (255, 0, 0), 10)
# Flip the image horizontally for a selfie-view display.
cv2.imshow('MediaPipe Hands', cv2.flip(image, 1))
if cv2.waitKey(5) & 0xFF == 27:
break
cap.release()
""" etatDuPouce=["neutre","baissé","levé"]
i=0
if results.multi_hand_landmarks:
if(results.multi_hand_landmarks.gestures.categories[0].categoryName==Thumb_Up):
cv2.putText(image, str(results.multi_hand_landmarks.gestures.categories[0].categoryName), (50, 450), cv2.FONT_HERSHEY_SIMPLEX, 3, (255, 0, 0), 10)
else:
cv2.putText(image, "raté", (50, 450), cv2.FONT_HERSHEY_SIMPLEX, 3, (255, 0, 0), 10)
"""

View File

@@ -1,75 +0,0 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
trainSet = datasets.ImageFolder(r'C:\Users\kesha\Desktop\TelecomParis\PACT\DownloadedDataset\train',
transform = transforms.ToTensor())
valSet = datasets.ImageFolder(r'C:\Users\kesha\Desktop\TelecomParis\PACT\DownloadedDataset\val',
transform = transforms.ToTensor())
trainloader = torch.utils.data.DataLoader(trainSet,
batch_size = 50,
shuffle = True)
valloader = torch.utils.data.DataLoader(valSet,
batch_size = 50,
shuffle = True)
class Net(nn.Module):
def __init__(self):
super().__init__()
#nn.Conv2d(channels_in, out_channels/number of filters, kernel size)
self.conv1 = nn.Conv2d(3, 16, 3)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(16, 32, 3)
self.conv3 = nn.Conv2d(32, 64, 3)
self.fc1 = nn.Linear(64*14*14, 16)
self.fc2 = nn.Linear(16, 6)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
#size = 16*126*126 then 16*63*63
x = self.pool(F.relu(self.conv2(x)))
#size = 32*61*61 then 32*30*30
x = self.pool(F.relu(self.conv3(x)))
#size = 64*28*28 then 64*14*14
x = torch.flatten(x, 1)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return x
net = Net()
print(net)
criterion = nn.CrossEntropyLoss()
optimizer = optim.RMSprop(net.parameters(), lr=0.001)
device = torch.device('cuda')
for epoch in range(1, 7):
print('Starting epoch ' + str(epoch))
current_loss = 0
Epoch = []
Loss = []
for i, data in enumerate(trainloader, 0):
inputs, labels = data
#très important
optimizer.zero_grad()
output = net(inputs)
loss = criterion(output, labels)
loss.backward()
optimizer.step()
current_loss += loss.item()
print('epoch: ', epoch, " loss: ", current_loss)
Loss.append(current_loss)
Epoch.append(epoch)
plt.plot(Epoch, Loss)
plt.title('Valeur de la fonction cost en fonction de l\'epoch')
plt.show()
#to save a model: torch.save(net.state_dict(), file_location)

View File

@@ -0,0 +1,27 @@
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

View File

@@ -0,0 +1 @@
*.wav

View File

@@ -0,0 +1,19 @@
FROM python:3.8
#Ne pas créer les fichiers .pyc
ENV PYTHONDONTWRITEBYTECODE=1
#Afficher les logs directement dans le terminal
ENV PYTHONUNBUFFERED=1
#Installation des dépendances de opencv
RUN apt-get update
RUN apt-get install ffmpeg libsm6 libxext6 portaudio19-dev python3-pyaudio pulseaudio -y
# Installation des dépendances python
COPY requirements.txt .
RUN python -m pip install -r requirements.txt
# Création du répertoire de travail
WORKDIR /app
COPY . /app
CMD ["python", "main.py"]

Binary file not shown.

View File

@@ -0,0 +1,22 @@
{
"ennuyant": {
"grade": 2,
"display": "Ennuyant"
},
"genial": {
"grade": 9,
"display": "Génial"
},
"j_ai_beaucoup_aime": {
"grade": 9,
"display": "J'ai beaucoup aimé"
},
"j_ai_trouve_ca_genial": {
"grade": 10,
"display": "J'ai trouvé ça génial"
},
"nul": {
"grade": 0,
"display": "Nul"
}
}

View File

@@ -0,0 +1,145 @@
import librosa
import os
import numpy as np
import scipy.spatial.distance as dist
import pyaudio
import wave
import json
def dp(distmat):
N,M = distmat.shape
# Initialisons the cost matrix
costmat =np.zeros((N+1,M+1))
for i in range (1,N+1):
costmat[i,0]=np.inf
for i in range (1,M+1):
costmat[0,i]=np.inf
for i in range (N):
for j in range (M):
#on calcule le cout minimal pour chaque chemin.pour atteindre the costmat[i][j] il y a trois chemins possibles on choisit celui de cout minimal
penalty = [
costmat[i,j], # cas T==0
costmat[i,j+1] , # cas T==1
costmat[i+1,j]] # cas T==2
ipenalty = np.argmin(penalty)
costmat[i+1,j+1] = distmat[i,j] + penalty[ipenalty]
#enlever les valeurs de l infini
costmat = costmat[1: , 1:]
return (costmat, costmat[-1, -1]/(N+M))
def calculate_mfcc(audio, sr):
# Define parameters for MFCC calculation
n_mfcc = 13
n_fft = 2048
hop_length = 512
fmin = 0
fmax = sr/2
# Calculate MFCCs
mfccs = librosa.feature.mfcc(y=audio, sr=sr, n_mfcc=n_mfcc, n_fft=n_fft, hop_length=hop_length, fmin=fmin, fmax=fmax)
return mfccs.T
def calculate_dtw_cost(mfccs_query , mfccs_train):
distmat = dist.cdist(mfccs_query, mfccs_train,"cosine")
costmat,mincost = dp(distmat)
return mincost
def recognize_speech(audio_query, audio_train_list, sr):#sr frequence d echantillonnage
# Calculate MFCCs for query audio
mfccs_query = calculate_mfcc(audio_query, sr)
# Calculate DTW cost for each audio in training data
dtw_costs = []
for audio_train in audio_train_list:
mfccs_train = calculate_mfcc(audio_train, sr)
mincost = calculate_dtw_cost(mfccs_query, mfccs_train)
dtw_costs.append(mincost)
# Find index of word with lowest DTW cost
index = np.argmin(dtw_costs)
# Return recognized word
return index
def record_audio(filename, duration, sr):
chunk = 1024
sample_format = pyaudio.paInt16
channels = 1
record_seconds = duration
filename = f"{filename}.wav"
p = pyaudio.PyAudio()
stream = p.open(format=sample_format,
channels=channels,
rate=sr,
frames_per_buffer=chunk,
input=True)
frames = []
print(f"Enregistrement en cours...")
for i in range(0, int(sr / chunk * record_seconds)):
data = stream.read(chunk)
frames.append(data)
stream.stop_stream()
stream.close()
p.terminate()
print("Enregistrement terminé")
wf = wave.open(filename, "wb")
wf.setnchannels(channels)
wf.setsampwidth(p.get_sample_size(sample_format))
wf.setframerate(sr)
wf.writeframes(b"".join(frames))
wf.close()
print(f"Fichier enregistré sous {filename}")
def coupe_silence(signal):
t = 0
if signal[t] == 0 :
p = 0
while signal[t+p] == 0 :
if p == 88 :
signal = signal[:t] + signal[t+p:]
coupe_silence(signal)
else :
p = p+1
def init_database():
data_dir = "audio_data/"
words = []
files = []
for word in os.listdir(data_dir):
if not os.path.isfile(os.path.join(data_dir, word)):
for file in os.listdir(os.path.join(data_dir,word)):
if os.path.isfile(os.path.join(data_dir, word,file)):
print(word,os.path.join(data_dir, word,file))
words.append(word)
files.append(os.path.join(data_dir, word,file))
return words,files
def get_word_metadata(word):
with open("audio_data/metadata.json") as f:
data = json.loads(f.read())
return data[word]
#Todo : detecte si pas de note donnée
def get_grade():
sr = 44100 # fréquence d'échantillonnage
duration = 6 # durée d'enregistrement en secondes
filename = "recording" # nom du fichier à enregistrer
data_dir = "audio_data/"
record_audio(filename, duration, sr)
audio_query, sr = librosa.load(f'{filename}.wav', sr=sr)
coupe_silence(audio_query)
words, files = init_database()
audio_train_list = [librosa.load(file, sr=sr)[0] for file in files]
recognized_word_index = recognize_speech(audio_query, audio_train_list, sr)
recognized_word = words[recognized_word_index]
return get_word_metadata(recognized_word)

View File

@@ -0,0 +1,97 @@
import cv2
import mediapipe as mp
import numpy as np
class HandDetector():
def __init__(self):
self.mp_drawing = mp.solutions.drawing_utils
self.mp_drawing_styles = mp.solutions.drawing_styles
self.mp_hands = mp.solutions.hands
self.cap = cv2.VideoCapture(0)
self.hands = self.mp_hands.Hands(
model_complexity=0,
min_detection_confidence=0.5,
min_tracking_confidence=0.5)
#Paramètres
self.BUFFER_LENGTH = 30
self.DETECTION_THRESHOLD = 3/4
self.resultBuffer = []
def reset(self):
self.resultBuffer = []
def reconnaissancePouce(self,handLandmarks):
etatDuPouce=["neutre","thumbs_down","thumbs_up"]
i=0
j=0
for cpt in range (0,4):
V1=[handLandmarks[(4*cpt)+6][0]-handLandmarks[(4*cpt)+5][0],handLandmarks[(4*cpt)+6][1]-handLandmarks[(4*cpt)+5][1]]
V2=[handLandmarks[(4*cpt)+8][0]-handLandmarks[(4*cpt)+6][0],handLandmarks[(4*cpt)+8][1]-handLandmarks[(4*cpt)+6][1]]
j=np.dot(V1,V2)
if (j>0.005):
return etatDuPouce[0]
V1=[handLandmarks[4][0]-handLandmarks[1][0],handLandmarks[4][1]-handLandmarks[1][1]]
V2=[handLandmarks[2][0]-handLandmarks[1][0],handLandmarks[2][1]-handLandmarks[1][1]]
if((np.dot(V1,V2))>0 and (handLandmarks[4][1]>handLandmarks[2][1])):
i=1
elif(np.dot(V1,V2)>0 and handLandmarks[4][1]<handLandmarks[2][1]):
i=2
return etatDuPouce[i]
def detect(self):
if self.cap.isOpened():
success, image = self.cap.read()
if not success:
print("Ignoring empty camera frame.")
# If loading a video, use 'break' instead of 'continue'.
return False
# To improve performance, optionally mark the image as not writeable to
# pass by reference.
image.flags.writeable = False
results = self.hands.process(image)
# print(results)
if results.multi_hand_landmarks:
handsPositions = []
for hand_landmarks in results.multi_hand_landmarks:
handLandmarks = []
# Fill list with x and y positions of each landmark
for landmarks in hand_landmarks.landmark:
handLandmarks.append([landmarks.x, landmarks.y])
#On ajoute la position de chaque mains a une liste
handsPositions.append(self.reconnaissancePouce(handLandmarks))
#On calcule le résultat suivant la position des deux mains
if(len(handsPositions) == 2):
if(handsPositions[0] == handsPositions[1]):
thumbState = handsPositions[0]
elif(handsPositions[0] == "neutre"):
thumbState = handsPositions[1]
elif(handsPositions[1] == "neutre"):
thumbState = handsPositions[0]
else:
thumbState = "neutre"
else:
thumbState = handsPositions[0]
self.resultBuffer.append(thumbState)
if(len(self.resultBuffer) > self.BUFFER_LENGTH):
self.resultBuffer.pop(0)
thumbsUpCount = sum(map(lambda x : x == "thumbs_up", self.resultBuffer))
thumbsDownCount = sum(map(lambda x : x == "thumbs_down", self.resultBuffer))
if(thumbsUpCount > self.DETECTION_THRESHOLD * self.BUFFER_LENGTH):
result = "thumbs_up"
elif(thumbsDownCount > self.DETECTION_THRESHOLD * self.BUFFER_LENGTH):
result = "thumbs_down"
else:
result = False
if(thumbState != "neutre"):
return thumbState, handLandmarks[9], np.linalg.norm(np.array(handLandmarks[9]) - np.array(handLandmarks[0])), result
return False

View File

@@ -0,0 +1,5 @@
from manager import Manager
if __name__ == "__main__":
print("backend started")
m = Manager()
m.loop()

View File

@@ -0,0 +1,92 @@
from hand_detector import HandDetector
from audio_detector import get_grade
from network import ApiClient, WebsocketServer
import time
#Classe qui coordonne les différents modules et qui s'occupe de construire l'avis au fur et a mesure
class Manager():
def __init__(self):
self.state = 0
self.defualtAvis = {
"note": None,
"commentaire": None,
"notes_autres": {}
}
self.TIMEOUT_CAMERA = 5
self.avis = self.defualtAvis
self.server = WebsocketServer(None)
self.server.start()
self.handDetector = HandDetector()
self.api = ApiClient()
self.timeLastChange = time.time()
self.isLastHandPacketEmpty = False
print("Backend ready")
#Boucle principale
def loop(self):
while(True):
if(self.state == 0):
self.sleep()
if(self.state == 1):
self.camera()
if(self.state == 2):
self.audio()
if(self.state == 3):
self.thankYou()
time.sleep(0.01)
#Fonction qui est executée pendant que la borne est en veille, reveille la borne si une main est detectée
def sleep(self):
res = self.handDetector.detect()
if(res != False):
self.state = 1
self.timeLastChange = time.time()
self.server.sendMessage({"type": "state", "state": 1})
#Envoie la position de la main a l'écran et passe a l'étape suivante si une main est detectée pendant assez longtemps
def camera(self):
if(time.time() - self.timeLastChange > self.TIMEOUT_CAMERA):
self.server.sendMessage({"type":"reset"})
self.reset()
return
res = self.handDetector.detect()
if(res != False):
state, coords, size, finalDecision = res
self.server.sendMessage({"type": "effects", "effects": [{"type": state, "x":coords[0], "y": coords[1], "width": size, "height": size}]})
self.isLastHandPacketEmpty = False
if(finalDecision != False):
self.avis["note"] = 10 if finalDecision == "thumbs_up" else 0
self.state = 2
self.timeLastChange = time.time()
self.server.sendMessage({"type": "state", "state": 2})
elif self.isLastHandPacketEmpty == False:
self.server.sendMessage({"type":"effects","effects":[]})
self.isLastHandPacketEmpty = True
def audio(self):
result = get_grade()
if(result != False):
self.server.sendMessage({"type":"new_grade","word":result["display"]})
self.avis["notes_autres"]["test"] = result["grade"]
time.sleep(3)
self.state = 3
self.timeLastChange = time.time()
self.server.sendMessage({"type": "state", "state": 3})
def thankYou(self):
time.sleep(10)
print("Reseting...")
self.timeLastChange = time.time()
self.server.sendMessage({"type": "state", "state": 0})
res = self.api.send(self.avis["note"],self.avis["notes_autres"]["test"])
print(res.text)
self.reset()
def reset(self):
self.state = 0
self.avis = self.defualtAvis
self.handDetector.reset()

View File

@@ -0,0 +1,49 @@
import requests
import asyncio
import json
import os
import threading
import websockets
class WebsocketServer(threading.Thread):
def __init__(self, onMessage, port=os.getenv("PORT"), host=os.getenv("HOST")):
threading.Thread.__init__(self)
self.host = host
self.port = port
self.messageQueue = []
self.onMessage = onMessage
def run(self):
print("server thread started")
asyncio.run(self.runServer())
async def runServer(self):
async with websockets.serve(self.handler, self.host, self.port):
await asyncio.Future()
async def handler(self,websocket):
while True:
for msg in self.messageQueue:
# print("sending", json.dumps(msg))
await websocket.send(json.dumps(msg))
self.messageQueue.pop(0)
await asyncio.sleep(0.01)
def sendMessage(self,message):
self.messageQueue.append(message)
class ApiClient():
def __init__(self, host=os.getenv("API_HOST"), port=os.getenv("API_PORT")):
self.host = host
self.port = port
def send(self,note,note_autre):
#Exemple ajout d'un commentaire depuis la borne (site ou geste)
avis = {
"note": note,
"source": "borne",
"commentaire":"",
#Optionel
"notes_autre": '{"proprete":'+str(note_autre)+',"calme":10}',
}
return requests.post("http://"+self.host+":"+self.port+"/add_review", data=avis)

Binary file not shown.

View File

@@ -0,0 +1,8 @@
websockets
requests
opencv-python
mediapipe
numpy
pyaudio
librosa
scipy

View File

@@ -1,444 +1,428 @@
-- phpMyAdmin SQL Dump SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
-- version 4.9.5deb2 SET AUTOCOMMIT = 0;
-- https://www.phpmyadmin.net/ START TRANSACTION;
--
-- Host: localhost:3306
-- Generation Time: Dec 26, 2022 at 10:31 AM --
-- Server version: 8.0.31-0ubuntu0.20.04.1 -- Database: `telereview`
-- PHP Version: 7.4.3 --
CREATE DATABASE IF NOT EXISTS `telereview`;
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; USE `telereview`;
SET AUTOCOMMIT = 0;
START TRANSACTION; -- --------------------------------------------------------
SET time_zone = "+00:00";
--
-- Table structure for table `borne_auteurs`
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; --
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; CREATE TABLE `borne_auteurs` (
/*!40101 SET NAMES utf8mb4 */; `id` int NOT NULL,
`sexe` tinytext ,
-- `age` tinyint DEFAULT NULL
-- Database: `telereview` ) ;
--
CREATE DATABASE IF NOT EXISTS `telereview` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; -- --------------------------------------------------------
USE `telereview`;
--
-- -------------------------------------------------------- -- Table structure for table `borne_avis`
--
--
-- Table structure for table `borne_auteurs` CREATE TABLE `borne_avis` (
-- `id` int NOT NULL,
`id_auteur` int NOT NULL,
CREATE TABLE `borne_auteurs` ( `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`id` int NOT NULL, `note_principale` tinyint NOT NULL,
`sexe` tinytext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci, `commentaire` text NOT NULL,
`age` tinyint DEFAULT NULL `source_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; ) ;
-- -------------------------------------------------------- -- --------------------------------------------------------
-- --
-- Table structure for table `borne_avis` -- Table structure for table `borne_criteres`
-- --
CREATE TABLE `borne_avis` ( CREATE TABLE `borne_criteres` (
`id` int NOT NULL, `id` int NOT NULL,
`id_auteur` int NOT NULL, `nom` text NOT NULL
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, ) ;
`note_principale` tinyint NOT NULL,
`commentaire` text NOT NULL, --
`source_id` int NOT NULL -- Dumping data for table `borne_criteres`
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; --
-- -------------------------------------------------------- INSERT INTO `borne_criteres` (`id`, `nom`) VALUES
(1, 'proprete'),
-- (2, 'calme'),
-- Table structure for table `borne_criteres` (3, 'attente');
--
-- --------------------------------------------------------
CREATE TABLE `borne_criteres` (
`id` int NOT NULL, --
`nom` text NOT NULL -- Table structure for table `borne_notes_autre`
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; --
-- CREATE TABLE `borne_notes_autre` (
-- Dumping data for table `borne_criteres` `id` int NOT NULL,
-- `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`critere_id` int NOT NULL,
INSERT INTO `borne_criteres` (`id`, `nom`) VALUES `avis_id` int NOT NULL,
(1, 'proprete'), `note` int NOT NULL COMMENT 'Note sur 10'
(2, 'calme'), ) ;
(3, 'attente');
-- --------------------------------------------------------
-- --------------------------------------------------------
--
-- -- Table structure for table `reseaux_sociaux_auteurs`
-- Table structure for table `borne_notes_autre` --
--
CREATE TABLE `reseaux_sociaux_auteurs` (
CREATE TABLE `borne_notes_autre` ( `id` int NOT NULL,
`id` int NOT NULL, `nom_utilisateur` text NOT NULL,
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `source_id` int NOT NULL,
`critere_id` int NOT NULL, `lien` text NOT NULL
`avis_id` int NOT NULL, ) ;
`note` int NOT NULL COMMENT 'Note sur 10'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- --------------------------------------------------------
-- -------------------------------------------------------- --
-- Table structure for table `reseaux_sociaux_avis`
-- --
-- Table structure for table `reseaux_sociaux_auteurs`
-- CREATE TABLE `reseaux_sociaux_avis` (
`id` int NOT NULL,
CREATE TABLE `reseaux_sociaux_auteurs` ( `date` date NOT NULL,
`id` int NOT NULL, `source_id` int NOT NULL,
`nom_utilisateur` text NOT NULL, `note` tinyint DEFAULT NULL,
`source_id` int NOT NULL, `commentaire` text ,
`lien` text NOT NULL `auteur_id` int NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; `lien_source` text
) ;
-- --------------------------------------------------------
-- --------------------------------------------------------
--
-- Table structure for table `reseaux_sociaux_avis` --
-- -- Table structure for table `sources`
--
CREATE TABLE `reseaux_sociaux_avis` (
`id` int NOT NULL, CREATE TABLE `sources` (
`date` date NOT NULL, `id` int NOT NULL,
`source_id` int NOT NULL, `nom` text NOT NULL
`note` tinyint DEFAULT NULL, ) ;
`commentaire` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
`auteur_id` int NOT NULL, --
`lien_source` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci -- Dumping data for table `sources`
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; --
-- -------------------------------------------------------- INSERT INTO `sources` (`id`, `nom`) VALUES
(1, 'website'),
-- (2, 'borne');
-- Table structure for table `sources`
-- -- --------------------------------------------------------
CREATE TABLE `sources` ( --
`id` int NOT NULL, -- Table structure for table `stats_autres_annee`
`nom` text NOT NULL --
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE `stats_autres_annee` (
-- `id` int NOT NULL,
-- Dumping data for table `sources` `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- `critere_id` int NOT NULL,
`note` float NOT NULL
INSERT INTO `sources` (`id`, `nom`) VALUES ) ;
(1, 'website'),
(2, 'borne'), -- --------------------------------------------------------
(3, 'instagram');
--
-- -------------------------------------------------------- -- Table structure for table `stats_autres_jour`
--
--
-- Table structure for table `stats_autres_annee` CREATE TABLE `stats_autres_jour` (
-- `id` int NOT NULL,
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
CREATE TABLE `stats_autres_annee` ( `critere_id` int NOT NULL,
`id` int NOT NULL, `note` float NOT NULL
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, ) ;
`critere_id` int NOT NULL,
`note` float NOT NULL -- --------------------------------------------------------
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
--
-- -------------------------------------------------------- -- Table structure for table `stats_autres_mois`
--
--
-- Table structure for table `stats_autres_jour` CREATE TABLE `stats_autres_mois` (
-- `id` int NOT NULL,
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
CREATE TABLE `stats_autres_jour` ( `critere_id` int NOT NULL,
`id` int NOT NULL, `note` float NOT NULL
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, ) ;
`critere_id` int NOT NULL,
`note` float NOT NULL -- --------------------------------------------------------
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
--
-- -------------------------------------------------------- -- Table structure for table `stats_autres_semaine`
--
--
-- Table structure for table `stats_autres_mois` CREATE TABLE `stats_autres_semaine` (
-- `id` int NOT NULL,
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
CREATE TABLE `stats_autres_mois` ( `critere_id` int NOT NULL,
`id` int NOT NULL, `note` float NOT NULL
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, ) ;
`critere_id` int NOT NULL,
`note` float NOT NULL -- --------------------------------------------------------
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
--
-- -------------------------------------------------------- -- Table structure for table `stats_general_annee`
--
--
-- Table structure for table `stats_autres_semaine` CREATE TABLE `stats_general_annee` (
-- `id` int NOT NULL,
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
CREATE TABLE `stats_autres_semaine` ( `nb_avis` int NOT NULL,
`id` int NOT NULL, `moyenne_globale` float DEFAULT NULL,
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `moyenne_site` float DEFAULT NULL,
`critere_id` int NOT NULL, `moyenne_borne` float DEFAULT NULL,
`note` float NOT NULL `dist_age` text DEFAULT NULL COMMENT 'Distribution de l''age des auteurs',
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; `dist_sexe` text DEFAULT NULL COMMENT 'Distribution du sexe des auteurs'
) ;
-- --------------------------------------------------------
-- --------------------------------------------------------
--
-- Table structure for table `stats_general_annee` --
-- -- Table structure for table `stats_general_jour`
--
CREATE TABLE `stats_general_annee` (
`id` int NOT NULL, CREATE TABLE `stats_general_jour` (
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `id` int NOT NULL,
`moyenne_globale` float NOT NULL, `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`moyenne_site` float NOT NULL, `nb_avis` int NOT NULL,
`moyenne_borne` float NOT NULL, `moyenne_globale` float DEFAULT NULL,
`dist_age` text NOT NULL COMMENT 'Distribution de l''age des auteurs', `moyenne_site` float DEFAULT NULL,
`dist_sexe` text NOT NULL COMMENT 'Distribution du sexe des auteurs' `moyenne_borne` float DEFAULT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; `dist_age` text DEFAULT NULL COMMENT 'Distribution de l''age des auteurs',
`dist_sexe` text DEFAULT NULL COMMENT 'Distribution du sexe des auteurs'
-- -------------------------------------------------------- ) ;
-- -- --------------------------------------------------------
-- Table structure for table `stats_general_jour`
-- --
-- Table structure for table `stats_general_mois`
CREATE TABLE `stats_general_jour` ( --
`id` int NOT NULL,
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, CREATE TABLE `stats_general_mois` (
`moyenne_globale` float DEFAULT NULL, `id` int NOT NULL,
`moyenne_site` float DEFAULT NULL, `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`moyenne_borne` float DEFAULT NULL, `nb_avis` int NOT NULL,
`dist_age` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT 'Distribution de l''age des auteurs', `moyenne_globale` float DEFAULT NULL,
`dist_sexe` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT 'Distribution du sexe des auteurs' `moyenne_site` float DEFAULT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; `moyenne_borne` float DEFAULT NULL,
`dist_age` text DEFAULT NULL COMMENT 'Distribution de l''age des auteurs',
-- -------------------------------------------------------- `dist_sexe` text DEFAULT NULL COMMENT 'Distribution du sexe des auteurs'
) ;
--
-- Table structure for table `stats_general_mois` -- --------------------------------------------------------
--
--
CREATE TABLE `stats_general_mois` ( -- Table structure for table `stats_general_semaine`
`id` int NOT NULL, --
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`moyenne_globale` float NOT NULL, CREATE TABLE `stats_general_semaine` (
`moyenne_site` float NOT NULL, `id` int NOT NULL,
`moyenne_borne` float NOT NULL, `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`dist_age` text NOT NULL COMMENT 'Distribution de l''age des auteurs', `nb_avis` int NOT NULL,
`dist_sexe` text NOT NULL COMMENT 'Distribution du sexe des auteurs' `moyenne_globale` float DEFAULT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; `moyenne_site` float DEFAULT NULL,
`moyenne_borne` float DEFAULT NULL,
-- -------------------------------------------------------- `dist_age` text DEFAULT NULL COMMENT 'Distribution de l''age des auteurs',
`dist_sexe` text DEFAULT NULL COMMENT 'Distribution du sexe des auteurs'
-- ) ;
-- Table structure for table `stats_general_semaine`
-- --
-- Indexes for dumped tables
CREATE TABLE `stats_general_semaine` ( --
`id` int NOT NULL,
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, --
`moyenne_globale` float NOT NULL, -- Indexes for table `borne_auteurs`
`moyenne_site` float NOT NULL, --
`moyenne_borne` float NOT NULL, ALTER TABLE `borne_auteurs`
`dist_age` text NOT NULL COMMENT 'Distribution de l''age des auteurs', ADD PRIMARY KEY (`id`);
`dist_sexe` text NOT NULL COMMENT 'Distribution du sexe des auteurs'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; --
-- Indexes for table `borne_avis`
-- --
-- Indexes for dumped tables ALTER TABLE `borne_avis`
-- ADD PRIMARY KEY (`id`);
-- --
-- Indexes for table `borne_auteurs` -- Indexes for table `borne_criteres`
-- --
ALTER TABLE `borne_auteurs` ALTER TABLE `borne_criteres`
ADD PRIMARY KEY (`id`); ADD PRIMARY KEY (`id`);
-- --
-- Indexes for table `borne_avis` -- Indexes for table `borne_notes_autre`
-- --
ALTER TABLE `borne_avis` ALTER TABLE `borne_notes_autre`
ADD PRIMARY KEY (`id`); ADD PRIMARY KEY (`id`);
-- --
-- Indexes for table `borne_criteres` -- Indexes for table `reseaux_sociaux_auteurs`
-- --
ALTER TABLE `borne_criteres` ALTER TABLE `reseaux_sociaux_auteurs`
ADD PRIMARY KEY (`id`); ADD PRIMARY KEY (`id`);
-- --
-- Indexes for table `borne_notes_autre` -- Indexes for table `reseaux_sociaux_avis`
-- --
ALTER TABLE `borne_notes_autre` ALTER TABLE `reseaux_sociaux_avis`
ADD PRIMARY KEY (`id`); ADD PRIMARY KEY (`id`);
-- --
-- Indexes for table `reseaux_sociaux_auteurs` -- Indexes for table `sources`
-- --
ALTER TABLE `reseaux_sociaux_auteurs` ALTER TABLE `sources`
ADD PRIMARY KEY (`id`); ADD PRIMARY KEY (`id`);
-- --
-- Indexes for table `reseaux_sociaux_avis` -- Indexes for table `stats_autres_annee`
-- --
ALTER TABLE `reseaux_sociaux_avis` ALTER TABLE `stats_autres_annee`
ADD PRIMARY KEY (`id`); ADD PRIMARY KEY (`id`);
-- --
-- Indexes for table `sources` -- Indexes for table `stats_autres_jour`
-- --
ALTER TABLE `sources` ALTER TABLE `stats_autres_jour`
ADD PRIMARY KEY (`id`); ADD PRIMARY KEY (`id`);
-- --
-- Indexes for table `stats_autres_annee` -- Indexes for table `stats_autres_mois`
-- --
ALTER TABLE `stats_autres_annee` ALTER TABLE `stats_autres_mois`
ADD PRIMARY KEY (`id`); ADD PRIMARY KEY (`id`);
-- --
-- Indexes for table `stats_autres_jour` -- Indexes for table `stats_autres_semaine`
-- --
ALTER TABLE `stats_autres_jour` ALTER TABLE `stats_autres_semaine`
ADD PRIMARY KEY (`id`); ADD PRIMARY KEY (`id`);
-- --
-- Indexes for table `stats_autres_mois` -- Indexes for table `stats_general_annee`
-- --
ALTER TABLE `stats_autres_mois` ALTER TABLE `stats_general_annee`
ADD PRIMARY KEY (`id`); ADD PRIMARY KEY (`id`);
-- --
-- Indexes for table `stats_autres_semaine` -- Indexes for table `stats_general_jour`
-- --
ALTER TABLE `stats_autres_semaine` ALTER TABLE `stats_general_jour`
ADD PRIMARY KEY (`id`); ADD PRIMARY KEY (`id`);
-- --
-- Indexes for table `stats_general_annee` -- Indexes for table `stats_general_mois`
-- --
ALTER TABLE `stats_general_annee` ALTER TABLE `stats_general_mois`
ADD PRIMARY KEY (`id`); ADD PRIMARY KEY (`id`);
-- --
-- Indexes for table `stats_general_jour` -- Indexes for table `stats_general_semaine`
-- --
ALTER TABLE `stats_general_jour` ALTER TABLE `stats_general_semaine`
ADD PRIMARY KEY (`id`); ADD PRIMARY KEY (`id`);
-- --
-- Indexes for table `stats_general_mois` -- AUTO_INCREMENT for dumped tables
-- --
ALTER TABLE `stats_general_mois`
ADD PRIMARY KEY (`id`); --
-- AUTO_INCREMENT for table `borne_auteurs`
-- --
-- Indexes for table `stats_general_semaine` ALTER TABLE `borne_auteurs`
-- MODIFY `id` int NOT NULL AUTO_INCREMENT;
ALTER TABLE `stats_general_semaine`
ADD PRIMARY KEY (`id`); --
-- AUTO_INCREMENT for table `borne_avis`
-- --
-- AUTO_INCREMENT for dumped tables ALTER TABLE `borne_avis`
-- MODIFY `id` int NOT NULL AUTO_INCREMENT;
-- --
-- AUTO_INCREMENT for table `borne_auteurs` -- AUTO_INCREMENT for table `borne_criteres`
-- --
ALTER TABLE `borne_auteurs` ALTER TABLE `borne_criteres`
MODIFY `id` int NOT NULL AUTO_INCREMENT; MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
-- --
-- AUTO_INCREMENT for table `borne_avis` -- AUTO_INCREMENT for table `borne_notes_autre`
-- --
ALTER TABLE `borne_avis` ALTER TABLE `borne_notes_autre`
MODIFY `id` int NOT NULL AUTO_INCREMENT; MODIFY `id` int NOT NULL AUTO_INCREMENT;
-- --
-- AUTO_INCREMENT for table `borne_criteres` -- AUTO_INCREMENT for table `reseaux_sociaux_auteurs`
-- --
ALTER TABLE `borne_criteres` ALTER TABLE `reseaux_sociaux_auteurs`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4; MODIFY `id` int NOT NULL AUTO_INCREMENT;
-- --
-- AUTO_INCREMENT for table `borne_notes_autre` -- AUTO_INCREMENT for table `reseaux_sociaux_avis`
-- --
ALTER TABLE `borne_notes_autre` ALTER TABLE `reseaux_sociaux_avis`
MODIFY `id` int NOT NULL AUTO_INCREMENT; MODIFY `id` int NOT NULL AUTO_INCREMENT;
-- --
-- AUTO_INCREMENT for table `reseaux_sociaux_auteurs` -- AUTO_INCREMENT for table `sources`
-- --
ALTER TABLE `reseaux_sociaux_auteurs` ALTER TABLE `sources`
MODIFY `id` int NOT NULL AUTO_INCREMENT; MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
-- --
-- AUTO_INCREMENT for table `reseaux_sociaux_avis` -- AUTO_INCREMENT for table `stats_autres_annee`
-- --
ALTER TABLE `reseaux_sociaux_avis` ALTER TABLE `stats_autres_annee`
MODIFY `id` int NOT NULL AUTO_INCREMENT; MODIFY `id` int NOT NULL AUTO_INCREMENT;
-- --
-- AUTO_INCREMENT for table `sources` -- AUTO_INCREMENT for table `stats_autres_jour`
-- --
ALTER TABLE `sources` ALTER TABLE `stats_autres_jour`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4; MODIFY `id` int NOT NULL AUTO_INCREMENT;
-- --
-- AUTO_INCREMENT for table `stats_autres_annee` -- AUTO_INCREMENT for table `stats_autres_mois`
-- --
ALTER TABLE `stats_autres_annee` ALTER TABLE `stats_autres_mois`
MODIFY `id` int NOT NULL AUTO_INCREMENT; MODIFY `id` int NOT NULL AUTO_INCREMENT;
-- --
-- AUTO_INCREMENT for table `stats_autres_jour` -- AUTO_INCREMENT for table `stats_autres_semaine`
-- --
ALTER TABLE `stats_autres_jour` ALTER TABLE `stats_autres_semaine`
MODIFY `id` int NOT NULL AUTO_INCREMENT; MODIFY `id` int NOT NULL AUTO_INCREMENT;
-- --
-- AUTO_INCREMENT for table `stats_autres_mois` -- AUTO_INCREMENT for table `stats_general_annee`
-- --
ALTER TABLE `stats_autres_mois` ALTER TABLE `stats_general_annee`
MODIFY `id` int NOT NULL AUTO_INCREMENT; MODIFY `id` int NOT NULL AUTO_INCREMENT;
-- --
-- AUTO_INCREMENT for table `stats_autres_semaine` -- AUTO_INCREMENT for table `stats_general_jour`
-- --
ALTER TABLE `stats_autres_semaine` ALTER TABLE `stats_general_jour`
MODIFY `id` int NOT NULL AUTO_INCREMENT; MODIFY `id` int NOT NULL AUTO_INCREMENT;
-- --
-- AUTO_INCREMENT for table `stats_general_annee` -- AUTO_INCREMENT for table `stats_general_mois`
-- --
ALTER TABLE `stats_general_annee` ALTER TABLE `stats_general_mois`
MODIFY `id` int NOT NULL AUTO_INCREMENT; MODIFY `id` int NOT NULL AUTO_INCREMENT;
-- --
-- AUTO_INCREMENT for table `stats_general_jour` -- AUTO_INCREMENT for table `stats_general_semaine`
-- --
ALTER TABLE `stats_general_jour` ALTER TABLE `stats_general_semaine`
MODIFY `id` int NOT NULL AUTO_INCREMENT; MODIFY `id` int NOT NULL AUTO_INCREMENT;
COMMIT;
--
-- AUTO_INCREMENT for table `stats_general_mois`
--
ALTER TABLE `stats_general_mois`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `stats_general_semaine`
--
ALTER TABLE `stats_general_semaine`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

112
code/docker-compose.yaml Normal file
View File

@@ -0,0 +1,112 @@
version: "3.9"
services:
#Base de donnée mysql de la borne sur laquelle est stockée tous les avis et les stats
db:
image: mysql:latest
container_name: db
expose:
- 3306
volumes:
- ./db:/docker-entrypoint-initdb.d
restart: always
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost", "-uroot"] # Command to check health.
interval: 5s # Interval between health checks.
timeout: 5s # Timeout for each health checking.
retries: 20 # Hou many times retries.
start_period: 10s # Estimated time to boot.
environment:
MYSQL_ROOT_PASSWORD: telereview
MYSQL_DATABASE: telereview
#Interface d'aministration pour la bdd
phpmyadmin:
image: phpmyadmin:latest
restart: always
container_name: phpmyadmin
depends_on:
db:
condition: service_healthy
environment:
PMA_ARBITRARY: 1
PMA_HOST: db
PMA_USER: root
PMA_PASSWORD: telereview
ports:
- 8000:80
#API de gestion des avis, permet d'ajouter ou de récuperer des avis ou les stats sur les avis par des requêtes HTTP
reviews_api:
container_name: reviews_api
expose:
- 8080
ports:
- 8080:8080
environment:
- NODE_ENV=production
- DB_USER=root
- DB_PASSWORD=telereview
- DB_HOST=db
- DB_NAME=telereview
- PORT=8080
depends_on:
db:
condition: service_healthy
build: ./reviews_api
restart: always
# Serveur web de l'interface de la borne
interface_borne:
image: httpd:latest
volumes:
- ./interface_borne:/usr/local/apache2/htdocs/
container_name: interface_borne
ports:
- 8888:80
#Serveur web de l'interface admin
interface_admin:
image: httpd:latest
volumes:
- ./interface_admin/out:/usr/local/apache2/htdocs/
container_name: interface_admin
ports:
- 800:80
#Formulaire de retour d'avis
formulaire:
image: httpd:latest
volumes:
- ./formulaire:/usr/local/apache2/htdocs/
container_name: formulaire
ports:
- 80:80
# #Backend de la borne : scripts pythons de reconnaissances video et audio
# #Envoient les infos a l'interface de la borne par websocket pour mettre a jour l'interface rapidement
# #Met a jour les avis en faisant des requêtes a l'API
backend_reconnaissance:
build: ./backend_reconnaissance
container_name: backend_reconnaissance
restart: always
devices:
- /dev/video3:/dev/video0
- /dev/snd:/dev/snd
environment:
- PORT=5000
- HOST=backend_reconnaissance
- API_HOST=reviews_api
- API_PORT=8080
ports:
#Ce container est le serveur websocker dont le client est l'interface de la borne qui tourne dans le navigateur
- 5000:5000
user: root
video_loopback:
build: ./video_loopback
container_name: video_loopback
restart: always
devices:
- /dev/video0:/dev/video0
- /dev/video2:/dev/video1
- /dev/video3:/dev/video2

3
code/interface_admin/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.next
package-lock.json
out

View File

@@ -0,0 +1,34 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@@ -0,0 +1,58 @@
import React from 'react'
import { Card, Col, Row, Table } from 'react-bootstrap';
import { BsPersonFill } from 'react-icons/bs';
import styles from '../styles/Avis.module.css'
export default function Avis({review}) {
const {date, note_principale,notes_autres, commentaire, sexe_auteur, nom_source, age_auteur} = review;
return (
<Card>
<Card.Title>Avis</Card.Title>
<Card.Body>
<Row>
<h2>Auteur</h2>
<Col xs={1}>
<BsPersonFill className={styles.personIcon} />
</Col>
<Col className='d-flex flex-column'>
<p>Age : {age_auteur}</p>
<p>Sexe : {sexe_auteur}</p>
<p>Date de publication : {date}</p>
<p>Source : {nom_source}</p>
</Col>
</Row>
<Row>
<h2>Notes</h2>
<Table>
<thead>
<tr>
<th>Critère</th>
<th>Note</th>
</tr>
</thead>
<tbody>
<tr>
<td>Général</td>
<td>{note_principale} / 10</td>
</tr>
{notes_autres && notes_autres.map(({ critere, note }) => {
return <tr key={critere}>
<td>{critere}</td>
<td>{note}/10</td>
</tr>
})}
</tbody>
</Table>
</Row>
<Row>
<Card>
<Card.Header>Commentaire</Card.Header>
<Card.Body>
{commentaire}
</Card.Body>
</Card>
</Row>
</Card.Body>
</Card>
)
}

View File

@@ -0,0 +1,34 @@
import { useRouter } from 'next/router';
import React from 'react'
import { Table } from 'react-bootstrap'
import styles from '../styles/AvisList.module.css'
export default function AvisList({ avis }) {
const router = useRouter();
function handleClick(id) {
router.push(`/avis/${id}`);
}
return (
<Table>
<thead>
<tr>
<th>Date</th>
<th>Note globale</th>
<th>Commentaire</th>
<th>Source</th>
</tr>
</thead>
<tbody>
{avis.map(({ id, note_principale, commentaire, date, nom_source }) => {
return <tr onClick={() => handleClick(id)} key={id} className={styles.row}>
<td>{date}</td>
<td>{note_principale} / 10</td>
<td>{commentaire}</td>
<td>{nom_source}</td>
</tr>
})}
</tbody>
</Table>
)
}

View File

@@ -0,0 +1,41 @@
import React from 'react'
import { Bar } from 'react-chartjs-2'
import Chart from 'chart.js/auto'; //NE SURTOUT PAS SUPPRIMER CET IMPORT
export default function ComparativeBarChart({ xlabels, data0, label0, data1, label1}) {
return (
<Bar
options={{
responsive: true,
interaction: {
intersect: false,
},
scales: {
x: {
stacked: true,
},
y: {
stacked: true
}
}
}}
data={{
labels: xlabels,
datasets: [
{
label: label0,
data: data0,
backgroundColor: "#FF3B30",
stack: "stack0"
},
{
label: label1,
data: data1,
backgroundColor: "#0000FF",
stack: "stack1"
}
]
}}
/>
)
}

View File

@@ -0,0 +1,23 @@
import React from 'react'
import Link from 'next/link'
import Container from 'react-bootstrap/Container';
import Nav from 'react-bootstrap/Nav';
import Navbar from 'react-bootstrap/Navbar';
export default function Menu() {
return (
<Navbar bg="light" expand="lg">
<Container>
<Navbar.Brand href="#home">Téléreview</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="me-auto">
<Link href="/" passHref legacyBehavior><Nav.Link>Accueil</Nav.Link></Link>
<Link href="/stats" passHref legacyBehavior><Nav.Link>Statistiques</Nav.Link></Link>
<Link href="/avis" passHref legacyBehavior><Nav.Link>Avis</Nav.Link></Link>
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
)
}

View File

@@ -0,0 +1,34 @@
import React, { useRef } from 'react'
import { Bar } from 'react-chartjs-2'
import Chart from 'chart.js/auto'; //NE SURTOUT PAS SUPPRIMER CET IMPORT
export default function StatBarChart({labels, data}) {
return (
<Bar
options={{
redraw: true,
responsive: true,
interaction: {
intersect: false,
},
scales: {
x: {
stacked: true,
},
y: {
stacked: true
}
}
}}
data={{
labels: labels,
datasets: [
{
data: data,
backgroundColor: "#FF3B30",
},
]
}}
/>
)
}

View File

@@ -0,0 +1,3 @@
export const api = {
HOST: 'localhost:8080'
}

View File

@@ -0,0 +1,32 @@
import { useEffect, useState } from "react";
import { api } from "../config/reviewsApi";
function useReview(reviewId) {
const [review, setReview] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
async function fetchData(id) {
const response = await fetch('http://' + api.HOST + `/borne/get_review?id=${id}`)
if (response.ok) {
const jsonData = await response.json();
setReview(jsonData);
setLoading(false);
setError(false);
} else {
setError(true);
setLoading(false);
}
}
useEffect(() => {
if (reviewId) {
fetchData(reviewId);
}
}, [reviewId])
return { review, loading, error }
}
export default useReview;

View File

@@ -0,0 +1,30 @@
import { useEffect, useState } from "react";
import { api } from "../config/reviewsApi";
export default function useReviews() {
const [reviews, setReviews] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
async function fetchLastReviews(limit=100) {
setLoading(true);
const response = await fetch('http://' + api.HOST + '/borne/get_last_reviews', {
method: 'GET'
})
if(response.ok) {
let json = await response.json()
setReviews(json);
setError(false);
setLoading(false);
}else {
setLoading(false);
setError(true);
}
}
useEffect(() => {
fetchLastReviews();
}, [])
return {reviews, error, loading, fetchLastReviews};
}

View File

@@ -0,0 +1,26 @@
import { useEffect, useState } from "react";
import { api } from "../config/reviewsApi";
export default function useStats(limit, interval) {
const [stats, setStats] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
async function fetchData(limit, interval) {
const response = await fetch("http://" + api.HOST + `/borne/get_stats?interval=${interval}&limit=${limit}`)
if(response.ok) {
const data = await response.json();
setStats(data);
setError(false);
}else {
setError(true)
}
setLoading(false);
}
useEffect(() => {
fetchData(limit, interval);
}, [limit, interval])
return {stats, loading, error};
}

View File

@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig

View File

@@ -0,0 +1,26 @@
{
"name": "interface-admin",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"export": "next export",
"lint": "next lint"
},
"dependencies": {
"@next/font": "13.1.6",
"bootstrap": "^5.2.3",
"chart.js": "^4.2.0",
"date-fns": "^2.29.3",
"eslint": "8.33.0",
"eslint-config-next": "13.1.6",
"next": "13.1.6",
"react": "18.2.0",
"react-bootstrap": "^2.7.0",
"react-chartjs-2": "^5.2.0",
"react-dom": "18.2.0",
"react-icons": "^4.7.1"
}
}

View File

@@ -0,0 +1,13 @@
import Menu from '../components/Menu'
import '../styles/globals.css'
import 'bootstrap/dist/css/bootstrap.css';
import { Container } from 'react-bootstrap';
export default function App({ Component, pageProps }) {
return <>
<Menu />
<Container fluid="md">
<Component {...pageProps} />
</Container>
</>
}

View File

@@ -0,0 +1,16 @@
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="en">
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}

View File

@@ -0,0 +1,12 @@
import { useRouter } from 'next/router';
import React from 'react'
import Avis from '../../components/Avis';
import useReview from '../../hooks/review';
export default function AvisPage() {
const router = useRouter();
const {id} = router.query;
const {review, loading, error} = useReview(id);
return (
!loading && !error && <Avis review={review}/>
)
}

View File

@@ -0,0 +1,74 @@
import React, { useEffect, useState } from 'react'
import { Card, Container, Form, Row } from 'react-bootstrap'
import AvisList from '../../components/AvisList';
import useReviews from '../../hooks/reviews';
import styles from '../../styles/AvisListPage.module.css'
export default function AvisListPage() {
const [minGrade, setMinGrade] = useState(0);
const [maxGrade, setMaxGrade] = useState(10);
const [sources, setSources] = useState({'borne': true, 'website': true})
const [filteredReviews, setFilteredREviews] = useState([])
const {reviews, error, loading} = useReviews();
useEffect(() => {
const newReviews = reviews.filter((review) => review.note_principale >= minGrade && review.note_principale <= maxGrade && sources[review.nom_source])
setFilteredREviews(newReviews)
}, [reviews, minGrade, maxGrade, sources])
useEffect(() => {
if(minGrade > maxGrade) {
setMinGrade(maxGrade);
}
}, [maxGrade]);
useEffect(() => {
if(minGrade > maxGrade) {
setMaxGrade(minGrade);
}
}, [minGrade])
return (
<Container fluid>
<Card>
<Card.Header>Tous les avis</Card.Header>
<Card.Body>
<Row>
<Form>
<Form.Group>
<Form.Label>Types d'avis</Form.Label>
<Form.Check
type="switch"
label="Borne"
onChange={(e) => setSources({...sources, 'borne': e.target.checked})}
checked={sources['borne']}
/>
<Form.Check
type="switch"
label="QR Code"
onChange={(e) => setSources({...sources, 'website': e.target.checked})}
checked={sources['website']}
/>
</Form.Group>
<Form.Group>
<Form.Label>Note</Form.Label>
<div className='d-flex flex-row justify-content-around col-md-6'>
<div>Min : {minGrade}/10</div>
<div className={styles.sliderContainer}>
<input type="range" value={minGrade} onChange={(e) => setMinGrade(Number(e.target.value))} min="0" max="10" step="1" className={styles.slider}></input>
<input type="range" value={maxGrade} onChange={(e) => setMaxGrade(Number(e.target.value))} min="0" max="10" step="1" className={styles.slider}></input>
</div>
<div>Max : {maxGrade}/10</div>
</div>
</Form.Group>
</Form>
</Row>
<Row>
{!loading && !error && <AvisList avis={filteredReviews} />}
</Row>
</Card.Body>
</Card>
</Container >
)
}

View File

@@ -0,0 +1,128 @@
import Head from 'next/head'
import { Card, Container } from 'react-bootstrap'
import ComparativeBarChart from '../components/ComparativeBarChart'
import { useEffect, useState } from 'react'
import styles from "../styles/Home.module.css"
import useStats from '../hooks/stats'
import getDay from 'date-fns/getDay'
import getWeek from '../util'
export default function Home() {
const [datasets, setDatasets] = useState(null);
const [averages, setAverages] = useState(null);
const [differences, setDifferences] = useState(null);
useEffect(() => {
if (datasets) {
let newAverages = []
let newDifferences = []
for (let i = 0; i < datasets.length; i++) {
let currentEntriesCount = 0;
let previousEntriesCount = 0;
for (let x of datasets[i].current) {
if (x != null) {
currentEntriesCount++;
}
}
for (let x of datasets[i].previous) {
if (x != null) {
previousEntriesCount++;
}
}
if (currentEntriesCount != 0) {
newAverages[i] = datasets[i].current.reduce((a, b) => a + b) / currentEntriesCount;
if (previousEntriesCount > 0) {
newDifferences[i] = newAverages[i] - datasets[i].previous.reduce((a, b) => a + b) / datasets[i].previous.length
} else {
newDifferences[i] = newAverages[i]
}
} else {
newDifferences[i] = 0;
newAverages[i] = 0;
}
}
setAverages(newAverages);
setDifferences(newDifferences);
}
}, [datasets]);
const { stats, loading, error } = useStats(14, "jour");
useEffect(() => {
if (!error && !loading) {
let reviewCount = [null, null, null, null, null, null, null];
let reviewCountPrev = [null, null, null, null, null, null, null]
let reviewAvg = [null, null, null, null, null, null, null]
let reviewAvgPrev = [null, null, null, null, null, null, null]
for (let i = 0; i < stats.length; i++) {
let date = new Date(Date.parse(stats[i].date))
let now = new Date();
let day = (date.getDay() - 1) % 7;
let week = getWeek(date, 1);
let thisWeek = getWeek(now, 1);
if (week == thisWeek) {
reviewCount[day] = stats[i].nb_avis;
reviewAvg[day] = stats[i].moyenne_globale;
} else if (week = thisWeek - 1) {
reviewAvgPrev[day] = stats[i].moyenne_globale;
reviewCountPrev[day] = stats[i].nb_avis;
}
}
setDatasets([
{ title: "Nombre d'avis", current: reviewCount, previous: reviewCountPrev },
{ title: "Notes moyennes", current: reviewAvg, previous: reviewAvgPrev }
])
}
}, [stats]);
function dataVisualizer(title, current, previous, average, difference) {
return <div key={title}>
<h3>{title}</h3>
<Card className={styles.averageCard}>
<Card.Title>Moyenne</Card.Title>
<Card.Body className={styles.averageCardBody}>
<div
className={styles.averageMainValue}
>
{Math.round(average * 1e2) / 1e2}
</div>
<div
className={[styles.averageCardSecondaryValue, difference >= 0 ? styles.averagePositive : styles.averageNegative].join(' ')}
>
{(difference >= 0 ? "+" : "-") + Math.round(difference * 1e2) / 1e2}
</div>
</Card.Body>
</Card>
<ComparativeBarChart
xlabels={["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"]}
label0="Cette semaine"
label1="La semaine dernière"
data0={current}
data1={previous}
/>
<hr />
</div>
}
return (
<>
<Head>
<title>Telereview</title>
<meta name="description" content="Page d'accueil" />
</Head>
<Container fluid>
<Card>
<Card.Header as="h2">Vos performances cette semaine</Card.Header>
<Card.Body>
{datasets && averages && differences && datasets.map((set, i) => dataVisualizer(set.title, set.current, set.previous, averages[i], differences[i]))}
</Card.Body>
<div className='col col-12 col-lg-8 mx-auto'>
</div>
</Card>
</Container>
</>
)
}

View File

@@ -0,0 +1,68 @@
import React, { useEffect, useState } from 'react'
import { Card, Container, Form, Row } from 'react-bootstrap';
import StatBarChart from '../components/StatBarChart';
import useStats from '../hooks/stats';
export default function Stats() {
const [statName,setStatName] = useState("moyenne_globale")
const [timeInterval, setTimeInterval] = useState("jour")
const [chartReady, setChartReady] = useState(false);
const [xlabels, setXlabels] = useState([]);
const [plotData, setPlotData] = useState([]);
const {loading, error, stats} = useStats(10,timeInterval);
useEffect(() => {
if(!loading && !error) {
let newXlabels = [];
let newPlotData = [];
for(let i = 0; i < stats.length; i++) {
newXlabels.push(stats[i].date);
newPlotData.push(stats[i][statName]);
}
setXlabels(newXlabels);
setPlotData(newPlotData);
setChartReady(true);
}else {
setChartReady(false);
}
}, [stats, statName, timeInterval, loading, error])
return (
<Container fluid>
<Card>
<Card.Header>Tous les avis</Card.Header>
<Card.Body>
<Row>
<Form>
<Form.Group>
<Form.Label>Statistique</Form.Label>
<Form.Select value={statName} onChange={(e) => setStatName(e.target.value)}>
<option value="moyenne_globale">Moyenne globale</option>
<option value="nb_avis">Nombre d'avis</option>
<option value="moyenne_site">Moyenne du formulaire</option>
<option value = "moyenne_borne">Moyenne sur la borne</option>
<option value="dist_sexes">Distribution sexes</option>
</Form.Select>
</Form.Group>
<Form.Group>
<Form.Label>Periode</Form.Label>
<Form.Select value={timeInterval} onChange={(e) => setTimeInterval(e.target.value)}>
<option value="jour">Jour</option>
<option value="semaine">Semaine</option>
<option value="mois">Mois</option>
<option value = "annee">Année</option>
</Form.Select>
</Form.Group>
</Form>
</Row>
<Row>
{error && <p>Error</p>}
{chartReady && <StatBarChart data={plotData} labels={xlabels} />}
</Row>
</Card.Body>
</Card>
</Container>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="31" fill="none"><g opacity=".9"><path fill="url(#a)" d="M13 .4v29.3H7V6.3h-.2L0 10.5V5L7.2.4H13Z"/><path fill="url(#b)" d="M28.8 30.1c-2.2 0-4-.3-5.7-1-1.7-.8-3-1.8-4-3.1a7.7 7.7 0 0 1-1.4-4.6h6.2c0 .8.3 1.4.7 2 .4.5 1 .9 1.7 1.2.7.3 1.6.4 2.5.4 1 0 1.7-.2 2.5-.5.7-.3 1.3-.8 1.7-1.4.4-.6.6-1.2.6-2s-.2-1.5-.7-2.1c-.4-.6-1-1-1.8-1.4-.8-.4-1.8-.5-2.9-.5h-2.7v-4.6h2.7a6 6 0 0 0 2.5-.5 4 4 0 0 0 1.7-1.3c.4-.6.6-1.3.6-2a3.5 3.5 0 0 0-2-3.3 5.6 5.6 0 0 0-4.5 0 4 4 0 0 0-1.7 1.2c-.4.6-.6 1.2-.6 2h-6c0-1.7.6-3.2 1.5-4.5 1-1.3 2.2-2.3 3.8-3C25 .4 26.8 0 28.8 0s3.8.4 5.3 1.1c1.5.7 2.7 1.7 3.6 3a7.2 7.2 0 0 1 1.2 4.2c0 1.6-.5 3-1.5 4a7 7 0 0 1-4 2.2v.2c2.2.3 3.8 1 5 2.2a6.4 6.4 0 0 1 1.6 4.6c0 1.7-.5 3.1-1.4 4.4a9.7 9.7 0 0 1-4 3.1c-1.7.8-3.7 1.1-5.8 1.1Z"/></g><defs><linearGradient id="a" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient><linearGradient id="b" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@@ -0,0 +1,5 @@
.personIcon {
width: 100%;
height: 100%;
/* font-size: 50px; */
}

View File

@@ -0,0 +1,6 @@
/* ==== TABLEAU ==== */
.row:hover {
cursor: pointer;
background-color: #EEE;
}

View File

@@ -0,0 +1,57 @@
/* ==== SLIDER ==== */
.sliderContainer {
position: relative;
width: 300px;
}
.sliderContainer > input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
pointer-events: all;
width: 24px;
height: 24px;
background-color: #fff;
border-radius: 50%;
box-shadow: 0 0 0 1px #C6C6C6;
cursor: pointer;
z-index: 99;
}
.sliderContainer > input[type=range]::-moz-range-thumb {
z-index: 99;
pointer-events: all;
width: 24px;
height: 24px;
background-color: #fff;
border-radius: 50%;
box-shadow: 0 0 0 1px #C6C6C6;
cursor: pointer;
}
.sliderContainer > input[type=range]::-webkit-slider-thumb:hover {
background: #f7f7f7;
}
.sliderContainer >input[type=range]::-webkit-slider-thumb:active {
box-shadow: inset 0 0 3px #387bbe, 0 0 9px #387bbe;
-webkit-box-shadow: inset 0 0 3px #387bbe, 0 0 9px #387bbe;
}
.sliderContainer > input[type=range]::-moz-range-thumb:hover {
background: #f7f7f7;
}
.sliderContainer >input[type=range]::-moz-range-thumb:active {
box-shadow: inset 0 0 3px #387bbe, 0 0 9px #387bbe;
-webkit-box-shadow: inset 0 0 3px #387bbe, 0 0 9px #387bbe;
}
.sliderContainer >input[type="range"] {
-webkit-appearance: none;
appearance: none;
height: 2px;
width: 100%;
position: absolute;
background-color: #C6C6C6;
pointer-events: none;
}

View File

@@ -0,0 +1,29 @@
.averageCard {
width: min-content;
margin: 0 auto;
padding: 10px;
text-align: center;
min-width: 300px;
}
.averageCardBody {
display: flex;
align-items: center;
margin: 0 auto;
}
.averageMainValue {
font-size: 40px;
}
.averageSecondaryValue {
font-size: 20px;
}
.averagePositive {
color: green;
}
.averageNegative {
color: red;
}

View File

@@ -0,0 +1,34 @@
/**
* Returns the week number for this date. dowOffset is the day of week the week
* "starts" on for your locale - it can be from 0 to 6. If dowOffset is 1 (Monday),
* the week returned is the ISO 8601 week number.
* @param int dowOffset
* @return int
*/
export default function getWeek (date,dowOffset) {
/*getWeek() was developed by Nick Baicoianu at MeanFreePath: http://www.meanfreepath.com */
dowOffset = typeof(dowOffset) == 'number' ? dowOffset : 0; //default dowOffset to zero
var newYear = new Date(date.getFullYear(),0,1);
var day = newYear.getDay() - dowOffset; //the day of week the year begins on
day = (day >= 0 ? day : day + 7);
var daynum = Math.floor((date.getTime() - newYear.getTime() -
(date.getTimezoneOffset()-newYear.getTimezoneOffset())*60000)/86400000) + 1;
var weeknum;
//if the year starts before the middle of a week
if(day < 4) {
weeknum = Math.floor((daynum+day-1)/7) + 1;
if(weeknum > 52) {
nYear = new Date(date.getFullYear() + 1,0,1);
nday = nYear.getDay() - dowOffset;
nday = nday >= 0 ? nday : nday + 7;
/*if the next year starts before the middle of
the week, it is week #1 of that year*/
weeknum = nday < 4 ? 1 : 53;
}
}
else {
weeknum = Math.floor((daynum+day-1)/7);
}
return weeknum;
};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,56 @@
* {
font-family: Arial, Helvetica, sans-serif;
}
html, body {
margin: 0;
height: 100%;
}
.page {
width: 100%;
height: 100%;
}
#camera > video, #camera > canvas {
position: absolute;
top: 0;
left: 0;
text-align: center;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
}
#camera > video {
z-index: 0;
}
#camera > canvas {
z-index: 1;
}
.instructions {
width: max-content;
height: 300px;
margin: auto;
background: #A6CC00;
padding: 20px;
border-radius: 10px;
border: 3px #6B8000 solid;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
text-align: center;
}
.instructions > .title {
border-bottom: 3px #6B8000 solid;
}
.instructions > table, .instructions > th,.instructions > td {
border: 1px solid #6B8000;
border-collapse: collapse;
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

@@ -0,0 +1,22 @@
class AudioPage {
constructor() {
this.isEnabled = false;
this.DOMElement = document.getElementById("audio");
}
set enabled(isEnabled) {
this.isEnabled = isEnabled;
this.DOMElement.style.display = isEnabled ? "block" : "none";
document.getElementById("grade").innerHTML = "";
}
setGrade(grade) {
if(this.isEnabled) {
document.getElementById("grade").innerHTML = grade.toString();
}
}
reset() {
document.getElementById("grade").innerHTML = "";
}
}

View File

@@ -0,0 +1,153 @@
class CameraPage {
constructor() {
this.spinnerWeight = 10;
this.spinnerColor = "#0F0FFF"
this.canvas = document.getElementById("overlay-canvas");
this.ctx = this.canvas.getContext("2d");
this.video = document.getElementById("camera-video");
this.width;
this.height; //calcule automatiquement en fonction de la largeur du flux vidéo
this.videoWidth;
this.videoHeight;
this.streaming = false;
this.activeEffects = [];
this.images = {};
this._startup();
this._loadImages();
this._enabled = false;
this.DOMElement = document.getElementById("camera");
}
set enabled(val) {
this._enabled = val;
this.DOMElement.style.display = val ? "block" : "none";
if (val) {
this._frame();
this.video.play();
}else {
this.video.pause();
}
}
get enabled() {
return this._enabled;
}
_startup() {
navigator.mediaDevices
.getUserMedia({ video: true, audio: false })
.then((stream) => {
this.video.srcObject = stream;
this.video.play();
})
.catch((err) => {
console.error(`Erreur pendant la lecture de la camera: ${err}`);
});
this.video.addEventListener(
"canplay",
(ev) => {
if (!this.streaming) {
//calcul de la taille de la vidéo en fonction de la taille de la fenêtre pour qu'elle soit toujours visible
let aspectRatio = this.video.videoWidth / this.video.videoHeight;
if (window.innerHeight * aspectRatio > window.innerWidth) {
this.width = window.innerWidth;
this.height = window.innerWidth / aspectRatio;
} else {
this.width = window.innerHeight * aspectRatio;
this.height = window.innerHeight;
}
this.videoHeight = this.video.videoHeight;
this.videoWidth = this.video.videoWidth;
this.video.setAttribute("width", this.width);
this.video.setAttribute("height", this.height);
this.canvas.setAttribute("width", this.width);
this.canvas.setAttribute("height", this.height);
this.streaming = true;
}
},
false
);
}
_loadImages() {
this.images.thumbsUp = new Image();
this.images.thumbsUp.src = "assets/img/thumbs_up.png";
this.images.thumbsDown = new Image();
this.images.thumbsDown.src = "assets/img/thumbs_down.png";
}
_frame() {
if (this.streaming && this.enabled && this.width && this.height) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this._drawEffects();
}
if (this.enabled) {
requestAnimationFrame(() => this._frame());
}
}
_scaleEffect(x, y, width, height) {
let xScale = this.width / this.videoWidth;
let yScale = this.height / this.videoHeight;
return {
x: x * xScale,
y: y * yScale,
width: width * xScale,
height: height * yScale
}
}
_drawEffects() {
for (let effect of this.activeEffects) {
let { x, y, width, height } = this._scaleEffect(effect.x, effect.y, effect.width, effect.height);
width = width * this.videoWidth * 2;
height = height * this.videoHeight * 2;
x = x * this.videoWidth - width / 2;
y = y * this.videoHeight - height / 2;
console.log(width, height);
if (effect.type == "thumbs_down") {
this._drawThumbsDown(x, y, width, height);
}
if (effect.type == "thumbs_up") {
this._drawThumbsUp(x, y, width, height);
}
if (effect.type == "loading") {
this._drawLoading(x, y, width, effect.progress);
}
}
}
_drawLoading(x, y, radius, progress) {
this.ctx.lineWidth = this.spinnerWeight;
this.ctx.strokeStyle = this.spinnerColor;
this.ctx.beginPath();
this.ctx.arc(x, y, radius, 0, progress * 2 * Math.PI);
this.ctx.stroke();
}
_drawThumbsDown(x, y, width, height) {
this.ctx.beginPath();
this.ctx.drawImage(this.images.thumbsDown, x, y, width, height);
this.ctx.stroke();
}
_drawThumbsUp(x, y, width, height) {
this.ctx.beginPath();
this.ctx.drawImage(this.images.thumbsUp, x, y, width, height);
this.ctx.stroke();
}
setEffects(effects) {
this.activeEffects = effects;
}
reset() {
this.activeEffects = [];
}
}

View File

@@ -0,0 +1,5 @@
let stateManager;
window.addEventListener("load", () => {
stateManager = new StateManager();
}, false);

View File

@@ -0,0 +1,22 @@
class WebsocketClient {
constructor(onNewEffects, onNewState, onNewGrade, onReset) {
this.socket = new WebSocket("ws://localhost:5000");
this.socket.addEventListener("open", (event) => {
this.socket.send("connected");
console.log("connected")
});
this.socket.onmessage = (event) => {
let msg = JSON.parse(event.data);
if (msg.type == "effects") {
onNewEffects(msg.effects);
}else if(msg.type == "state") {
onNewState(msg.state);
}else if(msg.type == "new_grade") {
onNewGrade(Number(msg.grade));
}else if(msg.type == "reset") {
onReset();
}
};
}
}

View File

@@ -0,0 +1,12 @@
class SleepingPage {
constructor(onWakeUp) {
this.onWakeUp = onWakeUp;
this.isEnabled = false;
this.DOMElement = document.getElementById("sleeping-page");
}
set enabled(isEnabled) {
this.isEnabled = isEnabled;
this.DOMElement.style.display = isEnabled ? "block" : "none";
}
}

View File

@@ -0,0 +1,63 @@
const STATE = {
sleeping: 0,
video: 1,
audio: 2,
thankYou: 3,
};
class StateManager {
constructor() {
this._state = STATE.sleeping;
this._cameraPage = new CameraPage();
this._sleepingPage = new SleepingPage();
this._audioPage = new AudioPage();
this._thankYouPage = new ThankYouPage();
this.wsClient = new WebsocketClient(
(effects) => {
this.setState(STATE.video);
this._cameraPage.setEffects(effects)
},
(state) => this.setState(state),
(grade) => this._audioPage.setGrade(grade),
() => this.reset(),
);
this._sleepingPage.enabled = true;
this._cameraPage.enabled = false;
this._audioPage.enabled = false;
this._thankYouPage.enabled = false;
}
setState(newState) {
console.log({current:this._state,new:newState})
if(this._state == STATE.sleeping && newState == STATE.video) {
this._cameraPage.enabled = true;
this._sleepingPage.enabled = false;
this._state = newState;
}else if(this._state == STATE.video && newState == STATE.audio) {
this._cameraPage.enabled = false;
this._audioPage.enabled = true;
this._state = newState;
}else if(this._state == STATE.audio && newState == STATE.thankYou) {
this._audioPage.enabled = false;
this._thankYouPage.enabled = true;
this._state = newState;
}else if(this._state == STATE.thankYou && newState == STATE.sleeping) {
this._thankYouPage.enabled = false;
this._sleepingPage.enabled = true;
this._state = newState;
}
}
reset() {
this._state = 0;
this._cameraPage.enabled = false;
this._audioPage.enabled = false;
this._thankYouPage.enabled = false;
this._audioPage.reset();
this._cameraPage.reset();
this._sleepingPage.enabled = true;
}
}

View File

@@ -0,0 +1,11 @@
class ThankYouPage {
constructor() {
this.isEnabled = false;
this.DOMElement = document.getElementById("thank-you");
}
set enabled(isEnabled) {
this.isEnabled = isEnabled;
this.DOMElement.style.display = isEnabled ? "block" : "none";
}
}

View File

@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="assets/css/main.css">
<!-- <link rel="stylesheet" href="assets/css/bootstrap-grid.min.css"> -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
<title>Téléreview</title>
</head>
<body>
<div id="sleeping-page" class="page">
<div class="instructions">
<div class="title">
<h1>Votre avis nous intéresse</h1>
</div>
<span>Faites un</span>
<img width=50 src="assets/img/thumbs_up.png">
<span>ou un</span>
<img width=50 src="assets/img/thumbs_down.png">
<span> avec votre main pour commencer</span>
</div>
</div>
<div id="camera">
<canvas id="overlay-canvas"></canvas>
<video id="camera-video"></video>
</div>
<div id="audio">
<div class="instructions">
<div class="title">
<h1>Dites-nous en plus</h1>
</div>
<p>Donnez une note sur 10 au critère suivant</p>
<table>
<tr>
<th>Critère</td>
<th>Note / 10</td>
</tr>
<tr>
<td>Calme</td>
<td><span id="grade"></span>/10</td>
</tr>
</table>
</div>
</div>
<div id="thank-you">
<div class="instructions">
<div class="title">
<h1>Merci pour votre avis</h1>
</div>
<span>Nous esperons vous revoir bientôt</span>
</div>
</div>
<script src="assets/js/camera_page.js"></script>
<script src="assets/js/network.js"></script>
<script src="assets/js/thank_you_page.js"></script>
<script src="assets/js/audio_page.js"></script>
<script src="assets/js/sleeping_page.js"></script>
<script src="assets/js/state_manager.js"></script>
<script src="assets/js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,24 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/charts
**/docker-compose*
**/compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

View File

@@ -0,0 +1,15 @@
FROM node:lts-alpine
WORKDIR /usr/src/app
#installation des dépendances
COPY ["package.json", "package-lock.json*", "npm-shrinkwrap.json*", "./"]
RUN npm install --production --silent && mv node_modules ../
#On copie le code source
COPY . .
#On change le propriétaire du dossier
RUN chown -R node /usr/src/app
#On change l'utilisateur
USER node
CMD ["node", "index.js"]

View File

@@ -1,31 +1,35 @@
# Installation # Serveur de traitement des données
* Pour faire fonctinoner le serveur sur vos machines il y a 3 choses a faire Ce serveur s'occupe de fournir une API web pour pourvoir ajouter des avis sur la borne, réxupérer des avis, calculer et fournir les statistiques sur ces avis
1. Installer node js : https://nodejs.org/en/download/
2. Ouvrir un terminal et aller dans ce dossier (code/server) et tapper `npm install` pour installer les pacakges nécessaires # Installation (Si vous voulez le lancer hors du container docker)
3. copier le fichier `.env_template` et le nommer `.env` et remplir les variables (cela est fait pour le pas mettre les mots de passes sur le gitlab, faites attention de ne jamais commit le fichier .env !!) * Pour faire fonctinoner le serveur sur vos machines il y a 3 choses a faire
4. pour lancer le serveur faire `node index.js` 1. Installer node js : https://nodejs.org/en/download/
2. Ouvrir un terminal et aller dans ce dossier (code/server) et tapper `npm install` pour installer les pacakges nécessaires
# Utilisation 3. copier le fichier `.env_template` et le nommer `.env` et remplir les variables (cela est fait pour le pas mettre les mots de passes sur le gitlab, faites attention de ne jamais commit le fichier .env !!)
## Avis laissés sur la borne (hors réseaux sociaux) 4. pour lancer le serveur faire `node index.js`
### Routes GET :
- `/borne/get_last_reviews?limit=LIM` : renvoie les LIM derniers avis sur la borne # Utilisation
- `/borne/get_review?id=ID` : renvoie les infos sur l'avis d'in ID ## Avis laissés sur la borne (hors réseaux sociaux)
- `/borne/get_criteres` : renvoie les criteres de notations valide pour les notes autres ### Routes GET :
- `/borne/notes_autres?critere=CRIT&limit=LIM` : renvoie les LIM dernières notes sur le critère CRIT - `/borne/get_last_reviews?limit=LIM` : renvoie les LIM derniers avis sur la borne
- `/borne/notes_autres?id=ID&limit=LIM` : renvoie toutes les notes spécifiques liées à l'avis ID - `/borne/get_review?id=ID` : renvoie les infos sur l'avis d'in ID
- `/borne/get_criteres` : renvoie les criteres de notations valide pour les notes autres
### Routes POST - `/borne/notes_autres?critere=CRIT&limit=LIM` : renvoie les LIM dernières notes sur le critère CRIT
- `/add_review` : Ajoute une review et un auteur, paramètres POST : - `/borne/notes_autres?id=ID&limit=LIM` : renvoie toutes les notes spécifiques liées à l'avis ID
* [OBLIGATOIRE] `note` : note principale de la review entre 0 et 10 compris - `get_stats?interval=INTERVAL&limit=LIM`: interval est "jour", "mois", "annee", "semaine", l'interval de calcul de données demandé, LIM est le nombre des stats à récupérer
* [OBLIGATOIRE] `source` : nom de la source de l'avis, doit être `borne` ou `website` pour resp la borne et le site
* `auteur_age` : age de l'auteur ### Routes POST
* `auteur_sexe` : sexe de l'auteur (valeurs valide 'f', 'h', 'a') - `/add_review` : Ajoute une review et un auteur, paramètres POST :
* `commentaire` : Commentaire laissé avec l'avis * [OBLIGATOIRE] `note` : note principale de la review entre 0 et 10 compris
* `notes autres` : sous la forme * [OBLIGATOIRE] `source` : nom de la source de l'avis, doit être `borne` ou `website` pour resp la borne et le site
```json * `auteur_age` : age de l'auteur
{ * `auteur_sexe` : sexe de l'auteur (valeurs valide 'f', 'h', 'a')
"critere1": 8, * `commentaire` : Commentaire laissé avec l'avis
"critere2": 2, * `notes autres` : sous la forme
"critere3": 0 ```json
} {
``` "critere1": 8,
"critere2": 2,
"critere3": 0
}
```

View File

@@ -1,200 +1,237 @@
import conn from '../database.js'; import conn from '../database.js';
/** /**
* Renvoie les derniers avis laissés sur la borne trié par ordre chronologque décroissant * Renvoie les derniers avis laissés sur la borne trié par ordre chronologque décroissant
* @param {Number} limit Nombre d'avis a afficher * @param {Number} limit Nombre d'avis a afficher
* @returns Une liste d'objets de la forme {id: id de l'avis, date: date de l'avis, note_principale: note sur 10, commentaire: avis textuel, nom_source, sexe_auteur, age_auteur} * @returns Une liste d'objets de la forme {id: id de l'avis, date: date de l'avis, note_principale: note sur 10, commentaire: avis textuel, nom_source, sexe_auteur, age_auteur}
*/ */
const getLastReviews = (limit=10) => { const getLastReviews = (limit = 10) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let sql = `SELECT borne_avis.id,date,note_principale,commentaire,sources.nom as nom_source, borne_auteurs.sexe as sexe_auteur, borne_auteurs.age as age_auteur let sql = `SELECT borne_avis.id,date,note_principale,commentaire,sources.nom as nom_source, borne_auteurs.sexe as sexe_auteur, borne_auteurs.age as age_auteur
FROM borne_avis FROM borne_avis
JOIN sources ON sources.id = source_id JOIN sources ON sources.id = source_id
JOIN borne_auteurs ON borne_auteurs.id = id_auteur JOIN borne_auteurs ON borne_auteurs.id = id_auteur
ORDER BY borne_avis.id DESC LIMIT ?`; ORDER BY borne_avis.id DESC LIMIT ?`;
conn.query(sql, [limit], (err, res) => { conn.query(sql, [limit], (err, res) => {
if(err) { if (err) {
reject(err); reject(err);
}else { } else {
resolve(res) resolve(res)
} }
}) })
}) })
} }
/** /**
* Renvoie les informations sur un avis avec un ID spécifique * Renvoie les informations sur un avis avec un ID spécifique
* @param {Number} id Id de la review * @param {Number} id Id de la review
* @returns Un objet de la forme des objets dans la table borne_avis ayant l'id id s'il existe, renvoie une erreur sinon * @returns Un objet de la forme des objets dans la table borne_avis ayant l'id id s'il existe, renvoie une erreur sinon
*/ */
const getReviewFromId = (id) => { const getReviewFromId = (id) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let sql = `SELECT * FROM borne_avis WHERE id = ? LIMIT 1`; let sql = `SELECT borne_avis.id,date,note_principale,commentaire,sources.nom as nom_source, borne_auteurs.sexe as sexe_auteur, borne_auteurs.age as age_auteur
conn.query(sql, [id], (err, res) => { FROM borne_avis
if(err) { JOIN sources ON sources.id = source_id
reject(err); JOIN borne_auteurs ON borne_auteurs.id = id_auteur
}else { WHERE borne_avis.id = ?
if(res.length != 1) { LIMIT 1`;
reject(new Error("Avis avec cet ID non trouvé")) conn.query(sql, [id], (err, res) => {
}else { if (err) {
resolve(res[0]) reject(err);
} } else {
if(res.length != 1) { if (res.length != 1) {
reject(new Error("Avis avec cet ID non trouvé")) reject(new Error("Avis avec cet ID non trouvé"))
}else { } else {
resolve(res[0]) resolve(res[0])
} }
} if (res.length != 1) {
}) reject(new Error("Avis avec cet ID non trouvé"))
}) } else {
} resolve(res[0])
}
/** }
* Renvoie tout les critères de notation valides })
* @returns Une liste d'objets de la forme {id,nom} })
*/ }
const getCriteres = () => {
return new Promise((resolve, reject) => { /**
let sql = `SELECT * FROM borne_criteres`; * Renvoie tout les critères de notation valides
conn.query(sql, [limit], (err, res) => { * @returns Une liste d'objets de la forme {id,nom}
if(err) { */
reject(err); const getCriteres = () => {
}else { return new Promise((resolve, reject) => {
resolve(res) let sql = `SELECT * FROM borne_criteres`;
} conn.query(sql, [limit], (err, res) => {
}) if (err) {
}) reject(err);
} } else {
resolve(res)
/** }
* Renvoie les "limit" notes les plus récentes laissées pour un critère spécifié })
* @param {String} critere Nom de critère })
* @param {Number} limit Nombre max de note a afficher }
* @returns une liste d'objets de la forme {id,date,critere,note,avis_id:id de l'avis lié a cette note}
*/ /**
const getNotesAutresFromCritere = (critere,limit=10) => { * Renvoie les "limit" notes les plus récentes laissées pour un critère spécifié
return new Promise((resolve, reject) => { * @param {String} critere Nom de critère
let sql = `SELECT borne_notes_autre.id as id,date,borne_criteres.nom as critere, note, avis_id * @param {Number} limit Nombre max de note a afficher
FROM borne_notes_autre * @returns une liste d'objets de la forme {id,date,critere,note,avis_id:id de l'avis lié a cette note}
JOIN borne_criteres ON borne_criteres.id = critere_id */
WHERE borne_criteres.nom = ? const getNotesAutresFromCritere = (critere, limit = 10) => {
ORDER BY borne_notes_autre.id DESC LIMIT ? ;`; return new Promise((resolve, reject) => {
conn.query(sql, [critere,limit], (err, res) => { let sql = `SELECT borne_notes_autre.id as id,date,borne_criteres.nom as critere, note, avis_id
if(err) { FROM borne_notes_autre
reject(err); JOIN borne_criteres ON borne_criteres.id = critere_id
}else { WHERE borne_criteres.nom = ?
resolve(res) ORDER BY borne_notes_autre.id DESC LIMIT ? ;`;
} conn.query(sql, [critere, limit], (err, res) => {
}) if (err) {
}) reject(err);
} } else {
resolve(res)
/** }
* Renvoie toutes les notes sur des critères spécifiques laissée pour un avis spécifique })
* @param {Number} reviewId Id de l'avis })
* @returns une liste d'objets de la forme {id,critere:nom du critère, note:note sur 10} }
*/
const getNotesAutresFromReview = (reviewId) => { /**
return new Promise((resolve, reject) => { * Renvoie toutes les notes sur des critères spécifiques laissée pour un avis spécifique
let sql = `SELECT borne_notes_autre.id as id,borne_criteres.nom as critere, note * @param {Number} reviewId Id de l'avis
FROM borne_notes_autre * @returns une liste d'objets de la forme {id,critere:nom du critère, note:note sur 10}
JOIN borne_criteres on borne_criteres.id = critere_id */
WHERE avis_id = ? const getNotesAutresFromReview = (reviewId) => {
ORDER BY borne_notes_autre.id DESC`; return new Promise((resolve, reject) => {
conn.query(sql, [reviewId], (err, res) => { let sql = `SELECT borne_notes_autre.id as id,borne_criteres.nom as critere, note
if(err) { FROM borne_notes_autre
reject(err); JOIN borne_criteres on borne_criteres.id = critere_id
}else { WHERE avis_id = ?
resolve(res) ORDER BY borne_notes_autre.id DESC`;
} conn.query(sql, [reviewId], (err, res) => {
}) if (err) {
}) reject(err);
} } else {
resolve(res)
/** }
* Renvoie les dernières statistiques })
* @param {String} interval Interval de temps de la statistique, valeurs possibles : "jour","mois","semaine","annee" })
* @param {Number} limit Nombre max de stats a renvoyer }
* @returns Une liste d'objet ou chaque objet correspond a une stat sur une periode donnée (par ex une stat hebdo datée du 07/01 correspond a une stat sur la semaine du 01/01 au 07/01), ces objets sont de la forme
*/ /**
const getStats = (interval, limit=10) => { * Renvoie les dernières statistiques
return new Promise((resolve, reject) => { * @param {String} interval Interval de temps de la statistique, valeurs possibles : "jour","mois","semaine","annee"
if(!["jour","mois","semaine","annee"].includes(interval)) { * @param {Number} limit Nombre max de stats a renvoyer
reject(new Error("Invalid time interval")); * @returns Une liste d'objet ou chaque objet correspond a une stat sur une periode donnée (par ex une stat hebdo datée du 07/01 correspond a une stat sur la semaine du 01/01 au 07/01), ces objets sont de la forme
return; */
} const getStats = (interval, limit = 10) => {
let sql = `SELECT * FROM stats_general_${interval} ORDER BY id DESC LIMIT ?;`; return new Promise((resolve, reject) => {
conn.query(sql, [limit], (err, res) => { if (!["jour", "mois", "semaine", "annee"].includes(interval)) {
if(err) { reject(new Error("Invalid time interval"));
reject(err); return;
}else { }
resolve(res) let sql = `SELECT * FROM stats_general_${interval} ORDER BY id DESC LIMIT ?;`;
} conn.query(sql, [limit], (err, res) => {
}) if (err) {
}) reject(err);
} } else {
resolve(res)
/* }
Ces fonction sont des handlers pour les routes express, elles sont appelées par les routes et renvoient les données au format JSON })
*/ })
export const handleGetLastReviews = (req, res) => { }
getLastReviews(req.query.limit)
.then((reviews) => { /*
res.send(reviews); Ces fonction sont des handlers pour les routes express, elles sont appelées par les routes et renvoient les données au format JSON
}) */
.catch((err) => { export const handleGetLastReviews = (req, res) => {
res.status(500).send("Error: " + err.message); if (req.query.limit) {
}); getLastReviews(Number(req.query.limit))
} .then((reviews) => {
res.send(reviews);
export const handleGetReview = (req, res) => { })
getReviewFromId(req.query.id) .catch((err) => {
.then((review) => { res.status(500).send("Error: " + err.message);
res.send(review); });
}) } else {
.catch((err) => { getLastReviews()
res.status(500).send("Error: " + err.message); .then((reviews) => {
}); res.send(reviews);
} })
.catch((err) => {
export const handleGetCriteres = (req, res) => { res.status(500).send("Error: " + err.message);
getCriteres() });
.then((criteres) => { }
res.send(criteres); }
})
.catch((err) => { export const handleGetReview = (req, res) => {
res.status(500).send("Error: " + err.message); getReviewFromId(req.query.id)
}); .then((review) => {
} getNotesAutresFromReview(req.query.id).then((notesAutres) => {
res.send({ ...review, notes_autres: notesAutres });
export const handleGetNotesAutres = (req, res) => { })
if(req.query.critere) { })
getNotesAutresFromCritere(req.query.critere, req.query.limit) .catch((err) => {
.then((notes) => { res.status(500).send("Error: " + err.message);
res.send(notes); });
}) }
.catch((err) => {
res.status(500).send("Error: " + err.message); export const handleGetCriteres = (req, res) => {
}); getCriteres()
}else if(req.query.id) { .then((criteres) => {
getNotesAutresFromReview(req.query.id) res.send(criteres);
.then((notes) => { })
res.send(notes); .catch((err) => {
}) res.status(500).send("Error: " + err.message);
.catch((err) => { });
res.status(500).send("Error: " + err.message); }
});
}else { export const handleGetNotesAutres = (req, res) => {
res.status(500).send("Error: no critere or id specified"); if (req.query.critere) {
} if (req.query.limit) {
} getNotesAutresFromCritere(req.query.critere, Number(req.query.limit))
.then((notes) => {
export const handleGetStats = (req, res) => { res.send(notes);
getStats(req.query.interval, req.query.limit) })
.then((stats) => { .catch((err) => {
res.send(stats); res.status(500).send("Error: " + err.message);
}) });
.catch((err) => { } else {
res.status(500).send("Error: " + err.message); getNotesAutresFromCritere(req.query.critere)
}); .then((notes) => {
res.send(notes);
})
.catch((err) => {
res.status(500).send("Error: " + err.message);
});
}
} else if (req.query.id) {
getNotesAutresFromReview(req.query.id)
.then((notes) => {
res.send(notes);
})
.catch((err) => {
res.status(500).send("Error: " + err.message);
});
} else {
res.status(500).send("Error: no critere or id specified");
}
}
export const handleGetStats = (req, res) => {
if (req.query.limit) {
getStats(req.query.interval, Number(req.query.limit))
.then((stats) => {
res.send(stats);
})
.catch((err) => {
res.status(500).send("Error: " + err.message);
});
} else {
getStats(req.query.interval)
.then((stats) => {
res.send(stats);
})
.catch((err) => {
res.status(500).send("Error: " + err.message);
});
}
} }

View File

@@ -1,84 +1,84 @@
import { Auteur, Review } from './structures.js'; import { Auteur, Review } from './structures.js';
import conn from '../database.js'; import conn from '../database.js';
import {getSourceId} from '../utils.js'; import {getSourceId} from '../utils.js';
/** /**
* Ajoute un nouvel auteur de commentaire a la BDD * Ajoute un nouvel auteur de commentaire a la BDD
* @param {Auteur} author L'auteur a ajouter * @param {Auteur} author L'auteur a ajouter
* @returns une Promise qui renvoie l'id de l'utilisateur * @returns une Promise qui renvoie l'id de l'utilisateur
*/ */
const addAuteur = (author) => { const addAuteur = (author) => {
return new Promise((resolve,reject) => { return new Promise((resolve,reject) => {
const sql = "INSERT INTO borne_auteurs (age, sexe) VALUES (?);" const sql = "INSERT INTO borne_auteurs (age, sexe) VALUES (?);"
conn.query(sql, [[author.age, author.sexe]], (err, res) => { conn.query(sql, [[author.age, author.sexe]], (err, res) => {
if(err) { if(err) {
reject(err) reject(err)
}else { }else {
resolve(res.insertId); resolve(res.insertId);
} }
}) })
}) })
} }
//Ajoute une note sur un critère spécifique dans la BDD //Ajoute une note sur un critère spécifique dans la BDD
const addSpecificRating = (reviewId, label, value) => { const addSpecificRating = (reviewId, label, value) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const sql = "INSERT INTO borne_notes_autre(critere_id, avis_id, note) VALUES ((SELECT id FROM borne_criteres WHERE borne_criteres.nom = ?), ?, ?)" const sql = "INSERT INTO borne_notes_autre(critere_id, avis_id, note) VALUES ((SELECT id FROM borne_criteres WHERE borne_criteres.nom = ?), ?, ?)"
conn.query(sql, [label,reviewId, value], (err, res) => { conn.query(sql, [label,reviewId, value], (err, res) => {
if(err) { if(err) {
reject(err); reject(err);
}else { }else {
resolve(res.insertId); resolve(res.insertId);
} }
}) })
}) })
} }
/** /**
* Ajoute un avis a la base de donnée * Ajoute un avis a la base de donnée
* @param {Review} review la review a ajouter * @param {Review} review la review a ajouter
* @param {Number} authorId l'ID de l'auteur de l'avis dans la BDD * @param {Number} authorId l'ID de l'auteur de l'avis dans la BDD
* @param {Number} sourceId l'ID de la source de l'avis dans la BDD * @param {Number} sourceId l'ID de la source de l'avis dans la BDD
* @returns une Promise qui renvoie l'id de l'avis * @returns une Promise qui renvoie l'id de l'avis
*/ */
const addReview = (review, authorId, sourceId) => { const addReview = (review, authorId, sourceId) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const sql = "INSERT INTO borne_avis (id_auteur, note_principale, commentaire, source_id) VALUES (?);" const sql = "INSERT INTO borne_avis (id_auteur, note_principale, commentaire, source_id) VALUES (?);"
conn.query(sql, [[authorId, review.note, review.commentaire, sourceId]], (err, res) => { conn.query(sql, [[authorId, review.note, review.commentaire, sourceId]], (err, res) => {
if(err) { if(err) {
reject(err) reject(err)
}else { }else {
resolve(res.insertId); resolve(res.insertId);
} }
}) })
}) })
} }
/** /**
* Traite une requête POST qui contient les valeurs nécessaires pour ajouter un nouvel avis dans la BDD * Traite une requête POST qui contient les valeurs nécessaires pour ajouter un nouvel avis dans la BDD
* Essaie d'ajouter l'avis et l'auteur dans la BDD, sinon renvoie l'erreur avec un code 500 * Essaie d'ajouter l'avis et l'auteur dans la BDD, sinon renvoie l'erreur avec un code 500
* @param {*} req requete * @param {*} req requete
* @param {*} res reponse * @param {*} res reponse
*/ */
export const addReviewFromRequest = async (req,res) => { export const addReviewFromRequest = async (req,res) => {
try { try {
let notes_autre = {} let notes_autre = {}
try{ try{
notes_autre = JSON.parse(req.body.notes_autre); notes_autre = JSON.parse(req.body.notes_autre);
}catch(err){}; }catch(err){};
const author = new Auteur(req.body.auteur_age,req.body.auteur_sexe); const author = new Auteur(req.body.auteur_age,req.body.auteur_sexe);
const review = new Review(author, req.body.note, req.body.source, req.body.commentaire, notes_autre) const review = new Review(author, req.body.note, req.body.source, req.body.commentaire, notes_autre)
let authorId = await addAuteur(author); let authorId = await addAuteur(author);
let sourceId = await getSourceId(review.source); let sourceId = await getSourceId(review.source);
let reviewId = await addReview(review, authorId, sourceId ); let reviewId = await addReview(review, authorId, sourceId );
for(let label in review.notesAutre) { for(let label in review.notesAutre) {
await addSpecificRating(reviewId, label, review.notesAutre[label]); await addSpecificRating(reviewId, label, review.notesAutre[label]);
} }
res.send("success") res.send("success")
}catch(err) { }catch(err) {
res.status(500).send("Error : " + err.message) res.status(500).send("Error : " + err.message)
} }
} }

View File

@@ -1,52 +1,52 @@
//Liste des valeurs valides pour le sexe d'un utilisateur //Liste des valeurs valides pour le sexe d'un utilisateur
const validSexes = ["h","f","a"]; const validSexes = ["h","f","a"];
//Classe pour représenter un avis laissé sur la borne que ce soit par l'interface web ou par un geste //Classe pour représenter un avis laissé sur la borne que ce soit par l'interface web ou par un geste
export class Review { export class Review {
/** /**
* Constructeur * Constructeur
* @param {Auteur} auteur L'auteur de l'avis * @param {Auteur} auteur L'auteur de l'avis
* @param {Number} note La note principale entre 0 et 10 compris * @param {Number} note La note principale entre 0 et 10 compris
* @param {String} source La source de l'avis, doit être une entrée dans la table soruces de la bdd * @param {String} source La source de l'avis, doit être une entrée dans la table soruces de la bdd
* @param {String} commentaire Le commentaire lié a l'avis * @param {String} commentaire Le commentaire lié a l'avis
* @param {Object} notesAutre Les notes secondaires laissées, sous la forme d'un object ou les clés sont les labels de chaque notes et les valeurs sont des notes entre 0 et 10 compris * @param {Object} notesAutre Les notes secondaires laissées, sous la forme d'un object ou les clés sont les labels de chaque notes et les valeurs sont des notes entre 0 et 10 compris
*/ */
constructor(auteur, note, source, commentaire=null, notesAutre={}) { constructor(auteur, note, source, commentaire=null, notesAutre={}) {
this.auteur = auteur; this.auteur = auteur;
this.note = note; this.note = note;
this.source = source; this.source = source;
this.commentaire = commentaire; this.commentaire = commentaire;
this.notesAutre = notesAutre; this.notesAutre = notesAutre;
//On vérifie si toutes les données sont correctes //On vérifie si toutes les données sont correctes
if(note < 0 || note > 10) { if(note < 0 || note > 10) {
throw new Error("Note principale invalide"); throw new Error("Note principale invalide");
} }
for(let nom in notesAutre) { for(let nom in notesAutre) {
if(notesAutre[nom] < 0 || notesAutre[nom] > 10) { if(notesAutre[nom] < 0 || notesAutre[nom] > 10) {
throw new Error("Note " + notesAutre[nom] +"/10 invalide"); throw new Error("Note " + notesAutre[nom] +"/10 invalide");
} }
} }
} }
} }
//Classe qui représente l'auteur d'un avis //Classe qui représente l'auteur d'un avis
export class Auteur { export class Auteur {
/** /**
* Constructeur * Constructeur
* @param {Number} age L'age de l'auteur * @param {Number} age L'age de l'auteur
* @param {String} sexe Le sexe de la personne (doit être dans l'array validSexes défini au début de ce fichier) * @param {String} sexe Le sexe de la personne (doit être dans l'array validSexes défini au début de ce fichier)
*/ */
constructor(age=null, sexe=null) { constructor(age=null, sexe=null) {
this.age = age; this.age = age;
this.sexe = sexe; this.sexe = sexe;
//Verification des données //Verification des données
if(sexe != undefined && !validSexes.includes(sexe) ) { if(sexe != undefined && !validSexes.includes(sexe) ) {
throw new Error("Sexe invalide"); throw new Error("Sexe invalide");
} }
if(age != undefined && age <= 0) { if(age != undefined && age <= 0) {
throw new Error("Age invalide"); throw new Error("Age invalide");
} }
} }
} }

View File

@@ -1,15 +1,15 @@
import * as dotenv from 'dotenv' import * as dotenv from 'dotenv'
import mysql from 'mysql2'; import mysql from 'mysql2';
dotenv.config(); dotenv.config();
const conn = mysql.createConnection({ const conn = mysql.createConnection({
host: process.env.DB_HOST, host: process.env.DB_HOST,
user: process.env.DB_USER, user: process.env.DB_USER,
password: process.env.DB_PASSWORD, password: process.env.DB_PASSWORD,
database: process.env.DB_NAME, database: process.env.DB_NAME,
multipleStatements: true, multipleStatements: true,
}); });
conn.connect(); conn.connect();
export default conn; export default conn;

View File

@@ -1,28 +1,33 @@
import * as dotenv from 'dotenv'; import * as dotenv from 'dotenv';
import express from 'express'; import express from 'express';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import { addReviewFromRequest } from './borne/post_handler.js'; import { addReviewFromRequest } from './borne/post_handler.js';
import { addSocialReviewFromRequest } from './reseaux_sociaux/post_handler.js'; import { addSocialReviewFromRequest } from './reseaux_sociaux/post_handler.js';
import { startCronJobs } from './stats/update_stats.js'; import { startCronJobs, manualUpdateStats } from './stats/update_stats.js';
import * as borneHandler from './borne/get_handler.js'; import * as borneHandler from './borne/get_handler.js';
import cors from "cors";
const app = express();
const app = express();
app.use(bodyParser.urlencoded({extended:true})) app.use(bodyParser.urlencoded({extended:true}))
dotenv.config() app.use(cors({origin:'*'}))
app.post('/add_review', (req,res) => addReviewFromRequest(req,res)); dotenv.config()
app.post('/add_social_review', (req,res) => addSocialReviewFromRequest(req,res)); app.post('/add_review', (req,res) => addReviewFromRequest(req,res));
app.post('/add_social_review', (req,res) => addSocialReviewFromRequest(req,res));
app.get('/borne/get_last_reviews', borneHandler.handleGetLastReviews);
app.get('/borne/get_review', borneHandler.handleGetReview); app.get('/borne/get_last_reviews', borneHandler.handleGetLastReviews);
app.get('/borne/get_criteres', borneHandler.handleGetCriteres); app.get('/borne/get_review', borneHandler.handleGetReview);
app.get('/borne/notes_autres', borneHandler.handleGetNotesAutres); app.get('/borne/get_criteres', borneHandler.handleGetCriteres);
app.get('/borne/get_stats', borneHandler.handleGetStats); app.get('/borne/notes_autres', borneHandler.handleGetNotesAutres);
app.get('/borne/get_stats', borneHandler.handleGetStats);
startCronJobs(); app.get('/update_stats', (req, res) => {
manualUpdateStats();
app.listen(process.env.PORT, () => { res.send("OK");
console.log("Server démaré sur le port " + process.env.PORT) })
})
startCronJobs();
app.listen(process.env.PORT, () => {
console.log("Server démaré sur le port " + process.env.PORT)
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,20 @@
{ {
"name": "server", "name": "server",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "Telereview", "author": "Telereview",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"body-parser": "^1.20.1", "body-parser": "^1.20.1",
"cron": "^2.1.0", "cors": "^2.8.5",
"dotenv": "^16.0.3", "cron": "^2.1.0",
"express": "^4.18.2", "dotenv": "^16.0.3",
"mysql2": "^2.3.3" "express": "^4.18.2",
} "mysql2": "^2.3.3"
} }
}

View File

@@ -1,85 +1,85 @@
import { ReseauxAuteur, ReseauxReview } from './structures.js'; import { ReseauxAuteur, ReseauxReview } from './structures.js';
import conn from '../database.js'; import conn from '../database.js';
import {getSourceId} from '../utils.js'; import {getSourceId} from '../utils.js';
/**Récupérer l'id d'un auteur particulier dans la base de donnée s'il existe /**Récupérer l'id d'un auteur particulier dans la base de donnée s'il existe
* @param {ReseauxAuteur} author L'auteur dont on veut l'id* * @param {ReseauxAuteur} author L'auteur dont on veut l'id*
* @returns Une Promise qui donne l'id de l'utilisateur ou null si il n'existe pas * @returns Une Promise qui donne l'id de l'utilisateur ou null si il n'existe pas
*/ */
const getAuteurId = (author) => { const getAuteurId = (author) => {
return new Promise(async (resolve,reject) => { return new Promise(async (resolve,reject) => {
const sql = "SELECT id FROM reseaux_sociaux_auteurs WHERE nom_utilisateur = ? AND source_id = ? AND lien = ?;" const sql = "SELECT id FROM reseaux_sociaux_auteurs WHERE nom_utilisateur = ? AND source_id = ? AND lien = ?;"
let sourceId = await getSourceId(author.source); let sourceId = await getSourceId(author.source);
let query = conn.query(sql, [author.nom, sourceId, author.lien], (err, res) => { let query = conn.query(sql, [author.nom, sourceId, author.lien], (err, res) => {
if(err) { if(err) {
reject(new Error(err.message)) reject(new Error(err.message))
}else { }else {
if(res.length > 0) { if(res.length > 0) {
resolve(res[0].id); resolve(res[0].id);
}else { }else {
resolve(null); resolve(null);
} }
} }
}) })
}) })
} }
/** /**
* ajoute un auteur de commentaire sur les réseaux sociaux a la bdd * ajoute un auteur de commentaire sur les réseaux sociaux a la bdd
* @param {ReseauxAuteur} author * @param {ReseauxAuteur} author
* @returns Une Promise qui donne l'id de l'utilisateur * @returns Une Promise qui donne l'id de l'utilisateur
*/ */
const addAuteur = (author, sourceId) => { const addAuteur = (author, sourceId) => {
return new Promise((resolve,reject) => { return new Promise((resolve,reject) => {
const sql = "INSERT INTO reseaux_sociaux_auteurs (nom_utilisateur, source_id, lien) VALUES (?);" const sql = "INSERT INTO reseaux_sociaux_auteurs (nom_utilisateur, source_id, lien) VALUES (?);"
conn.query(sql, [[author.nom,sourceId, author.lien]], (err, res) => { conn.query(sql, [[author.nom,sourceId, author.lien]], (err, res) => {
if(err) { if(err) {
reject(new Error(err.message)) reject(new Error(err.message))
}else { }else {
resolve(res.insertId); resolve(res.insertId);
} }
}) })
}) })
} }
/** /**
* Ajoute un avis dans la BDD * Ajoute un avis dans la BDD
* @param {ReseauxReview} review L'avis a ajouter * @param {ReseauxReview} review L'avis a ajouter
* @param {Number} authorId ID de l'auteur dans la bdd * @param {Number} authorId ID de l'auteur dans la bdd
* @param {Number} sourceId ID Du réseau social source dans la bdd * @param {Number} sourceId ID Du réseau social source dans la bdd
* @returns une Promise qui renvoie l'ID de la review * @returns une Promise qui renvoie l'ID de la review
*/ */
const addReview = (review, authorId, sourceId) => { const addReview = (review, authorId, sourceId) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const sql = "INSERT INTO reseaux_sociaux_avis (source_id, note, commentaire, auteur_id, lien_source, date) VALUES (?);" const sql = "INSERT INTO reseaux_sociaux_avis (source_id, note, commentaire, auteur_id, lien_source, date) VALUES (?);"
conn.query(sql, [[sourceId, review.note, review.commentaire, authorId, review.lien, review.date]], (err, res) => { conn.query(sql, [[sourceId, review.note, review.commentaire, authorId, review.lien, review.date]], (err, res) => {
if(err) { if(err) {
reject(new Error(err.message)) reject(new Error(err.message))
}else { }else {
resolve(res.insertId); resolve(res.insertId);
} }
}) })
}) })
} }
/** /**
* Traite une requête POST pour ajouter un avis récupéré sur les réseaux sociaux * Traite une requête POST pour ajouter un avis récupéré sur les réseaux sociaux
* @param {*} req * @param {*} req
* @param {*} res * @param {*} res
*/ */
export const addSocialReviewFromRequest = async (req,res) => { export const addSocialReviewFromRequest = async (req,res) => {
try { try {
const author = new ReseauxAuteur(req.body.auteur_nom, req.body.source, req.body.auteur_lien); const author = new ReseauxAuteur(req.body.auteur_nom, req.body.source, req.body.auteur_lien);
const review = new ReseauxReview(author, req.body.source, req.body.date, req.body.note, req.body.commentaire, req.body.lien) const review = new ReseauxReview(author, req.body.source, req.body.date, req.body.note, req.body.commentaire, req.body.lien)
let sourceId = await getSourceId(review.source); let sourceId = await getSourceId(review.source);
let authorId = await getAuteurId(author); let authorId = await getAuteurId(author);
if(authorId == null) { if(authorId == null) {
authorId = await addAuteur(author, sourceId); authorId = await addAuteur(author, sourceId);
} }
await addReview(review, authorId, sourceId); await addReview(review, authorId, sourceId);
res.send("success") res.send("success")
}catch(err) { }catch(err) {
res.status(500).send("Error : " + err.message) res.status(500).send("Error : " + err.message)
} }
} }

View File

@@ -1,39 +1,39 @@
import { Review } from "../borne/structures.js"; import { Review } from "../borne/structures.js";
export class ReseauxReview extends Review{ export class ReseauxReview extends Review{
/** /**
* *
* @param {ReseauxAuteur} auteur Auteur de l'avis * @param {ReseauxAuteur} auteur Auteur de l'avis
* @param {String} source La source de l'avis * @param {String} source La source de l'avis
* @param {String} date La date de l'avis au format YYYY-MM-DD * @param {String} date La date de l'avis au format YYYY-MM-DD
* @param {Number} note Nombre entre 0 et 10, la note attribuée * @param {Number} note Nombre entre 0 et 10, la note attribuée
* @param {String} commentaire Le commentaire laissé par l'utilisateur * @param {String} commentaire Le commentaire laissé par l'utilisateur
* @param {String} lien Lien vers le commentaire * @param {String} lien Lien vers le commentaire
*/ */
constructor(auteur, source, date, note=null, commentaire=null, lien=null,) { constructor(auteur, source, date, note=null, commentaire=null, lien=null,) {
super(auteur,note,source,commentaire); super(auteur,note,source,commentaire);
this.lien = lien; this.lien = lien;
this.date = date; this.date = date;
if((typeof lien !== "string" && lien != null)) { if((typeof lien !== "string" && lien != null)) {
throw new Error("Lien invalide"); throw new Error("Lien invalide");
} }
} }
} }
export class ReseauxAuteur { export class ReseauxAuteur {
/** /**
* *
* @param {String} nom Nom de l'utilisateur * @param {String} nom Nom de l'utilisateur
* @param {String} source Réseau social de provenance de l'utilisateur * @param {String} source Réseau social de provenance de l'utilisateur
* @param {String} lien Lien vers le profil de l'utilisateur * @param {String} lien Lien vers le profil de l'utilisateur
*/ */
constructor(nom, source, lien=null) { constructor(nom, source, lien=null) {
this.nom = nom; this.nom = nom;
this.source = source; this.source = source;
this.lien = lien; this.lien = lien;
if((typeof this.nom !== "string") || (typeof this.source !== "string")){ if((typeof this.nom !== "string") || (typeof this.source !== "string")){
throw new Error("Auteur invalide"); throw new Error("Auteur invalide");
} }
} }
} }

View File

@@ -1,50 +1,54 @@
/* /*
Dans cette requête il faut replacer DAY_COUNT_DELAY par le nombre de jours sur lequel calculer les stats, STATS_GENERAL_TABLE_NAME par la table dans laquelle mettre les stats globales (par exemple stats_general_jour si les valeurs sont calculées sur 1 jour) et pareil pour STATS_AUTRES_TABLE_NAME Dans cette requête il faut replacer DAY_COUNT_DELAY par le nombre de jours sur lequel calculer les stats, STATS_GENERAL_TABLE_NAME par la table dans laquelle mettre les stats globales (par exemple stats_general_jour si les valeurs sont calculées sur 1 jour) et pareil pour STATS_AUTRES_TABLE_NAME
C'est un peu bizzare comme technique mais j'ai pas trouvé de meilleur solution C'est un peu bizzare comme technique mais j'ai pas trouvé de meilleur solution
*/ */
SET @date_limite = DATE_ADD(NOW(), INTERVAL -DAY_COUNT_DELAY DAY); SET @date_limite = DATE_ADD(NOW(), INTERVAL -DAY_COUNT_DELAY DAY);
/* /*
On récupère les notes notes moyennes sur la periode, en séparant global, borne et site On récupère les notes notes moyennes sur la periode, en séparant global, borne et site
*/ */
SELECT @moyenne_globale:=AVG(note_principale) SELECT @nb_avis:=COUNT(*)
FROM borne_avis FROM borne_avis
WHERE borne_avis.date > @date_limite; WHERE borne_avis.date > @date_limite;
SELECT @moyenne_borne:=AVG(note_principale) SELECT @moyenne_globale:=AVG(note_principale)
FROM borne_avis FROM borne_avis
JOIN sources ON sources.id = borne_avis.source_id WHERE borne_avis.date > @date_limite;
WHERE borne_avis.date > @date_limite AND sources.nom = "borne";
SELECT @moyenne_borne:=AVG(note_principale)
SELECT @moyenne_site:=AVG(note_principale) FROM borne_avis
FROM borne_avis JOIN sources ON sources.id = borne_avis.source_id
JOIN sources ON sources.id = borne_avis.source_id WHERE borne_avis.date > @date_limite AND sources.nom = "borne";
WHERE borne_avis.date > @date_limite AND sources.nom = "website";
SELECT @moyenne_site:=AVG(note_principale)
/* FROM borne_avis
On récupère la distribution de sexes JOIN sources ON sources.id = borne_avis.source_id
*/ WHERE borne_avis.date > @date_limite AND sources.nom = "website";
SELECT @stats_f:=COUNT(*) FROM borne_avis /*
JOIN borne_auteurs ON borne_avis.id_auteur = borne_auteurs.id On récupère la distribution de sexes
WHERE sexe='f' AND date > @date_limite; */
SELECT @stats_h:=COUNT(*) FROM borne_avis
JOIN borne_auteurs ON borne_avis.id_auteur = borne_auteurs.id SELECT @stats_f:=COUNT(*) FROM borne_avis
WHERE sexe='h' AND date > @date_limite; JOIN borne_auteurs ON borne_avis.id_auteur = borne_auteurs.id
SELECT @stats_a:=COUNT(*) FROM borne_avis WHERE sexe='f' AND date > @date_limite;
JOIN borne_auteurs ON borne_avis.id_auteur = borne_auteurs.id SELECT @stats_h:=COUNT(*) FROM borne_avis
WHERE sexe='a' AND date > @date_limite; JOIN borne_auteurs ON borne_avis.id_auteur = borne_auteurs.id
WHERE sexe='h' AND date > @date_limite;
SET @dist_sexe = CONCAT(@stats_f,",",@stats_h,",",@stats_a); SELECT @stats_a:=COUNT(*) FROM borne_avis
JOIN borne_auteurs ON borne_avis.id_auteur = borne_auteurs.id
INSERT INTO STATS_GENERAL_TABLE_NAME (moyenne_globale, moyenne_borne, moyenne_site, dist_sexe) VALUES (@moyenne_globale, @moyenne_borne, @moyenne_site, @dist_sexe); WHERE sexe='a' AND date > @date_limite;
INSERT INTO STATS_AUTRES_TABLE_NAME (critere_id, note) SET @dist_sexe = CONCAT(@stats_f,",",@stats_h,",",@stats_a);
SELECT critere_id, AVG(note) as moyenne FROM borne_notes_autre
WHERE borne_notes_autre.date > @date_limite INSERT INTO STATS_GENERAL_TABLE_NAME (moyenne_globale, nb_avis, moyenne_borne, moyenne_site, dist_sexe) VALUES (@moyenne_globale, @nb_avis, @moyenne_borne, @moyenne_site, @dist_sexe);
GROUP BY critere_id
INSERT INTO STATS_AUTRES_TABLE_NAME (critere_id, note)
/* SELECT critere_id, AVG(note) as moyenne FROM borne_notes_autre
TODO : Calcul de la distribution d'age WHERE borne_notes_autre.date > @date_limite
GROUP BY critere_id
/*
TODO : Calcul de la distribution d'age
*/ */

View File

@@ -1,69 +1,76 @@
import conn from '../database.js'; import conn from '../database.js';
import fs from "fs"; import fs from "fs";
import { CronJob } from 'cron'; import { CronJob } from 'cron';
/** /**
* Calcules les stats sur une periode donnée et les stocke dans la BDD * Calcules les stats sur une periode donnée et les stocke dans la BDD
* @param {Number} timePeriod Periode de temps sur laquelle calculer les données. Par exemple 7 si on veut faire les stats des 7 derniers jours * @param {Number} timePeriod Periode de temps sur laquelle calculer les données. Par exemple 7 si on veut faire les stats des 7 derniers jours
* @param {String} generalTableName Nom de la table dans laquelle mettre les statistiques générales (tables valides : stats_general_jour stats_general_semaine stats_general_mois stats_general_annee) * @param {String} generalTableName Nom de la table dans laquelle mettre les statistiques générales (tables valides : stats_general_jour stats_general_semaine stats_general_mois stats_general_annee)
* @param {*} otherTableName Nom de la table dans laquelle mettre les statistiques spécifiques (tables valides : stats_autres_jour stats_autres_autres_autres_mois stats_general_annee) * @param {*} otherTableName Nom de la table dans laquelle mettre les statistiques spécifiques (tables valides : stats_autres_jour stats_autres_autres_autres_mois stats_general_annee)
* @returns Une Promise qui résout si la requête a fonctionnée * @returns Une Promise qui résout si la requête a fonctionnée
*/ */
const computeStats = async (timePeriod, generalTableName, otherTableName) => { const computeStats = async (timePeriod, generalTableName, otherTableName) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let sql = fs.readFileSync("stats/update_request.sql").toString(); let sql = fs.readFileSync("stats/update_request.sql").toString();
sql = sql sql = sql
.replace(new RegExp("DAY_COUNT_DELAY",'g'), timePeriod.toString()) .replace(new RegExp("DAY_COUNT_DELAY",'g'), timePeriod.toString())
.replace(new RegExp("STATS_GENERAL_TABLE_NAME", 'g'), generalTableName) .replace(new RegExp("STATS_GENERAL_TABLE_NAME", 'g'), generalTableName)
.replace(new RegExp('STATS_AUTRES_TABLE_NAME', 'g'), otherTableName) .replace(new RegExp('STATS_AUTRES_TABLE_NAME', 'g'), otherTableName)
conn.query(sql, (err, res) => { conn.query(sql, (err, res) => {
if(err) { if(err) {
reject(err) reject(err)
}else { }else {
resolve(); resolve();
} }
}); });
}) })
} }
export const startCronJobs = () => { export const startCronJobs = () => {
//Update les stats journalières tout les jours a minuit //Update les stats journalières tout les jours a minuit
new CronJob( new CronJob(
'0 0 * * * * ', '0 0 * * * * ',
() => { () => {
computeStats(1,"stats_general_jour","stats_autres_jour"); computeStats(1,"stats_general_jour","stats_autres_jour");
}, },
null, null,
true true
) )
//update les stats de la semaine tous les lundis a minuit //update les stats de la semaine tous les lundis a minuit
new CronJob( new CronJob(
'0 0 * * 1 * ', '0 0 * * 1 * ',
() => { () => {
computeStats(7,"stats_general_semaine","stats_autres_jour"); computeStats(7,"stats_general_semaine","stats_autres_jour");
}, },
null, null,
true true
) )
//update les stats mensuelles les 1er du mois a minuit //update les stats mensuelles les 1er du mois a minuit
new CronJob( new CronJob(
'0 0 1 * * * ', '0 0 1 * * * ',
() => { () => {
computeStats(30, "stats_general_mois","stats_autres_mois"); computeStats(30, "stats_general_mois","stats_autres_mois");
}, },
null, null,
true true
) )
//update les stats annuelles les premire de l'an a minuit //update les stats annuelles les premire de l'an a minuit
new CronJob( new CronJob(
'0 0 1 1 * * ', '0 0 1 1 * * ',
() => { () => {
computeStats(365, "stats_general_annee","stats_autres_annee"); computeStats(365, "stats_general_annee","stats_autres_annee");
}, },
null, null,
true true
) )
console.log("All cronjobs initiated") console.log("All cronjobs initiated")
}
export function manualUpdateStats() {
computeStats(1, "stats_general_jour", "stats_autres_jour");
computeStats(7, "stats_general_semaine", "stats_autres_jour");
computeStats(30, "stats_general_mois", "stats_autres_mois");
computeStats(365, "stats_general_annee", "stats_autres_annee");
} }

View File

@@ -1,19 +1,19 @@
import conn from './database.js'; import conn from './database.js';
/** /**
* Renvoie l'ID dans la BDD d'une source de donnée * Renvoie l'ID dans la BDD d'une source de donnée
* @param {String} source la source dont on veut récup l'id * @param {String} source la source dont on veut récup l'id
* @returns une promise qui renvoie l'id de la source * @returns une promise qui renvoie l'id de la source
*/ */
export const getSourceId = (source) => { export const getSourceId = (source) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const sql = "SELECT id from sources WHERE nom = ?"; const sql = "SELECT id from sources WHERE nom = ?";
conn.query(sql, [source], (err, res) => { conn.query(sql, [source], (err, res) => {
if(res.length == 0) { if(res.length == 0) {
reject(new Error("Invalid source")) reject(new Error("Invalid source"))
}else { }else {
resolve(res[0].id); resolve(res[0].id);
} }
}) })
}) })
} }

View File

@@ -1,29 +0,0 @@
import requests
#Exemple ajout d'un commentaire depuis la borne (site ou geste)
avis = {
"note": 8,
"source": "borne",
#Optionel
"auteur_age": 20,
"notes_autre": '{"proprete":8,"calme":10}',
"auteur_sexe": 'f',
"commentaire": "Commentaire"
}
# res = requests.post("http://localhost:8080/add_review", data=avis)
# print(res.text)
#Exemple ajout d'un commentaire trouvé sur les réseaux sociaux
avis = {
"auteur_nom": "michel",
"source": "instagram",
"note": 8,
"date": "2022-12-24",
#Optionel
"commentaire": "J'ai beaucoup aimé !",
"lien": "https://instagram.com/si_insta_avait_des_liens_vers_des_commentaires_faudrait_le_mettre_ici",
"auteur_lien": "https://instagram.com/michel",
}
res = requests.post("http://localhost:8080/add_social_review", data=avis)
print(res.text)

2
code/setup.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
sudo modprobe v4l2loopback devices=2

View File

@@ -0,0 +1,3 @@
FROM alpine:latest
RUN apk add --no-cache ffmpeg
CMD ["ffmpeg","-video_size","640x480","-f","video4linux2","-i","/dev/video0","-codec","copy","-f","v4l2","/dev/video1","-codec","copy","-f","v4l2","/dev/video2", "-loglevel","debug"]

3
docs/charte_graphique.md Normal file
View File

@@ -0,0 +1,3 @@
# Codes hex couleurs
#6B8000
#A6CC00

12
docs/dupliquer_camera.md Normal file
View File

@@ -0,0 +1,12 @@
# Methode pour accéder au flux video a la fois depuis firefox et depuis opencv
* installer v4l2loopback :
* Télécharger : `git clone https://github.com/umlaeute/v4l2loopback.git`
* Installer avec : `make` puis `sudo make install`
* activer le module : `sudo modprobe v4l2loopback devices=2`
* Erreur possible : opperation not permitted : il faut désactiver secure boot
* OU `apt update && apt install v4l2loopback-dkms v4l2loopback-utils`
* [Innutile si container video_loopback present] Faire looper la camera /dev/video0 sur les autres
* installer ffmpeg : `sudo apt get install ffmpeg`
* activer le loopback : `ffmpeg -video_size 640x480 -f video4linux2 -i /dev/video0 -codec copy -f v4l2 /dev/video1 -codec copy -f v4l2 /dev/video2`
Maintenant on peut par exemple utiliser /dev/video2 sur firefox et /dev/video13sur opencv sans que cela ne pose de problème

16
docs/liste_pages_web.md Normal file
View File

@@ -0,0 +1,16 @@
Liste des pages web à concevoir dans tout le projet
* Interface de la borne
* Interface avec retour vidéo et effets quand un gest est détécté
* page de remerciment, avec indication que la personne peut parler pour développer son avis
* Page de remerciment
* Formulaire de retour d'avis
* Formulaire
* Page de remerciement
* Interface admin
* Page principale avec les stats principales
* Page liste des avis récents
* page stats détaillées sur la borne ou on peut changer l'intervalle de calcul des stats
* Page avec les derniers avis récup sur les réseaux sociaux
* Page avec les stats récup sur les réseaux sociaux

View File

@@ -0,0 +1,6 @@
Truc a corriger sur le rapport
* XRemplacer les screen par des vrai graphiques
* XAjouter des standard deviation aux graphiques
* XPeut être ajouter des valeurs absolues sur les benchmark
* X Regarder si utiliser un power saving mode sur le CPU est intéressant (température vs perofrmances)
* X Dire que c'est pas du temps mais que ça nous convient

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -0,0 +1,5 @@
FROM alpine:latest
RUN apk add --no-cache sysbench
WORKDIR /app
COPY benchmark_script.sh /app/benchmark_script.sh
CMD ["sh","benchmark_script.sh"]

View File

@@ -0,0 +1,5 @@
sysbench --test=cpu run >>sysbench.log
sysbench --test=memory run >>sysbench.log
sysbench --test=fileio --file-test-mode=rndrw prepare
sysbench --test=fileio --file-test-mode=rndrw run >>sysbench.log
sysbench --test=fileio cleanup

View File

@@ -0,0 +1,114 @@
sysbench 1.0.20 (using system LuaJIT 2.1.0-beta3)
Running the test with following options:
Number of threads: 1
Initializing random number generator from current time
Prime numbers limit: 10000
Initializing worker threads...
Threads started!
CPU speed:
events per second: 613.74
General statistics:
total time: 10.0013s
total number of events: 6140
Latency (ms):
min: 1.62
avg: 1.63
max: 2.10
95th percentile: 1.64
sum: 9998.37
Threads fairness:
events (avg/stddev): 6140.0000/0.00
execution time (avg/stddev): 9.9984/0.00
sysbench 1.0.20 (using system LuaJIT 2.1.0-beta3)
Running the test with following options:
Number of threads: 1
Initializing random number generator from current time
Running memory speed test with the following options:
block size: 1KiB
total size: 102400MiB
operation: write
scope: global
Initializing worker threads...
Threads started!
Total operations: 33390517 (3338076.27 per second)
32607.93 MiB transferred (3259.84 MiB/sec)
General statistics:
total time: 10.0000s
total number of events: 33390517
Latency (ms):
min: 0.00
avg: 0.00
max: 0.10
95th percentile: 0.00
sum: 4949.30
Threads fairness:
events (avg/stddev): 33390517.0000/0.00
execution time (avg/stddev): 4.9493/0.00
sysbench 1.0.20 (using system LuaJIT 2.1.0-beta3)
Running the test with following options:
Number of threads: 1
Initializing random number generator from current time
Extra file open flags: (none)
128 files, 16MiB each
2GiB total file size
Block size 16KiB
Number of IO requests: 0
Read/Write ratio for combined random IO test: 1.50
Periodic FSYNC enabled, calling fsync() each 100 requests.
Calling fsync() at the end of test, Enabled.
Using synchronous I/O mode
Doing random r/w test
Initializing worker threads...
Threads started!
File operations:
reads/s: 789.46
writes/s: 526.31
fsyncs/s: 1691.85
Throughput:
read, MiB/s: 12.34
written, MiB/s: 8.22
General statistics:
total time: 10.0292s
total number of events: 30045
Latency (ms):
min: 0.00
avg: 0.33
max: 17.49
95th percentile: 2.48
sum: 9966.50
Threads fairness:
events (avg/stddev): 30045.0000/0.00
execution time (avg/stddev): 9.9665/0.00

View File

@@ -0,0 +1,114 @@
sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)
Running the test with following options:
Number of threads: 1
Initializing random number generator from current time
Prime numbers limit: 10000
Initializing worker threads...
Threads started!
CPU speed:
events per second: 552.76
General statistics:
total time: 10.0015s
total number of events: 5530
Latency (ms):
min: 1.79
avg: 1.81
max: 2.34
95th percentile: 1.82
sum: 9999.03
Threads fairness:
events (avg/stddev): 5530.0000/0.00
execution time (avg/stddev): 9.9990/0.00
sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)
Running the test with following options:
Number of threads: 1
Initializing random number generator from current time
Running memory speed test with the following options:
block size: 1KiB
total size: 102400MiB
operation: write
scope: global
Initializing worker threads...
Threads started!
Total operations: 28620880 (2861268.15 per second)
27950.08 MiB transferred (2794.21 MiB/sec)
General statistics:
total time: 10.0001s
total number of events: 28620880
Latency (ms):
min: 0.00
avg: 0.00
max: 0.10
95th percentile: 0.00
sum: 5501.03
Threads fairness:
events (avg/stddev): 28620880.0000/0.00
execution time (avg/stddev): 5.5010/0.00
sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)
Running the test with following options:
Number of threads: 1
Initializing random number generator from current time
Extra file open flags: (none)
128 files, 16MiB each
2GiB total file size
Block size 16KiB
Number of IO requests: 0
Read/Write ratio for combined random IO test: 1.50
Periodic FSYNC enabled, calling fsync() each 100 requests.
Calling fsync() at the end of test, Enabled.
Using synchronous I/O mode
Doing random r/w test
Initializing worker threads...
Threads started!
File operations:
reads/s: 785.13
writes/s: 523.42
fsyncs/s: 1687.54
Throughput:
read, MiB/s: 12.27
written, MiB/s: 8.18
General statistics:
total time: 10.0083s
total number of events: 29866
Latency (ms):
min: 0.00
avg: 0.33
max: 18.46
95th percentile: 2.48
sum: 9963.06
Threads fairness:
events (avg/stddev): 29866.0000/0.00
execution time (avg/stddev): 9.9631/0.00

View File

@@ -0,0 +1,8 @@
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4ff5ebc88a1f code-backend_reconaissance "python main.py" 3 hours ago Up 2 hours 0.0.0.0:5000->5000/tcp, :::5000->5000/tcp backend_reconaissance
02e8569d1926 code-video_loopback "ffmpeg -video_size …" 3 hours ago Up 2 hours video_loopback
d50295f99ae5 phpmyadmin:latest "/docker-entrypoint.…" 4 hours ago Up 2 hours 0.0.0.0:8000->80/tcp, :::8000->80/tcp phpmyadmin
a5efc4ddae1b code-reviews_api "docker-entrypoint.s…" 4 hours ago Up 2 hours 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp reviews_api
830786df4f5f httpd:latest "httpd-foreground" 4 hours ago Up 2 hours 0.0.0.0:8888->80/tcp, :::8888->80/tcp interface_borne
2fd04a8fe768 mysql:latest "docker-entrypoint.s…" 4 hours ago Up 2 hours (healthy) 3306/tcp, 33060/tcp db
9999c72bb59f httpd:latest "httpd-foreground" 4 hours ago Up 2 hours 0.0.0.0:80->80/tcp, :::80->80/tcp interface_admin

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,111 @@
# Rapport PAN2
## Choix du materiel
* **Ecran** (InnoLux N133HSE): Pour l'écran nous avons choisi un écran 13.3" (non tactile), nous avons estimé la taille suffisante pour un confort de lecture à distance raisonable de la borne. De plus cet écran présente un système de montage à vis qui facilitera son intégration dans la borne.
* **Camera** : Nous avions le choix entre 2 camera
* Une webcam Logitech c525
* Grand choix de résolutions avec un framerate élevé par exemple 640x480@30fps, 960x720@30fps, 1920x1080@5fps
* Mais champs de vision beaucoup trop faible pour notre application
* Une camera de surveillance grand angle de la marque ELP
* Choix de résoltions restreint, mais utilisable dans des bonnes conditions de luminosité
<img src="img/formats_camera.png" height=250>
* Champs de vision adapté a notre utilisation
* Cette camera apporte des distortions importantes sur les bords, cela compléxifie la reconnaissance d'image.
* Nous avons choisi la deuxième camera pour les raisons citées ci-dessus
* **Ordinateur** :
Nous avons à notre disposition un AMD Cubi doté de 4Go de RAM, 128Go de SSD et un Intel Core i3 5005U. L'objectif du reste de ce document est d'évaluer si les performances de cet ordinateur sont suffisantes pour notre application.
## Choix du système d'exploitation
Nous avons choisi d'installer debian 11, une distribution de linux légère avec laquelle nous étions familiers. Pour l'environement de bureau nous avons choisi LXDE là aussi pour le minimalisme qu'il présente au vu des performances de la machine. Nous avons par la suite désinstallé tout les packets installé par défaut par LXDE dont nous n'avions pas besoin. (Par exemple les jeux, la calculatrice, etc.)
## Benchmarking
Pour évaluer la capacité du materiel à supporter la charge de notre application, nous avons executé en parallèle les applications qui seront utilisés pour le produit final ou une application aux besoins équivalents quand cela n'était pas possible. De ce fait ce benchmark n'est pas exact mais nous donnera un ordre d'idée sur les besoins de notre projet.
### Liste des modules à executer
Pour ce faire nous avons mis en place un container Docker par module de notre application :
* **db** : un serveur mysql basique pour la base de donnée
* **phpmyadmin** : l'interface phpmyadmin pour gérer la base de donnée. Ce container n'est pas absolument nécessaire au produit final mais permet d'administrer facilement la base de donnée
* **review_api** : un serveur express pour servir l'api permettant de récupérer et ajouter des avis dans la base de donnée, ainsi que de calculer les statistiques.
* **interface_borne**: un serveur apache2 permettant de servir l'interface graphique de la borne.
* **interface_admin**: idem pour l'interface graphique, ce serveur pourait être fusioné avec
**interface_borne** mais dans le cadre de ce benchmarking on les gardera séparés.
* **backend_reconnaissance**: Ce container s'occupera de la reconnaissance audio et video de la borne. Cependant ces deux processus ne seront jamais actifs en même temps, de plus la reconnaissance d'image sera la plus couteuse en terme de puissance de calcul. C'est pour cela qu'ici nous avons uniquement utilisé mediapipe hands (bibliothèque python de reconaissance de mains) avec une implémentation de la communication avec l'interface de la borne comme programme équivalent.
* **video_loopback** : ce container sert à contourner un problème que nous avons rencontré avec la gestion des camera par linux. En effet seul un programme peut accéder au flux d'une camera à la fois. C'est pour cela que nous avons utilisé `v4l2loopback` avec `ffmpeg` pour dupliquer le flux de notre camera dans 2 cameras virtuelles.
En parallèle firefox est ouvert pour afficher l'interface graphique de la borne.
### Résultats
En réglant la camera a 640x480@30fps, aucune perte d'image n'est observée dans le retour vidéo dans firefox.
Nous avons observé la capacité de Mediapipe l'image de la camera et de la communiquer à l'interface web qui affiche le résultat.
Nous avons obtenu les résultats suivants pour 2 modes de gestion de fréquence du processeur (la gestions automatique de base schedutil et performance qui utillise la fréquence maximale)
| CPU Scheduler | FPS moyens | Ecart type |
|--------------|------------|------------|
| schedutil |10.4 | 1.4 |
| performance | 10.1 | 0.9 |
Ce taux de rafraichissement est suffisant pour que l'application paraisse relativement réactive à l'utilisateur, bien que cela ne soit pas du temps réel.
Pendant ce temps l'api de traitement des avis et la base de donnée fonctionnent correctement en affichant une lattence de 8ms pour une récupération de la liste d'avis.
Pendance ce temps l'utilisation du processeur qui varie de 250% à 280% (sur 400% pour les 4 coeurs) et une utilisation de la RAM de 50% (1.96Go) ce qui nous laisse de la marge en cas d'ajout imprévu.
Les processus utilisant le plus de CPU sont la reconaissance d'image (70%) et firefox pour afficher l'interface de la borne (70-80%). En cas de besoin ces valeurs pourront être diminuées au prix de la fluidité du retour vidéo.
Pour la RAM c'est le serveur mysql (10%) et firefox (10%) qui consomment le plus.
Pour ce qui est de la température, comme la borne sera dans un environement fermé, il était important de tester le bon fonctionnement du materiel dans ces conditions. Nous avons laissé tourner l'application pendant 2h dans une boite en carton fermée. Au début du test la température du processeur était de 50°C, au bout de 2h la température était montée a 70°C, ce qui reste assez faible pour ne pas limiter les performances du CPU.
```
$ sensors
acpitz-acpi-0
Adapter: ACPI interface
temp1: +27.8°C (crit = +110.0°C)
temp2: +29.8°C (crit = +110.0°C)
coretemp-isa-0000
Adapter: ISA adapter
Package id 0: +69.0°C (high = +105.0°C, crit = +105.0°C)
Core 0: +69.0°C (high = +105.0°C, crit = +105.0°C)
Core 1: +68.0°C (high = +105.0°C, crit = +105.0°C)
```
<div style="page-break-after: always;"></div>
## Impact de l'utilisation de Docker
Nous nous sommes également posé la question de l'impact de l'utilisation de docker dans les performances de notre projet. Pour mesurer cela, nous avons effectué des benchmark directement sur le système puis dans un container Docker pour mesurer la différence. Nous avons utlisé sysbench pour évaluer les performances du CPU, de la RAM et du disque (écriture/lecture aléatoire).
Le script permettant de faire le benchmark
```sh
sysbench --test=cpu run >>sysbench.log
sysbench --test=memory run >>sysbench.log
sysbench --test=fileio --file-test-mode=rndrw prepare
sysbench --test=fileio --file-test-mode=rndrw run >>sysbench.log
sysbench --test=fileio cleanup
```
Le Dockerfile du container dans lequel nous avons exectué le même script
```Dockerfile
FROM alpine:latest
RUN apk add --no-cache sysbench
WORKDIR /app
COPY benchmark_script.sh /app/benchmark_script.sh
CMD ["sh","benchmark_script.sh"]
```
Les résultats de ce test on permis de conclure que l'impact de docker était négligeable.
<img src="img/benchmark.png" width=400>
| Type | CPU (Evts/s) | RAM (Mbi/s) | Disque lecture(Mbi/s) | Disque écriture (Mbi/s) |
|--------------|------------|------------|---|---|
| Normal | 613.14 | 3259.84 | 12.34 | 8.22 |
| Docker | 552.76 | 2794.21 |12.27 | 8.18 |
## Conclusion
Au vu des tests effectués le materiel dont nous disposons semble adapté à notre projet. Il serait cependant possible de réduire l'utilisation faite de l'ordinateur embarqué dans la borne en déplaçant la partie stockage et traitement des avis sur un autre serveur.

Binary file not shown.

View File

@@ -0,0 +1,11 @@
acpitz-acpi-0
Adapter: ACPI interface
temp1: +27.8°C (crit = +110.0°C)
temp2: +29.8°C (crit = +110.0°C)
coretemp-isa-0000
Adapter: ISA adapter
Package id 0: +69.0°C (high = +105.0°C, crit = +105.0°C)
Core 0: +69.0°C (high = +105.0°C, crit = +105.0°C)
Core 1: +68.0°C (high = +105.0°C, crit = +105.0°C)

Some files were not shown because too many files have changed in this diff Show More