haepada commited on
Commit
2f16de0
·
verified ·
1 Parent(s): ae13bc0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +150 -200
app.py CHANGED
@@ -20,7 +20,7 @@ WELCOME_MESSAGE = """
20
  이제, 평온함과 치유의 여정을 시작해보세요.
21
  """
22
 
23
- WORLDVIEW_MESSAGE = """
24
  ## 온천천 이야기 🌌
25
 
26
  온천천의 물줄기는 신성한 금샘에서 시작됩니다. 금샘은 생명과 창조의 원천이며,
@@ -32,48 +32,67 @@ WORLDVIEW_MESSAGE = """
32
  """
33
 
34
  class SimpleDB:
35
- def __init__(self, file_path="wishes.json"):
36
- self.file_path = file_path
37
- self.wishes = self._load_wishes()
38
-
39
- # wishes.json 파일이 없으면 생성
40
- if not os.path.exists(self.file_path):
41
- with open(self.file_path, 'w', encoding='utf-8') as f:
 
 
 
42
  json.dump([], f, ensure_ascii=False, indent=2)
43
-
44
- def _load_wishes(self):
45
  try:
46
- if os.path.exists(self.file_path):
47
- with open(self.file_path, 'r', encoding='utf-8') as f:
48
- return json.load(f)
49
- return []
50
  except Exception as e:
51
- print(f"Error loading wishes: {e}")
52
  return []
53
 
54
- def save_wish(self, name, wish, timestamp=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  if timestamp is None:
56
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
57
  wish_data = {
58
  "name": name,
59
  "wish": wish,
 
60
  "timestamp": timestamp
61
  }
62
  self.wishes.append(wish_data)
 
 
 
 
63
  try:
64
- with open(self.file_path, 'w', encoding='utf-8') as f:
65
- json.dump(self.wishes, f, ensure_ascii=False, indent=2)
66
  return True
67
  except Exception as e:
68
- print(f"Error saving wish: {e}")
69
  return False
70
 
 
 
 
71
  def get_all_wishes(self):
72
- """모든 소원 반환"""
73
  return self.wishes
74
 
75
  # API 설정
76
- HF_API_TOKEN = os.getenv("roots", "") # 기본값을 빈 문자열로 설정
77
  if not HF_API_TOKEN:
78
  print("Warning: HuggingFace API token not found. Some features may be limited.")
79
 
@@ -92,7 +111,6 @@ try:
92
  )
93
  except Exception as e:
94
  print(f"Error initializing AI models: {e}")
95
- # 기본 파이프라인 설정
96
  speech_recognizer = None
97
  text_analyzer = None
98
 
@@ -100,11 +118,11 @@ except Exception as e:
100
  os.makedirs("generated_images", exist_ok=True)
101
 
102
  # 음성 분석 관련 함수들
 
103
  def calculate_baseline_features(audio_data):
104
  try:
105
  if isinstance(audio_data, tuple):
106
  sr, y = audio_data
107
- # 데이터 타입을 float32로 변환
108
  y = y.astype(np.float32)
109
  elif isinstance(audio_data, str):
110
  y, sr = librosa.load(audio_data, sr=16000)
@@ -118,7 +136,6 @@ def calculate_baseline_features(audio_data):
118
 
119
  features = {
120
  "energy": float(np.mean(librosa.feature.rms(y=y))),
121
- # tempo 함수 업데이트
122
  "tempo": float(librosa.feature.tempo(y=y, sr=sr)[0]),
123
  "pitch": float(np.mean(librosa.feature.zero_crossing_rate(y=y))),
124
  "volume": float(np.mean(np.abs(y))),
@@ -130,7 +147,6 @@ def calculate_baseline_features(audio_data):
130
  return None
131
 
132
  def map_acoustic_to_emotion(features, baseline_features=None):
133
- """음향학적 특성을 감정으로 매핑"""
134
  if features is None:
135
  return {
136
  "primary": "알 수 없음",
@@ -151,9 +167,7 @@ def map_acoustic_to_emotion(features, baseline_features=None):
151
  pitch_norm = min(features["pitch"] * 2, 1)
152
 
153
  if baseline_features:
154
- if baseline_features["energy"] == 0 or baseline_features["tempo"] == 0 or baseline_features["pitch"] == 0:
155
- print("Invalid baseline features")
156
- else:
157
  energy_norm = (features["energy"] / baseline_features["energy"]) * 50
158
  tempo_norm = (features["tempo"] / baseline_features["tempo"])
159
  pitch_norm = (features["pitch"] / baseline_features["pitch"])
@@ -215,21 +229,15 @@ def analyze_voice(audio_data, state):
215
 
216
  try:
217
  sr, y = audio_data
218
- # 데이터 타입을 float32로 변환
219
  y = y.astype(np.float32)
220
 
221
  if len(y) == 0:
222
  return state, "음성이 감지되지 않았습니다.", "", "", ""
223
 
224
- # 음향학적 특성 분석
225
- acoustic_features = {
226
- "energy": float(np.mean(librosa.feature.rms(y=y))),
227
- "tempo": float(librosa.feature.tempo(y=y, sr=sr)[0]),
228
- "pitch": float(np.mean(librosa.feature.zero_crossing_rate(y=y))),
229
- "volume": float(np.mean(np.abs(y)))
230
- }
231
 
232
-
233
  # 음성 인식
234
  if speech_recognizer:
235
  try:
@@ -257,7 +265,6 @@ def analyze_voice(audio_data, state):
257
  text_sentiment = {"label": "unknown", "score": 0.0}
258
  text_result = "텍스트 감정 분석을 수행할 수 없습니다."
259
 
260
- # 결과 포맷팅
261
  voice_result = (
262
  f"음성 감정: {voice_emotion['primary']} "
263
  f"(강도: {voice_emotion['intensity']:.1f}%, 신뢰도: {voice_emotion['confidence']:.2f})\n"
@@ -279,8 +286,8 @@ def analyze_voice(audio_data, state):
279
  print(f"Error in analyze_voice: {str(e)}")
280
  return state, f"오류 발생: {str(e)}", "", "", ""
281
 
 
282
  def generate_detailed_prompt(text, emotions, text_sentiment):
283
- """감정 기반 상세 프롬프트 생성"""
284
  emotion_colors = {
285
  "기쁨/열정": "밝은 노랑과 따뜻한 주황색",
286
  "분노/강조": "강렬한 빨강과 짙은 검정",
@@ -307,7 +314,6 @@ def generate_detailed_prompt(text, emotions, text_sentiment):
307
  return prompt
308
 
309
  def generate_image_from_prompt(prompt):
310
- """이미지 생성 함수"""
311
  if not prompt:
312
  print("No prompt provided")
313
  return None
@@ -327,7 +333,6 @@ def generate_image_from_prompt(prompt):
327
  )
328
 
329
  if response.status_code == 200:
330
- # 이미지를 임시 파일로 저장
331
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
332
  image_path = f"generated_images/image_{timestamp}.png"
333
  os.makedirs("generated_images", exist_ok=True)
@@ -335,7 +340,7 @@ def generate_image_from_prompt(prompt):
335
  with open(image_path, "wb") as f:
336
  f.write(response.content)
337
 
338
- return image_path # 파일 경로 반환
339
  else:
340
  print(f"Error: {response.status_code}")
341
  print(f"Response: {response.text}")
@@ -344,46 +349,7 @@ def generate_image_from_prompt(prompt):
344
  print(f"Error generating image: {str(e)}")
345
  return None
346
 
347
- def save_reflection_fixed(text, state):
348
- if not text.strip():
349
- return state, []
350
-
351
- try:
352
- current_time = datetime.now().strftime("%H:%M:%S")
353
- if text_analyzer:
354
- sentiment = text_analyzer(text)[0]
355
- sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
356
-
357
- # 감정 분석 결과를 프롬프트에 반영
358
- emotion_prompt = generate_detailed_prompt(
359
- text,
360
- {"primary": sentiment['label'], "intensity": sentiment['score'] * 100,
361
- "characteristics": ["텍스트 기반 감정"], "confidence": sentiment['score']},
362
- sentiment
363
- )
364
- state = {**state, "final_prompt": emotion_prompt}
365
- else:
366
- sentiment_text = "분석 불가"
367
-
368
- new_reflection = [current_time, text, sentiment_text]
369
- reflections = state.get("reflections", [])
370
- reflections.append(new_reflection)
371
- state = {**state, "reflections": reflections}
372
- return state, reflections
373
- except Exception as e:
374
- print(f"Error saving reflection: {e}")
375
- return state, state.get("reflections", [])
376
-
377
- def is_mobile(user_agent):
378
- return any(device in user_agent.lower() for device in ['mobile', 'android', 'iphone'])
379
-
380
- def handle_tab_change(tab_index, state):
381
- """탭 변경 시 상태 유지"""
382
- state = {**state, "current_tab": tab_index}
383
- return state
384
-
385
  def safe_state_update(state, updates):
386
- """안전한 상태 업데이트"""
387
  try:
388
  new_state = {**state, **updates}
389
  return new_state
@@ -401,55 +367,63 @@ def create_interface():
401
  "wish": None,
402
  "final_prompt": "",
403
  "image_path": None,
404
- "current_tab": 0,
405
- "is_mobile": False, # 모바일 여부
406
- "session_active": True # 세션 상태
407
  }
408
 
409
- with gr.Blocks(
410
- theme=gr.themes.Soft(),
411
- css="""
412
- @media (max-width: 600px) {
413
- .container { padding: 10px; }
414
- .gradio-row { flex-direction: column; }
415
- .gradio-button { width: 100%; margin: 5px 0; }
416
- .gradio-textbox { width: 100%; }
417
- }
418
- """
419
- ) as app:
 
420
  state = gr.State(value=initial_state)
421
 
422
  gr.Markdown("# 디지털 굿판")
423
  gr.Markdown("""
