Skip to content

Commit 78b435c

Browse files
committed
feat : 개선함
1 parent b71744f commit 78b435c

File tree

5 files changed

+168
-43
lines changed

5 files changed

+168
-43
lines changed

app/diary/models.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,8 @@ class Diary(Base):
2121

2222
user = relationship("User", back_populates="diaries")
2323
emotion = relationship(EmotionType, back_populates="diaries")
24-
25-
recommended_songs = relationship(
26-
"RecommendedSong",
27-
back_populates="diary",
28-
cascade="all, delete-orphan",
29-
foreign_keys="[RecommendedSong.diary_id]"
30-
)
31-
24+
recommended_songs = relationship("RecommendedSong", back_populates="diary", cascade="all, delete-orphan", foreign_keys="[RecommendedSong.diary_id]")
25+
emotion_tags = relationship("DiaryEmotionTag", back_populates="diary", cascade="all, delete-orphan")
3226

3327
class diaryEmbedding(Base):
3428
__tablename__ = "diaryEmbedding"
@@ -50,6 +44,7 @@ class RecommendedSong(Base):
5044
album_image = Column(String(512))
5145
best_lyric = Column(Text)
5246
similarity_score = Column(Float)
47+
youtube_url = Column(String(512), nullable=True)
5348
created_at = Column(DateTime, default=datetime.utcnow)
5449

5550
# ✅ 어떤 FK를 기준으로 연결할지 명시

app/diary/router.py

Lines changed: 125 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import os
2+
3+
import requests
14
from fastapi import APIRouter, Depends, HTTPException
25
from sqlalchemy.orm import Session
36
from sqlalchemy import extract, text, func
47
from app.database import get_db, get_mongodb, get_redis
5-
from app.emotion.models import model_index_to_db_emotion_id
8+
from app.emotion.models import model_index_to_db_emotion_id, DiaryEmotionTag
69
from app.emotion.router import predict_emotion
710
from app.statistics.models import EmotionStatistics
811
from app.user.auth import get_current_user
@@ -11,7 +14,7 @@
1114
DiaryPreviewResponse, RecommendSongResponse
1215
from app.user.models import User
1316
from app.embedding.models import kobert, save_diary_embedding, split_sentences, get_user_preferred_genres, \
14-
get_songs_by_genre, get_song_embeddings, calculate_similarity
17+
get_songs_by_genre
1518
from app.transaction import transactional_session
1619
from typing import List, Set
1720
import logging
@@ -22,6 +25,7 @@
2225
from datetime import datetime
2326

2427
router = APIRouter()
28+
YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY")
2529

2630
logging.basicConfig(level=logging.INFO)
2731
logger = logging.getLogger(__name__)
@@ -554,7 +558,7 @@ async def create_diary_with_emotion_based_recommendation(
554558
1: ["R&B/Soul", "댄스"],
555559
2: ["인디음악", "R&B/Soul"],
556560
3: ["R&B/Soul", "인디음악"],
557-
4: ["록/메탈", "인디음악"],
561+
4: ["R&B/Soul", "인디음악"],
558562
5: ["발라드", "록/메탈"],
559563
6: ["발라드", "R&B/Soul"],
560564
7: ["랩/힙합", "록/메탈"]
@@ -638,7 +642,7 @@ async def create_diary_with_emotion_based_recommendation(
638642
}
639643
))
640644

641-
top_3_raw = heapq.nlargest(10, heap, key=lambda x: (x[0], x[1]))
645+
top_3_raw = heapq.nlargest(7, heap, key=lambda x: (x[0], x[1]))
642646

643647
recent_lyrics = get_recently_recommended_lyrics(session, user_id=current_user.id)
644648

