haepada commited on
Commit
2b97308
·
verified ·
1 Parent(s): 22303ab

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +335 -337
app.py CHANGED
@@ -168,7 +168,6 @@ def calculate_baseline_features(audio_data):
168
  print(f"Error calculating baseline: {str(e)}")
169
  return None
170
 
171
-
172
  def map_acoustic_to_emotion(features, baseline_features=None):
173
  if features is None:
174
  return {
@@ -246,7 +245,6 @@ def map_acoustic_to_emotion(features, baseline_features=None):
246
 
247
  return emotions
248
 
249
-
250
  def analyze_voice(audio_data, state):
251
  if audio_data is None:
252
  return state, "음성을 먼저 녹음해주세요.", "", "", ""
@@ -344,7 +342,6 @@ def generate_detailed_prompt(text, emotions, text_sentiment):
344
 
345
  return prompt
346
 
347
-
348
  def generate_image_from_prompt(prompt):
349
  if not prompt:
350
  print("No prompt provided")
@@ -380,8 +377,7 @@ def generate_image_from_prompt(prompt):
380
  except Exception as e:
381
  print(f"Error generating image: {str(e)}")
382
  return None
383
-
384
-
385
  def create_pwa_files():
386
  """PWA 필요 파일들 생성"""
387
  # manifest.json 생성
@@ -397,14 +393,54 @@ def create_pwa_files():
397
  "theme_color": "#000000",
398
  "orientation": "portrait",
399
  "icons": [
400
- {"src": "/static/icons/icon-72x72.png", "sizes": "72x72", "type": "image/png", "purpose": "any maskable"},
401
- {"src": "/static/icons/icon-96x96.png", "sizes": "96x96", "type": "image/png", "purpose": "any maskable"},
402
- {"src": "/static/icons/icon-128x128.png", "sizes": "128x128", "type": "image/png", "purpose": "any maskable"},
403
- {"src": "/static/icons/icon-144x144.png", "sizes": "144x144", "type": "image/png", "purpose": "any maskable"},
404
- {"src": "/static/icons/icon-152x152.png", "sizes": "152x152", "type": "image/png", "purpose": "any maskable"},
405
- {"src": "/static/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable"},
406
- {"src": "/static/icons/icon-384x384.png", "sizes": "384x384", "type": "image/png", "purpose": "any maskable"},
407
- {"src": "/static/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408
  ]
409
  }
410
  with open(manifest_path, 'w', encoding='utf-8') as f:
@@ -524,7 +560,6 @@ self.addEventListener('fetch', event => {
524
  </body>
525
  </html>''')
526
 
527
-
528
  def safe_state_update(state, updates):
529
  try:
530
  new_state = {**state, **updates}
@@ -539,7 +574,6 @@ def safe_state_update(state, updates):
539
  print(f"State update error: {e}")
540
  return state
541
 
542
-
543
  def create_interface():
544
  db = SimpleDB()
545
 
@@ -554,368 +588,333 @@ def create_interface():
554
  }
555
 
556
  css = """
557
- @media (max-width: 600px) {
558
- #logo-image {
559
- content: url('/static/DIGITAL_GUTPAN_LOGO_m.png') !important;
560
- width: 75px !important;
561
- height: auto !important;
562
- }
563
- .container { padding: 10px !important; }
564
- .gradio-button {
565
- width: 100% !important;
566
- margin: 5px 0 !important;
567
- min-height: 44px !important;
568
- }
569
- .gradio-textbox,
570
- .gradio-audio,
571
- .gradio-image {
572
- width: 100% !important;
573
- }
574
- .gradio-dataframe {
575
- overflow-x: auto !important;
576
- max-width: 100% !important;
577
- }
578
- }
579
-
580
- /* 데스크톱 뷰 */
581
- @media (min-width: 601px) {
582
- #logo-image {
583
- content: url('/static/DIGITAL_GUTPAN_LOGO_w.png');
584
- width: auto;
585
- height: auto;
586
- }
587
- }
588
-
589
- /* 공통 스타일 */
590
- .gradio-button {
591
- transition: all 0.3s ease;
592
  }
593
- .gradio-button:active {
594
- transform: scale(0.98);
 
 
595
  }
596
- #logo-container {
597
- text-align: center;
598
- margin: 0 auto;
599
- padding: 20px 0;
 
 
 
 
600
  }
 
 
 
 
 
 
 
 
 
601
  """
602
 
603
  with gr.Blocks(theme=gr.themes.Soft(), css=css) as app:
604
  state = gr.State(value=initial_state)
605
-
606
- with gr.Row(elem_id="logo-container"):
607
- gr.Image(value="/static/DIGITAL_GUTPAN_LOGO_w.png",
608
- show_label=False,
609
- interactive=False,
610
- elem_id="logo-image")
611
 
612
  with gr.Tabs(selected=0) as tabs:
613
  # 입장 탭 (축원 포함)
614
  with gr.TabItem("입장") as tab_entrance:
615
  # 1단계: 첫 화면
616
- with gr.Column(visible=True) as welcome_section:
617
- # Main image
618
- gr.Image(
619
- value="static/main-image.png",
620
- show_label=False,
621
- interactive=False,
622
- elem_id="main-image"
623
  )
624
-
625
- # 기존 텍스트 및 다른 요소들
626
- gr.Markdown(WELCOME_MESSAGE)
627
- name_input = gr.Textbox(
628
- label="이름을 알려주세요",
629
- placeholder="이름을 입력해주세요",
630
- interactive=True
631
- )
632
- name_submit_btn = gr.Button("굿판 시작하기", variant="primary")
633
-
634
- gr.Markdown(
635
- """
636
- <p style="color: #374151; text-align: center; font-size: 0.9em;">
637
- ‘디지털 굿판’은 부산문화재단의 지원으로 루츠리딤이 제작한 다원예술 프로젝트입니다.<br>
638
- 본 프로젝트에서 기록된 정보 중<br>
639
- ‘송신’ 단계의 ‘소원전하기’를 제외한 모든 과정의 정보는 (목소리, 감상 등)<br>
640
- 별도로 저장되지 않으며 AI 학습이나 외부 공유에 사용되지 않습니다.
641
- </p>
642
- """,
643
- visible=True
644
- )
645
 
646
- # 2단계: 세계관 설명
647
- story_section = gr.Column(visible=False)
648
- with story_section:
649
- gr.Markdown(ONCHEON_STORY)
650
- continue_btn = gr.Button("준비하기", variant="primary")
651
-
652
- # 3단계: 축원 의식
653
- blessing_section = gr.Column(visible=False)
654
- with blessing_section:
655
- gr.Markdown("### 축원의식을 시작하겠습니다")
656
- gr.Markdown("'명짐 복짐 짊어지고 안가태평하시기를 비도발원 축원 드립니다'")
657
- baseline_audio = gr.Audio(
658
- label="축원 문장 녹음하기",
659
- sources=["microphone"],
660
- type="numpy",
661
- streaming=False
662
- )
663
- set_baseline_btn = gr.Button("축원 마치기", variant="primary")
664
- baseline_status = gr.Markdown("")
665
-
666
- # 4단계: 굿판 입장 안내
667
- entry_guide_section = gr.Column(visible=False)
668
- with entry_guide_section:
669
- gr.Markdown("## 굿판으로 입장하기")
670
- gr.Markdown("""
671
- * 청신 탭으로 이동해 주세요.
672
- * 부산광역시 동래구 온천장역에서 시작하면 더욱 깊은 경험을 시작할 수 있습니다.
673
- * (본격적인 경험을 시작하기에 앞서 이동을 권장드립니다)
674
- """)
675
- enter_btn = gr.Button("청신 의식 시작하기", variant="primary")
676
-
677
- with gr.TabItem("청신") as tab_listen:
678
- gr.Markdown("## 청신 - 소리로 정화하기")
679
- gr.Markdown("""
680
- 온천천의 소리를 들으며 마음을 정화해보세요.
681
 
682
- 🇫 앱은 온천천의 사운드스케이프를 녹음하여 제작되었으며,
683
- 온천천 온천장역에서 장전역까지 걸으며 더 깊은 체험이 가능합니다.
684
- """)
685
- play_music_btn = gr.Button("온천천의 소리 듣기", variant="secondary")
686
- with gr.Row():
687
- audio = gr.Audio(
688
- value="assets/main_music.mp3",
689
- type="filepath",
690
- label="온천천의 소리",
691
- interactive=False,
692
- show_download_button=True,
693
- visible=True
694
- )
695
- with gr.Column():
696
- reflection_input = gr.Textbox(
697
- label="지금 이 순간의 감상을 자유롭게 적어보세요",
698
- lines=3,
699
- max_lines=5
700
- )
701
- save_btn = gr.Button("감상 저장하기", variant="secondary")
702
- reflections_display = gr.Dataframe(
703
- headers=["시간", "감상", "감정 분석"],
704
- label="기록된 감상들",
705
- value=[], # 초기값은 빈 리스트
706
- interactive=False,
707
- wrap=True,
708
- row_count=(5, "dynamic") # 동적으로 행 수 조정
709
- )
710
-
711
- # 기원 탭
712
- with gr.TabItem("기원") as tab_wish:
713
- gr.Markdown("## 기원 - 소원을 전해보세요")
714
- with gr.Row():
715
- with gr.Column():
716
- voice_input = gr.Audio(
717
- label="소원을 나누고 싶은 마음을 말해주세요",
718
  sources=["microphone"],
719
  type="numpy",
720
  streaming=False
721
  )
722
- with gr.Row():
723
- clear_btn = gr.Button("녹음 지우기", variant="secondary")
724
- analyze_btn = gr.Button("소원 분석하기", variant="primary")
725
-
726
- with gr.Column():
727
- transcribed_text = gr.Textbox(
728
- label="인식된 텍스트",
729
- interactive=False
730
- )
731
- voice_emotion = gr.Textbox(
732
- label="음성 감정 분석",
733
- interactive=False
734
- )
735
- text_emotion = gr.Textbox(
736
- label="텍스트 감정 분석",
737
- interactive=False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
738
  )
739
-
740
- # 송신
741
- with gr.TabItem("송신") as tab_send:
742
- gr.Markdown("## 송신 - 소지(소원지)를 그려 날려 태워봅시다")
743
- final_prompt = gr.Textbox(
744
- label="생성된 프롬프트",
745
- interactive=False,
746
- lines=3
747
- )
748
- generate_btn = gr.Button("마음의 그림 그리기", variant="primary")
749
- result_image = gr.Image(
750
- label="생성된 이미지",
751
- show_download_button=True
752
- )
753
-
754
- gr.Markdown("## 온천천에 전하고 싶은 소원을 남겨주세요")
755
- final_reflection = gr.Textbox(
756
- label="소원",
757
- placeholder="당신의 소원을 줄로 남겨주세요...",
758
- max_lines=3
759
- )
760
- save_final_btn = gr.Button("소원 전하기", variant="primary")
761
- gr.Markdown("""
762
- 🇫 여러분의 소원은 11월 25일 온천천 벽면에 설치될 소원나무에 전시될 예정입니다.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
763
  따뜻한 마음을 담아 작성해주세요.
764
  """)
765
- wishes_display = gr.Dataframe(
766
- headers=["시간", "소원", "이름"],
767
- label="기록된 소원들",
768
- value=[],
769
- interactive=False,
770
- wrap=True
771
- )
772
 
773
- # 이벤트 핸들러들
774
- def handle_name_submit(name, state):
775
- if not name.strip():
 
 
 
 
 
 
 
 
776
  return (
777
- gr.update(visible=True),
778
  gr.update(visible=False),
 
779
  gr.update(visible=False),
780
  gr.update(visible=False),
781
  state
782
  )
783
- state = safe_state_update(state, {"user_name": name})
784
- return (
785
- gr.update(visible=False),
786
- gr.update(visible=True),
787
- gr.update(visible=False),
788
- gr.update(visible=False),
789
- state
790
- )
791
-
792
- def handle_continue():
793
- return (
794
- gr.update(visible=False),
795
- gr.update(visible=False),
796
- gr.update(visible=True),
797
- gr.update(visible=False)
798
- )
799
-
800
- def handle_blessing_complete(audio, state):
801
- if audio is None:
802
- return state, "음성을 먼저 녹음해주세요.", gr.update(visible=True), gr.update(visible=False)
803
 
804
- try:
805
- # ... (기존 음성 처리 코드)
806
  return (
807
- state,
808
- "축원이 완료되었습니다.",
809
  gr.update(visible=False),
810
- gr.update(visible=True)
 
 
811
  )
812
- except Exception as e:
813
- return state, f"오류가 발생했습니다: {str(e)}", gr.update(visible=True), gr.update(visible=False)
814
-
815
- def handle_enter():
816
- return gr.update(selected=1) # 청신 탭으로 이동
817
-
818
- def handle_start():
819
- return gr.update(visible=False), gr.update(visible=True)
820
-
821
- def handle_baseline(audio, current_state):
822
- if audio is None:
823
- return current_state, "음성을 먼저 녹음해주세요.", gr.update(selected=0)
824
-
825
- try:
826
- sr, y = audio
827
- y = y.astype(np.float32)
828
- features = calculate_baseline_features((sr, y))
829
- if features:
830
- current_state = safe_state_update(current_state, {
831
- "baseline_features": features,
832
- "current_tab": 1
833
- })
834
- return current_state, "기준점이 설정되었습니다. 청신 탭으로 이동합니다.", gr.update(selected=1)
835
- return current_state, "기준점 설정에 실패했습니다. 다시 시도해주세요.", gr.update(selected=0)
836
- except Exception as e:
837
- print(f"Baseline error: {str(e)}")
838
- return current_state, "오류가 발생했습니다. 다시 시도해주세요.", gr.update(selected=0)
839
-
840
- def handle_save_reflection(text, state):
841
- if not text.strip():
842
- return state, []
843
 
844
- try:
845
- current_time = datetime.now().strftime("%H:%M:%S")
846
- if text_analyzer:
847
- sentiment = text_analyzer(text)[0]
848
- sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
849
- db.save_reflection(state.get("user_name", "익명"), text, sentiment)
850
- else:
851
- sentiment_text = "분석 불가"
852
- db.save_reflection(state.get("user_name", "익명"), text, {"label": "unknown", "score": 0.0})
853
-
854
- new_reflection = [current_time, text, sentiment_text]
855
- reflections = state.get("reflections", [])
856
- reflections.append(new_reflection)
857
- state = safe_state_update(state, {"reflections": reflections})
858
 
859
- return state, db.get_all_reflections()
860
- except Exception as e:
861
- print(f"Error saving reflection: {e}")
862
- return state, state.get("reflections", [])
863
-
864
- def save_reflection_fixed(text, state):
865
- if not text.strip():
866
- return state, []
 
 
867
 
868
- try:
869
- current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
870
- name = state.get("user_name", "익명")
 
 
 
 
 
 
871
 
872
- if text_analyzer:
873
- sentiment = text_analyzer(text)[0]
874
- sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
875
- else:
876
- sentiment_text = "분석 불가"
877
-
878
- # DB에 저장
879
- db.save_reflection(name, text, sentiment_text)
 
 
 
 
 
 
 
 
 
 
880
 
881
- # 화면에 표시할 데이터 형식으로 변환
882
- display_data = []
883
- all_reflections = db.get_all_reflections()
884
- for ref in all_reflections:
885
- display_data.append([
886
- ref["timestamp"],
887
- ref["reflection"],
888
- ref["sentiment"]
889
- ])
890
-
891
- # 상태 업데이트
892
- state = safe_state_update(state, {"reflections": display_data})
 
 
 
 
 
 
 
 
 
 
 
893
 
894
- return state, display_data
895
-
896
- except Exception as e:
897
- print(f"Error saving reflection: {e}")
898
- return state, []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
899
 
900
 
901
- def handle_save_wish(text, state):
902
- if not text.strip():
903
- return "소원을 입력해주세요.", []
904
-
905
- try:
906
- name = state.get("user_name", "익명")
907
- db.save_wish(name, text)
908
- wishes = db.get_all_wishes()
909
- wish_display_data = [
910
- [wish["timestamp"], wish["wish"], wish["name"]]
911
- for wish in wishes
912
- ]
913
- return "소원이 저장되었습니다.", wish_display_data
914
- except Exception as e:
915
- print(f"Error saving wish: {e}")
916
- return "오류가 발생했습니다.", []
917
 
918
- # 이벤트 연결
919
  name_submit_btn.click(
920
  fn=handle_name_submit,
921
  inputs=[name_input, state],
@@ -971,7 +970,6 @@ def create_interface():
971
  inputs=[final_reflection, state],
972
  outputs=[baseline_status, wishes_display]
973
  )
974
-
975
  return app
976
 
977
  if __name__ == "__main__":
@@ -992,4 +990,4 @@ if __name__ == "__main__":
992
  show_error=True,
993
  height=None,
994
  width="100%"
995
- )
 
168
  print(f"Error calculating baseline: {str(e)}")
169
  return None
170
 
 
171
  def map_acoustic_to_emotion(features, baseline_features=None):
172
  if features is None:
173
  return {
 
245
 
246
  return emotions
247
 
 
248
  def analyze_voice(audio_data, state):
249
  if audio_data is None:
250
  return state, "음성을 먼저 녹음해주세요.", "", "", ""
 
342
 
343
  return prompt
344
 
 
345
  def generate_image_from_prompt(prompt):
346
  if not prompt:
347
  print("No prompt provided")
 
377
  except Exception as e:
378
  print(f"Error generating image: {str(e)}")
379
  return None
380
+
 
381
  def create_pwa_files():
382
  """PWA 필요 파일들 생성"""
383
  # manifest.json 생성
 
393
  "theme_color": "#000000",
394
  "orientation": "portrait",
395
  "icons": [
396
+ {
397
+ "src": "/static/icons/icon-72x72.png",
398
+ "sizes": "72x72",
399
+ "type": "image/png",
400
+ "purpose": "any maskable"
401
+ },
402
+ {
403
+ "src": "/static/icons/icon-96x96.png",
404
+ "sizes": "96x96",
405
+ "type": "image/png",
406
+ "purpose": "any maskable"
407
+ },
408
+ {
409
+ "src": "/static/icons/icon-128x128.png",
410
+ "sizes": "128x128",
411
+ "type": "image/png",
412
+ "purpose": "any maskable"
413
+ },
414
+ {
415
+ "src": "/static/icons/icon-144x144.png",
416
+ "sizes": "144x144",
417
+ "type": "image/png",
418
+ "purpose": "any maskable"
419
+ },
420
+ {
421
+ "src": "/static/icons/icon-152x152.png",
422
+ "sizes": "152x152",
423
+ "type": "image/png",
424
+ "purpose": "any maskable"
425
+ },
426
+ {
427
+ "src": "/static/icons/icon-192x192.png",
428
+ "sizes": "192x192",
429
+ "type": "image/png",
430
+ "purpose": "any maskable"
431
+ },
432
+ {
433
+ "src": "/static/icons/icon-384x384.png",
434
+ "sizes": "384x384",
435
+ "type": "image/png",
436
+ "purpose": "any maskable"
437
+ },
438
+ {
439
+ "src": "/static/icons/icon-512x512.png",
440
+ "sizes": "512x512",
441
+ "type": "image/png",
442
+ "purpose": "any maskable"
443
+ }
444
  ]
445
  }
446
  with open(manifest_path, 'w', encoding='utf-8') as f:
 
560
  </body>
561
  </html>''')
562
 
 
563
  def safe_state_update(state, updates):
564
  try:
565
  new_state = {**state, **updates}
 
574
  print(f"State update error: {e}")
575
  return state
576
 
 
577
  def create_interface():
578
  db = SimpleDB()
579
 
 
588
  }
589
 
590
  css = """
591
+ @media (max-width: 600px) {
592
+ .container { padding: 10px !important; }
593
+ .gradio-row {
594
+ flex-direction: column !important;
595
+ gap: 10px !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
596
  }
597
+ .gradio-button {
598
+ width: 100% !important;
599
+ margin: 5px 0 !important;
600
+ min-height: 44px !important; /* 모바일 터치 영역 개선 */
601
  }
602
+ .gradio-textbox { width: 100% !important; }
603
+ .gradio-audio { width: 100% !important; }
604
+ .gradio-image { width: 100% !important; }
605
+ #audio-recorder { width: 100% !important; }
606
+ #result-image { width: 100% !important; }
607
+ .gradio-dataframe {
608
+ overflow-x: auto !important;
609
+ max-width: 100% !important;
610
  }
611
+ }
612
+
613
+ /* 전반적인 UI 개선 */
614
+ .gradio-button {
615
+ transition: all 0.3s ease;
616
+ }
617
+ .gradio-button:active {
618
+ transform: scale(0.98);
619
+ }
620
  """
621
 
622
  with gr.Blocks(theme=gr.themes.Soft(), css=css) as app:
623
  state = gr.State(value=initial_state)
624
+
625
+ gr.Markdown("# 디지털 굿판")
626
+ gr.Markdown("""
627
+ 1. 입장 → 2. 청신 → 3. 기원 → 4. 송신
628
+ 순서대로 진행해주세요.
629
+ """)
630
 
631
  with gr.Tabs(selected=0) as tabs:
632
  # 입장 탭 (축원 포함)
633
  with gr.TabItem("입장") as tab_entrance:
634
  # 1단계: 첫 화면
635
+ welcome_section = gr.Column(visible=True)
636
+ with welcome_section:
637
+ gr.Markdown(WELCOME_MESSAGE)
638
+ name_input = gr.Textbox(
639
+ label="이름을 알려주세요",
640
+ placeholder="이름을 입력해주세요",
641
+ interactive=True
642
  )
643
+ name_submit_btn = gr.Button("굿판 시작하기", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
644
 
645
+ # 2단계: 세계관 설명
646
+ story_section = gr.Column(visible=False)
647
+ with story_section:
648
+ gr.Markdown(ONCHEON_STORY)
649
+ continue_btn = gr.Button("준비하기", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
650
 
651
+ # 3단계: 축원 의식
652
+ blessing_section = gr.Column(visible=False)
653
+ with blessing_section:
654
+ gr.Markdown("### 축원의식을 시작하겠습니다")
655
+ gr.Markdown("'명짐 복짐 짊어지고 안가태평하시기를 비도발원 축원 드립니다'")
656
+ baseline_audio = gr.Audio(
657
+ label="축원 문장 녹음하기",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  sources=["microphone"],
659
  type="numpy",
660
  streaming=False
661
  )
662
+ set_baseline_btn = gr.Button("축원 마치기", variant="primary")
663
+ baseline_status = gr.Markdown("")
664
+
665
+ # 4단계: 굿판 입장 안내
666
+ entry_guide_section = gr.Column(visible=False)
667
+ with entry_guide_section:
668
+ gr.Markdown("## 굿판으로 입장하기")
669
+ gr.Markdown("""
670
+ * 청신 탭으로 이동해 주세요.
671
+ * 부산광역시 동래구 온천장역에서 시작하면 더욱 깊은 경험을 시작할 수 있습니다.
672
+ * (본격적인 경험을 시작하기에 앞서 이동을 권장드립니다)
673
+ """)
674
+ enter_btn = gr.Button("청신 의식 시작하기", variant="primary")
675
+
676
+ with gr.TabItem("청신") as tab_listen:
677
+ gr.Markdown("## 청신 - 소리로 정화하기")
678
+ gr.Markdown("""
679
+ 온천천의 소리를 들으며 마음을 정화해보세요.
680
+
681
+ 💫 이 앱은 온천천의 사운드스케이프를 녹음하여 제작되었으며,
682
+ 온천천 온천장역에서 장전역까지 걸으며 더 깊은 체험이 가능합니다.
683
+ """)
684
+ play_music_btn = gr.Button("온천천의 소리 듣기", variant="secondary")
685
+ with gr.Row():
686
+ audio = gr.Audio(
687
+ value="assets/main_music.mp3",
688
+ type="filepath",
689
+ label="온천천의 소리",
690
+ interactive=False,
691
+ show_download_button=True,
692
+ visible=True
693
  )
694
+ with gr.Column():
695
+ reflection_input = gr.Textbox(
696
+ label="지금 순간의 감상을 자유롭게 적어보세요",
697
+ lines=3,
698
+ max_lines=5
699
+ )
700
+ save_btn = gr.Button("감상 저장하기", variant="secondary")
701
+ reflections_display = gr.Dataframe(
702
+ headers=["시간", "감상", "감정 분석"],
703
+ label="기록된 감상들",
704
+ value=[], # 초기값은 빈 리스트
705
+ interactive=False,
706
+ wrap=True,
707
+ row_count=(5, "dynamic") # 동적으로 행 수 조정
708
+ )
709
+
710
+ # 기원
711
+ with gr.TabItem("기원") as tab_wish:
712
+ gr.Markdown("## 기원 - 소원을 전해보세요")
713
+ with gr.Row():
714
+ with gr.Column():
715
+ voice_input = gr.Audio(
716
+ label="소원을 나누고 싶은 마음을 말해주세요",
717
+ sources=["microphone"],
718
+ type="numpy",
719
+ streaming=False
720
+ )
721
+ with gr.Row():
722
+ clear_btn = gr.Button("녹음 지우기", variant="secondary")
723
+ analyze_btn = gr.Button("소원 분석하기", variant="primary")
724
+
725
+ with gr.Column():
726
+ transcribed_text = gr.Textbox(
727
+ label="인식된 텍스트",
728
+ interactive=False
729
+ )
730
+ voice_emotion = gr.Textbox(
731
+ label="음성 감정 분석",
732
+ interactive=False
733
+ )
734
+ text_emotion = gr.Textbox(
735
+ label="텍스트 감정 분석",
736
+ interactive=False
737
+ )
738
+
739
+ # 송신 탭
740
+ with gr.TabItem("송신") as tab_send:
741
+ gr.Markdown("## 송신 - 소지(소원지)를 그려 날려 태워봅시다")
742
+ final_prompt = gr.Textbox(
743
+ label="생성된 프롬프트",
744
+ interactive=False,
745
+ lines=3
746
+ )
747
+ generate_btn = gr.Button("마음의 그림 그리기", variant="primary")
748
+ result_image = gr.Image(
749
+ label="생성된 이미지",
750
+ show_download_button=True
751
+ )
752
+
753
+ gr.Markdown("## 온천천에 전하고 싶은 소원을 남겨주세요")
754
+ final_reflection = gr.Textbox(
755
+ label="소원",
756
+ placeholder="당신의 소원을 한 줄로 남겨주세요...",
757
+ max_lines=3
758
+ )
759
+ save_final_btn = gr.Button("소원 전하기", variant="primary")
760
+ gr.Markdown("""
761
+ 💫 여러분의 소원은 11월 25일 온천천 벽면에 설치될 소원나무에 전시될 예정입니다.
762
  따뜻한 마음을 담아 작성해주세요.
763
  """)
764
+ wishes_display = gr.Dataframe(
765
+ headers=["시간", "소원", "이름"],
766
+ label="기록된 소원들",
767
+ value=[],
768
+ interactive=False,
769
+ wrap=True
770
+ )
771
 
772
+ # 이벤트 핸들러들
773
+ def handle_name_submit(name, state):
774
+ if not name.strip():
775
+ return (
776
+ gr.update(visible=True),
777
+ gr.update(visible=False),
778
+ gr.update(visible=False),
779
+ gr.update(visible=False),
780
+ state
781
+ )
782
+ state = safe_state_update(state, {"user_name": name})
783
  return (
 
784
  gr.update(visible=False),
785
+ gr.update(visible=True),
786
  gr.update(visible=False),
787
  gr.update(visible=False),
788
  state
789
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
790
 
791
+ def handle_continue():
 
792
  return (
 
 
793
  gr.update(visible=False),
794
+ gr.update(visible=False),
795
+ gr.update(visible=True),
796
+ gr.update(visible=False)
797
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
798
 
799
+ def handle_blessing_complete(audio, state):
800
+ if audio is None:
801
+ return state, "음성을 먼저 녹음해주세요.", gr.update(visible=True), gr.update(visible=False)
 
 
 
 
 
 
 
 
 
 
 
802
 
803
+ try:
804
+ # ... (기존 음성 처리 코드)
805
+ return (
806
+ state,
807
+ "축원이 완료되었습니다.",
808
+ gr.update(visible=False),
809
+ gr.update(visible=True)
810
+ )
811
+ except Exception as e:
812
+ return state, f"오류가 발생했습니다: {str(e)}", gr.update(visible=True), gr.update(visible=False)
813
 
814
+ def handle_enter():
815
+ return gr.update(selected=1) # 청신 탭으로 이동
816
+
817
+ def handle_start():
818
+ return gr.update(visible=False), gr.update(visible=True)
819
+
820
+ def handle_baseline(audio, current_state):
821
+ if audio is None:
822
+ return current_state, "음성을 먼저 녹음해주세요.", gr.update(selected=0)
823
 
824
+ try:
825
+ sr, y = audio
826
+ y = y.astype(np.float32)
827
+ features = calculate_baseline_features((sr, y))
828
+ if features:
829
+ current_state = safe_state_update(current_state, {
830
+ "baseline_features": features,
831
+ "current_tab": 1
832
+ })
833
+ return current_state, "기준점이 설정되었습니다. 청신 탭으로 이동합니다.", gr.update(selected=1)
834
+ return current_state, "기준점 설정에 실패했습니다. 다시 시도해주세요.", gr.update(selected=0)
835
+ except Exception as e:
836
+ print(f"Baseline error: {str(e)}")
837
+ return current_state, "오류가 발생했습니다. 다시 시도해주세요.", gr.update(selected=0)
838
+
839
+ def handle_save_reflection(text, state):
840
+ if not text.strip():
841
+ return state, []
842
 
843
+ try:
844
+ current_time = datetime.now().strftime("%H:%M:%S")
845
+ if text_analyzer:
846
+ sentiment = text_analyzer(text)[0]
847
+ sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
848
+ db.save_reflection(state.get("user_name", "익명"), text, sentiment)
849
+ else:
850
+ sentiment_text = "분석 불가"
851
+ db.save_reflection(state.get("user_name", "익명"), text, {"label": "unknown", "score": 0.0})
852
+
853
+ new_reflection = [current_time, text, sentiment_text]
854
+ reflections = state.get("reflections", [])
855
+ reflections.append(new_reflection)
856
+ state = safe_state_update(state, {"reflections": reflections})
857
+
858
+ return state, db.get_all_reflections()
859
+ except Exception as e:
860
+ print(f"Error saving reflection: {e}")
861
+ return state, state.get("reflections", [])
862
+
863
+ def save_reflection_fixed(text, state):
864
+ if not text.strip():
865
+ return state, []
866
 
867
+ try:
868
+ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
869
+ name = state.get("user_name", "익명")
870
+
871
+ if text_analyzer:
872
+ sentiment = text_analyzer(text)[0]
873
+ sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
874
+ else:
875
+ sentiment_text = "분석 불가"
876
+
877
+ # DB에 저장
878
+ db.save_reflection(name, text, sentiment_text)
879
+
880
+ # 화면에 표시할 데이터 형식으로 변환
881
+ display_data = []
882
+ all_reflections = db.get_all_reflections()
883
+ for ref in all_reflections:
884
+ display_data.append([
885
+ ref["timestamp"],
886
+ ref["reflection"],
887
+ ref["sentiment"]
888
+ ])
889
+
890
+ # 상태 업데이트
891
+ state = safe_state_update(state, {"reflections": display_data})
892
+
893
+ return state, display_data
894
+
895
+ except Exception as e:
896
+ print(f"Error saving reflection: {e}")
897
+ return state, []
898
 
899
 
900
+ def handle_save_wish(text, state):
901
+ if not text.strip():
902
+ return "소원을 입력해주세요.", []
903
+
904
+ try:
905
+ name = state.get("user_name", "익명")
906
+ db.save_wish(name, text)
907
+ wishes = db.get_all_wishes()
908
+ wish_display_data = [
909
+ [wish["timestamp"], wish["wish"], wish["name"]]
910
+ for wish in wishes
911
+ ]
912
+ return "소원이 저장되었습니다.", wish_display_data
913
+ except Exception as e:
914
+ print(f"Error saving wish: {e}")
915
+ return "오류가 발생했습니다.", []
916
 
917
+ # 이벤트 연결
918
  name_submit_btn.click(
919
  fn=handle_name_submit,
920
  inputs=[name_input, state],
 
970
  inputs=[final_reflection, state],
971
  outputs=[baseline_status, wishes_display]
972
  )
 
973
  return app
974
 
975
  if __name__ == "__main__":
 
990
  show_error=True,
991
  height=None,
992
  width="100%"
993
+ )