424
- 1. 입장 → 2. 축원(기준 설정) → 3. 청신 → 4. 기원 → 5. 송신
425
- 순서대로 진행해주세요.
426
- """)
427
 
428
- with gr.Tabs() as tabs:
 
429
  with gr.TabItem("입장") as tab_entrance:
430
- gr.Markdown(WELCOME_MESSAGE)
431
- name_input = gr.Textbox(
432
- label="이름을 알려주세요",
433
- placeholder="이름을 입력해주세요",
434
- interactive=True
435
- )
436
- worldview_display = gr.Markdown(visible=False)
437
- start_btn = gr.Button("여정 시작하기", variant="primary")
438
-
439
- with gr.TabItem("축원") as tab_baseline:
440
- gr.Markdown("### 축원의 문장을 평온한 마음으로 읽어주세요")
441
- gr.Markdown("'당신의 건강과 행복이 늘 가득하기를'")
442
- baseline_audio = gr.Audio(
443
- label="축원 문장 녹음하기",
444
- sources=["microphone"],
445
- type="numpy",
446
- streaming=False,
447
- min_width=300, # 최소 너비 설정
448
- elem_id="audio-recorder" # CSS 식별자 추가
449
- )
450
- set_baseline_btn = gr.Button("기준점 설정 완료", variant="primary")
451
- baseline_status = gr.Markdown("")
 
 
 
 
 
 
 
452
 
 
453
  with gr.TabItem("청신") as tab_listen:
454
  gr.Markdown("## 청신 - 소리로 정화하기")
455
  gr.Markdown("""
@@ -483,6 +457,7 @@ def create_interface():
483
  wrap=True
484
  )
485
 
 
486
  with gr.TabItem("기원") as tab_wish:
487
  gr.Markdown("## 기원 - 소원을 전해보세요")
488
  with gr.Row():
@@ -511,8 +486,9 @@ def create_interface():
511
  interactive=False
512
  )
513
 
 
514
  with gr.TabItem("송신") as tab_send:
515
- gr.Markdown("## 송신 -소지(소원지)를 그려 날려 태워봅시다")
516
  final_prompt = gr.Textbox(
517
  label="생성된 프롬프트",
518
  interactive=False,
@@ -521,9 +497,7 @@ def create_interface():
521
  generate_btn = gr.Button("마음의 그림 그리기", variant="primary")
522
  result_image = gr.Image(
523
  label="생성된 이미지",
524
- show_download_button=True,
525
- min_width=300,
526
- elem_id="result-image"
527
  )
528
 
529
  gr.Markdown("## 온천천에 전하고 싶은 소원을 남겨주세요")
@@ -538,69 +512,42 @@ def create_interface():
538
  따뜻한 마음을 담아 작성해주세요.
539
  """)
540
  wishes_display = gr.Dataframe(
541
- headers=["시간", "소원", "이름"], # 헤더 수정
542
  label="기록된 소원들",
543
  value=[],
544
  interactive=False,
545
  wrap=True
546
  )
547
 
548
- # 이벤트 연결
549
- def handle_start(name, current_state):
550
  if not name.strip():
551
- return (
552
- "이름을 입력해주세요",
553
- gr.update(visible=False),
554
- current_state,
555
- gr.update(selected=0) # 현재 탭 유지
556
- )
557
- current_state = {**current_state, "user_name": name}
558
- return (
559
- WORLDVIEW_MESSAGE,
560
- gr.update(visible=True),
561
- current_state,
562
- gr.update(selected=1) # 축원 탭(두 번째 탭)으로 이동
563
- )
564
-
565
  def handle_baseline(audio, current_state):
566
  if audio is None:
567
- return current_state, "음성을 먼저 녹음해주세요.", gr.update(selected=1) # 현재 탭 유지
568
 
569
  try:
570
  sr, y = audio
571
  y = y.astype(np.float32)
572
  features = calculate_baseline_features((sr, y))
573
  if features:
574
- current_state = {**current_state, "baseline_features": features}
575
- return (
576
- current_state,
577
- "기준점이 설정되었습니다. 청신 탭으로 이동합니다.",
578
- gr.update(selected=2) # 청신 탭으로 이동
579
- )
580
- return current_state, "기준점 설정에 실패했습니다. 다시 시도해주세요.", gr.update(selected=1)
581
  except Exception as e:
582
  print(f"Baseline error: {str(e)}")
583
- return current_state, "오류가 발생했습니다. 다시 시도해주세요.", gr.update(selected=1)
584
 
585
- # 음악 재생 이벤트 핸들러 수정
586
- def play_music():
587
- try:
588
- return "assets/main_music.mp3"
589
- except Exception as e:
590
- print(f"Error playing music: {e}")
591
- return None
592
-
593
- def handle_analysis(audio, current_state):
594
- state, text, voice_result, text_result, prompt = analyze_voice(audio, current_state)
595
- return state, text, voice_result, text_result, prompt
596
-
597
- def handle_image_generation(prompt): # 들여쓰기 수정
598
- image_path = generate_image_from_prompt(prompt)
599
- if image_path and os.path.exists(image_path):
600
- return image_path
601
- return None
602
-
603
- def save_reflection_fixed(text, state): # 들여쓰기 수정
604
  if not text.strip():
605
  return state, []
606
 
@@ -609,82 +556,85 @@ def create_interface():
609
  if text_analyzer:
610
  sentiment = text_analyzer(text)[0]
611
  sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
 
612
  else:
613
  sentiment_text = "분석 불가"
 
614
 
615
  new_reflection = [current_time, text, sentiment_text]
616
  reflections = state.get("reflections", [])
617
  reflections.append(new_reflection)
618
- state = {**state, "reflections": reflections}
619
- return state, reflections
 
620
  except Exception as e:
621
  print(f"Error saving reflection: {e}")
622
  return state, state.get("reflections", [])
623
 
624
- def save_wish(text, state): # 들여쓰기 수정
625
  if not text.strip():
626
  return "소원을 입력해주세요.", []
627
 
628
  try:
629
- current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 날짜 포함
630
  name = state.get("user_name", "익명")
631
-
632
- # DB에 저장
633
  db.save_wish(name, text)
634
-
635
- # 모든 소원 불러오기
636
- all_wishes = db.wishes # SimpleDB에서 모든 소원 가져오기
637
-
638
- # 표시할 데이터 형식으로 변환
639
  wish_display_data = [
640
  [wish["timestamp"], wish["wish"], wish["name"]]
641
- for wish in all_wishes
642
  ]
643
-
644
  return "소원이 저장되었습니다.", wish_display_data
645
-
646
  except Exception as e:
647
  print(f"Error saving wish: {e}")
648
  return "오류가 발생했습니다.", []
649
 
650
- # 버튼 이벤트 연결
 
 
 
 
 
651
  start_btn.click(
652
  fn=handle_start,
653
- inputs=[name_input, state],
654
- outputs=[worldview_display, worldview_display, state, tabs]
655
  )
656
 
657
  set_baseline_btn.click(
658
  fn=handle_baseline,
659
  inputs=[baseline_audio, state],
660
- outputs=[state, baseline_status, tabs] # tabs 추가
661
  )
662
 
663
  play_music_btn.click(
664
- fn=play_music,
665
  outputs=[audio]
666
  )
667
 
 
 
 
 
 
 
 
 
 
 
 
668
  analyze_btn.click(
669
- fn=handle_analysis,
670
  inputs=[voice_input, state],
671
  outputs=[state, transcribed_text, voice_emotion, text_emotion, final_prompt]
672
  )
673
 
674
  generate_btn.click(
675
- fn=handle_image_generation,
676
  inputs=[final_prompt],
677
  outputs=[result_image]
678
  )
679
 
680
- save_btn.click(
681
- fn=save_reflection_fixed,
682
- inputs=[reflection_input, state],
683
- outputs=[state, reflections_display]
684
- )
685
-
686
  save_final_btn.click(
687
- fn=save_wish,
688
  inputs=[final_reflection, state],
689
  outputs=[baseline_status, wishes_display]
690
  )
@@ -699,6 +649,6 @@ if __name__ == "__main__":
699
  server_name="0.0.0.0",
700
  server_port=7860,
701
  show_error=True,
702
- height=None, # 자동 높이 조정
703
- width="100%" # 전체 너비 사용
704
  )
 
20
  이제, 평온함과 치유의 여정을 시작해보세요.
21
  """
22
 
23
+ ONCHEON_STORY = """
24
  ## 온천천 이야기 🌌
25
 
26
  온천천의 물줄기는 신성한 금샘에서 시작됩니다. 금샘은 생명과 창조의 원천이며,
 
32
  """
33
 
34
  class SimpleDB:
35
+ def __init__(self, reflections_path="data/reflections.json", wishes_path="data/wishes.json"):
36
+ self.reflections_path = reflections_path
37
+ self.wishes_path = wishes_path
38
+ os.makedirs('data', exist_ok=True)
39
+ self.reflections = self._load_json(reflections_path)
40
+ self.wishes = self._load_json(wishes_path)
41
+
42
+ def _load_json(self, file_path):
43
+ if not os.path.exists(file_path):
44
+ with open(file_path, 'w', encoding='utf-8') as f:
45
  json.dump([], f, ensure_ascii=False, indent=2)
 
 
46
  try:
47
+ with open(file_path, 'r', encoding='utf-8') as f:
48
+ return json.load(f)
 
 
49
  except Exception as e:
50
+ print(f"Error loading {file_path}: {e}")
51
  return []
52
 
53
+ def save_reflection(self, name, reflection, sentiment, timestamp=None):
54
+ if timestamp is None:
55
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
56
+ reflection_data = {
57
+ "name": name,
58
+ "reflection": reflection,
59
+ "sentiment": sentiment,
60
+ "timestamp": timestamp
61
+ }
62
+ self.reflections.append(reflection_data)
63
+ self._save_json(self.reflections_path, self.reflections)
64
+ return True
65
+
66
+ def save_wish(self, name, wish, emotion_data=None, timestamp=None):
67
  if timestamp is None:
68
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
69
  wish_data = {
70
  "name": name,
71
  "wish": wish,
72
+ "emotion": emotion_data,
73
  "timestamp": timestamp
74
  }
75
  self.wishes.append(wish_data)
76
+ self._save_json(self.wishes_path, self.wishes)
77
+ return True
78
+
79
+ def _save_json(self, file_path, data):
80
  try:
81
+ with open(file_path, 'w', encoding='utf-8') as f:
82
+ json.dump(data, f, ensure_ascii=False, indent=2)
83
  return True
84
  except Exception as e:
85
+ print(f"Error saving to {file_path}: {e}")
86
  return False
87
 
88
+ def get_all_reflections(self):
89
+ return self.reflections
90
+
91
  def get_all_wishes(self):
 
92
  return self.wishes
93
 
94
  # API 설정
95
+ HF_API_TOKEN = os.getenv("roots", "")
96
  if not HF_API_TOKEN:
97
  print("Warning: HuggingFace API token not found. Some features may be limited.")
98
 
 
111
  )
