haepada commited on
Commit
1089706
·
verified ·
1 Parent(s): 97bda44

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +348 -398
app.py CHANGED
@@ -168,6 +168,7 @@ def calculate_baseline_features(audio_data):
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,6 +246,7 @@ def map_acoustic_to_emotion(features, baseline_features=None):
245
 
246
  return emotions
247
 
 
248
  def analyze_voice(audio_data, state):
249
  if audio_data is None:
250
  return state, "음성을 먼저 녹음해주세요.", "", "", ""
@@ -342,6 +344,7 @@ def generate_detailed_prompt(text, emotions, text_sentiment):
342
 
343
  return prompt
344
 
 
345
  def generate_image_from_prompt(prompt):
346
  if not prompt:
347
  print("No prompt provided")
@@ -377,7 +380,8 @@ def generate_image_from_prompt(prompt):
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,54 +397,14 @@ def create_pwa_files():
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,6 +524,7 @@ self.addEventListener('fetch', event => {
560
  </body>
561
  </html>''')
562
 
 
563
  def safe_state_update(state, updates):
564
  try:
565
  new_state = {**state, **updates}
@@ -574,6 +539,7 @@ def safe_state_update(state, updates):
574
  print(f"State update error: {e}")
575
  return state
576
 
 
577
  def create_interface():
578
  db = SimpleDB()
579
 
@@ -656,395 +622,379 @@ def create_interface():
656
  """
657
 
658
  with gr.Blocks(theme=gr.themes.Soft(), css=css) as app:
659
- state = gr.State(value=initial_state)
660
-
661
- # 화면 크기에 따라 로고 이미지 설정
662
- logo_image_path = "/static/DIGITAL_GUTPAN_LOGO_m.png" if request.user_agent.platform == 'mobile' else "/static/DIGITAL_GUTPAN_LOGO_w.png"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
663
 
664
- with gr.Row(elem_id="logo-container"):
665
- gr.Image(value=logo_image_path, show_label=False, interactive=False, elem_id="logo-image")
666
-
667
- with gr.Tabs(selected=0) as tabs:
668
- # 입장 (축원 포함)
669
- with gr.TabItem("입장") as tab_entrance:
670
- # 1단계: 첫 화면
671
- with gr.Column(visible=True) as welcome_section:
672
- # Main image
673
- gr.Image(
674
- value="static/main-image.png",
675
- show_label=False,
676
- interactive=False,
677
- elem_id="main-image"
678
- )
679
 
680
- # 기존 텍스트 및 다른 요소들
681
- gr.Markdown(WELCOME_MESSAGE)
682
- name_input = gr.Textbox(
683
- label="이름을 알려주세요",
684
- placeholder="이름을 입력해주세요",
685
- interactive=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
686
  )
687
- name_submit_btn = gr.Button("굿판 시작하기", variant="primary")
688
-
689
- gr.Markdown(
690
- """
691
- <p style="color: #374151; text-align: center; font-size: 0.9em;">
692
- ‘디지털 굿판’은 부산문화재단의 지원으로 루츠리딤이 제작한 다원예술 프로젝트입니다.<br>
693
- 본 프로젝트에서 기록된 정보 중<br>
694
- ‘송신’ 단계의 ‘소원전하기’를 제외한 모든 과정의 정보는 (목소리, 감상 등)<br>
695
- 별도로 저장되지 않으며 AI 학습이나 외부 공유에 사용되지 않습니다.
696
- </p>
697
- """,
698
- visible=True
699
  )
700
-
701
 
702
- # 2단계: 세계관 설명
703
- story_section = gr.Column(visible=False)
704
- with story_section:
705
- gr.Markdown(ONCHEON_STORY)
706
- continue_btn = gr.Button("준비하기", variant="primary")
707
-
708
- # 3단계: 축원 의식
709
- blessing_section = gr.Column(visible=False)
710
- with blessing_section:
711
- gr.Markdown("### 축원의식을 시작하겠습니다")
712
- gr.Markdown("'명짐 복짐 짊어지고 안가태평하시기를 비도발원 축원 드립니다'")
713
- baseline_audio = gr.Audio(
714
- label="축원 문장 녹음하기",
715
  sources=["microphone"],
716
  type="numpy",
717
  streaming=False
718
  )
719
- set_baseline_btn = gr.Button("축원 마치기", variant="primary")
720
- baseline_status = gr.Markdown("")
721
-
722
- # 4단계: 굿판 입장 안내
723
- entry_guide_section = gr.Column(visible=False)
724
- with entry_guide_section:
725
- gr.Markdown("## 굿판으로 입장하기")
726
- gr.Markdown("""
727
- * 청신 탭으로 이동해 주세요.
728
- * 부산광역시 동래구 온천장역에서 시작하면 더욱 깊은 경험을 시작할 수 있습니다.
729
- * (본격적인 경험을 시작하기에 앞서 이동을 권장드립니다)
730
- """)
731
- enter_btn = gr.Button("청신 의식 시작하기", variant="primary")
732
-
733
- with gr.TabItem("청신") as tab_listen:
734
- gr.Markdown("## 청신 - 소리로 정화하기")
735
- gr.Markdown("""
736
- 온천천의 소리를 들으며 마음을 정화해보세요.
737
-
738
- 💫 이 앱은 온천천의 사운드스케이프를 녹음하여 제작되었으며,
739
- 온천천 온천장역에서 장전역까지 걸으며 더 깊은 체험이 가능합니다.
740
- """)
741
- play_music_btn = gr.Button("온천천의 소리 듣기", variant="secondary")
742
- with gr.Row():
743
- audio = gr.Audio(
744
- value="assets/main_music.mp3",
745
- type="filepath",
746
- label="온천천의 소리",
747
- interactive=False,
748
- show_download_button=True,
749
- visible=True
750
  )
751
- with gr.Column():
752
- reflection_input = gr.Textbox(
753
- label="지금 이 순간의 감상을 자유롭게 적어보세요",
754
- lines=3,
755
- max_lines=5
756
- )
757
- save_btn = gr.Button("감상 저장하기", variant="secondary")
758
- reflections_display = gr.Dataframe(
759
- headers=["시간", "감상", "감정 분석"],
760
- label="기록된 감상들",
761
- value=[], # 초기값은 리스트
762
- interactive=False,
763
- wrap=True,
764
- row_count=(5, "dynamic") # 동적으로 행 수 조정
765
- )
766
-
767
- # 기원 탭
768
- with gr.TabItem("기원") as tab_wish:
769
- gr.Markdown("## 기원 - 소원을 전해보세요")
770
- with gr.Row():
771
- with gr.Column():
772
- voice_input = gr.Audio(
773
- label="소원을 나누고 싶은 마음을 말해주세요",
774
- sources=["microphone"],
775
- type="numpy",
776
- streaming=False
777
- )
778
- with gr.Row():
779
- clear_btn = gr.Button("녹음 지우기", variant="secondary")
780
- analyze_btn = gr.Button("소원 분석하기", variant="primary")
781
-
782
- with gr.Column():
783
- transcribed_text = gr.Textbox(
784
- label="인식된 텍스트",
785
- interactive=False
786
- )
787
- voice_emotion = gr.Textbox(
788
- label="음성 감정 분석",
789
- interactive=False
790
- )
791
- text_emotion = gr.Textbox(
792
- label="텍스트 감정 분석",
793
- interactive=False
794
- )
795
-
796
- # 송신 탭
797
- with gr.TabItem("송신") as tab_send:
798
- gr.Markdown("## 송신 - 소지(소원지)를 그려 날려 태워봅시다")
799
- final_prompt = gr.Textbox(
800
- label="생성된 프롬프트",
801
- interactive=False,
802
- lines=3
803
- )
804
- generate_btn = gr.Button("마음의 그림 그리기", variant="primary")
805
- result_image = gr.Image(
806
- label="생성된 이미지",
807
- show_download_button=True
808
- )
809
-
810
- gr.Markdown("## 온천천에 전하고 싶은 소원을 남겨주세요")
811
- final_reflection = gr.Textbox(
812
- label="소원",
813
- placeholder="당신의 소원을 한 줄로 남겨주세요...",
814
- max_lines=3
815
- )
816
- save_final_btn = gr.Button("소원 전하기", variant="primary")
817
- gr.Markdown("""
818
- 💫 여러분의 소원은 11월 25일 온천천 벽면에 설치될 소원나무에 전시될 예정입니다.
819
  따뜻한 마음을 담아 작성해주세요.
820
  """)
821
- wishes_display = gr.Dataframe(
822
- headers=["시간", "소원", "이름"],
823
- label="기록된 소원들",
824
- value=[],
825
- interactive=False,
826
- wrap=True
827
- )
828
 
829
- # 이벤트 핸들러들
830
- def handle_name_submit(name, state):
831
- if not name.strip():
832
- return (
833
- gr.update(visible=True),
834
- gr.update(visible=False),
835
- gr.update(visible=False),
836
- gr.update(visible=False),
837
- state
838
- )
839
- state = safe_state_update(state, {"user_name": name})
840
  return (
841
- gr.update(visible=False),
842
  gr.update(visible=True),
843
  gr.update(visible=False),
844
  gr.update(visible=False),
 
845
  state
846
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
847
 
848
- def handle_continue():
 
849
  return (
 
 
850
  gr.update(visible=False),
851
- gr.update(visible=False),
852
- gr.update(visible=True),
853
- gr.update(visible=False)
854
  )
855
-
856
- def handle_blessing_complete(audio, state):
857
- if audio is None:
858
- return state, "음성을 먼저 녹음해주세요.", gr.update(visible=True), gr.update(visible=False)
859
-
860
- try:
861
- # ... (기존 음성 처리 코드)
862
- return (
863
- state,
864
- "축원이 완료되었습니다.",
865
- gr.update(visible=False),
866
- gr.update(visible=True)
867
- )
868
- except Exception as e:
869
- return state, f"오류가 발생했습니다: {str(e)}", gr.update(visible=True), gr.update(visible=False)
870
-
871
- def handle_enter():
872
- return gr.update(selected=1) # 청신 탭으로 이동
873
 
874
- def handle_start():
875
- return gr.update(visible=False), gr.update(visible=True)
876
 
877
- def handle_baseline(audio, current_state):
878
- if audio is None:
879
- return current_state, "음성을 먼저 녹음해주세요.", gr.update(selected=0)
880
-
881
- try:
882
- sr, y = audio
883
- y = y.astype(np.float32)
884
- features = calculate_baseline_features((sr, y))
885
- if features:
886
- current_state = safe_state_update(current_state, {
887
- "baseline_features": features,
888
- "current_tab": 1
889
- })
890
- return current_state, "기준점이 설정되었습니다. 청신 탭으로 이동합니다.", gr.update(selected=1)
891
- return current_state, "기준점 설정에 실패했습니다. 다시 시도해주세요.", gr.update(selected=0)
892
- except Exception as e:
893
- print(f"Baseline error: {str(e)}")
894
- return current_state, "오류가 발생했습니다. 다시 시도해주세요.", gr.update(selected=0)
895
 
896
- def handle_save_reflection(text, state):
897
- if not text.strip():
898
- return state, []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
899
 
900
- try:
901
- current_time = datetime.now().strftime("%H:%M:%S")
902
- if text_analyzer:
903
- sentiment = text_analyzer(text)[0]
904
- sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
905
- db.save_reflection(state.get("user_name", "익명"), text, sentiment)
906
- else:
907
- sentiment_text = "분석 불가"
908
- db.save_reflection(state.get("user_name", "익명"), text, {"label": "unknown", "score": 0.0})
909
-
910
- new_reflection = [current_time, text, sentiment_text]
911
- reflections = state.get("reflections", [])
912
- reflections.append(new_reflection)
913
- state = safe_state_update(state, {"reflections": reflections})
914
-
915
- return state, db.get_all_reflections()
916
- except Exception as e:
917
- print(f"Error saving reflection: {e}")
918
- return state, state.get("reflections", [])
919
 
920
- def save_reflection_fixed(text, state):
921
- if not text.strip():
922
- return state, []
923
-
924
- try:
925
- current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
926
- name = state.get("user_name", "익명")
927
-
928
- if text_analyzer:
929
- sentiment = text_analyzer(text)[0]
930
- sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
931
- else:
932
- sentiment_text = "분석 불가"
933
 
934
- # DB에 저장
935
- db.save_reflection(name, text, sentiment_text)
936
-
937
- # 화면에 표시할 데이터 형식으로 변환
938
- display_data = []
939
- all_reflections = db.get_all_reflections()
940
- for ref in all_reflections:
941
- display_data.append([
942
- ref["timestamp"],
943
- ref["reflection"],
944
- ref["sentiment"]
945
- ])
946
-
947
- # 상태 업데이트
948
- state = safe_state_update(state, {"reflections": display_data})
949
-
950
- return state, display_data
951
-
952
- except Exception as e:
953
- print(f"Error saving reflection: {e}")
954
- return state, []
 
 
 
 
 
 
 
 
 
 
955
 
956
 
957
- def handle_save_wish(text, state):
958
- if not text.strip():
959
- return "소원을 입력해주세요.", []
960
-
961
- try:
962
- name = state.get("user_name", "익명")
963
- db.save_wish(name, text)
964
- wishes = db.get_all_wishes()
965
- wish_display_data = [
966
- [wish["timestamp"], wish["wish"], wish["name"]]
967
- for wish in wishes
968
- ]
969
- return "소원이 저장되었습니다.", wish_display_data
970
- except Exception as e:
971
- print(f"Error saving wish: {e}")
972
- return "오류가 발생했습니다.", []
973
-
974
- # 이벤트 연결
975
- name_submit_btn.click(
976
- fn=handle_name_submit,
977
- inputs=[name_input, state],
978
- outputs=[welcome_section, story_section, blessing_section, entry_guide_section, state]
979
- )
980
 
981
- continue_btn.click(
982
- fn=handle_continue,
983
- outputs=[story_section, welcome_section, blessing_section, entry_guide_section]
984
- )
985
 
986
- set_baseline_btn.click(
987
- fn=handle_blessing_complete,
988
- inputs=[baseline_audio, state],
989
- outputs=[state, baseline_status, blessing_section, entry_guide_section]
990
- )
991
 
992
- enter_btn.click(
993
- fn=handle_enter,
994
- outputs=[tabs]
995
- )
996
 
997
- play_music_btn.click(
998
- fn=lambda: "assets/main_music.mp3",
999
- outputs=[audio]
1000
- )
1001
 
1002
- save_btn.click(
1003
- fn=save_reflection_fixed, # handle_save_reflection -> save_reflection_fixed
1004
- inputs=[reflection_input, state],
1005
- outputs=[state, reflections_display]
1006
- )
1007
 
1008
- clear_btn.click(
1009
- fn=lambda: None,
1010
- outputs=[voice_input]
1011
- )
1012
 
1013
- analyze_btn.click(
1014
- fn=analyze_voice,
1015
- inputs=[voice_input, state],
1016
- outputs=[state, transcribed_text, voice_emotion, text_emotion, final_prompt]
1017
- )
1018
 
1019
- generate_btn.click(
1020
- fn=generate_image_from_prompt,
1021
- inputs=[final_prompt],
1022
- outputs=[result_image]
1023
- )
 
 
 
 
 
 
1024
 
1025
- save_final_btn.click(
1026
- fn=handle_save_wish,
1027
- inputs=[final_reflection, state],
1028
- outputs=[baseline_status, wishes_display]
1029
- )
1030
  return app
1031
 
 
1032
  if __name__ == "__main__":
1033
- # 필요한 디렉토리 생성
1034
- for directory in ['static/icons', 'assets', 'templates', 'data', 'generated_images']:
1035
- os.makedirs(directory, exist_ok=True)
1036
-
1037
- # PWA 파일 생성
1038
- create_pwa_files()
1039
-
1040
- # Gradio 인터페이스 생성 및 실행
1041
- demo = create_interface()
1042
- demo.launch(
1043
- server_name="0.0.0.0",
1044
- server_port=7860,
1045
- share=True,
1046
- debug=True,
1047
- show_error=True,
1048
- height=None,
1049
- width="100%"
1050
- )
 
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
 
247
  return emotions
248
 
249
+
250
  def analyze_voice(audio_data, state):
251
  if audio_data is None:
252
  return state, "음성을 먼저 녹음해주세요.", "", "", ""
 
344
 
345
  return prompt
346
 
347
+
348
  def generate_image_from_prompt(prompt):
349
  if not prompt:
350
  print("No prompt provided")
 
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
  "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
  </body>
525
  </html>''')
526
 
527
+
528
  def safe_state_update(state, updates):
529
  try:
530
  new_state = {**state, **updates}
 
539
  print(f"State update error: {e}")
540
  return state
541
 
542
+
543
  def create_interface():
544
  db = SimpleDB()
545
 
 
622
  """
623
 
624
  with gr.Blocks(theme=gr.themes.Soft(), css=css) as app:
625
+ state = gr.State(value=initial_state)
626
+
627
+ # 화면 크기에 따라 로고 이미지 설정
628
+ logo_image_path = "/static/DIGITAL_GUTPAN_LOGO_m.png" if request.user_agent.platform == 'mobile' else "/static/DIGITAL_GUTPAN_LOGO_w.png"
629
+
630
+ with gr.Row(elem_id="logo-container"):
631
+ gr.Image(value=logo_image_path, show_label=False, interactive=False, elem_id="logo-image")
632
+
633
+ with gr.Tabs(selected=0) as tabs:
634
+ # 입장 탭 (축원 포함)
635
+ with gr.TabItem("입장") as tab_entrance:
636
+ # 1단계: 첫 화면
637
+ with gr.Column(visible=True) as welcome_section:
638
+ # Main image
639
+ gr.Image(
640
+ value="static/main-image.png",
641
+ show_label=False,
642
+ interactive=False,
643
+ elem_id="main-image"
644
+ )
645
+
646
+ # 기존 텍스트 및 다른 요소들
647
+ gr.Markdown(WELCOME_MESSAGE)
648
+ name_input = gr.Textbox(
649
+ label="이름을 알려주세요",
650
+ placeholder="이름을 입력해주세요",
651
+ interactive=True
652
+ )
653
+ name_submit_btn = gr.Button("굿판 시작하기", variant="primary")
654
+
655
+ gr.Markdown(
656
+ """
657
+ <p style="color: #374151; text-align: center; font-size: 0.9em;">
658
+ ‘디지털 굿판’은 부산문화재단의 지원으로 루츠리딤이 제작한 다원예술 프로젝트입니다.<br>
659
+ 본 프로젝트에서 기록된 정보 중<br>
660
+ ‘송신’ 단계의 ‘소원전하기’를 제외한 모든 과정의 정보는 (목소리, 감상 등)<br>
661
+ 별도로 저장되지 않으며 AI 학습이나 외부 공유에 사용되지 않습니다.
662
+ </p>
663
+ """,
664
+ visible=True
665
+ )
666
 
667
+ # 2단계: 세계관 설명
668
+ story_section = gr.Column(visible=False)
669
+ with story_section:
670
+ gr.Markdown(ONCHEON_STORY)
671
+ continue_btn = gr.Button("준비하기", variant="primary")
 
 
 
 
 
 
 
 
 
 
672
 
673
+ # 3단계: 축원 의식
674
+ blessing_section = gr.Column(visible=False)
675
+ with blessing_section:
676
+ gr.Markdown("### 축원의식을 시작하겠습니다")
677
+ gr.Markdown("'명짐 복짐 짊어지고 안가태평하시기를 비도발원 축원 드립니다'")
678
+ baseline_audio = gr.Audio(
679
+ label="축원 문장 녹음하기",
680
+ sources=["microphone"],
681
+ type="numpy",
682
+ streaming=False
683
+ )
684
+ set_baseline_btn = gr.Button("축원 마치기", variant="primary")
685
+ baseline_status = gr.Markdown("")
686
+
687
+ # 4단계: 굿판 입장 안내
688
+ entry_guide_section = gr.Column(visible=False)
689
+ with entry_guide_section:
690
+ gr.Markdown("## 굿판으로 입장하기")
691
+ gr.Markdown("""
692
+ * 청신 탭으로 이동해 주세요.
693
+ * 부산광역시 동래구 온천장역에서 시작하면 더욱 깊은 경험을 시작할 수 있습니다.
694
+ * (본격적인 경험을 시작하기에 앞서 이동을 권장드립니다)
695
+ """)
696
+ enter_btn = gr.Button("청신 의식 시작하기", variant="primary")
697
+
698
+ with gr.TabItem("청신") as tab_listen:
699
+ gr.Markdown("## 청신 - 소리로 정화하기")
700
+ gr.Markdown("""
701
+ 온천천의 소리를 들으며 마음을 정화해보세요.
702
+
703
+ 🇫 이 앱은 온천천의 사운드스케이프를 녹음하여 제작되었으며,
704
+ 온천천 온천장역에서 장전역까지 걸으며 더 깊은 체험이 가능합니다.
705
+ """)
706
+ play_music_btn = gr.Button("온천천의 소리 듣기", variant="secondary")
707
+ with gr.Row():
708
+ audio = gr.Audio(
709
+ value="assets/main_music.mp3",
710
+ type="filepath",
711
+ label="온천천의 소리",
712
+ interactive=False,
713
+ show_download_button=True,
714
+ visible=True
715
+ )
716
+ with gr.Column():
717
+ reflection_input = gr.Textbox(
718
+ label="지금 이 순간의 감상을 자유롭게 적어보세요",
719
+ lines=3,
720
+ max_lines=5
721
  )
722
+ save_btn = gr.Button("감상 저장하기", variant="secondary")
723
+ reflections_display = gr.Dataframe(
724
+ headers=["시간", "감상", "감정 분석"],
725
+ label="기록된 감상들",
726
+ value=[], # 초기값은 리스트
727
+ interactive=False,
728
+ wrap=True,
729
+ row_count=(5, "dynamic") # 동적으로 조정
 
 
 
 
730
  )
 
731
 
732
+ # 기원
733
+ with gr.TabItem("기원") as tab_wish:
734
+ gr.Markdown("## 기원 - 소원을 전해보세요")
735
+ with gr.Row():
736
+ with gr.Column():
737
+ voice_input = gr.Audio(
738
+ label="소원을 나누고 싶은 마음을 말해주세요",
 
 
 
 
 
 
739
  sources=["microphone"],
740
  type="numpy",
741
  streaming=False
742
  )
743
+ with gr.Row():
744
+ clear_btn = gr.Button("녹음 지우기", variant="secondary")
745
+ analyze_btn = gr.Button("소원 분석하기", variant="primary")
746
+
747
+ with gr.Column():
748
+ transcribed_text = gr.Textbox(
749
+ label="인식된 텍스트",
750
+ interactive=False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
751
  )
752
+ voice_emotion = gr.Textbox(
753
+ label="음성 감정 분석",
754
+ interactive=False
755
+ )
756
+ text_emotion = gr.Textbox(
757
+ label="텍스트 감정 분석",
758
+ interactive=False
759
+ )
760
+
761
+ # 송신 탭
762
+ with gr.TabItem("송신") as tab_send:
763
+ gr.Markdown("## 송신 - 소지(소원지)를 그려 날려 태워봅시다")
764
+ final_prompt = gr.Textbox(
765
+ label="생성된 프롬프트",
766
+ interactive=False,
767
+ lines=3
768
+ )
769
+ generate_btn = gr.Button("마음의 그림 그리기", variant="primary")
770
+ result_image = gr.Image(
771
+ label="생성된 이미지",
772
+ show_download_button=True
773
+ )
774
+
775
+ gr.Markdown("## 온천천에 전하고 싶은 소원을 남겨주세요")
776
+ final_reflection = gr.Textbox(
777
+ label="소원",
778
+ placeholder="당신의 소원을 한 줄로 남겨주세요...",
779
+ max_lines=3
780
+ )
781
+ save_final_btn = gr.Button("소원 전하기", variant="primary")
782
+ gr.Markdown("""
783
+ 🇫 여러분의 소원은 11월 25일 온천천 벽면에 설치될 소원나무에 전시될 예정입니다.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
784
  따뜻한 마음을 담아 작성해주세요.
785
  """)
786
+ wishes_display = gr.Dataframe(
787
+ headers=["시간", "소원", "이름"],
788
+ label="기록된 소원들",
789
+ value=[],
790
+ interactive=False,
791
+ wrap=True
792
+ )
793
 
794
+ # 이벤트 핸들러들
795
+ def handle_name_submit(name, state):
796
+ if not name.strip():
 
 
 
 
 
 
 
 
797
  return (
 
798
  gr.update(visible=True),
799
  gr.update(visible=False),
800
  gr.update(visible=False),
801
+ gr.update(visible=False),
802
  state
803
  )
804
+ state = safe_state_update(state, {"user_name": name})
805
+ return (
806
+ gr.update(visible=False),
807
+ gr.update(visible=True),
808
+ gr.update(visible=False),
809
+ gr.update(visible=False),
810
+ state
811
+ )
812
+
813
+ def handle_continue():
814
+ return (
815
+ gr.update(visible=False),
816
+ gr.update(visible=False),
817
+ gr.update(visible=True),
818
+ gr.update(visible=False)
819
+ )
820
+
821
+ def handle_blessing_complete(audio, state):
822
+ if audio is None:
823
+ return state, "음성을 먼저 녹음해주세요.", gr.update(visible=True), gr.update(visible=False)
824
 
825
+ try:
826
+ # ... (기존 음성 처리 코드)
827
  return (
828
+ state,
829
+ "축원이 완료되었습니다.",
830
  gr.update(visible=False),
831
+ gr.update(visible=True)
 
 
832
  )
833
+ except Exception as e:
834
+ return state, f"오류가 발생했습니다: {str(e)}", gr.update(visible=True), gr.update(visible=False)
835
+
836
+ def handle_enter():
837
+ return gr.update(selected=1) # 청신 탭으로 이동
 
 
 
 
 
 
 
 
 
 
 
 
 
838
 
839
+ def handle_start():
840
+ return gr.update(visible=False), gr.update(visible=True)
841
 
842
+ def handle_baseline(audio, current_state):
843
+ if audio is None:
844
+ return current_state, "음성을 먼저 녹음해주세요.", gr.update(selected=0)
845
+
846
+ try:
847
+ sr, y = audio
848
+ y = y.astype(np.float32)
849
+ features = calculate_baseline_features((sr, y))
850
+ if features:
851
+ current_state = safe_state_update(current_state, {
852
+ "baseline_features": features,
853
+ "current_tab": 1
854
+ })
855
+ return current_state, "기준점이 설정되었습니다. 청신 탭으로 이동합니다.", gr.update(selected=1)
856
+ return current_state, "기준점 설정에 실패했습니다. 다시 시도해주세요.", gr.update(selected=0)
857
+ except Exception as e:
858
+ print(f"Baseline error: {str(e)}")
859
+ return current_state, "오류가 발생했습니다. 다시 시도해주세요.", gr.update(selected=0)
860
 
861
+ def handle_save_reflection(text, state):
862
+ if not text.strip():
863
+ return state, []
864
+
865
+ try:
866
+ current_time = datetime.now().strftime("%H:%M:%S")
867
+ if text_analyzer:
868
+ sentiment = text_analyzer(text)[0]
869
+ sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
870
+ db.save_reflection(state.get("user_name", "익명"), text, sentiment)
871
+ else:
872
+ sentiment_text = "분석 불가"
873
+ db.save_reflection(state.get("user_name", "익명"), text, {"label": "unknown", "score": 0.0})
874
+
875
+ new_reflection = [current_time, text, sentiment_text]
876
+ reflections = state.get("reflections", [])
877
+ reflections.append(new_reflection)
878
+ state = safe_state_update(state, {"reflections": reflections})
879
 
880
+ return state, db.get_all_reflections()
881
+ except Exception as e:
882
+ print(f"Error saving reflection: {e}")
883
+ return state, state.get("reflections", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
884
 
885
+ def save_reflection_fixed(text, state):
886
+ if not text.strip():
887
+ return state, []
 
 
 
 
 
 
 
 
 
 
888
 
889
+ try:
890
+ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
891
+ name = state.get("user_name", "익명")
892
+
893
+ if text_analyzer:
894
+ sentiment = text_analyzer(text)[0]
895
+ sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
896
+ else:
897
+ sentiment_text = "분석 불가"
898
+
899
+ # DB에 저장
900
+ db.save_reflection(name, text, sentiment_text)
901
+
902
+ # 화면에 표시할 데이터 형식으로 변환
903
+ display_data = []
904
+ all_reflections = db.get_all_reflections()
905
+ for ref in all_reflections:
906
+ display_data.append([
907
+ ref["timestamp"],
908
+ ref["reflection"],
909
+ ref["sentiment"]
910
+ ])
911
+
912
+ # 상태 업데이트
913
+ state = safe_state_update(state, {"reflections": display_data})
914
+
915
+ return state, display_data
916
+
917
+ except Exception as e:
918
+ print(f"Error saving reflection: {e}")
919
+ return state, []
920
 
921
 
922
+ def handle_save_wish(text, state):
923
+ if not text.strip():
924
+ return "소원을 입력해주세요.", []
925
+
926
+ try:
927
+ name = state.get("user_name", "익명")
928
+ db.save_wish(name, text)
929
+ wishes = db.get_all_wishes()
930
+ wish_display_data = [
931
+ [wish["timestamp"], wish["wish"], wish["name"]]
932
+ for wish in wishes
933
+ ]
934
+ return "소원이 저장되었습니다.", wish_display_data
935
+ except Exception as e:
936
+ print(f"Error saving wish: {e}")
937
+ return "오류가 발생했습니다.", []
938
+
939
+ # 이벤트 연결
940
+ name_submit_btn.click(
941
+ fn=handle_name_submit,
942
+ inputs=[name_input, state],
943
+ outputs=[welcome_section, story_section, blessing_section, entry_guide_section, state]
944
+ )
945
 
946
+ continue_btn.click(
947
+ fn=handle_continue,
948
+ outputs=[story_section, welcome_section, blessing_section, entry_guide_section]
949
+ )
950
 
951
+ set_baseline_btn.click(
952
+ fn=handle_blessing_complete,
953
+ inputs=[baseline_audio, state],
954
+ outputs=[state, baseline_status, blessing_section, entry_guide_section]
955
+ )
956
 
957
+ enter_btn.click(
958
+ fn=handle_enter,
959
+ outputs=[tabs]
960
+ )
961
 
962
+ play_music_btn.click(
963
+ fn=lambda: "assets/main_music.mp3",
964
+ outputs=[audio]
965
+ )
966
 
967
+ save_btn.click(
968
+ fn=save_reflection_fixed, # handle_save_reflection -> save_reflection_fixed
969
+ inputs=[reflection_input, state],
970
+ outputs=[state, reflections_display]
971
+ )
972
 
973
+ clear_btn.click(
974
+ fn=lambda: None,
975
+ outputs=[voice_input]
976
+ )
977
 
978
+ analyze_btn.click(
979
+ fn=analyze_voice,
980
+ inputs=[voice_input, state],
981
+ outputs=[state, transcribed_text, voice_emotion, text_emotion, final_prompt]
982
+ )
983
 
984
+ generate_btn.click(
985
+ fn=generate_image_from_prompt,
986
+ inputs=[final_prompt],
987
+ outputs=[result_image]
988
+ )
989
+
990
+ save_final_btn.click(
991
+ fn=handle_save_wish,
992
+ inputs=[final_reflection, state],
993
+ outputs=[baseline_status, wishes_display]
994
+ )
995
 
 
 
 
 
 
996
  return app
997
 
998
+
999
  if __name__ == "__main__":
1000
+