Skip to content

Instantly share code, notes, and snippets.

@pgmrDohan
Created September 6, 2025 08:30
Show Gist options
  • Select an option

  • Save pgmrDohan/24f7db721ea86267741d663eb7862d7a to your computer and use it in GitHub Desktop.

Select an option

Save pgmrDohan/24f7db721ea86267741d663eb7862d7a to your computer and use it in GitHub Desktop.
안동대학교 SW 캠프
<!DOCTYPE html>
<html lang="kr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>플레이리스트 추천기</title>
<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=Jua&display=swap" rel="stylesheet">
<style>
body {
font-family: "Jua", sans-serif;
background-color: #f0f0f0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
color: #333;
}
h1 {
font-weight: 400;
font-size: 60px;
margin-bottom: 20px;
text-align: center;
}
#video {
border: 1px solid #ccc;
border-radius: 10px;
margin-bottom: 20px;
}
#canvas {
display: none;
}
img {
border: 1px solid #ccc;
border-radius: 10px;
margin: 20px 0;
max-width: 640px;
max-height: 480px;
}
button {
padding: 10px 20px;
border: none;
border-radius: 5px;
background-color: #007BFF;
color: white;
font-size: 18px;
cursor: pointer;
transition: background-color 0.3s, transform 0.3s;
margin-top: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
button:hover {
background-color: #0056b3;
transform: translateY(-2px);
}
button:active {
transform: translateY(1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
#upload {
display: none; /* 초기 상태에서 숨김 */
}
</style>
</head>
<body>
<h1>플레이리스트 추천기</h1>
<video id="video" width="640" height="480" autoplay></video>
<canvas id="canvas" width="640" height="480"></canvas>
<img id="photo" style="display: none;" />
<button id="capture">사진 찍기</button>
<button id="upload" style="display: none;">업로드</button>
<form id="uploadForm" action="/upload-images/" method="post" enctype="multipart/form-data" style="display: none;">
<input type="hidden" name="in_files" id="hiddenInput">
</form>
<script>
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
const captureButton = document.getElementById('capture');
const uploadButton = document.getElementById('upload');
const photo = document.getElementById('photo');
let stream;
navigator.mediaDevices.getUserMedia({ video: true })
.then(s => {
stream = s;
video.srcObject = stream;
})
.catch(err => {
console.error("웹캠을 사용할 수 없습니다: ", err);
});
captureButton.addEventListener('click', () => {
context.drawImage(video, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => {
const file = new File([blob], 'capture.png', { type: 'image/png' });
const formData = new FormData();
formData.append('in_files', file);
// 업로드 버튼 보이기
uploadButton.style.display = 'inline';
stream.getTracks().forEach(track => track.stop());
video.style.display = 'none'; // 비디오 요소 숨기기
photo.src = URL.createObjectURL(blob); // 캡처한 이미지를 img 요소에 표시
photo.style.display = 'block'; // img 요소 보이기
// 업로드 버튼 클릭 시 폼 전송
uploadButton.addEventListener('click', () => {
fetch('/upload-images/', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(result => {
if (result.url) {
window.location.href = result.url;
} else {
console.error('응답에 url이 없습니다:', result);
}
})
.catch(error => {
console.error('업로드 실패:', error);
});
});
});
});
</script>
</body>
</html>
from typing import List
import os
import random
import datetime
import secrets
import json
import cv2
import numpy as np
from fastapi import FastAPI, File, UploadFile, Request, status
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from keras.models import load_model
import trafilatura
from bs4 import BeautifulSoup
app = FastAPI()
# 이미지 및 템플릿 디렉토리 설정
IMG_DIR = './photo/'
templates = Jinja2Templates(directory="templates")
# 얼굴 인식을 위한 Haar Cascade 로드
face_cascade = cv2.CascadeClassifier('./haarcascade_frontalface_default.xml')
# 표정 레이블과 모델 로드
expression_labels = ['Angry', 'Disgust', 'Fear', 'Happy', 'Sad', 'Surprise', 'Neutral']
model = load_model('./emotion_model.hdf5')
# 이미지의 정적 디렉토리 마운트
app.mount("/images", StaticFiles(directory=IMG_DIR), name="photo")
@app.get("/", response_class=HTMLResponse)
async def read_item(request: Request):
# 기본 페이지 렌더링
return templates.TemplateResponse(name="index.html", context={"request": request})
@app.post('/upload-images')
async def upload_images(in_files: List[UploadFile] = File(...)):
# 업로드된 파일 처리
for file in in_files:
current_time = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
saved_file_name = f"{current_time}_{secrets.token_hex(8)}.png" # 파일 확장자 추가
file_location = os.path.join(IMG_DIR, saved_file_name)
with open(file_location, "wb+") as file_object:
file_object.write(file.file.read())
# 저장된 이미지 경로로 AI 처리로 리다이렉션
return JSONResponse(content={"url": f"/ai?img={file_location}"}, status_code=status.HTTP_200_OK)
@app.get("/ai")
async def analyze_image(request: Request, img: str):
# 업로드된 이미지 읽기 및 처리
image = cv2.imread(img)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 이미지에서 얼굴 감지
faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))
# 감지된 각 얼굴에 대해 감정 분석
expression_label = "Neutral" # 기본값 설정
for (x, y, w, h) in faces:
face_roi = gray[y:y+h, x:x+w]
face_roi = cv2.resize(face_roi, (64, 64)) / 255.0 # 크기 조정 및 정규화
face_roi = np.expand_dims(face_roi, axis=(0, -1)) # 배치 및 채널 차원 추가
# 감정 예측
output = model.predict(face_roi)[0]
expression_label = expression_labels[np.argmax(output)]
# 감지된 감정에 따라 음악 추천 가져오기
res = trafilatura.fetch_url(f'https://8tracks.com/explore/{expression_label.lower()}/hot')
soup = BeautifulSoup(res, "html.parser")
titles = soup.find_all('div', attrs={'class': 'mix_square'})
# 음악 믹스 링크 수집
links = [title.find('a', class_='mix_url')['href'] for title in titles]
# 랜덤으로 선택된 음악 믹스 URL로 리다이렉션
music_mix_url = f"https://8tracks.com{random.choice(links)}"
# 이미지 파일 삭제
os.remove(img)
return RedirectResponse(url=music_mix_url)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment