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
|
@@ -25,14 +27,35 @@ text_analyzer = pipeline(
|
|
25 |
model="nlptown/bert-base-multilingual-uncased-sentiment"
|
26 |
)
|
27 |
|
28 |
-
def
|
29 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
# 음성 특성 정규화
|
31 |
-
energy_norm = min(features["energy"] * 100, 100)
|
32 |
-
tempo_norm = min(features["tempo"] / 200, 1)
|
33 |
-
pitch_norm = min(features["pitch"] * 2, 1)
|
34 |
-
|
35 |
-
#
|
|
|
|
|
|
|
|
|
|
|
36 |
emotions = {
|
37 |
"primary": "",
|
38 |
"intensity": energy_norm,
|
@@ -40,8 +63,8 @@ def map_acoustic_to_emotion(features):
|
|
40 |
"secondary": "",
|
41 |
"characteristics": []
|
42 |
}
|
43 |
-
|
44 |
-
#
|
45 |
if energy_norm > 70:
|
46 |
if tempo_norm > 0.6:
|
47 |
emotions["primary"] = "기쁨/열정"
|
@@ -50,7 +73,6 @@ def map_acoustic_to_emotion(features):
|
|
50 |
emotions["primary"] = "분노/강조"
|
51 |
emotions["characteristics"].append("강한 음성 강도")
|
52 |
emotions["confidence"] = energy_norm / 100
|
53 |
-
|
54 |
elif pitch_norm > 0.6:
|
55 |
if energy_norm > 50:
|
56 |
emotions["primary"] = "놀람/흥분"
|
@@ -59,7 +81,6 @@ def map_acoustic_to_emotion(features):
|
|
59 |
emotions["primary"] = "관심/호기심"
|
60 |
emotions["characteristics"].append("음고 변화가 큼")
|
61 |
emotions["confidence"] = pitch_norm
|
62 |
-
|
63 |
elif energy_norm < 30:
|
64 |
if tempo_norm < 0.4:
|
65 |
emotions["primary"] = "슬픔/우울"
|
@@ -68,7 +89,6 @@ def map_acoustic_to_emotion(features):
|
|
68 |
emotions["primary"] = "피로/무기력"
|
69 |
emotions["characteristics"].append("낮은 에너지 레벨")
|
70 |
emotions["confidence"] = (30 - energy_norm) / 30
|
71 |
-
|
72 |
else:
|
73 |
if tempo_norm > 0.5:
|
74 |
emotions["primary"] = "평온/안정"
|
@@ -77,15 +97,14 @@ def map_acoustic_to_emotion(features):
|
|
77 |
emotions["primary"] = "차분/진지"
|
78 |
emotions["characteristics"].append("안정적인 음성 특성")
|
79 |
emotions["confidence"] = 0.5
|
80 |
-
|
81 |
-
# 음성 특성 상세 분석
|
82 |
emotions["details"] = {
|
83 |
"energy_level": f"{energy_norm:.1f}%",
|
84 |
"speech_rate": f"{'빠름' if tempo_norm > 0.6 else '보통' if tempo_norm > 0.4 else '느림'}",
|
85 |
"pitch_variation": f"{'높음' if pitch_norm > 0.6 else '보통' if pitch_norm > 0.3 else '낮음'}",
|
86 |
"voice_volume": f"{'큼' if features['volume'] > 0.7 else '보통' if features['volume'] > 0.3 else '작음'}"
|
87 |
}
|
88 |
-
|
89 |
return emotions
|
90 |
|
91 |
def generate_image_from_prompt(prompt):
|
@@ -116,7 +135,6 @@ def generate_image_from_prompt(prompt):
|
|
116 |
print(f"Error: {response.status_code}")
|
117 |
print(f"Response: {response.text}")
|
118 |
return None
|
119 |
-
|
120 |
except Exception as e:
|
121 |
print(f"Error generating image: {str(e)}")
|
122 |
return None
|
@@ -133,8 +151,7 @@ def generate_detailed_prompt(text, emotions, text_sentiment):
|
|
133 |
"평온/안정": "부드러운 초록과 베이지",
|
134 |
"차분/진지": "차분한 남색과 깊은 보라"
|
135 |
}
|
136 |
-
|
137 |
-
# 감정 강도에 따른 시각적 표현
|
138 |
if emotions["intensity"] > 70:
|
139 |
visual_style = "역동적인 붓질과 강한 대비"
|
140 |
elif emotions["intensity"] > 40:
|
@@ -142,7 +159,6 @@ def generate_detailed_prompt(text, emotions, text_sentiment):
|
|
142 |
else:
|
143 |
visual_style = "부드러운 그라데이션과 차분한 톤"
|
144 |
|
145 |
-
# 프롬프트 구성
|
146 |
prompt = f"한국 전통 민화 스타일의 추상화, {emotion_colors.get(emotions['primary'], '자연스러운 색상')} 기반. "
|
147 |
prompt += f"{visual_style}로 표현된 {emotions['primary']}의 감정. "
|
148 |
prompt += f"음성의 특징({', '.join(emotions['characteristics'])})을 화면의 동적 요소로 표현. "
|
@@ -154,11 +170,12 @@ def create_interface():
|
|
154 |
with gr.Blocks(theme=gr.themes.Soft()) as app:
|
155 |
state = gr.State({
|
156 |
"user_name": "",
|
|
|
157 |
"reflections": [],
|
158 |
"voice_analysis": None,
|
159 |
"final_prompt": ""
|
160 |
})
|
161 |
-
|
162 |
# 헤더
|
163 |
header = gr.Markdown("# 디지털 굿판")
|
164 |
user_display = gr.Markdown("")
|
@@ -166,10 +183,22 @@ z
|
|
166 |
with gr.Tabs() as tabs:
|
167 |
# 입장
|
168 |
with gr.Tab("입장"):
|
169 |
-
gr.Markdown("
|
170 |
name_input = gr.Textbox(label="이름을 알려주세요")
|
171 |
start_btn = gr.Button("여정 시작하기")
|
172 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
173 |
# 청신
|
174 |
with gr.Tab("청신"):
|
175 |
with gr.Row():
|
@@ -192,25 +221,6 @@ z
|
|
192 |
label="기록된 감상들"
|
193 |
)
|
194 |
|
195 |
-
def save_reflection(text, state):
|
196 |
-
"""감상 저장"""
|
197 |
-
if not text.strip():
|
198 |
-
return state, state["reflections"]
|
199 |
-
|
200 |
-
try:
|
201 |
-
current_time = datetime.now().strftime("%H:%M:%S")
|
202 |
-
sentiment = text_analyzer(text)[0]
|
203 |
-
new_reflection = [current_time, text, f"{sentiment['label']} ({sentiment['score']:.2f})"]
|
204 |
-
|
205 |
-
if "reflections" not in state:
|
206 |
-
state["reflections"] = []
|
207 |
-
|
208 |
-
state["reflections"].append(new_reflection)
|
209 |
-
return state, state["reflections"]
|
210 |
-
except Exception as e:
|
211 |
-
print(f"Error in save_reflection: {str(e)}")
|
212 |
-
return state, []
|
213 |
-
|
214 |
# 기원
|
215 |
with gr.Tab("기원"):
|
216 |
gr.Markdown("## 기원 - 목소리로 전하기")
|
@@ -254,10 +264,52 @@ z
|
|
254 |
type="pil"
|
255 |
)
|
256 |
|
257 |
-
# 인터페이스 함수들
|
258 |
def start_journey(name):
|
259 |
"""여정 시작"""
|
260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
261 |
|
262 |
def clear_voice_input():
|
263 |
"""음성 입력 초기화"""
|
@@ -271,7 +323,6 @@ z
|
|
271 |
try:
|
272 |
y, sr = librosa.load(audio_path, sr=16000)
|
273 |
|
274 |
-
# 음향학적 특성 분석
|
275 |
acoustic_features = {
|
276 |
"energy": float(np.mean(librosa.feature.rms(y=y))),
|
277 |
"tempo": float(librosa.beat.tempo(y)[0]),
|
@@ -279,17 +330,14 @@ z
|
|
279 |
"volume": float(np.mean(np.abs(y)))
|
280 |
}
|
281 |
|
282 |
-
#
|
283 |
-
|
|
|
284 |
|
285 |
-
# 음성 인식
|
286 |
transcription = speech_recognizer(y)
|
287 |
text = transcription["text"]
|
288 |
-
|
289 |
-
# 텍스트 감정 분석
|
290 |
text_sentiment = text_analyzer(text)[0]
|
291 |
|
292 |
-
# 결과 포맷팅
|
293 |
voice_result = (
|
294 |
f"음성 감정: {emotions['primary']} "
|
295 |
f"(강도: {emotions['intensity']:.1f}%, 신뢰도: {emotions['confidence']:.2f})\n"
|
@@ -301,9 +349,11 @@ z
|
|
301 |
f"- 음성 크기: {emotions['details']['voice_volume']}"
|
302 |
)
|
303 |
|
304 |
-
|
|
|
|
|
305 |
|
306 |
-
|
307 |
prompt = generate_detailed_prompt(text, emotions, text_sentiment)
|
308 |
|
309 |
return state, text, voice_result, text_result, prompt
|
@@ -312,11 +362,17 @@ z
|
|
312 |
|
313 |
# 이벤트 연결
|
314 |
start_btn.click(
|
315 |
-
fn=
|
316 |
inputs=[name_input],
|
317 |
outputs=[user_display, tabs]
|
318 |
)
|
319 |
|
|
|
|
|
|
|
|
|
|
|
|
|
320 |
save_btn.click(
|
321 |
fn=save_reflection,
|
322 |
inputs=[reflection_input, state],
|
@@ -341,6 +397,8 @@ z
|
|
341 |
outputs=[result_image]
|
342 |
)
|
343 |
|
|
|
|
|
344 |
if __name__ == "__main__":
|
345 |
demo = create_interface()
|
346 |
demo.launch(debug=True)
|
|
|
1 |
+
```python
|
2 |
+
# 1/2 시작 - app.py
|
3 |
import gradio as gr
|
4 |
import numpy as np
|
5 |
import librosa
|
|
|
27 |
model="nlptown/bert-base-multilingual-uncased-sentiment"
|
28 |
)
|
29 |
|
30 |
+
def calculate_baseline_features(audio_path):
|
31 |
+
"""기준점 음성 특성 분석"""
|
32 |
+
try:
|
33 |
+
y, sr = librosa.load(audio_path, sr=16000)
|
34 |
+
features = {
|
35 |
+
"energy": float(np.mean(librosa.feature.rms(y=y))),
|
36 |
+
"tempo": float(librosa.beat.tempo(y)[0]),
|
37 |
+
"pitch": float(np.mean(librosa.feature.zero_crossing_rate(y))),
|
38 |
+
"volume": float(np.mean(np.abs(y))),
|
39 |
+
"mfcc": librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13).mean(axis=1).tolist()
|
40 |
+
}
|
41 |
+
return features
|
42 |
+
except Exception as e:
|
43 |
+
print(f"Error calculating baseline: {str(e)}")
|
44 |
+
return None
|
45 |
+
|
46 |
+
def map_acoustic_to_emotion(features, baseline_features=None):
|
47 |
+
"""음향학적 특성을 감정으로 매핑 (기준점 대비)"""
|
48 |
# 음성 특성 정규화
|
49 |
+
energy_norm = min(features["energy"] * 100, 100)
|
50 |
+
tempo_norm = min(features["tempo"] / 200, 1)
|
51 |
+
pitch_norm = min(features["pitch"] * 2, 1)
|
52 |
+
|
53 |
+
# 기준점이 있는 경우 상대적 변화 계산
|
54 |
+
if baseline_features:
|
55 |
+
energy_norm = (features["energy"] / baseline_features["energy"]) * 50
|
56 |
+
tempo_norm = (features["tempo"] / baseline_features["tempo"])
|
57 |
+
pitch_norm = (features["pitch"] / baseline_features["pitch"])
|
58 |
+
|
59 |
emotions = {
|
60 |
"primary": "",
|
61 |
"intensity": energy_norm,
|
|
|
63 |
"secondary": "",
|
64 |
"characteristics": []
|
65 |
}
|
66 |
+
|
67 |
+
# 감정 매핑 로직
|
68 |
if energy_norm > 70:
|
69 |
if tempo_norm > 0.6:
|
70 |
emotions["primary"] = "기쁨/열정"
|
|
|
73 |
emotions["primary"] = "분노/강조"
|
74 |
emotions["characteristics"].append("강한 음성 강도")
|
75 |
emotions["confidence"] = energy_norm / 100
|
|
|
76 |
elif pitch_norm > 0.6:
|
77 |
if energy_norm > 50:
|
78 |
emotions["primary"] = "놀람/흥분"
|
|
|
81 |
emotions["primary"] = "관심/호기심"
|
82 |
emotions["characteristics"].append("음고 변화가 큼")
|
83 |
emotions["confidence"] = pitch_norm
|
|
|
84 |
elif energy_norm < 30:
|
85 |
if tempo_norm < 0.4:
|
86 |
emotions["primary"] = "슬픔/우울"
|
|
|
89 |
emotions["primary"] = "피로/무기력"
|
90 |
emotions["characteristics"].append("낮은 에너지 레벨")
|
91 |
emotions["confidence"] = (30 - energy_norm) / 30
|
|
|
92 |
else:
|
93 |
if tempo_norm > 0.5:
|
94 |
emotions["primary"] = "평온/안정"
|
|
|
97 |
emotions["primary"] = "차분/진지"
|
98 |
emotions["characteristics"].append("안정적인 음성 특성")
|
99 |
emotions["confidence"] = 0.5
|
100 |
+
|
|
|
101 |
emotions["details"] = {
|
102 |
"energy_level": f"{energy_norm:.1f}%",
|
103 |
"speech_rate": f"{'빠름' if tempo_norm > 0.6 else '보통' if tempo_norm > 0.4 else '느림'}",
|
104 |
"pitch_variation": f"{'높음' if pitch_norm > 0.6 else '보통' if pitch_norm > 0.3 else '낮음'}",
|
105 |
"voice_volume": f"{'큼' if features['volume'] > 0.7 else '보통' if features['volume'] > 0.3 else '작음'}"
|
106 |
}
|
107 |
+
|
108 |
return emotions
|
109 |
|
110 |
def generate_image_from_prompt(prompt):
|
|
|
135 |
print(f"Error: {response.status_code}")
|
136 |
print(f"Response: {response.text}")
|
137 |
return None
|
|
|
138 |
except Exception as e:
|
139 |
print(f"Error generating image: {str(e)}")
|
140 |
return None
|
|
|
151 |
"평온/안정": "부드러운 초록과 베이지",
|
152 |
"차분/진지": "차분한 남색과 깊은 보라"
|
153 |
}
|
154 |
+
|
|
|
155 |
if emotions["intensity"] > 70:
|
156 |
visual_style = "역동적인 붓질과 강한 대비"
|
157 |
elif emotions["intensity"] > 40:
|
|
|
159 |
else:
|
160 |
visual_style = "부드러운 그라데이션과 차분한 톤"
|
161 |
|
|
|
162 |
prompt = f"한국 전통 민화 스타일의 추상화, {emotion_colors.get(emotions['primary'], '자연스러운 색상')} 기반. "
|
163 |
prompt += f"{visual_style}로 표현된 {emotions['primary']}의 감정. "
|
164 |
prompt += f"음성의 특징({', '.join(emotions['characteristics'])})을 화면의 동적 요소로 표현. "
|
|
|
170 |
with gr.Blocks(theme=gr.themes.Soft()) as app:
|
171 |
state = gr.State({
|
172 |
"user_name": "",
|
173 |
+
"baseline_features": None, # 개인화된 기준점 저장
|
174 |
"reflections": [],
|
175 |
"voice_analysis": None,
|
176 |
"final_prompt": ""
|
177 |
})
|
178 |
+
|
179 |
# 헤더
|
180 |
header = gr.Markdown("# 디지털 굿판")
|
181 |
user_display = gr.Markdown("")
|
|
|
183 |
with gr.Tabs() as tabs:
|
184 |
# 입장
|
185 |
with gr.Tab("입장"):
|
186 |
+
gr.Markdown("### 디지털 굿판에 오신 것을 환영합니다")
|
187 |
name_input = gr.Textbox(label="이름을 알려주세요")
|
188 |
start_btn = gr.Button("여정 시작하기")
|
189 |
|
190 |
+
# 기준 설정
|
191 |
+
with gr.Tab("기준 설정"):
|
192 |
+
gr.Markdown("### 축원의 문장을 평온한 마음으로 읽어주세요")
|
193 |
+
gr.Markdown("'당신의 건강과 행복이 늘 가득하기를'")
|
194 |
+
baseline_audio = gr.Audio(
|
195 |
+
label="축원 문장 녹음하기",
|
196 |
+
sources=["microphone"],
|
197 |
+
type="filepath"
|
198 |
+
)
|
199 |
+
set_baseline_btn = gr.Button("기준점 설정 완료")
|
200 |
+
baseline_status = gr.Markdown("")
|
201 |
+
|
202 |
# 청신
|
203 |
with gr.Tab("청신"):
|
204 |
with gr.Row():
|
|
|
221 |
label="기록된 감상들"
|
222 |
)
|
223 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
224 |
# 기원
|
225 |
with gr.Tab("기원"):
|
226 |
gr.Markdown("## 기원 - 목소리로 전하기")
|
|
|
264 |
type="pil"
|
265 |
)
|
266 |
|
|
|
267 |
def start_journey(name):
|
268 |
"""여정 시작"""
|
269 |
+
welcome_text = f"""
|
270 |
+
# 환영합니다, {name}님의 디지털 굿판
|
271 |
+
|
272 |
+
## 굿판의 세계관 🌌
|
273 |
+
|
274 |
+
디지털 굿판은 현대 도시 속에서 잊혀진 전통 굿의 정수를 담아낸 **디지털 의례의 공간**입니다.
|
275 |
+
이곳에서는 사람들의 목소리와 감정을 통해 **영적 교감**을 나누고, **자연과 도시의 에너지**를 연결하며,
|
276 |
+
평온함과 치유를 경험하게 됩니다.
|
277 |
+
|
278 |
+
## 여정을 시작하며 🚀
|
279 |
+
|
280 |
+
먼저, 평온한 마음으로 축원의 문장을 읽어주세요.
|
281 |
+
이는 당신의 감정을 더 정확하게 이해하기 위한 기준점이 될 것입니다.
|
282 |
+
"""
|
283 |
+
return welcome_text, gr.update(selected="기준 설정")
|
284 |
+
|
285 |
+
def set_baseline(audio_path, state):
|
286 |
+
"""기준점 설정"""
|
287 |
+
if audio_path is None:
|
288 |
+
return state, "먼저 축원 문장을 녹음해주세요."
|
289 |
+
|
290 |
+
baseline_features = calculate_baseline_features(audio_path)
|
291 |
+
state = state.copy()
|
292 |
+
state["baseline_features"] = baseline_features
|
293 |
+
return state, "기준점이 설정되었습니다. 이제 청신 단계로 이동하실 수 있습니다.", gr.update(selected="청신")
|
294 |
+
|
295 |
+
def save_reflection(text, state):
|
296 |
+
"""감상 저장"""
|
297 |
+
if not text.strip():
|
298 |
+
return state, state["reflections"]
|
299 |
+
|
300 |
+
try:
|
301 |
+
current_time = datetime.now().strftime("%H:%M:%S")
|
302 |
+
sentiment = text_analyzer(text)[0]
|
303 |
+
new_reflection = [current_time, text, f"{sentiment['label']} ({sentiment['score']:.2f})"]
|
304 |
+
|
305 |
+
if "reflections" not in state:
|
306 |
+
state["reflections"] = []
|
307 |
+
|
308 |
+
state["reflections"].append(new_reflection)
|
309 |
+
return state, state["reflections"]
|
310 |
+
except Exception as e:
|
311 |
+
print(f"Error in save_reflection: {str(e)}")
|
312 |
+
return state, []
|
313 |
|
314 |
def clear_voice_input():
|
315 |
"""음성 입력 초기화"""
|
|
|
323 |
try:
|
324 |
y, sr = librosa.load(audio_path, sr=16000)
|
325 |
|
|
|
326 |
acoustic_features = {
|
327 |
"energy": float(np.mean(librosa.feature.rms(y=y))),
|
328 |
"tempo": float(librosa.beat.tempo(y)[0]),
|
|
|
330 |
"volume": float(np.mean(np.abs(y)))
|
331 |
}
|
332 |
|
333 |
+
# 기준점이 있는 경우 상대적 분석
|
334 |
+
baseline = state.get("baseline_features")
|
335 |
+
emotions = map_acoustic_to_emotion(acoustic_features, baseline)
|
336 |
|
|
|
337 |
transcription = speech_recognizer(y)
|
338 |
text = transcription["text"]
|
|
|
|
|
339 |
text_sentiment = text_analyzer(text)[0]
|
340 |
|
|
|
341 |
voice_result = (
|
342 |
f"음성 감정: {emotions['primary']} "
|
343 |
f"(강도: {emotions['intensity']:.1f}%, 신뢰도: {emotions['confidence']:.2f})\n"
|
|
|
349 |
f"- 음성 크기: {emotions['details']['voice_volume']}"
|
350 |
)
|
351 |
|
352 |
+
if baseline:
|
353 |
+
voice_result += "\n\n[기준점 대비 분석]\n"
|
354 |
+
voice_result += f"기준 상태와 비교한 감정 강도 변화: {emotions['intensity']-50:.1f}%"
|
355 |
|
356 |
+
text_result = f"텍스트 감정 분석 (1-5): {text_sentiment['score']}"
|
357 |
prompt = generate_detailed_prompt(text, emotions, text_sentiment)
|
358 |
|
359 |
return state, text, voice_result, text_result, prompt
|
|
|
362 |
|
363 |
# 이벤트 연결
|
364 |
start_btn.click(
|
365 |
+
fn=start_journey,
|
366 |
inputs=[name_input],
|
367 |
outputs=[user_display, tabs]
|
368 |
)
|
369 |
|
370 |
+
set_baseline_btn.click(
|
371 |
+
fn=set_baseline,
|
372 |
+
inputs=[baseline_audio, state],
|
373 |
+
outputs=[state, baseline_status, tabs]
|
374 |
+
)
|
375 |
+
|
376 |
save_btn.click(
|
377 |
fn=save_reflection,
|
378 |
inputs=[reflection_input, state],
|
|
|
397 |
outputs=[result_image]
|
398 |
)
|
399 |
|
400 |
+
return app
|
401 |
+
|
402 |
if __name__ == "__main__":
|
403 |
demo = create_interface()
|
404 |
demo.launch(debug=True)
|