Update app.py
Browse files
app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
#
|
| 2 |
import gradio as gr
|
| 3 |
import numpy as np
|
| 4 |
import librosa
|
|
@@ -81,13 +81,20 @@ WORLDVIEW_MESSAGE = """
|
|
| 81 |
온천천 온천장역에서 장전역까지 걸으며 더 깊은 체험이 가능합니다.
|
| 82 |
"""
|
| 83 |
|
| 84 |
-
def calculate_baseline_features(
|
| 85 |
"""기준점 음성 특성 분석"""
|
| 86 |
try:
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
features = {
|
| 89 |
"energy": float(np.mean(librosa.feature.rms(y=y))),
|
| 90 |
-
"tempo": float(librosa.beat.tempo(y)[0]),
|
| 91 |
"pitch": float(np.mean(librosa.feature.zero_crossing_rate(y))),
|
| 92 |
"volume": float(np.mean(np.abs(y))),
|
| 93 |
"mfcc": librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13).mean(axis=1).tolist()
|
|
@@ -158,32 +165,38 @@ def map_acoustic_to_emotion(features, baseline_features=None):
|
|
| 158 |
|
| 159 |
return emotions
|
| 160 |
|
| 161 |
-
def analyze_voice(
|
| 162 |
"""통합 음성 분석"""
|
| 163 |
-
if
|
| 164 |
return state, "음성을 먼저 녹음해주세요.", "", "", ""
|
| 165 |
-
|
| 166 |
try:
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
# 음향학적 특성 분석
|
| 170 |
acoustic_features = {
|
| 171 |
"energy": float(np.mean(librosa.feature.rms(y=y))),
|
| 172 |
-
"tempo": float(librosa.beat.tempo(y)[0]),
|
| 173 |
"pitch": float(np.mean(librosa.feature.zero_crossing_rate(y))),
|
| 174 |
"volume": float(np.mean(np.abs(y)))
|
| 175 |
}
|
| 176 |
|
| 177 |
# 음성 감정 분석
|
| 178 |
voice_emotion = map_acoustic_to_emotion(acoustic_features, state.get("baseline_features"))
|
| 179 |
-
|
| 180 |
# 음성 인식
|
| 181 |
-
transcription = speech_recognizer(y)
|
| 182 |
text = transcription["text"]
|
| 183 |
-
|
| 184 |
# 텍스트 감정 분석
|
| 185 |
text_sentiment = text_analyzer(text)[0]
|
| 186 |
-
|
| 187 |
# 결과 포맷팅
|
| 188 |
voice_result = (
|
| 189 |
f"음성 감정: {voice_emotion['primary']} "
|
|
@@ -195,12 +208,15 @@ def analyze_voice(audio_path, state):
|
|
| 195 |
f"- 음높이 변화: {voice_emotion['details']['pitch_variation']}\n"
|
| 196 |
f"- 음성 크기: {voice_emotion['details']['voice_volume']}"
|
| 197 |
)
|
| 198 |
-
|
| 199 |
-
text_result = f"텍스트 감정 분석 (1-5): {text_sentiment['score']}"
|
| 200 |
-
|
| 201 |
# 프롬프트 생성
|
| 202 |
prompt = generate_detailed_prompt(text, voice_emotion, text_sentiment)
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
| 204 |
return state, text, voice_result, text_result, prompt
|
| 205 |
except Exception as e:
|
| 206 |
return state, f"오류 발생: {str(e)}", "", "", ""
|
|
@@ -228,8 +244,8 @@ def generate_detailed_prompt(text, emotions, text_sentiment):
|
|
| 228 |
prompt = f"한국 전통 민화 스타일의 추상화, {emotion_colors.get(emotions['primary'], '자연스러운 색상')} 기반. "
|
| 229 |
prompt += f"{visual_style}로 표현된 {emotions['primary']}의 감정. "
|
| 230 |
prompt += f"음성의 특징({', '.join(emotions['characteristics'])})을 화면의 동적 요소로 표현. "
|
| 231 |
-
prompt += f"발화 내용 '{text}'에서 느껴지는 감정(
|
| 232 |
-
|
| 233 |
return prompt
|
| 234 |
|
| 235 |
def generate_image_from_prompt(prompt):
|
|
@@ -238,8 +254,8 @@ def generate_image_from_prompt(prompt):
|
|
| 238 |
try:
|
| 239 |
if not prompt:
|
| 240 |
print("No prompt provided")
|
| 241 |
-
return None
|
| 242 |
-
|
| 243 |
response = requests.post(
|
| 244 |
API_URL,
|
| 245 |
headers=headers,
|
|
@@ -252,7 +268,7 @@ def generate_image_from_prompt(prompt):
|
|
| 252 |
}
|
| 253 |
}
|
| 254 |
)
|
| 255 |
-
|
| 256 |
if response.status_code == 200:
|
| 257 |
print("Image generated successfully")
|
| 258 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
@@ -269,33 +285,27 @@ def generate_image_from_prompt(prompt):
|
|
| 269 |
print(f"Error generating image: {str(e)}")
|
| 270 |
return None, None
|
| 271 |
|
| 272 |
-
def handle_image_timeout(result_image):
|
| 273 |
-
"""이미지 자동 사라짐을 처리하는 함수"""
|
| 274 |
-
time.sleep(IMAGE_DISPLAY_TIME)
|
| 275 |
-
result_image.update(value=None)
|
| 276 |
-
|
| 277 |
def save_reflection(text, state):
|
| 278 |
"""감상 저장"""
|
| 279 |
if not text.strip():
|
| 280 |
-
return state, state
|
| 281 |
-
|
| 282 |
try:
|
| 283 |
current_time = datetime.now().strftime("%H:%M:%S")
|
| 284 |
sentiment = text_analyzer(text)[0]
|
| 285 |
new_reflection = [current_time, text, f"{sentiment['label']} ({sentiment['score']:.2f})"]
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
state
|
| 291 |
-
return state, state["reflections"]
|
| 292 |
except Exception as e:
|
| 293 |
print(f"Error in save_reflection: {str(e)}")
|
| 294 |
return state, []
|
| 295 |
|
| 296 |
def create_interface():
|
| 297 |
db = SimpleDB()
|
| 298 |
-
|
| 299 |
with gr.Blocks(theme=gr.themes.Soft()) as app:
|
| 300 |
state = gr.State({
|
| 301 |
"user_name": "",
|
|
@@ -324,7 +334,8 @@ def create_interface():
|
|
| 324 |
gr.Markdown("'당신의 건강과 행복이 늘 가득하기를'")
|
| 325 |
baseline_audio = gr.Audio(
|
| 326 |
label="축원 문장 녹음하기",
|
| 327 |
-
|
|
|
|
| 328 |
)
|
| 329 |
set_baseline_btn = gr.Button("기준점 설정 완료")
|
| 330 |
baseline_status = gr.Markdown("")
|
|
@@ -334,7 +345,7 @@ def create_interface():
|
|
| 334 |
play_music_btn = gr.Button("온천천의 소리 듣기")
|
| 335 |
with gr.Row():
|
| 336 |
audio = gr.Audio(
|
| 337 |
-
value=
|
| 338 |
type="filepath",
|
| 339 |
label="온천천의 소리",
|
| 340 |
interactive=False,
|
|
@@ -348,7 +359,10 @@ def create_interface():
|
|
| 348 |
save_btn = gr.Button("감상 저장하기")
|
| 349 |
reflections_display = gr.Dataframe(
|
| 350 |
headers=["시간", "감상", "감정 분석"],
|
| 351 |
-
label="기록된 감상들"
|
|
|
|
|
|
|
|
|
|
| 352 |
)
|
| 353 |
|
| 354 |
with gr.Tab("기원"):
|
|
@@ -357,11 +371,12 @@ def create_interface():
|
|
| 357 |
with gr.Column():
|
| 358 |
voice_input = gr.Audio(
|
| 359 |
label="소원을 나누고 싶은 마음을 말해주세요",
|
| 360 |
-
|
|
|
|
| 361 |
)
|
| 362 |
clear_btn = gr.Button("녹음 지우기")
|
| 363 |
analyze_btn = gr.Button("소원 분석하기")
|
| 364 |
-
|
| 365 |
with gr.Column():
|
| 366 |
transcribed_text = gr.Textbox(label="인식된 텍스트")
|
| 367 |
voice_emotion = gr.Textbox(label="음성 감정 분석")
|
|
@@ -376,40 +391,28 @@ def create_interface():
|
|
| 376 |
)
|
| 377 |
generate_btn = gr.Button("마음의 그림 그리기")
|
| 378 |
result_image = gr.Image(label="생성된 이미지")
|
| 379 |
-
|
| 380 |
gr.Markdown("## 마지막 감상을 남겨주세요")
|
| 381 |
final_reflection = gr.Textbox(
|
| 382 |
label="마지막 감상",
|
| 383 |
placeholder="한 줄로 남겨주세요..."
|
| 384 |
)
|
| 385 |
save_final_btn = gr.Button("감상 남기기")
|
| 386 |
-
|
| 387 |
-
def delayed_image_clear():
|
| 388 |
-
time.sleep(IMAGE_DISPLAY_TIME)
|
| 389 |
-
return gr.update(value=None)
|
| 390 |
-
|
| 391 |
-
def on_image_generated(prompt):
|
| 392 |
-
image_content = generate_image_from_prompt(prompt)
|
| 393 |
-
if image_content:
|
| 394 |
-
# Start a timer to clear the image
|
| 395 |
-
timer = threading.Timer(IMAGE_DISPLAY_TIME, lambda: result_image.update(value=None))
|
| 396 |
-
timer.start()
|
| 397 |
-
return image_content
|
| 398 |
-
return None
|
| 399 |
|
| 400 |
# 이벤트 연결
|
| 401 |
start_btn.click(
|
| 402 |
-
fn=lambda name: (
|
| 403 |
WORLDVIEW_MESSAGE if name.strip() else "이름을 입력해주세요",
|
| 404 |
gr.update(visible=True) if name.strip() else gr.update(),
|
| 405 |
-
{"user_name": name} if name.strip() else state
|
| 406 |
),
|
| 407 |
-
inputs=[name_input],
|
| 408 |
outputs=[worldview_display, tabs, state]
|
| 409 |
)
|
| 410 |
|
| 411 |
set_baseline_btn.click(
|
| 412 |
-
fn=lambda x, s: ({"baseline_features": calculate_baseline_features(x)}, "기준점이 설정되었습니다."),
|
| 413 |
inputs=[baseline_audio, state],
|
| 414 |
outputs=[state, baseline_status]
|
| 415 |
)
|
|
@@ -432,19 +435,29 @@ def create_interface():
|
|
| 432 |
)
|
| 433 |
|
| 434 |
generate_btn.click(
|
| 435 |
-
fn=
|
| 436 |
inputs=[final_prompt],
|
| 437 |
outputs=[result_image]
|
| 438 |
)
|
| 439 |
|
| 440 |
save_final_btn.click(
|
| 441 |
-
fn=lambda t, s: (
|
|
|
|
|
|
|
|
|
|
| 442 |
inputs=[final_reflection, state],
|
| 443 |
-
outputs=[
|
| 444 |
)
|
| 445 |
|
| 446 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
|
|
|
|
| 448 |
if __name__ == "__main__":
|
| 449 |
demo = create_interface()
|
| 450 |
-
demo.launch(debug=True)
|
|
|
|
|
|
| 1 |
+
# 라이브러리 임포트 및 초기 설정
|
| 2 |
import gradio as gr
|
| 3 |
import numpy as np
|
| 4 |
import librosa
|
|
|
|
| 81 |
온천천 온천장역에서 장전역까지 걸으며 더 깊은 체험이 가능합니다.
|
| 82 |
"""
|
| 83 |
|
| 84 |
+
def calculate_baseline_features(audio_data):
|
| 85 |
"""기준점 음성 특성 분석"""
|
| 86 |
try:
|
| 87 |
+
if isinstance(audio_data, tuple):
|
| 88 |
+
sr, y = audio_data
|
| 89 |
+
elif isinstance(audio_data, str):
|
| 90 |
+
y, sr = librosa.load(audio_data, sr=16000)
|
| 91 |
+
else:
|
| 92 |
+
print("Unsupported audio format")
|
| 93 |
+
return None
|
| 94 |
+
|
| 95 |
features = {
|
| 96 |
"energy": float(np.mean(librosa.feature.rms(y=y))),
|
| 97 |
+
"tempo": float(librosa.beat.tempo(y, sr=sr)[0]),
|
| 98 |
"pitch": float(np.mean(librosa.feature.zero_crossing_rate(y))),
|
| 99 |
"volume": float(np.mean(np.abs(y))),
|
| 100 |
"mfcc": librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13).mean(axis=1).tolist()
|
|
|
|
| 165 |
|
| 166 |
return emotions
|
| 167 |
|
| 168 |
+
def analyze_voice(audio_data, state):
|
| 169 |
"""통합 음성 분석"""
|
| 170 |
+
if audio_data is None:
|
| 171 |
return state, "음성을 먼저 녹음해주세요.", "", "", ""
|
| 172 |
+
|
| 173 |
try:
|
| 174 |
+
if isinstance(audio_data, tuple):
|
| 175 |
+
sr, y = audio_data
|
| 176 |
+
elif isinstance(audio_data, str):
|
| 177 |
+
y, sr = librosa.load(audio_data, sr=16000)
|
| 178 |
+
else:
|
| 179 |
+
print("Unsupported audio format")
|
| 180 |
+
return state, "오디오 형식을 지원하지 않습니다.", "", "", ""
|
| 181 |
+
|
| 182 |
# 음향학적 특성 분석
|
| 183 |
acoustic_features = {
|
| 184 |
"energy": float(np.mean(librosa.feature.rms(y=y))),
|
| 185 |
+
"tempo": float(librosa.beat.tempo(y, sr=sr)[0]),
|
| 186 |
"pitch": float(np.mean(librosa.feature.zero_crossing_rate(y))),
|
| 187 |
"volume": float(np.mean(np.abs(y)))
|
| 188 |
}
|
| 189 |
|
| 190 |
# 음성 감정 분석
|
| 191 |
voice_emotion = map_acoustic_to_emotion(acoustic_features, state.get("baseline_features"))
|
| 192 |
+
|
| 193 |
# 음성 인식
|
| 194 |
+
transcription = speech_recognizer(y, sampling_rate=sr)
|
| 195 |
text = transcription["text"]
|
| 196 |
+
|
| 197 |
# 텍스트 감정 분석
|
| 198 |
text_sentiment = text_analyzer(text)[0]
|
| 199 |
+
|
| 200 |
# 결과 포맷팅
|
| 201 |
voice_result = (
|
| 202 |
f"음성 감정: {voice_emotion['primary']} "
|
|
|
|
| 208 |
f"- 음높이 변화: {voice_emotion['details']['pitch_variation']}\n"
|
| 209 |
f"- 음성 크기: {voice_emotion['details']['voice_volume']}"
|
| 210 |
)
|
| 211 |
+
|
| 212 |
+
text_result = f"텍스트 감정 분석 (1-5): {text_sentiment['label']} (점수: {text_sentiment['score']:.2f})"
|
| 213 |
+
|
| 214 |
# 프롬프트 생성
|
| 215 |
prompt = generate_detailed_prompt(text, voice_emotion, text_sentiment)
|
| 216 |
+
|
| 217 |
+
# 상태 업데이트
|
| 218 |
+
state = {**state, "final_prompt": prompt}
|
| 219 |
+
|
| 220 |
return state, text, voice_result, text_result, prompt
|
| 221 |
except Exception as e:
|
| 222 |
return state, f"오류 발생: {str(e)}", "", "", ""
|
|
|
|
| 244 |
prompt = f"한국 전통 민화 스타일의 추상화, {emotion_colors.get(emotions['primary'], '자연스러운 색상')} 기반. "
|
| 245 |
prompt += f"{visual_style}로 표현된 {emotions['primary']}의 감정. "
|
| 246 |
prompt += f"음성의 특징({', '.join(emotions['characteristics'])})을 화면의 동적 요소로 표현. "
|
| 247 |
+
prompt += f"발화 내용 '{text}'에서 느껴지는 감정({text_sentiment['label']} - 점수: {text_sentiment['score']:.2f})을 은유적 이미지로 담아내기."
|
| 248 |
+
|
| 249 |
return prompt
|
| 250 |
|
| 251 |
def generate_image_from_prompt(prompt):
|
|
|
|
| 254 |
try:
|
| 255 |
if not prompt:
|
| 256 |
print("No prompt provided")
|
| 257 |
+
return None, None
|
| 258 |
+
|
| 259 |
response = requests.post(
|
| 260 |
API_URL,
|
| 261 |
headers=headers,
|
|
|
|
| 268 |
}
|
| 269 |
}
|
| 270 |
)
|
| 271 |
+
|
| 272 |
if response.status_code == 200:
|
| 273 |
print("Image generated successfully")
|
| 274 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
| 285 |
print(f"Error generating image: {str(e)}")
|
| 286 |
return None, None
|
| 287 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
def save_reflection(text, state):
|
| 289 |
"""감상 저장"""
|
| 290 |
if not text.strip():
|
| 291 |
+
return state, state.get("reflections", [])
|
| 292 |
+
|
| 293 |
try:
|
| 294 |
current_time = datetime.now().strftime("%H:%M:%S")
|
| 295 |
sentiment = text_analyzer(text)[0]
|
| 296 |
new_reflection = [current_time, text, f"{sentiment['label']} ({sentiment['score']:.2f})"]
|
| 297 |
+
|
| 298 |
+
reflections = state.get("reflections", [])
|
| 299 |
+
reflections.append(new_reflection)
|
| 300 |
+
state = {**state, "reflections": reflections}
|
| 301 |
+
return state, reflections
|
|
|
|
| 302 |
except Exception as e:
|
| 303 |
print(f"Error in save_reflection: {str(e)}")
|
| 304 |
return state, []
|
| 305 |
|
| 306 |
def create_interface():
|
| 307 |
db = SimpleDB()
|
| 308 |
+
|
| 309 |
with gr.Blocks(theme=gr.themes.Soft()) as app:
|
| 310 |
state = gr.State({
|
| 311 |
"user_name": "",
|
|
|
|
| 334 |
gr.Markdown("'당신의 건강과 행복이 늘 가득하기를'")
|
| 335 |
baseline_audio = gr.Audio(
|
| 336 |
label="축원 문장 녹음하기",
|
| 337 |
+
source="microphone",
|
| 338 |
+
type="numpy"
|
| 339 |
)
|
| 340 |
set_baseline_btn = gr.Button("기준점 설정 완료")
|
| 341 |
baseline_status = gr.Markdown("")
|
|
|
|
| 345 |
play_music_btn = gr.Button("온천천의 소리 듣기")
|
| 346 |
with gr.Row():
|
| 347 |
audio = gr.Audio(
|
| 348 |
+
value="oncheoncheon_sound.wav",
|
| 349 |
type="filepath",
|
| 350 |
label="온천천의 소리",
|
| 351 |
interactive=False,
|
|
|
|
| 359 |
save_btn = gr.Button("감상 저장하기")
|
| 360 |
reflections_display = gr.Dataframe(
|
| 361 |
headers=["시간", "감상", "감정 분석"],
|
| 362 |
+
label="기록된 감상들",
|
| 363 |
+
datatype=["str", "str", "str"],
|
| 364 |
+
row_count=(0, "dynamic"),
|
| 365 |
+
col_count=(3, "fixed")
|
| 366 |
)
|
| 367 |
|
| 368 |
with gr.Tab("기원"):
|
|
|
|
| 371 |
with gr.Column():
|
| 372 |
voice_input = gr.Audio(
|
| 373 |
label="소원을 나누고 싶은 마음을 말해주세요",
|
| 374 |
+
source="microphone",
|
| 375 |
+
type="numpy"
|
| 376 |
)
|
| 377 |
clear_btn = gr.Button("녹음 지우기")
|
| 378 |
analyze_btn = gr.Button("소원 분석하기")
|
| 379 |
+
|
| 380 |
with gr.Column():
|
| 381 |
transcribed_text = gr.Textbox(label="인식된 텍스트")
|
| 382 |
voice_emotion = gr.Textbox(label="음성 감정 분석")
|
|
|
|
| 391 |
)
|
| 392 |
generate_btn = gr.Button("마음의 그림 그리기")
|
| 393 |
result_image = gr.Image(label="생성된 이미지")
|
| 394 |
+
|
| 395 |
gr.Markdown("## 마지막 감상을 남겨주세요")
|
| 396 |
final_reflection = gr.Textbox(
|
| 397 |
label="마지막 감상",
|
| 398 |
placeholder="한 줄로 남겨주세요..."
|
| 399 |
)
|
| 400 |
save_final_btn = gr.Button("감상 남기기")
|
| 401 |
+
save_final_status = gr.Markdown("")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
|
| 403 |
# 이벤트 연결
|
| 404 |
start_btn.click(
|
| 405 |
+
fn=lambda name, state: (
|
| 406 |
WORLDVIEW_MESSAGE if name.strip() else "이름을 입력해주세요",
|
| 407 |
gr.update(visible=True) if name.strip() else gr.update(),
|
| 408 |
+
{**state, "user_name": name} if name.strip() else state
|
| 409 |
),
|
| 410 |
+
inputs=[name_input, state],
|
| 411 |
outputs=[worldview_display, tabs, state]
|
| 412 |
)
|
| 413 |
|
| 414 |
set_baseline_btn.click(
|
| 415 |
+
fn=lambda x, s: ({**s, "baseline_features": calculate_baseline_features(x)}, "기준점이 설정되었습니다."),
|
| 416 |
inputs=[baseline_audio, state],
|
| 417 |
outputs=[state, baseline_status]
|
| 418 |
)
|
|
|
|
| 435 |
)
|
| 436 |
|
| 437 |
generate_btn.click(
|
| 438 |
+
fn=lambda prompt: generate_image_from_prompt(prompt)[0],
|
| 439 |
inputs=[final_prompt],
|
| 440 |
outputs=[result_image]
|
| 441 |
)
|
| 442 |
|
| 443 |
save_final_btn.click(
|
| 444 |
+
fn=lambda t, s: (
|
| 445 |
+
db.save_wish(s.get("user_name", "익명"), t),
|
| 446 |
+
"감상이 저장되었습니다."
|
| 447 |
+
),
|
| 448 |
inputs=[final_reflection, state],
|
| 449 |
+
outputs=[save_final_status]
|
| 450 |
)
|
| 451 |
|
| 452 |
+
play_music_btn.click(
|
| 453 |
+
fn=lambda: "oncheoncheon_sound.wav",
|
| 454 |
+
outputs=[audio]
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
return app
|
| 458 |
|
| 459 |
+
# 파일 맨 아래 부분
|
| 460 |
if __name__ == "__main__":
|
| 461 |
demo = create_interface()
|
| 462 |
+
demo.launch(debug=True, share=True)
|
| 463 |
+
|