112
  except Exception as e:
113
  print(f"Error initializing AI models: {e}")
 
114
  speech_recognizer = None
115
  text_analyzer = None
116
 
 
118
  os.makedirs("generated_images", exist_ok=True)
119
 
120
  # 음성 분석 관련 함수들
121
+ ```python
122
  def calculate_baseline_features(audio_data):
123
  try:
124
  if isinstance(audio_data, tuple):
125
  sr, y = audio_data
 
126
  y = y.astype(np.float32)
127
  elif isinstance(audio_data, str):
128
  y, sr = librosa.load(audio_data, sr=16000)
 
136
 
137
  features = {
138
  "energy": float(np.mean(librosa.feature.rms(y=y))),
 
139
  "tempo": float(librosa.feature.tempo(y=y, sr=sr)[0]),
140
  "pitch": float(np.mean(librosa.feature.zero_crossing_rate(y=y))),
141
  "volume": float(np.mean(np.abs(y))),
 
147
  return None
148
 
149
  def map_acoustic_to_emotion(features, baseline_features=None):
 
150
  if features is None:
151
  return {
152
  "primary": "알 수 없음",
 
167
  pitch_norm = min(features["pitch"] * 2, 1)
168
 
169
  if baseline_features:
170
+ if baseline_features["energy"] > 0 and baseline_features["tempo"] > 0 and baseline_features["pitch"] > 0:
 
 
171
  energy_norm = (features["energy"] / baseline_features["energy"]) * 50
172
  tempo_norm = (features["tempo"] / baseline_features["tempo"])
173
  pitch_norm = (features["pitch"] / baseline_features["pitch"])
 
229
 
230
  try:
231
  sr, y = audio_data
 
232
  y = y.astype(np.float32)
233
 
234
  if len(y) == 0:
235
  return state, "음성이 감지되지 않았습니다.", "", "", ""
236
 
237
+ acoustic_features = calculate_baseline_features((sr, y))
238
+ if acoustic_features is None:
239
+ return state, "음성 분석에 실패했습니다.", "", "", ""
 
 
 
 
240
 
 
241
  # 음성 인식
242
  if speech_recognizer:
243
  try:
 
265
  text_sentiment = {"label": "unknown", "score": 0.0}
266
  text_result = "텍스트 감정 분석을 수행할 수 없습니다."
267
 
 
268
  voice_result = (
269
  f"음성 감정: {voice_emotion['primary']} "
270
  f"(강도: {voice_emotion['intensity']:.1f}%, 신뢰도: {voice_emotion['confidence']:.2f})\n"
 
286
  print(f"Error in analyze_voice: {str(e)}")
287
  return state, f"오류 발생: {str(e)}", "", "", ""
288
 
289
+
290
  def generate_detailed_prompt(text, emotions, text_sentiment):
 
291
  emotion_colors = {
292
  "기쁨/열정": "밝은 노랑과 따뜻한 주황색",
293
  "분노/강조": "강렬한 빨강과 짙은 검정",
 
314
  return prompt
315
 
316
  def generate_image_from_prompt(prompt):
 
317
  if not prompt:
318
  print("No prompt provided")
319
  return None
 
333
  )
334
 
335
  if response.status_code == 200:
 
336
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
337
  image_path = f"generated_images/image_{timestamp}.png"
338
  os.makedirs("generated_images", exist_ok=True)
 
340
  with open(image_path, "wb") as f:
341
  f.write(response.content)
342
 
343
+ return image_path
344
  else:
345
  print(f"Error: {response.status_code}")
346
  print(f"Response: {response.text}")
 
349
  print(f"Error generating image: {str(e)}")
350
  return None
351
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  def safe_state_update(state, updates):
 
353
  try:
354
  new_state = {**state, **updates}
355
  return new_state
 
367
  "wish": None,
368
  "final_prompt": "",
369
  "image_path": None,
370
+ "current_tab": 0
 
 
371
  }
372
 
373
+ css = """
374
+ @media (max-width: 600px) {
375
+ .container { padding: 10px; }
376
+ .gradio-row { flex-direction: column; }
377
+ .gradio-button { width: 100%; margin: 5px 0; }
378
+ .gradio-textbox { width: 100%; }
379
+ #audio-recorder { width: 100%; }
380
+ #result-image { width: 100%; }
381
+ }
382
+ """
383
+
384
+ with gr.Blocks(theme=gr.themes.Soft(), css=css) as app:
385
  state = gr.State(value=initial_state)
386
 
387
  gr.Markdown("# 디지털 굿판")
388
  gr.Markdown("""
389
+ 1. 입장 → 2. 청신 → 3. 기원 → 4. 송신
390
+ 순서대로 진행해주세요.
391
+ """)
392
 
393
+ with gr.Tabs(selected=0) as tabs:
394
+ # 입장 탭 (축원 포함)
395
  with gr.TabItem("입장") as tab_entrance:
396
+ # 첫 화면
397
+ welcome_section = gr.Column(visible=True)
398
+ with welcome_section:
399
+ gr.Markdown(WELCOME_MESSAGE)
400
+ name_input = gr.Textbox(
401
+ label="이름을 알려주세요",
402
+ placeholder="이름을 입력해주세요",
403
+ interactive=True
404
+ )
405
+
406
+ # 온천천 이야기
407
+ story_section = gr.Column(visible=False)
408
+ with story_section:
409
+ gr.Markdown(ONCHEON_STORY)
410
+ start_btn = gr.Button("여정 시작하기", variant="primary")
411
+
412
+ # 축원 섹션
413
+ blessing_section = gr.Column(visible=False)
414
+ with blessing_section:
415
+ gr.Markdown("### 축원의 문장을 평온한 마음으로 읽어주세요")
416
+ gr.Markdown("'명짐 복짐 짊어지고 안가태평하시기를 비도발원 축원 드립니다'")
417
+ baseline_audio = gr.Audio(
418
+ label="축원 문장 녹음하기",
419
+ sources=["microphone"],
420
+ type="numpy",
421
+ streaming=False
422
+ )
423
+ set_baseline_btn = gr.Button("기준점 설정 완료", variant="primary")
424
+ baseline_status = gr.Markdown("")
425
 
426
+ # 청신 탭
427
  with gr.TabItem("청신") as tab_listen:
428
  gr.Markdown("## 청신 - 소리로 정화하기")
429
  gr.Markdown("""
 
457
  wrap=True
458
  )
459
 
460
+ # 기원 탭
461
  with gr.TabItem("기원") as tab_wish:
462
  gr.Markdown("## 기원 - 소원을 전해보세요")
463
  with gr.Row():
 
486
  interactive=False
487
  )
488
 
489
+ # 송신 탭
490
  with gr.TabItem("송신") as tab_send:
491
+ gr.Markdown("## 송신 - 소지(소원지)를 그려 날려 태워봅시다")
492
  final_prompt = gr.Textbox(
493
  label="생성된 프롬프트",
494
  interactive=False,
 
497
  generate_btn = gr.Button("마음의 그림 그리기", variant="primary")
498
  result_image = gr.Image(
499
  label="생성된 이미지",
500
+ show_download_button=True
 
 
501
  )
502
 
503
  gr.Markdown("## 온천천에 전하고 싶은 소원을 남겨주세요")
 
512
  따뜻한 마음을 담아 작성해주세요.
513
  """)
514
  wishes_display = gr.Dataframe(
515
+ headers=["시간", "소원", "이름"],
516
  label="기록된 소원들",
517
  value=[],
518
  interactive=False,
519
  wrap=True
520
  )
521
 
522
+ # 이벤트 핸들러들
523
+ def handle_name_input(name):
524
  if not name.strip():
525
+ return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
526
+ return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
527
+
528
+ def handle_start():
529
+ return gr.update(visible=False), gr.update(visible=True)
530
+
 
 
 
 
 
 
 
 
531
  def handle_baseline(audio, current_state):
532
  if audio is None:
533
+ return current_state, "음성을 먼저 녹음해주세요.", gr.update(selected=0)
534
 
535
  try:
536
  sr, y = audio
537
  y = y.astype(np.float32)
538
  features = calculate_baseline_features((sr, y))
539
  if features:
540
+ current_state = safe_state_update(current_state, {
541
+ "baseline_features": features,
542
+ "current_tab": 1
543
+ })
544
+ return current_state, "기준점이 설정되었습니다. 청신 탭으로 이동합니다.", gr.update(selected=1)
545
+ return current_state, "기준점 설정에 실패했습니다. 다시 시도해주세요.", gr.update(selected=0)
 
546
  except Exception as e:
547
  print(f"Baseline error: {str(e)}")
548
+ return current_state, "오류가 발생했습니다. 다시 시도해주세요.", gr.update(selected=0)
549
 
550
+ def handle_save_reflection(text, state):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  if not text.strip():
552
  return state, []
553
 
 
556
  if text_analyzer:
557
  sentiment = text_analyzer(text)[0]
558
  sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
559
+ db.save_reflection(state.get("user_name", "익명"), text, sentiment)
560
  else:
561
  sentiment_text = "분석 불가"
562
+ db.save_reflection(state.get("user_name", "익명"), text, {"label": "unknown", "score": 0.0})
563
 
564
  new_reflection = [current_time, text, sentiment_text]
565
  reflections = state.get("reflections", [])
566
  reflections.append(new_reflection)
567
+ state = safe_state_update(state, {"reflections": reflections})
568
+
569
+ return state, db.get_all_reflections()
570
  except Exception as e:
571
  print(f"Error saving reflection: {e}")
572
  return state, state.get("reflections", [])
573
 
574
+ def handle_save_wish(text, state):
575
  if not text.strip():
576
  return "소원을 입력해주세요.", []
577
 
578
  try:
 
579
  name = state.get("user_name", "익명")
 
 
580
  db.save_wish(name, text)
581
+ wishes = db.get_all_wishes()
 
 
 
 
582
  wish_display_data = [
583
  [wish["timestamp"], wish["wish"], wish["name"]]
584
+ for wish in wishes
585
  ]
 
586
  return "소원이 저장되었습니다.", wish_display_data
 
587
  except Exception as e:
588
  print(f"Error saving wish: {e}")
589
  return "오류가 발생했습니다.", []
590
 
591
+ # 이벤트 연결
592
+ name_input.submit(
593
+ fn=handle_name_input,
594
+ outputs=[welcome_section, story_section, blessing_section]
595
+ )
596
+
597
  start_btn.click(
598
  fn=handle_start,
599
+ outputs=[story_section, blessing_section]
 
600
  )
601
 
602
  set_baseline_btn.click(
603
  fn=handle_baseline,
604
  inputs=[baseline_audio, state],
605
+ outputs=[state, baseline_status, tabs]
606
  )
607
 
608
  play_music_btn.click(
609
+ fn=lambda: "assets/main_music.mp3",
610
  outputs=[audio]
611
  )
612
 
613
+ save_btn.click(
614
+ fn=handle_save_reflection,
615
+ inputs=[reflection_input, state],
616
+ outputs=[state, reflections_display]
617
+ )
618
+
619
+ clear_btn.click(
620
+ fn=lambda: None,
621
+ outputs=[voice_input]
622
+ )
623
+
624
  analyze_btn.click(
625
+ fn=analyze_voice,
626
  inputs=[voice_input, state],
627
  outputs=[state, transcribed_text, voice_emotion, text_emotion, final_prompt]
628
  )
629
 
630
  generate_btn.click(
631
+ fn=generate_image_from_prompt,
632
  inputs=[final_prompt],
633
  outputs=[result_image]
634
  )
635
 
 
 
 
 
 
 
636
  save_final_btn.click(
637
+ fn=handle_save_wish,
638
  inputs=[final_reflection, state],
639
  outputs=[baseline_status, wishes_display]
640
  )
 
649
  server_name="0.0.0.0",
650
  server_port=7860,
651
  show_error=True,
652
+ height=None,
653
+ width="100%"
654
  )