Commit
·
8b7b432
0
Parent(s):
Initial commit
Browse files- .gitignore +16 -0
- README.md +61 -0
- app.py +7 -0
- backend/app.py +128 -0
- backend/config.py +8 -0
- backend/models/diary.py +14 -0
- backend/requirements.txt +7 -0
- backend/run_server.sh +5 -0
- frontend/index.html +26 -0
- frontend/script.js +60 -0
- frontend/style.css +95 -0
.gitignore
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Python
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
5 |
+
venv/
|
6 |
+
.env
|
7 |
+
|
8 |
+
# IDE
|
9 |
+
.vscode/
|
10 |
+
.idea/
|
11 |
+
|
12 |
+
# Logs
|
13 |
+
*.log
|
14 |
+
|
15 |
+
# Environment variables
|
16 |
+
.env
|
README.md
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: My Diary AI
|
3 |
+
emoji: 📝
|
4 |
+
colorFrom: blue
|
5 |
+
colorTo: pink
|
6 |
+
sdk: gradio
|
7 |
+
sdk_version: 4.8.0
|
8 |
+
app_file: app.py
|
9 |
+
pinned: false
|
10 |
+
---
|
11 |
+
|
12 |
+
AI를 활용한 일기 생성 프로젝트입니다.
|
13 |
+
|
14 |
+
## 특징
|
15 |
+
- 키워드를 기반으로 자세한 일기 생성
|
16 |
+
- 감정과 상황을 생생하게 표현
|
17 |
+
- Hugging Face의 한국어 언어 모델 활용
|
18 |
+
|
19 |
+
## 설치 방법
|
20 |
+
|
21 |
+
```bash
|
22 |
+
git clone https://huggingface.co/Ping9gu/my-diary
|
23 |
+
cd my-diary
|
24 |
+
```
|
25 |
+
|
26 |
+
## 저장소 클론
|
27 |
+
```bash
|
28 |
+
git clone https://github.com/Ping9gu/my-diary.git
|
29 |
+
```
|
30 |
+
|
31 |
+
## 가상환경 생성 및 활성화
|
32 |
+
```bash
|
33 |
+
python -m venv venv
|
34 |
+
source venv/bin/activate
|
35 |
+
```
|
36 |
+
|
37 |
+
## 의존성 설치
|
38 |
+
```bash
|
39 |
+
pip install -r requirements.txt
|
40 |
+
```
|
41 |
+
|
42 |
+
## 환경 설정
|
43 |
+
1. .env 파일 생성하고 다음 내용 추가:
|
44 |
+
```
|
45 |
+
HUGGINGFACE_API_KEY=your_token_here
|
46 |
+
MODEL_ID=nlpai-lab/kullm-polyglot-5.8b-v2
|
47 |
+
```
|
48 |
+
|
49 |
+
## 서버 실행
|
50 |
+
```bash
|
51 |
+
cd backend
|
52 |
+
./run_server.sh
|
53 |
+
```
|
54 |
+
|
55 |
+
## 사용 방법
|
56 |
+
1. 웹 브라우저에서 http://localhost:5000 접속
|
57 |
+
2. 키워드 입력
|
58 |
+
3. "일기 생성하기" 버튼 클릭
|
59 |
+
|
60 |
+
## 라이선스
|
61 |
+
MIT License
|
app.py
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
from backend.app import create_interface
|
3 |
+
|
4 |
+
demo = create_interface()
|
5 |
+
|
6 |
+
if __name__ == "__main__":
|
7 |
+
demo.launch()
|
backend/app.py
ADDED
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, request, jsonify, send_from_directory
|
2 |
+
from flask_cors import CORS
|
3 |
+
from huggingface_hub import InferenceClient
|
4 |
+
import os
|
5 |
+
from dotenv import load_dotenv
|
6 |
+
|
7 |
+
load_dotenv()
|
8 |
+
|
9 |
+
app = Flask(__name__)
|
10 |
+
CORS(app, resources={
|
11 |
+
r"/*": {
|
12 |
+
"origins": ["http://your-frontend-domain"],
|
13 |
+
"methods": ["GET", "POST", "OPTIONS"],
|
14 |
+
"allow_headers": ["Content-Type", "Accept"]
|
15 |
+
}
|
16 |
+
})
|
17 |
+
|
18 |
+
# Hugging Face 클라이언트 초기화
|
19 |
+
client = InferenceClient(
|
20 |
+
model=os.getenv("MODEL_ID"),
|
21 |
+
token=os.getenv("HUGGINGFACE_API_KEY")
|
22 |
+
)
|
23 |
+
|
24 |
+
# 더 큰 한국어 모델로 변경
|
25 |
+
model_name = "nlpai-lab/kullm-polyglot-5.8b-v2" # 더 나은 품질의 모델
|
26 |
+
|
27 |
+
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
28 |
+
model = AutoModelForCausalLM.from_pretrained(
|
29 |
+
model_name,
|
30 |
+
device_map="auto",
|
31 |
+
torch_dtype=torch.float16,
|
32 |
+
low_cpu_mem_usage=True
|
33 |
+
)
|
34 |
+
|
35 |
+
# device_map="auto"를 사용하므로 .to(device) 호출 제거
|
36 |
+
# device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
37 |
+
# model = model.to(device) # 이 줄 제거
|
38 |
+
|
39 |
+
# 루트 경로 핸들러 추가
|
40 |
+
@app.route('/')
|
41 |
+
def serve_frontend():
|
42 |
+
return send_from_directory('../frontend', 'index.html')
|
43 |
+
|
44 |
+
# 정적 파일 서빙을 위한 라우트 추가
|
45 |
+
@app.route('/<path:path>')
|
46 |
+
def serve_static(path):
|
47 |
+
return send_from_directory('../frontend', path)
|
48 |
+
|
49 |
+
# favicon.ico 핸들러 추가
|
50 |
+
@app.route('/favicon.ico')
|
51 |
+
def favicon():
|
52 |
+
return '', 204 # No Content 응답 반환
|
53 |
+
|
54 |
+
@app.route('/api/generate-diary', methods=['POST'])
|
55 |
+
def generate_diary():
|
56 |
+
try:
|
57 |
+
data = request.json
|
58 |
+
if not data or 'keywords' not in data:
|
59 |
+
return jsonify({"error": "키워드가 필요합니다"}), 400
|
60 |
+
|
61 |
+
keywords = data.get('keywords', '').strip()
|
62 |
+
if not keywords:
|
63 |
+
return jsonify({"error": "키워드가 비어있습니다"}), 400
|
64 |
+
|
65 |
+
prompt = f"""다음은 오늘 있었던 일의 요약입니다. 이것을 바탕으로 생생하고 감동적인 일기를 작성해주세요.
|
66 |
+
|
67 |
+
[상세 요구사항]
|
68 |
+
1. 도입부:
|
69 |
+
- 그날의 날씨나 분위기로 시작
|
70 |
+
- 상황과 등장인물 소개
|
71 |
+
|
72 |
+
2. 전개:
|
73 |
+
- 구체적인 대화와 행동 묘사
|
74 |
+
- 오감을 사용한 장면 묘사
|
75 |
+
- 등장인물들의 표정과 감정 변화
|
76 |
+
|
77 |
+
3. 감정과 생각:
|
78 |
+
- 내면의 감정을 섬세하게 표현
|
79 |
+
- 사건에 대한 나의 생각과 깨달음
|
80 |
+
- 다른 사람들의 감정에 대한 공감
|
81 |
+
|
82 |
+
4. 문체:
|
83 |
+
- 문어체와 구어체를 적절히 혼용
|
84 |
+
- 비유와 은유를 활용한 표현
|
85 |
+
- 반복을 피하고 다양한 어휘 사용
|
86 |
+
|
87 |
+
5. 마무리:
|
88 |
+
- 그날의 경험이 주는 의미
|
89 |
+
- 앞으로의 기대나 다짐
|
90 |
+
|
91 |
+
요약:
|
92 |
+
{keywords}
|
93 |
+
|
94 |
+
===
|
95 |
+
오늘의 일기:
|
96 |
+
오늘은 """
|
97 |
+
|
98 |
+
# Hugging Face API를 통한 텍스트 생성
|
99 |
+
parameters = {
|
100 |
+
"max_new_tokens": 768,
|
101 |
+
"temperature": 0.88,
|
102 |
+
"top_p": 0.95,
|
103 |
+
"repetition_penalty": 1.35,
|
104 |
+
"top_k": 50,
|
105 |
+
"do_sample": True,
|
106 |
+
"num_return_sequences": 1
|
107 |
+
}
|
108 |
+
|
109 |
+
response = client.text_generation(
|
110 |
+
prompt,
|
111 |
+
**parameters
|
112 |
+
)
|
113 |
+
|
114 |
+
if not response:
|
115 |
+
return jsonify({"error": "일기 생성에 실패했습니다"}), 500
|
116 |
+
|
117 |
+
# 프롬프트 제거하고 생성된 텍스트만 반환
|
118 |
+
diary_content = response.split("오늘은 ")[-1].strip()
|
119 |
+
diary_content = "오늘은 " + diary_content
|
120 |
+
|
121 |
+
return jsonify({"diary": diary_content})
|
122 |
+
|
123 |
+
except Exception as e:
|
124 |
+
print(f"Error generating diary: {str(e)}")
|
125 |
+
return jsonify({"error": f"일기 생성 중 오류가 발생했습니다: {str(e)}"}), 500
|
126 |
+
|
127 |
+
if __name__ == '__main__':
|
128 |
+
app.run(debug=True)
|
backend/config.py
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from dotenv import load_dotenv
|
3 |
+
|
4 |
+
load_dotenv()
|
5 |
+
|
6 |
+
class Config:
|
7 |
+
SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key')
|
8 |
+
DEEPSEEK_API_KEY = os.getenv('DEEPSEEK_API_KEY')
|
backend/models/diary.py
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
|
3 |
+
class Diary:
|
4 |
+
def __init__(self, content, keywords):
|
5 |
+
self.content = content
|
6 |
+
self.keywords = keywords
|
7 |
+
self.created_at = datetime.now()
|
8 |
+
|
9 |
+
def to_dict(self):
|
10 |
+
return {
|
11 |
+
'content': self.content,
|
12 |
+
'keywords': self.keywords,
|
13 |
+
'created_at': self.created_at.isoformat()
|
14 |
+
}
|
backend/requirements.txt
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
flask==2.3.3
|
2 |
+
flask-cors==4.0.0
|
3 |
+
requests==2.31.0
|
4 |
+
python-dotenv==1.0.0
|
5 |
+
werkzeug==2.3.7
|
6 |
+
bitsandbytes>=0.41.1
|
7 |
+
huggingface-hub>=0.17.0
|
backend/run_server.sh
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/bin/bash
|
2 |
+
screen -S diary-server -dm bash -c '
|
3 |
+
source venv/bin/activate
|
4 |
+
python app.py
|
5 |
+
'
|
frontend/index.html
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="ko">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>AI 일기 도우미</title>
|
7 |
+
<link rel="stylesheet" href="style.css">
|
8 |
+
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
<h1>AI 일기 도우미</h1>
|
12 |
+
<form>
|
13 |
+
<textarea placeholder="오늘은 무슨 일이 있었어?"></textarea>
|
14 |
+
<button type="submit">일기 생성하기</button>
|
15 |
+
</form>
|
16 |
+
<div id="loading-container" style="display: none;">
|
17 |
+
<div class="loading-spinner"></div>
|
18 |
+
<div class="loading-text">일기를 생성하는 중입니다...</div>
|
19 |
+
<div class="loading-progress">
|
20 |
+
<div class="progress-bar"></div>
|
21 |
+
</div>
|
22 |
+
</div>
|
23 |
+
<div id="result"></div>
|
24 |
+
<script src="script.js"></script>
|
25 |
+
</body>
|
26 |
+
</html>
|
frontend/script.js
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
document.addEventListener('DOMContentLoaded', function() {
|
2 |
+
const diaryForm = document.querySelector('form');
|
3 |
+
const keywordsInput = document.querySelector('textarea');
|
4 |
+
const resultDiv = document.querySelector('#result');
|
5 |
+
const generateButton = document.querySelector('button');
|
6 |
+
const loadingContainer = document.querySelector('#loading-container');
|
7 |
+
const progressBar = document.querySelector('.progress-bar');
|
8 |
+
|
9 |
+
diaryForm.addEventListener('submit', async function(e) {
|
10 |
+
e.preventDefault();
|
11 |
+
|
12 |
+
const keywords = keywordsInput.value.trim();
|
13 |
+
if (!keywords) {
|
14 |
+
resultDiv.textContent = "키워드를 입력해주세요";
|
15 |
+
return;
|
16 |
+
}
|
17 |
+
|
18 |
+
try {
|
19 |
+
generateButton.disabled = true;
|
20 |
+
resultDiv.textContent = "";
|
21 |
+
loadingContainer.style.display = 'block';
|
22 |
+
progressBar.style.width = '0%';
|
23 |
+
|
24 |
+
// 프로그레스 바 애니메이션 시작 (60초에서 30초로 변경)
|
25 |
+
progressBar.style.animation = 'progress 30s linear';
|
26 |
+
|
27 |
+
const API_URL = 'http://your-ec2-instance-ip:5000';
|
28 |
+
|
29 |
+
const response = await fetch(`${API_URL}/api/generate-diary`, {
|
30 |
+
method: 'POST',
|
31 |
+
headers: {
|
32 |
+
'Content-Type': 'application/json',
|
33 |
+
'Accept': 'application/json'
|
34 |
+
},
|
35 |
+
body: JSON.stringify({
|
36 |
+
keywords: keywords
|
37 |
+
})
|
38 |
+
});
|
39 |
+
|
40 |
+
if (!response.ok) {
|
41 |
+
const errorText = await response.text();
|
42 |
+
throw new Error(`서버 응답 오류: ${response.status}`);
|
43 |
+
}
|
44 |
+
|
45 |
+
const data = await response.json();
|
46 |
+
|
47 |
+
if (data.error) {
|
48 |
+
resultDiv.textContent = `오류: ${data.error}`;
|
49 |
+
} else {
|
50 |
+
resultDiv.textContent = data.diary || '일기 생성에 실패했습니다.';
|
51 |
+
}
|
52 |
+
} catch (error) {
|
53 |
+
resultDiv.textContent = `오류가 발생했습니다: ${error.message}`;
|
54 |
+
} finally {
|
55 |
+
generateButton.disabled = false;
|
56 |
+
loadingContainer.style.display = 'none';
|
57 |
+
progressBar.style.animation = 'none';
|
58 |
+
}
|
59 |
+
});
|
60 |
+
});
|
frontend/style.css
ADDED
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
body {
|
2 |
+
font-family: 'Noto Sans KR', sans-serif;
|
3 |
+
margin: 0;
|
4 |
+
padding: 20px;
|
5 |
+
background-color: #f5f5f5;
|
6 |
+
}
|
7 |
+
|
8 |
+
.container {
|
9 |
+
max-width: 800px;
|
10 |
+
margin: 0 auto;
|
11 |
+
background-color: white;
|
12 |
+
padding: 20px;
|
13 |
+
border-radius: 10px;
|
14 |
+
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
15 |
+
}
|
16 |
+
|
17 |
+
.input-section {
|
18 |
+
margin: 20px 0;
|
19 |
+
}
|
20 |
+
|
21 |
+
textarea {
|
22 |
+
width: 100%;
|
23 |
+
height: 100px;
|
24 |
+
padding: 10px;
|
25 |
+
margin-bottom: 10px;
|
26 |
+
border: 1px solid #ddd;
|
27 |
+
border-radius: 5px;
|
28 |
+
}
|
29 |
+
|
30 |
+
button {
|
31 |
+
background-color: #007bff;
|
32 |
+
color: white;
|
33 |
+
border: none;
|
34 |
+
padding: 10px 20px;
|
35 |
+
border-radius: 5px;
|
36 |
+
cursor: pointer;
|
37 |
+
}
|
38 |
+
|
39 |
+
button:hover {
|
40 |
+
background-color: #0056b3;
|
41 |
+
}
|
42 |
+
|
43 |
+
.diary-section {
|
44 |
+
margin-top: 20px;
|
45 |
+
padding: 20px;
|
46 |
+
border: 1px solid #ddd;
|
47 |
+
border-radius: 5px;
|
48 |
+
}
|
49 |
+
|
50 |
+
#loading-container {
|
51 |
+
text-align: center;
|
52 |
+
margin: 20px 0;
|
53 |
+
}
|
54 |
+
|
55 |
+
.loading-spinner {
|
56 |
+
width: 50px;
|
57 |
+
height: 50px;
|
58 |
+
border: 5px solid #f3f3f3;
|
59 |
+
border-top: 5px solid #3498db;
|
60 |
+
border-radius: 50%;
|
61 |
+
animation: spin 1s linear infinite;
|
62 |
+
margin: 0 auto;
|
63 |
+
}
|
64 |
+
|
65 |
+
.loading-text {
|
66 |
+
margin: 10px 0;
|
67 |
+
color: #666;
|
68 |
+
}
|
69 |
+
|
70 |
+
.loading-progress {
|
71 |
+
width: 100%;
|
72 |
+
max-width: 300px;
|
73 |
+
height: 4px;
|
74 |
+
background-color: #f3f3f3;
|
75 |
+
margin: 10px auto;
|
76 |
+
border-radius: 2px;
|
77 |
+
overflow: hidden;
|
78 |
+
}
|
79 |
+
|
80 |
+
.progress-bar {
|
81 |
+
width: 0%;
|
82 |
+
height: 100%;
|
83 |
+
background-color: #3498db;
|
84 |
+
animation: progress 60s linear;
|
85 |
+
}
|
86 |
+
|
87 |
+
@keyframes spin {
|
88 |
+
0% { transform: rotate(0deg); }
|
89 |
+
100% { transform: rotate(360deg); }
|
90 |
+
}
|
91 |
+
|
92 |
+
@keyframes progress {
|
93 |
+
0% { width: 0%; }
|
94 |
+
100% { width: 100%; }
|
95 |
+
}
|