Update app.py
Browse files
app.py
CHANGED
@@ -1,3 +1,5 @@
|
|
|
|
|
|
1 |
import gradio as gr
|
2 |
import numpy as np
|
3 |
import librosa
|
@@ -8,7 +10,7 @@ import requests
|
|
8 |
import json
|
9 |
import time
|
10 |
|
11 |
-
# 데이터 저장을 위한
|
12 |
class SimpleDB:
|
13 |
def __init__(self, file_path="wishes.json"):
|
14 |
self.file_path = file_path
|
@@ -27,15 +29,12 @@ class SimpleDB:
|
|
27 |
def save_wish(self, name, wish, timestamp=None):
|
28 |
if timestamp is None:
|
29 |
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
30 |
-
|
31 |
wish_data = {
|
32 |
"name": name,
|
33 |
"wish": wish,
|
34 |
"timestamp": timestamp
|
35 |
}
|
36 |
-
|
37 |
self.wishes.append(wish_data)
|
38 |
-
|
39 |
try:
|
40 |
with open(self.file_path, 'w', encoding='utf-8') as f:
|
41 |
json.dump(self.wishes, f, ensure_ascii=False, indent=2)
|
@@ -44,12 +43,11 @@ class SimpleDB:
|
|
44 |
print(f"Error saving wish: {e}")
|
45 |
return False
|
46 |
|
47 |
-
# 환경변수 설정
|
48 |
HF_API_TOKEN = os.getenv("roots")
|
49 |
if not HF_API_TOKEN:
|
50 |
raise ValueError("roots token not found in environment variables")
|
51 |
|
52 |
-
# API 설정
|
53 |
API_URL = "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-xl-base-1.0"
|
54 |
headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
|
55 |
|
@@ -64,7 +62,7 @@ text_analyzer = pipeline(
|
|
64 |
)
|
65 |
|
66 |
# 상수 정의
|
67 |
-
IMAGE_DISPLAY_TIME = 30
|
68 |
WELCOME_MESSAGE = """
|
69 |
# 디지털 굿판에 오신 것을 환영합니다
|
70 |
|
@@ -85,7 +83,159 @@ WORLDVIEW_MESSAGE = """
|
|
85 |
"""
|
86 |
|
87 |
|
88 |
-
# Part 2/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
|
90 |
def generate_image_from_prompt(prompt):
|
91 |
"""이미지 생성 함수"""
|
@@ -110,7 +260,7 @@ def generate_image_from_prompt(prompt):
|
|
110 |
|
111 |
if response.status_code == 200:
|
112 |
print("Image generated successfully")
|
113 |
-
# 이미지 저장
|
114 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
115 |
image_path = f"generated_images/{timestamp}.png"
|
116 |
os.makedirs("generated_images", exist_ok=True)
|
@@ -125,64 +275,6 @@ def generate_image_from_prompt(prompt):
|
|
125 |
print(f"Error generating image: {str(e)}")
|
126 |
return None, None
|
127 |
|
128 |
-
def analyze_voice_with_retry(audio_path, state, max_retries=3):
|
129 |
-
"""음성 분석 함수 (재시도 로직 포함)"""
|
130 |
-
for attempt in range(max_retries):
|
131 |
-
try:
|
132 |
-
y, sr = librosa.load(audio_path, sr=16000)
|
133 |
-
|
134 |
-
acoustic_features = {
|
135 |
-
"energy": float(np.mean(librosa.feature.rms(y=y))),
|
136 |
-
"tempo": float(librosa.beat.tempo(y)[0]),
|
137 |
-
"pitch": float(np.mean(librosa.feature.zero_crossing_rate(y))),
|
138 |
-
"volume": float(np.mean(np.abs(y)))
|
139 |
-
}
|
140 |
-
|
141 |
-
transcription = speech_recognizer(y)
|
142 |
-
text = transcription["text"]
|
143 |
-
|
144 |
-
emotions = map_acoustic_to_emotion(acoustic_features, state.get("baseline_features"))
|
145 |
-
text_sentiment = text_analyzer(text)[0]
|
146 |
-
|
147 |
-
return {
|
148 |
-
"text": text,
|
149 |
-
"emotions": emotions,
|
150 |
-
"sentiment": text_sentiment,
|
151 |
-
"features": acoustic_features
|
152 |
-
}
|
153 |
-
except Exception as e:
|
154 |
-
if attempt == max_retries - 1:
|
155 |
-
raise e
|
156 |
-
print(f"Attempt {attempt + 1} failed, retrying...")
|
157 |
-
continue
|
158 |
-
|
159 |
-
def generate_detailed_prompt(text, emotions, text_sentiment):
|
160 |
-
"""감정 기반 상세 프롬프트 생성"""
|
161 |
-
emotion_colors = {
|
162 |
-
"기쁨/열정": "밝은 노랑과 따뜻한 주황색",
|
163 |
-
"분노/강조": "강렬한 빨강과 짙은 검정",
|
164 |
-
"놀람/흥분": "선명한 파랑과 밝은 보라",
|
165 |
-
"관심/호기심": "연한 하늘색과 민트색",
|
166 |
-
"슬픔/우울": "어두운 파랑과 회색",
|
167 |
-
"피로/무기력": "탁한 갈색과 짙은 회색",
|
168 |
-
"평온/안정": "부드러운 초록과 베이지",
|
169 |
-
"차분/진지": "차분한 남색과 깊은 보라"
|
170 |
-
}
|
171 |
-
|
172 |
-
if emotions["intensity"] > 70:
|
173 |
-
visual_style = "역동적인 붓질과 강한 대비"
|
174 |
-
elif emotions["intensity"] > 40:
|
175 |
-
visual_style = "균형잡힌 구도와 중간 톤의 조화"
|
176 |
-
else:
|
177 |
-
visual_style = "부드러운 그라데이션과 차분한 톤"
|
178 |
-
|
179 |
-
prompt = f"한국 전통 민화 스타일의 추상화, {emotion_colors.get(emotions['primary'], '자연스러운 색상')} 기반. "
|
180 |
-
prompt += f"{visual_style}로 표현된 {emotions['primary']}의 감정. "
|
181 |
-
prompt += f"음성의 특징({', '.join(emotions['characteristics'])})을 화면의 동적 요소로 표현. "
|
182 |
-
prompt += f"발화 내용 '{text}'에서 느껴지는 감정(강도: {text_sentiment['score']}/5)을 은유적 이미지로 담아내기."
|
183 |
-
|
184 |
-
return prompt
|
185 |
-
|
186 |
def update_final_prompt(state):
|
187 |
"""청신의 감상들을 종합하여 최종 프롬프트 업데이트"""
|
188 |
combined_prompt = "한국 전통 민화 스타일의 추상화, 온천천에서의 감상과 소원을 담아내기:\n\n"
|
@@ -221,9 +313,9 @@ def save_reflection(text, state):
|
|
221 |
print(f"Error in save_reflection: {str(e)}")
|
222 |
return state, []
|
223 |
|
224 |
-
|
|
|
225 |
def create_interface():
|
226 |
-
# DB 초기화
|
227 |
db = SimpleDB()
|
228 |
|
229 |
with gr.Blocks(theme=gr.themes.Soft()) as app:
|
@@ -240,10 +332,9 @@ def create_interface():
|
|
240 |
header = gr.Markdown("# 디지털 굿판")
|
241 |
user_display = gr.Markdown("")
|
242 |
|
243 |
-
# 탭 구성
|
244 |
with gr.Tabs() as tabs:
|
245 |
# 입장
|
246 |
-
with gr.Tab("입장"
|
247 |
gr.Markdown(WELCOME_MESSAGE)
|
248 |
name_input = gr.Textbox(
|
249 |
label="이름을 알려주세요",
|
@@ -251,17 +342,14 @@ def create_interface():
|
|
251 |
)
|
252 |
worldview_display = gr.Markdown(visible=False)
|
253 |
start_btn = gr.Button("여정 시작하기")
|
254 |
-
continue_btn = gr.Button("다음 단계로", visible=False)
|
255 |
|
256 |
# 기준 설정
|
257 |
-
with gr.Tab("기준 설정"
|
258 |
-
gr.Markdown("""
|
259 |
-
### 축원의 문장을 평온한 마음으로 읽어주세요
|
260 |
|
261 |
먼저, 평온한 마음으로 축원의 문장을 읽어주세요.
|
262 |
이 축원은 당신에게 평화와 안정을 불러일으키며,
|
263 |
-
감정을 정확히 이해하기 위한 **기준점**이 될 것입니다.
|
264 |
-
""")
|
265 |
gr.Markdown("'당신의 건강과 행복이 늘 가득하기를'")
|
266 |
baseline_audio = gr.Audio(
|
267 |
label="축원 문장 녹음하기",
|
@@ -271,14 +359,10 @@ def create_interface():
|
|
271 |
baseline_status = gr.Markdown("")
|
272 |
|
273 |
# 청신
|
274 |
-
with gr.Tab("청신"
|
275 |
-
gr.Markdown("""
|
276 |
-
## 청신 - 소리로 정화하기
|
277 |
|
278 |
-
온천천의 물소리에 귀 기울이며 **30분간 마음을 정화**해보세요.
|
279 |
-
장전역까지 이어지는 이 여정을 함께하며,
|
280 |
-
차분히 자연의 소리에 마음을 기울여보세요.
|
281 |
-
""")
|
282 |
play_music_btn = gr.Button("온천천의 소리 듣기")
|
283 |
with gr.Row():
|
284 |
audio = gr.Audio(
|
@@ -300,45 +384,25 @@ def create_interface():
|
|
300 |
)
|
301 |
|
302 |
# 기원
|
303 |
-
with gr.Tab("기원"
|
304 |
-
gr.Markdown(""
|
305 |
-
## 기원 - 소원을 전해보세요
|
306 |
-
|
307 |
-
당신의 소원을 온전히 담아 이곳에 전해주세요.
|
308 |
-
당신의 목소리와 감정이 영적 메시지가 되어 전해지며,
|
309 |
-
이 기원이 당신에게 평화와 안정을 불러오기를 바랍니다.
|
310 |
-
""")
|
311 |
with gr.Row():
|
312 |
with gr.Column():
|
313 |
voice_input = gr.Audio(
|
314 |
label="소원을 나누고 싶은 마음을 말해주세요",
|
315 |
sources=["microphone"]
|
316 |
)
|
|
|
317 |
analyze_btn = gr.Button("소원 분석하기")
|
318 |
|
319 |
with gr.Column():
|
320 |
-
transcribed_text = gr.Textbox(
|
321 |
-
|
322 |
-
|
323 |
-
)
|
324 |
-
voice_emotion = gr.Textbox(
|
325 |
-
label="음성 감정 분석",
|
326 |
-
interactive=False
|
327 |
-
)
|
328 |
-
text_emotion = gr.Textbox(
|
329 |
-
label="텍스트 감정 분석",
|
330 |
-
interactive=False
|
331 |
-
)
|
332 |
|
333 |
# 송신
|
334 |
-
with gr.Tab("송신"
|
335 |
-
gr.Markdown(""
|
336 |
-
## 송신 - 마음의 그림을 남기고, 보내기
|
337 |
-
|
338 |
-
당신의 마음을 시각화하여 그려봅니다.
|
339 |
-
자연과 영적 교감을 통해 얻은 평온과 치유의 흔적을,
|
340 |
-
하나의 그림으로 담아보세요.
|
341 |
-
""")
|
342 |
final_prompt = gr.Textbox(
|
343 |
label="생성된 프롬프트",
|
344 |
interactive=False,
|
@@ -348,54 +412,23 @@ def create_interface():
|
|
348 |
generate_btn = gr.Button("마음의 그림 그리기")
|
349 |
save_image_btn = gr.Button("이미지 저장하기")
|
350 |
result_image = gr.Image(label="생성된 이미지")
|
351 |
-
image_timer = gr.Markdown(
|
352 |
-
"이미지는 30초 후 자동으로 사라집니다...",
|
353 |
-
visible=False
|
354 |
-
)
|
355 |
-
|
356 |
-
# 최종 감상
|
357 |
-
gr.Markdown("""
|
358 |
-
## 마지막 감상을 남겨주세요
|
359 |
|
360 |
-
|
361 |
-
마지막으로 느낀 감상을 한 줄로 남겨주세요.
|
362 |
-
""")
|
363 |
final_reflection = gr.Textbox(
|
364 |
label="마지막 감상",
|
365 |
placeholder="한 줄로 남겨주세요..."
|
366 |
)
|
367 |
save_final_btn = gr.Button("감상 남기기")
|
368 |
|
369 |
-
# 이벤트 핸들러
|
370 |
-
def start_journey(name):
|
371 |
-
if not name.strip():
|
372 |
-
return "이름을 입력해주세요", gr.update(), gr.update()
|
373 |
-
|
374 |
-
state = {"user_name": name}
|
375 |
-
return (
|
376 |
-
WORLDVIEW_MESSAGE,
|
377 |
-
gr.update(visible=True),
|
378 |
-
gr.update(visible=True)
|
379 |
-
)
|
380 |
-
|
381 |
-
def save_wish_to_db(text, state):
|
382 |
-
if text and state.get("user_name"):
|
383 |
-
db.save_wish(state["user_name"], text)
|
384 |
-
return "소원이 안전하게 저장되었습니다."
|
385 |
-
return "저장에 실패했습니다."
|
386 |
-
|
387 |
-
def handle_image_generation(prompt):
|
388 |
-
image_content, image_path = generate_image_from_prompt(prompt)
|
389 |
-
if image_content:
|
390 |
-
gr.update(visible=True) # 타이머 표시
|
391 |
-
return image_content
|
392 |
-
return None
|
393 |
-
|
394 |
# 이벤트 연결
|
395 |
start_btn.click(
|
396 |
-
fn=
|
|
|
|
|
|
|
|
|
397 |
inputs=[name_input],
|
398 |
-
outputs=[worldview_display,
|
399 |
)
|
400 |
|
401 |
set_baseline_btn.click(
|
@@ -410,25 +443,30 @@ def create_interface():
|
|
410 |
outputs=[state, reflections_display]
|
411 |
)
|
412 |
|
|
|
|
|
|
|
|
|
|
|
413 |
analyze_btn.click(
|
414 |
fn=analyze_voice,
|
415 |
inputs=[voice_input, state],
|
416 |
-
outputs=[transcribed_text, voice_emotion, text_emotion,
|
417 |
)
|
418 |
|
419 |
generate_btn.click(
|
420 |
-
fn=
|
421 |
inputs=[final_prompt],
|
422 |
outputs=[result_image]
|
423 |
)
|
424 |
|
425 |
save_final_btn.click(
|
426 |
-
fn=
|
427 |
inputs=[final_reflection, state],
|
428 |
-
outputs=[gr.Markdown("")]
|
429 |
)
|
430 |
|
431 |
-
# 이미지 자동 사라짐
|
432 |
result_image.change(
|
433 |
fn=lambda: gr.update(value=None),
|
434 |
inputs=[],
|
@@ -441,4 +479,3 @@ def create_interface():
|
|
441 |
if __name__ == "__main__":
|
442 |
demo = create_interface()
|
443 |
demo.launch(debug=True)
|
444 |
-
|
|
|
1 |
+
|
2 |
+
# Part 1/4 - Imports and Initial Setup
|
3 |
import gradio as gr
|
4 |
import numpy as np
|
5 |
import librosa
|
|
|
10 |
import json
|
11 |
import time
|
12 |
|
13 |
+
# 데이터 저장을 위한 DB 클래스
|
14 |
class SimpleDB:
|
15 |
def __init__(self, file_path="wishes.json"):
|
16 |
self.file_path = file_path
|
|
|
29 |
def save_wish(self, name, wish, timestamp=None):
|
30 |
if timestamp is None:
|
31 |
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
32 |
wish_data = {
|
33 |
"name": name,
|
34 |
"wish": wish,
|
35 |
"timestamp": timestamp
|
36 |
}
|
|
|
37 |
self.wishes.append(wish_data)
|
|
|
38 |
try:
|
39 |
with open(self.file_path, 'w', encoding='utf-8') as f:
|
40 |
json.dump(self.wishes, f, ensure_ascii=False, indent=2)
|
|
|
43 |
print(f"Error saving wish: {e}")
|
44 |
return False
|
45 |
|
46 |
+
# 환경변수 및 API 설정
|
47 |
HF_API_TOKEN = os.getenv("roots")
|
48 |
if not HF_API_TOKEN:
|
49 |
raise ValueError("roots token not found in environment variables")
|
50 |
|
|
|
51 |
API_URL = "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-xl-base-1.0"
|
52 |
headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
|
53 |
|
|
|
62 |
)
|
63 |
|
64 |
# 상수 정의
|
65 |
+
IMAGE_DISPLAY_TIME = 30
|
66 |
WELCOME_MESSAGE = """
|
67 |
# 디지털 굿판에 오신 것을 환영합니다
|
68 |
|
|
|
83 |
"""
|
84 |
|
85 |
|
86 |
+
# Part 2/4 - Analysis Functions
|
87 |
+
def calculate_baseline_features(audio_path):
|
88 |
+
"""기준점 음성 특성 분석"""
|
89 |
+
try:
|
90 |
+
y, sr = librosa.load(audio_path, sr=16000)
|
91 |
+
features = {
|
92 |
+
"energy": float(np.mean(librosa.feature.rms(y=y))),
|
93 |
+
"tempo": float(librosa.beat.tempo(y)[0]),
|
94 |
+
"pitch": float(np.mean(librosa.feature.zero_crossing_rate(y))),
|
95 |
+
"volume": float(np.mean(np.abs(y))),
|
96 |
+
"mfcc": librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13).mean(axis=1).tolist()
|
97 |
+
}
|
98 |
+
return features
|
99 |
+
except Exception as e:
|
100 |
+
print(f"Error calculating baseline: {str(e)}")
|
101 |
+
return None
|
102 |
+
|
103 |
+
def map_acoustic_to_emotion(features, baseline_features=None):
|
104 |
+
"""음향학적 특성을 감정으로 매핑"""
|
105 |
+
energy_norm = min(features["energy"] * 100, 100)
|
106 |
+
tempo_norm = min(features["tempo"] / 200, 1)
|
107 |
+
pitch_norm = min(features["pitch"] * 2, 1)
|
108 |
+
|
109 |
+
if baseline_features:
|
110 |
+
energy_norm = (features["energy"] / baseline_features["energy"]) * 50
|
111 |
+
tempo_norm = (features["tempo"] / baseline_features["tempo"])
|
112 |
+
pitch_norm = (features["pitch"] / baseline_features["pitch"])
|
113 |
+
|
114 |
+
emotions = {
|
115 |
+
"primary": "",
|
116 |
+
"intensity": energy_norm,
|
117 |
+
"confidence": 0.0,
|
118 |
+
"secondary": "",
|
119 |
+
"characteristics": []
|
120 |
+
}
|
121 |
+
|
122 |
+
if energy_norm > 70:
|
123 |
+
if tempo_norm > 0.6:
|
124 |
+
emotions["primary"] = "기쁨/열정"
|
125 |
+
emotions["characteristics"].append("빠르고 활기찬 말하기 패턴")
|
126 |
+
else:
|
127 |
+
emotions["primary"] = "분노/강조"
|
128 |
+
emotions["characteristics"].append("강한 음성 강도")
|
129 |
+
emotions["confidence"] = energy_norm / 100
|
130 |
+
elif pitch_norm > 0.6:
|
131 |
+
if energy_norm > 50:
|
132 |
+
emotions["primary"] = "놀람/흥분"
|
133 |
+
emotions["characteristics"].append("높은 음고와 강한 강세")
|
134 |
+
else:
|
135 |
+
emotions["primary"] = "관심/호기심"
|
136 |
+
emotions["characteristics"].append("음고 변화가 큼")
|
137 |
+
emotions["confidence"] = pitch_norm
|
138 |
+
elif energy_norm < 30:
|
139 |
+
if tempo_norm < 0.4:
|
140 |
+
emotions["primary"] = "슬픔/우울"
|
141 |
+
emotions["characteristics"].append("느리고 약한 음성")
|
142 |
+
else:
|
143 |
+
emotions["primary"] = "피로/무기력"
|
144 |
+
emotions["characteristics"].append("낮은 에너지 레벨")
|
145 |
+
emotions["confidence"] = (30 - energy_norm) / 30
|
146 |
+
else:
|
147 |
+
if tempo_norm > 0.5:
|
148 |
+
emotions["primary"] = "평온/안정"
|
149 |
+
emotions["characteristics"].append("균형잡힌 말하기 패턴")
|
150 |
+
else:
|
151 |
+
emotions["primary"] = "차분/진지"
|
152 |
+
emotions["characteristics"].append("안정적인 음성 특성")
|
153 |
+
emotions["confidence"] = 0.5
|
154 |
+
|
155 |
+
emotions["details"] = {
|
156 |
+
"energy_level": f"{energy_norm:.1f}%",
|
157 |
+
"speech_rate": f"{'빠름' if tempo_norm > 0.6 else '보통' if tempo_norm > 0.4 else '느림'}",
|
158 |
+
"pitch_variation": f"{'높음' if pitch_norm > 0.6 else '보통' if pitch_norm > 0.3 else '낮음'}",
|
159 |
+
"voice_volume": f"{'큼' if features['volume'] > 0.7 else '보통' if features['volume'] > 0.3 else '작음'}"
|
160 |
+
}
|
161 |
+
|
162 |
+
return emotions
|
163 |
+
|
164 |
+
def analyze_voice(audio_path, state):
|
165 |
+
"""통합 음성 분석"""
|
166 |
+
if audio_path is None:
|
167 |
+
return state, "음성을 먼저 녹음해주세요.", "", "", ""
|
168 |
+
|
169 |
+
try:
|
170 |
+
y, sr = librosa.load(audio_path, sr=16000)
|
171 |
+
|
172 |
+
# 음향학적 특성 분석
|
173 |
+
acoustic_features = {
|
174 |
+
"energy": float(np.mean(librosa.feature.rms(y=y))),
|
175 |
+
"tempo": float(librosa.beat.tempo(y)[0]),
|
176 |
+
"pitch": float(np.mean(librosa.feature.zero_crossing_rate(y))),
|
177 |
+
"volume": float(np.mean(np.abs(y)))
|
178 |
+
}
|
179 |
+
|
180 |
+
# 음성 감정 분석
|
181 |
+
voice_emotion = map_acoustic_to_emotion(acoustic_features, state.get("baseline_features"))
|
182 |
+
|
183 |
+
# 음성 인식
|
184 |
+
transcription = speech_recognizer(y)
|
185 |
+
text = transcription["text"]
|
186 |
+
|
187 |
+
# 텍스트 감정 분석
|
188 |
+
text_sentiment = text_analyzer(text)[0]
|
189 |
+
|
190 |
+
# 결과 포맷팅
|
191 |
+
voice_result = (
|
192 |
+
f"음성 감정: {voice_emotion['primary']} "
|
193 |
+
f"(강도: {voice_emotion['intensity']:.1f}%, 신뢰도: {voice_emotion['confidence']:.2f})\n"
|
194 |
+
f"특징: {', '.join(voice_emotion['characteristics'])}\n"
|
195 |
+
f"상세 분석:\n"
|
196 |
+
f"- 에너지 레벨: {voice_emotion['details']['energy_level']}\n"
|
197 |
+
f"- 말하기 속도: {voice_emotion['details']['speech_rate']}\n"
|
198 |
+
f"- 음높이 변화: {voice_emotion['details']['pitch_variation']}\n"
|
199 |
+
f"- 음성 크기: {voice_emotion['details']['voice_volume']}"
|
200 |
+
)
|
201 |
+
|
202 |
+
text_result = f"텍스트 감정 분석 (1-5): {text_sentiment['score']}"
|
203 |
+
|
204 |
+
# 프롬프트 생성
|
205 |
+
prompt = generate_detailed_prompt(text, voice_emotion, text_sentiment)
|
206 |
+
|
207 |
+
return state, text, voice_result, text_result, prompt
|
208 |
+
except Exception as e:
|
209 |
+
return state, f"오류 발생: {str(e)}", "", "", ""
|
210 |
+
|
211 |
+
|
212 |
+
# Part 3/4 - Generation and Processing Functions
|
213 |
+
def generate_detailed_prompt(text, emotions, text_sentiment):
|
214 |
+
"""감정 기반 상세 프롬프트 생성"""
|
215 |
+
emotion_colors = {
|
216 |
+
"기쁨/열정": "밝은 노랑과 따뜻한 주황색",
|
217 |
+
"분노/강조": "강렬한 빨강과 짙은 검정",
|
218 |
+
"놀람/흥분": "선명한 파랑과 밝은 보라",
|
219 |
+
"관심/호기심": "연한 하늘색과 민트색",
|
220 |
+
"슬픔/우울": "어두운 파랑과 회색",
|
221 |
+
"피로/무기력": "탁한 갈색과 짙은 회색",
|
222 |
+
"평온/안정": "부드러운 초록과 베이지",
|
223 |
+
"차분/진지": "차분한 남색과 깊은 보라"
|
224 |
+
}
|
225 |
+
|
226 |
+
if emotions["intensity"] > 70:
|
227 |
+
visual_style = "역동적인 붓질과 강한 대비"
|
228 |
+
elif emotions["intensity"] > 40:
|
229 |
+
visual_style = "균형잡힌 구도와 중간 톤의 조화"
|
230 |
+
else:
|
231 |
+
visual_style = "부드러운 그라데이션과 차분한 톤"
|
232 |
+
|
233 |
+
prompt = f"한국 전통 민화 스타일의 추상화, {emotion_colors.get(emotions['primary'], '자연스러운 색상')} 기반. "
|
234 |
+
prompt += f"{visual_style}로 표현된 {emotions['primary']}의 감정. "
|
235 |
+
prompt += f"음성의 특징({', '.join(emotions['characteristics'])})을 화면의 동적 요소로 표현. "
|
236 |
+
prompt += f"발화 내용 '{text}'에서 느껴지는 감정(강도: {text_sentiment['score']}/5)을 은유적 이미지로 담아내기."
|
237 |
+
|
238 |
+
return prompt
|
239 |
|
240 |
def generate_image_from_prompt(prompt):
|
241 |
"""이미지 생성 함수"""
|
|
|
260 |
|
261 |
if response.status_code == 200:
|
262 |
print("Image generated successfully")
|
263 |
+
# 이미지 저장
|
264 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
265 |
image_path = f"generated_images/{timestamp}.png"
|
266 |
os.makedirs("generated_images", exist_ok=True)
|
|
|
275 |
print(f"Error generating image: {str(e)}")
|
276 |
return None, None
|
277 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
278 |
def update_final_prompt(state):
|
279 |
"""청신의 감상들을 종합하여 최종 프롬프트 업데이트"""
|
280 |
combined_prompt = "한국 전통 민화 스타일의 추상화, 온천천에서의 감상과 소원을 담아내기:\n\n"
|
|
|
313 |
print(f"Error in save_reflection: {str(e)}")
|
314 |
return state, []
|
315 |
|
316 |
+
|
317 |
+
# Part 4/4 - Interface and Main
|
318 |
def create_interface():
|
|
|
319 |
db = SimpleDB()
|
320 |
|
321 |
with gr.Blocks(theme=gr.themes.Soft()) as app:
|
|
|
332 |
header = gr.Markdown("# 디지털 굿판")
|
333 |
user_display = gr.Markdown("")
|
334 |
|
|
|
335 |
with gr.Tabs() as tabs:
|
336 |
# 입장
|
337 |
+
with gr.Tab("입장"):
|
338 |
gr.Markdown(WELCOME_MESSAGE)
|
339 |
name_input = gr.Textbox(
|
340 |
label="이름을 알려주세요",
|
|
|
342 |
)
|
343 |
worldview_display = gr.Markdown(visible=False)
|
344 |
start_btn = gr.Button("여정 시작하기")
|
|
|
345 |
|
346 |
# 기준 설정
|
347 |
+
with gr.Tab("기준 설정"):
|
348 |
+
gr.Markdown("""### 축원의 문장을 평온한 마음으로 읽어주세요
|
|
|
349 |
|
350 |
먼저, 평온한 마음으로 축원의 문장을 읽어주세요.
|
351 |
이 축원은 당신에게 평화와 안정을 불러일으키며,
|
352 |
+
감정을 정확히 이해하기 위한 **기준점**이 될 것입니다.""")
|
|
|
353 |
gr.Markdown("'당신의 건강과 행복이 늘 가득하기를'")
|
354 |
baseline_audio = gr.Audio(
|
355 |
label="축원 문장 녹음하기",
|
|
|
359 |
baseline_status = gr.Markdown("")
|
360 |
|
361 |
# 청신
|
362 |
+
with gr.Tab("청신"):
|
363 |
+
gr.Markdown("""## 청신 - 소리로 정화하기
|
|
|
364 |
|
365 |
+
온천천의 물소리에 귀 기울이며 **30분간 마음을 정화**해보세요.""")
|
|
|
|
|
|
|
366 |
play_music_btn = gr.Button("온천천의 소리 듣기")
|
367 |
with gr.Row():
|
368 |
audio = gr.Audio(
|
|
|
384 |
)
|
385 |
|
386 |
# 기원
|
387 |
+
with gr.Tab("기원"):
|
388 |
+
gr.Markdown("## 기원 - 소원을 전해보세요")
|
|
|
|
|
|
|
|
|
|
|
|
|
389 |
with gr.Row():
|
390 |
with gr.Column():
|
391 |
voice_input = gr.Audio(
|
392 |
label="소원을 나누고 싶은 마음을 말해주세요",
|
393 |
sources=["microphone"]
|
394 |
)
|
395 |
+
clear_btn = gr.Button("녹음 지우기")
|
396 |
analyze_btn = gr.Button("소원 분석하기")
|
397 |
|
398 |
with gr.Column():
|
399 |
+
transcribed_text = gr.Textbox(label="인식된 텍스트")
|
400 |
+
voice_emotion = gr.Textbox(label="음성 감정 분석")
|
401 |
+
text_emotion = gr.Textbox(label="텍스트 감정 분석")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
402 |
|
403 |
# 송신
|
404 |
+
with gr.Tab("송신"):
|
405 |
+
gr.Markdown("## 송신 - 마음의 그림을 남기고, 보내기")
|
|
|
|
|
|
|
|
|
|
|
|
|
406 |
final_prompt = gr.Textbox(
|
407 |
label="생성된 프롬프트",
|
408 |
interactive=False,
|
|
|
412 |
generate_btn = gr.Button("마음의 그림 그리기")
|
413 |
save_image_btn = gr.Button("이미지 저장하기")
|
414 |
result_image = gr.Image(label="생성된 이미지")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
415 |
|
416 |
+
gr.Markdown("## 마지막 감상을 남겨주세요")
|
|
|
|
|
417 |
final_reflection = gr.Textbox(
|
418 |
label="마지막 감상",
|
419 |
placeholder="한 줄로 남겨주세요..."
|
420 |
)
|
421 |
save_final_btn = gr.Button("감상 남기기")
|
422 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
423 |
# 이벤트 연결
|
424 |
start_btn.click(
|
425 |
+
fn=lambda name: (
|
426 |
+
WORLDVIEW_MESSAGE if name.strip() else "이름을 입력해주세요",
|
427 |
+
gr.update(visible=True) if name.strip() else gr.update(),
|
428 |
+
{"user_name": name} if name.strip() else state
|
429 |
+
),
|
430 |
inputs=[name_input],
|
431 |
+
outputs=[worldview_display, tabs, state]
|
432 |
)
|
433 |
|
434 |
set_baseline_btn.click(
|
|
|
443 |
outputs=[state, reflections_display]
|
444 |
)
|
445 |
|
446 |
+
clear_btn.click(
|
447 |
+
fn=lambda: None,
|
448 |
+
outputs=[voice_input]
|
449 |
+
)
|
450 |
+
|
451 |
analyze_btn.click(
|
452 |
fn=analyze_voice,
|
453 |
inputs=[voice_input, state],
|
454 |
+
outputs=[state, transcribed_text, voice_emotion, text_emotion, final_prompt]
|
455 |
)
|
456 |
|
457 |
generate_btn.click(
|
458 |
+
fn=lambda p: generate_image_from_prompt(p)[0],
|
459 |
inputs=[final_prompt],
|
460 |
outputs=[result_image]
|
461 |
)
|
462 |
|
463 |
save_final_btn.click(
|
464 |
+
fn=lambda t, s: (db.save_wish(s["user_name"], t), "감상이 저장되었습니다."),
|
465 |
inputs=[final_reflection, state],
|
466 |
+
outputs=[gr.Markdown("")]
|
467 |
)
|
468 |
|
469 |
+
# 이미지 자동 사라짐
|
470 |
result_image.change(
|
471 |
fn=lambda: gr.update(value=None),
|
472 |
inputs=[],
|
|
|
479 |
if __name__ == "__main__":
|
480 |
demo = create_interface()
|
481 |
demo.launch(debug=True)
|
|