Created
September 6, 2025 08:30
-
-
Save pgmrDohan/24f7db721ea86267741d663eb7862d7a to your computer and use it in GitHub Desktop.
안동대학교 SW 캠프
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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