@@ -678,20 +682,43 @@ async def create_diary_with_emotion_based_recommendation(
678682
session.commit()
679683
session.refresh(new_diary)
680684

685+
for eid, score in sorted(emotion_vote_counter.items(), key=lambda x: -x[1])[:3]:
686+
avg_score = score / len(sentences)
687+
if avg_score >= 0.15:
688+
tag = DiaryEmotionTag(
689+
diary_id=new_diary.id,
690+
emotiontype_id=model_index_to_db_emotion_id[eid],
691+
score=round(avg_score, 4)
692+
)
693+
session.add(tag)
694+
681695
save_diary_embedding(session, new_diary.id, combined_embedding)
682696

683-
recommended_songs = [
684-
{
685-
"song_id": match["song_id"],
686-
"song_name": match["metadata"]["song_name"],
687-
"best_lyric": " ".join(match["lyric_chunk"]),
688-
"similarity_score": round(float(sim), 4),
689-
"album_image": match["metadata"]["album_image"],
690-
"artist": match["metadata"]["artist"],
691-
"genre": match["metadata"]["genre"]
692-
}
693-
for sim, match in top_3
694-
]
697+
recommended_songs = []
698+
for sim, match in top_3:
699+
song_data = RecommendedSong(
700+
diary_id=new_diary.id,
701+
song_id=match["song_id"],
702+
song_name=match["metadata"]["song_name"],
703+
artist=match["metadata"]["artist"],
704+
genre=match["metadata"]["genre"],
705+
album_image=match["metadata"]["album_image"],
706+
best_lyric=" ".join(match["lyric_chunk"]),
707+
similarity_score=round(float(sim), 4)
708+
)
709+
session.add(song_data)
710+
session.flush() # ✅ id 부여를 위해 flush
711+
712+
recommended_songs.append({
713+
"id": song_data.id, # ✅ 여기에 id 포함
714+
"song_id": song_data.song_id,
715+
"song_name": song_data.song_name,
716+
"artist": song_data.artist,
717+
"genre": song_data.genre,
718+
"album_image": song_data.album_image,
719+
"best_lyric": song_data.best_lyric,
720+
"similarity_score": song_data.similarity_score,
721+
})
695722

696723
for song_data in recommended_songs:
697724
new_song = RecommendedSong(
@@ -714,6 +741,7 @@ async def create_diary_with_emotion_based_recommendation(
714741
"content": new_diary.content,
715742
"emotiontype_id": emotion_id_db,
716743
"confidence": confidence_full,
744+
"best_sentence": new_diary.best_sentence,
717745
"created_at": new_diary.created_at,
718746
"updated_at": new_diary.updated_at,
719747
"recommended_songs": recommended_songs,
@@ -745,6 +773,8 @@ def get_diary(
745773
RecommendedSong.diary_id == diary.id
746774
).order_by(RecommendedSong.similarity_score.desc()).all()
747775

776+
emotion_tags = db.query(DiaryEmotionTag).filter(DiaryEmotionTag.diary_id == diary.id).all()
777+
748778
main_song = db.query(RecommendedSong).get(diary.main_recommended_song_id)
749779

750780
return DiaryResponse(
@@ -753,9 +783,11 @@ def get_diary(
753783
content=diary.content,
754784
emotiontype_id=diary.emotiontype_id,
755785
confidence=diary.confidence,
786+
best_sentence=diary.best_sentence,
756787
created_at=diary.created_at,
757788
updated_at=diary.updated_at,
758789
recommended_songs=recommended_songs,
790+
emotion_tags=emotion_tags,
759791
main_recommend_song=main_song,
760792
top_emotions=[]
761793
)
@@ -808,6 +840,7 @@ def get_all_diaries(
808840
content=diary.content,
809841
emotiontype_id=diary.emotiontype_id,
810842
confidence=diary.confidence,
843+
best_sentence=diary.best_sentence,
811844
created_at=diary.created_at,
812845
updated_at=diary.updated_at,
813846
recommended_songs=recommended_songs,
@@ -916,17 +949,91 @@ async def set_main_song(
916949
current_user = Depends(get_current_user),
917950
db: Session = Depends(get_db)
918951
):
919-
diary = db.query(Diary).filter(Diary.id == diary_id, Diary.user_id == current_user.id).first()
952+
diary = db.query(Diary).filter(
953+
Diary.id == diary_id,
954+
Diary.user_id == current_user.id
955+
).first()
956+
920957
if not diary:
921958
raise HTTPException(status_code=404, detail="일기를 찾을 수 없습니다.")
922959

923960
song = db.query(RecommendedSong).filter(
924961
RecommendedSong.id == recommended_song_id,
925962
RecommendedSong.diary_id == diary.id
926963
).first()
964+
927965
if not song:
928966
raise HTTPException(status_code=400, detail="추천곡이 일기와 일치하지 않습니다.")
929967

968+
if not song.youtube_url:
969+
query = f"{song.song_name} {' '.join(song.artist)}"
970+
api_url = "https://www.googleapis.com/youtube/v3/search"
971+
params = {
972+
"part": "snippet",
973+
"q": query,
974+
"type": "video",
975+
"maxResults": 1,
976+
"key": YOUTUBE_API_KEY
977+
}
978+
979+
res = requests.get(api_url, params=params)
980+
data = res.json()
981+
982+
if not data.get("items"):
983+
raise HTTPException(status_code=404, detail="YouTube 영상이 없습니다.")
984+
985+
video_id = data["items"][0]["id"]["videoId"]
986+
song.youtube_url = f"https://www.youtube.com/watch?v={video_id}"
987+
988+
# 🎯 대표곡 저장
930989
diary.main_recommended_song_id = song.id
931990
db.commit()
932-
return {"message": "대표 음악이 설정되었습니다."}
991+
992+
return {
993+
"message": "대표 음악이 설정되었습니다.",
994+
"youtube_url": song.youtube_url
995+
}
996+
997+
@router.get("/recommended-songs/{recommended_song_id}/youtube-link-direct")
998+
def get_direct_youtube_link(
999+
recommended_song_id: int,
1000+
db: Session = Depends(get_db)
1001+
):
1002+
song = db.query(RecommendedSong).filter(RecommendedSong.id == recommended_song_id).first()
1003+
if not song:
1004+
raise HTTPException(status_code=404, detail="추천곡을 찾을 수 없습니다.")
1005+
1006+
# 이미 저장된 경우 → 캐시된 URL 반환
1007+
if song.youtube_url:
1008+
return {
1009+
"recommended_song_id": song.id,
1010+
"youtube_url": song.youtube_url
1011+
}
1012+
1013+
# YouTube 검색 API 호출
1014+
query = f"{song.song_name} {' '.join(song.artist)}"
1015+
api_url = "https://www.googleapis.com/youtube/v3/search"
1016+
params = {
1017+
"part": "snippet",
1018+
"q": query,
1019+
"type": "video",
1020+
"maxResults": 1,
1021+
"key": YOUTUBE_API_KEY
1022+
}
1023+
1024+
res = requests.get(api_url, params=params)
1025+
data = res.json()
1026+
1027+
if not data.get("items"):
1028+
raise HTTPException(status_code=404, detail="YouTube 영상이 없습니다.")
1029+
1030+
video_id = data["items"][0]["id"]["videoId"]
1031+
youtube_url = f"https://www.youtube.com/watch?v={video_id}"
1032+
1033+
song.youtube_url = youtube_url
1034+
db.commit()
1035+
1036+
return {
1037+
"recommended_song_id": song.id,
1038+
"youtube_url": youtube_url
1039+
}

app/diary/schemas.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class RecommendSongResponse(BaseModel):
3333
song_name: str
3434
artist: List[str]
3535
genre: str
36+
youtube_url: Optional[str] = None
3637
album_image: str
3738
best_lyric: str
3839
similarity_score: float
@@ -46,14 +47,15 @@ class DiaryResponse(BaseModel):
4647
content: str
4748
emotiontype_id: Optional[int] = None
4849
confidence: Optional[float] = None
50+
best_sentence: Optional[str] = None # 추가
4951
recommended_songs: List[RecommendSongResponse]
5052
main_recommend_song: Optional[RecommendSongResponse] = None
5153
top_emotions: list[EmotionScore] = None
5254
created_at: datetime
5355
updated_at: datetime
5456

5557
class Config:
56-
from_attributes = True # pydantic v2용 (orm_mode → from_attributes)
58+
from_attributes = True
5759

5860
class SentenceEmotion(BaseModel):
5961
sentence: str
@@ -106,3 +108,9 @@ class EmotionPreviewResponse(BaseModel):
106108
best_sentence: BestSentence
107109
sentence_emotions: List[SentenceEmotion]
108110

111+
class EmotionTag(BaseModel):
112+
emotiontype_id: int
113+
score: float
114+
115+
class Config:
116+
from_attributes = True

app/embedding/models.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,30 @@
1717
SPECIAL_SPLIT_PATTERNS = [
1818
r"(ㅋ+)", r"(ㅎ+)", r"(ㅠ+)", r"(ㅜ+)",
1919
r"(\.\.\.+)", r"(ㅡㅡ+)", r"(--+)", r"(;;+)",
20-
r"(!+)", r"(\?+)"
20+
r"(!+)", r"(\?+)", r"(~+)"
2121
]
2222

23-
MIN_SENTENCE_LENGTH = 5
23+
MIN_SENTENCE_LENGTH = 3
2424

2525
def split_sentences(text: str):
26-
"""Kiwi + 추가 패턴 기반 문장 분리"""
27-
if not text.strip():
28-
return []
29-
30-
# 1차: Kiwi 문장 분리
31-
sentences = [s.text for s in kiwi.split_into_sents(text)]
26+
# 1차: kiwi로 문장 단위 분리
27+
sentences = [s.text.strip() for s in kiwi.split_into_sents(text)]
3228

33-
refined_sentences = []
34-
for sentence in sentences:
35-
refined_sentences.extend(split_by_special_patterns(sentence))
29+
# 너무 짧은 문장은 다음 문장과 병합
30+
refined = []
31+
buffer = ""
32+
for s in sentences:
33+
if len(s) < 5:
34+
buffer += " " + s
35+
else:
36+
if buffer:
37+
refined.append(buffer.strip())
38+
buffer = ""
39+
refined.append(s)
40+
if buffer:
41+
refined.append(buffer.strip())
3642

37-
# 공백 제거 및 빈 문자열 제거
38-
final_sentences = [s.strip() for s in refined_sentences if s.strip()]
39-
return final_sentences
43+
return refined
4044

4145
def split_by_special_patterns(sentence: str):
4246
"""특정 감정 표현(ㅋㅋ, ㅠㅠ 등) 주변에서 문장 분리를 시도하되, 짧은 파편은 병합"""

app/emotion/models.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from transformers import BertForSequenceClassification
33
from kobert_tokenizer import KoBERTTokenizer
44

5-
from sqlalchemy import Column, Integer, ForeignKey, String
5+
from sqlalchemy import Column, Integer, ForeignKey, String, Float
66
from sqlalchemy.orm import relationship
77
from app.database import Base
88

@@ -32,7 +32,18 @@ class EmotionType(Base):
3232

3333
diaries = relationship("Diary", back_populates="emotion")
3434

35+
class DiaryEmotionTag(Base):
36+
__tablename__ = "diaryEmotionTags"
37+
38+
id = Column(Integer, primary_key=True, autoincrement=True)
39+
diary_id = Column(Integer, ForeignKey("diary.id", ondelete="CASCADE"), nullable=False)
40+
emotiontype_id = Column(Integer, ForeignKey("emotionType.id", ondelete="CASCADE"), nullable=False)
41+
score = Column(Float, nullable=False)
42+
43+
diary = relationship("Diary", back_populates="emotion_tags")
44+
emotion = relationship("EmotionType")
45+
3546
tokenizer = KoBERTTokenizer.from_pretrained("skt/kobert-base-v1")
3647
model = BertForSequenceClassification.from_pretrained("skt/kobert-base-v1", num_labels=8)
3748
model.load_state_dict(torch.load("app/emotion/best_model(7th).pt", map_location="cpu"))
38-
model.eval()
49+
model.eval()

0 commit comments

Comments
 (0)