haepada commited on
Commit
0dfe358
·
verified ·
1 Parent(s): 31c1906

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +372 -548
app.py CHANGED
@@ -1,25 +1,24 @@
1
- import gradio as gr
 
 
 
 
 
 
 
 
2
  import numpy as np
3
  import librosa
4
- from transformers import pipeline
5
  from datetime import datetime
6
- import os
7
  from flask import Flask, send_from_directory, render_template
 
 
8
  import requests
9
- import json
10
  from dotenv import load_dotenv
11
 
 
12
  app = Flask(__name__)
13
 
14
- # PWA 관련 라우트
15
- @app.route('/static/<path:path>')
16
- def serve_static(path):
17
- return send_from_directory('static', path)
18
-
19
- @app.route('/assets/<path:path>')
20
- def serve_assets(path):
21
- return send_from_directory('assets', path)
22
-
23
  # 환경변수 로드
24
  load_dotenv()
25
 
@@ -43,67 +42,19 @@ ONCHEON_STORY = """
43
  온천천 온천장역에서 장전역까지 걸으며 더 깊은 체험이 가능합니다.
44
  """
45
 
46
- class SimpleDB:
47
- def __init__(self, reflections_path="data/reflections.json", wishes_path="data/wishes.json"):
48
- self.reflections_path = reflections_path
49
- self.wishes_path = wishes_path
50
- os.makedirs('data', exist_ok=True)
51
- self.reflections = self._load_json(reflections_path)
52
- self.wishes = self._load_json(wishes_path)
53
-
54
- def _load_json(self, file_path):
55
- if not os.path.exists(file_path):
56
- with open(file_path, 'w', encoding='utf-8') as f:
57
- json.dump([], f, ensure_ascii=False, indent=2)
58
- try:
59
- with open(file_path, 'r', encoding='utf-8') as f:
60
- return json.load(f)
61
- except Exception as e:
62
- print(f"Error loading {file_path}: {e}")
63
- return []
64
-
65
- def save_reflection(self, name, reflection, sentiment, timestamp=None):
66
- if timestamp is None:
67
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
68
-
69
- reflection_data = {
70
- "timestamp": timestamp,
71
- "name": name,
72
- "reflection": reflection,
73
- "sentiment": sentiment
74
- }
75
-
76
- self.reflections.append(reflection_data)
77
- self._save_json(self.reflections_path, self.reflections)
78
- return True
79
-
80
- def save_wish(self, name, wish, emotion_data=None, timestamp=None):
81
- if timestamp is None:
82
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
83
- wish_data = {
84
- "name": name,
85
- "wish": wish,
86
- "emotion": emotion_data,
87
- "timestamp": timestamp
88
- }
89
- self.wishes.append(wish_data)
90
- self._save_json(self.wishes_path, self.wishes)
91
- return True
92
-
93
- def _save_json(self, file_path, data):
94
- try:
95
- with open(file_path, 'w', encoding='utf-8') as f:
96
- json.dump(data, f, ensure_ascii=False, indent=2)
97
- return True
98
- except Exception as e:
99
- print(f"Error saving to {file_path}: {e}")
100
- return False
101
 
102
- def get_all_reflections(self):
103
- return sorted(self.reflections, key=lambda x: x["timestamp"], reverse=True)
104
-
105
- def get_all_wishes(self):
106
- return self.wishes
107
 
108
  # API 설정
109
  HF_API_TOKEN = os.getenv("roots", "")
@@ -128,11 +79,76 @@ except Exception as e:
128
  speech_recognizer = None
129
  text_analyzer = None
130
 
131
- # 필요한 디렉토리 생성
132
- os.makedirs("generated_images", exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
  # 음성 분석 관련 함수들
135
  def calculate_baseline_features(audio_data):
 
136
  try:
137
  if isinstance(audio_data, tuple):
138
  sr, y = audio_data
@@ -160,6 +176,7 @@ def calculate_baseline_features(audio_data):
160
  return None
161
 
162
  def map_acoustic_to_emotion(features, baseline_features=None):
 
163
  if features is None:
164
  return {
165
  "primary": "알 수 없음",
@@ -236,7 +253,47 @@ def map_acoustic_to_emotion(features, baseline_features=None):
236
 
237
  return emotions
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  def analyze_voice(audio_data, state):
 
240
  if audio_data is None:
241
  return state, "음성을 먼저 녹음해주세요.", "", "", ""
242
 
@@ -300,225 +357,19 @@ def analyze_voice(audio_data, state):
300
  return state, f"오류 발생: {str(e)}", "", "", ""
301
 
302
 
303
- def generate_detailed_prompt(text, emotions, text_sentiment):
304
- emotion_colors = {
305
- "기쁨/열정": "밝은 노랑과 따뜻한 주황색",
306
- "분노/강조": "강렬한 빨강과 짙은 검정",
307
- "놀람/흥분": "선명한 파랑과 밝은 보라",
308
- "관심/호기심": "연한 하늘색과 민트색",
309
- "슬픔/우울": "어두운 파랑과 회색",
310
- "피로/무기력": "탁한 갈색과 짙은 회색",
311
- "평온/안정": "부드러운 초록과 베이지",
312
- "차분/진지": "차분한 남색과 깊은 보라"
313
- }
314
-
315
- if emotions["intensity"] > 70:
316
- visual_style = "역동적인 붓질과 강한 대비"
317
- elif emotions["intensity"] > 40:
318
- visual_style = "균형잡힌 구도와 중간 톤의 조화"
319
- else:
320
- visual_style = "부드러운 그라데이션과 차분한 톤"
321
-
322
- prompt = f"한국 전통 민화 스타일의 추상화, {emotion_colors.get(emotions['primary'], '자연스러운 색상')} 기반. "
323
- prompt += f"{visual_style}로 표현된 {emotions['primary']}의 감정. "
324
- prompt += f"음성의 특징({', '.join(emotions['characteristics'])})을 화면의 동적 요소로 표현. "
325
- prompt += f"발화 내용 '{text}'에서 느껴지는 감정({text_sentiment['label']} - 점수: {text_sentiment['score']:.2f})을 은유적 이미지로 담아내기."
326
-
327
- return prompt
328
-
329
- def generate_image_from_prompt(prompt):
330
- if not prompt:
331
- print("No prompt provided")
332
- return None
333
-
334
- try:
335
- response = requests.post(
336
- API_URL,
337
- headers=headers,
338
- json={
339
- "inputs": prompt,
340
- "parameters": {
341
- "negative_prompt": "ugly, blurry, poor quality, distorted",
342
- "num_inference_steps": 30,
343
- "guidance_scale": 7.5
344
- }
345
- }
346
- )
347
-
348
- if response.status_code == 200:
349
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
350
- image_path = f"generated_images/image_{timestamp}.png"
351
- os.makedirs("generated_images", exist_ok=True)
352
-
353
- with open(image_path, "wb") as f:
354
- f.write(response.content)
355
-
356
- return image_path
357
- else:
358
- print(f"Error: {response.status_code}")
359
- print(f"Response: {response.text}")
360
- return None
361
- except Exception as e:
362
- print(f"Error generating image: {str(e)}")
363
- return None
364
-
365
- def safe_state_update(state, updates):
366
- try:
367
- new_state = {**state, **updates}
368
- # 중요 상태값 검증
369
- if "user_name" in updates:
370
- new_state["user_name"] = str(updates["user_name"]).strip() or "익명"
371
- if "baseline_features" in updates:
372
- if updates["baseline_features"] is None:
373
- return state # baseline이 None이면 상태 업데이트 하지 않음
374
- return new_state
375
- except Exception as e:
376
- print(f"State update error: {e}")
377
- return state
378
-
379
- def create_gradio_interface():
380
- css = """
381
- @media (max-width: 600px) {
382
- .container { padding: 10px !important; }
383
- .gradio-row {
384
- flex-direction: column !important;
385
- gap: 10px !important;
386
- }
387
- .gradio-button {
388
- width: 100% !important;
389
- margin: 5px 0 !important;
390
- min-height: 44px !important; /* 모바일 터치 영역 개선 */
391
- }
392
- .gradio-textbox { width: 100% !important; }
393
- .gradio-audio { width: 100% !important; }
394
- .gradio-image { width: 100% !important; }
395
- #audio-recorder { width: 100% !important; }
396
- #result-image { width: 100% !important; }
397
- .gradio-dataframe {
398
- overflow-x: auto !important;
399
- max-width: 100% !important;
400
- }
401
- .container {
402
- padding: 10px !important;
403
- overflow-x: hidden !important;
404
- }
405
- .gradio-row {
406
- flex-direction: column !important;
407
- gap: 10px !important;
408
- }
409
- .gradio-button {
410
- width: 100% !important;
411
- margin: 5px 0 !important;
412
- min-height: 44px !important;
413
- touch-action: manipulation !important;
414
- user-select: none !important;
415
- }
416
- }
417
-
418
- /* 전반적인 UI 개선 */
419
- .gradio-button {
420
- transition: all 0.3s ease;
421
- }
422
- .gradio-button:active {
423
- transform: scale(0.98);
424
- }
425
-
426
- /* 모바일 최적화 */
427
- * {
428
- -webkit-tap-highlight-color: transparent;
429
- -webkit-touch-callout: none;
430
- }
431
-
432
- .record-button {
433
- position: relative !important;
434
- touch-action: manipulation !important;
435
- }
436
- """
437
-
438
- with gr.Blocks(
439
- theme=gr.themes.Soft(),
440
- css=css,
441
- analytics_enabled=False,
442
- title="디지털 굿판"
443
- ) as demo:
444
- demo = create_interface()
445
- return demo
446
-
447
- # 실행 코드 수정
448
- if __name__ == "__main__":
449
- # 필요한 디렉토리 생성
450
- for directory in ['static/icons', 'assets', 'templates', 'data', 'generated_images']:
451
- os.makedirs(directory, exist_ok=True)
452
-
453
- # PWA 파일 존재 확인 및 생성 (기존 코드 유지)
454
-
455
- # Gradio 앱 생성
456
- demo = create_gradio_interface()
457
-
458
- # Flask 앱에 Gradio 마운트
459
- app = gr.mount_gradio_app(app, demo, path="/gradio")
460
-
461
- # Flask 서버 실행
462
- app.run(
463
- host="0.0.0.0",
464
- port=7860,
465
- debug=True
466
- )
467
-
468
- # Gradio 앱 생성
469
- demo = create_gradio_interface()
470
-
471
- # Gradio 앱을 Flask에 마운트
472
- app = gr.mount_gradio_app(app, demo, path="/")
473
-
474
-
475
-
476
  def create_interface():
477
- db = SimpleDB()
 
478
 
479
  initial_state = {
480
  "user_name": "",
481
  "baseline_features": None,
482
- "reflections": [],
483
- "wish": None,
484
- "final_prompt": "",
485
- "image_path": None,
486
- "current_tab": 0
487
  }
488
 
489
- css = """
490
- @media (max-width: 600px) {
491
- .container { padding: 10px !important; }
492
- .gradio-row {
493
- flex-direction: column !important;
494
- gap: 10px !important;
495
- }
496
- .gradio-button {
497
- width: 100% !important;
498
- margin: 5px 0 !important;
499
- min-height: 44px !important; /* 모바일 터치 영역 개선 */
500
- }
501
- .gradio-textbox { width: 100% !important; }
502
- .gradio-audio { width: 100% !important; }
503
- .gradio-image { width: 100% !important; }
504
- #audio-recorder { width: 100% !important; }
505
- #result-image { width: 100% !important; }
506
- .gradio-dataframe {
507
- overflow-x: auto !important;
508
- max-width: 100% !important;
509
- }
510
- }
511
-
512
- /* 전반적인 UI 개선 */
513
- .gradio-button {
514
- transition: all 0.3s ease;
515
- }
516
- .gradio-button:active {
517
- transform: scale(0.98);
518
- }
519
- """
520
-
521
- with gr.Blocks(theme=gr.themes.Soft(), css=css) as app:
522
  state = gr.State(value=initial_state)
523
 
524
  gr.Markdown("# 디지털 굿판")
@@ -540,26 +391,37 @@ def create_interface():
540
  interactive=True
541
  )
542
  name_submit_btn = gr.Button("굿판 시작하기", variant="primary")
543
-
544
  # 2단계: 세계관 설명
545
  story_section = gr.Column(visible=False)
546
  with story_section:
547
  gr.Markdown(ONCHEON_STORY)
548
  continue_btn = gr.Button("준비하기", variant="primary")
549
-
550
- # 3단계: 축원 의식
551
  blessing_section = gr.Column(visible=False)
552
  with blessing_section:
553
  gr.Markdown("### 축원의식을 시작하겠습니다")
554
  gr.Markdown("'명짐 복짐 짊어지고 안가태평하시기를 비도발원 축원 드립니다'")
555
- baseline_audio = gr.Audio(
556
- label="축원 문장 녹음하기",
557
- sources=["microphone"],
558
- type="numpy",
559
- streaming=False
560
- )
561
- set_baseline_btn = gr.Button("축원 마치기", variant="primary")
562
- baseline_status = gr.Markdown("")
 
 
 
 
 
 
 
 
 
 
 
563
 
564
  # 4단계: 굿판 입장 안내
565
  entry_guide_section = gr.Column(visible=False)
@@ -572,6 +434,7 @@ def create_interface():
572
  """)
573
  enter_btn = gr.Button("청신 의식 시작하기", variant="primary")
574
 
 
575
  with gr.TabItem("청신") as tab_listen:
576
  gr.Markdown("## 청신 - 소리로 정화하기")
577
  gr.Markdown("""
@@ -590,21 +453,6 @@ def create_interface():
590
  show_download_button=True,
591
  visible=True
592
  )
593
- with gr.Column():
594
- reflection_input = gr.Textbox(
595
- label="지금 이 순간의 감상을 자유롭게 적어보세요",
596
- lines=3,
597
- max_lines=5
598
- )
599
- save_btn = gr.Button("감상 저장하기", variant="secondary")
600
- reflections_display = gr.Dataframe(
601
- headers=["시간", "감상", "감정 분석"],
602
- label="기록된 감상들",
603
- value=[], # 초기값은 빈 리스트
604
- interactive=False,
605
- wrap=True,
606
- row_count=(5, "dynamic") # 동적으로 행 수 조정
607
- )
608
 
609
  # 기원 탭
610
  with gr.TabItem("기원") as tab_wish:
@@ -617,6 +465,10 @@ def create_interface():
617
  type="numpy",
618
  streaming=False
619
  )
 
 
 
 
620
  with gr.Row():
621
  clear_btn = gr.Button("녹음 지우기", variant="secondary")
622
  analyze_btn = gr.Button("소원 분석하기", variant="primary")
@@ -637,18 +489,7 @@ def create_interface():
637
 
638
  # 송신 탭
639
  with gr.TabItem("송신") as tab_send:
640
- gr.Markdown("## 송신 - 소지(소원지)를 그려 날려 태워봅시다")
641
- final_prompt = gr.Textbox(
642
- label="생성�� 프롬프트",
643
- interactive=False,
644
- lines=3
645
- )
646
- generate_btn = gr.Button("마음의 그림 그리기", variant="primary")
647
- result_image = gr.Image(
648
- label="생성된 이미지",
649
- show_download_button=True
650
- )
651
-
652
  gr.Markdown("## 온천천에 전하고 싶은 소원을 남겨주세요")
653
  final_reflection = gr.Textbox(
654
  label="소원",
@@ -657,254 +498,229 @@ def create_interface():
657
  )
658
  save_final_btn = gr.Button("소원 전하기", variant="primary")
659
  gr.Markdown("""
660
- 💫 여러분의 소원은 11월 25일 온천천 벽면에 설치될 소원나무에 전시될 예정입니다.
661
- 따뜻한 마음을 담아 작성해주세요.
662
- """)
663
  wishes_display = gr.Dataframe(
664
  headers=["시간", "소원", "이름"],
665
  label="기록된 소원들",
666
- value=[],
667
  interactive=False,
668
  wrap=True
669
  )
670
 
671
  # 이벤트 핸들러들
672
- def handle_name_submit(name, state):
673
- if not name.strip():
674
- return (
675
- gr.update(visible=True),
676
- gr.update(visible=False),
677
- gr.update(visible=False),
678
- gr.update(visible=False),
679
- state
680
- )
681
- state = safe_state_update(state, {"user_name": name})
682
- return (
683
- gr.update(visible=False),
684
- gr.update(visible=True),
685
- gr.update(visible=False),
686
- gr.update(visible=False),
687
- state
688
- )
689
-
690
- def handle_continue():
691
- return (
692
- gr.update(visible=False),
693
- gr.update(visible=False),
694
- gr.update(visible=True),
695
- gr.update(visible=False)
696
- )
697
-
698
- def handle_blessing_complete(audio, state):
699
- if audio is None:
700
- return state, "음성을 먼저 녹음해주세요.", gr.update(visible=True), gr.update(visible=False)
701
-
702
- try:
703
- # ... (기존 음성 처리 코드)
704
- return (
705
- state,
706
- "축원이 완료되었습니다.",
707
- gr.update(visible=False),
708
- gr.update(visible=True)
709
- )
710
- except Exception as e:
711
- return state, f"오류가 발생했습니다: {str(e)}", gr.update(visible=True), gr.update(visible=False)
712
-
713
- def handle_enter():
714
- return gr.update(selected=1) # 청신 탭으로 이동
715
-
716
- def handle_start():
717
- return gr.update(visible=False), gr.update(visible=True)
718
-
719
- def handle_baseline(audio, current_state):
720
  if audio is None:
721
- return current_state, "음성을 먼저 녹음해주세요.", gr.update(selected=0)
722
-
723
- try:
724
- sr, y = audio
725
- y = y.astype(np.float32)
726
- features = calculate_baseline_features((sr, y))
727
- if features:
728
- current_state = safe_state_update(current_state, {
729
- "baseline_features": features,
730
- "current_tab": 1
731
- })
732
- return current_state, "기준점이 설정되었습니다. 청신 탭으로 이동합니다.", gr.update(selected=1)
733
- return current_state, "기준점 설정에 실패했습니다. 다시 시도해주세요.", gr.update(selected=0)
734
- except Exception as e:
735
- print(f"Baseline error: {str(e)}")
736
- return current_state, "오류가 발생했습니다. 다시 시도해주세요.", gr.update(selected=0)
737
-
738
- def handle_save_reflection(text, state):
739
- if not text.strip():
740
- return state, []
741
 
742
  try:
743
- current_time = datetime.now().strftime("%H:%M:%S")
744
- if text_analyzer:
745
- sentiment = text_analyzer(text)[0]
746
- sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
747
- db.save_reflection(state.get("user_name", "익명"), text, sentiment)
748
- else:
749
- sentiment_text = "분석 불가"
750
- db.save_reflection(state.get("user_name", "익명"), text, {"label": "unknown", "score": 0.0})
751
-
752
- new_reflection = [current_time, text, sentiment_text]
753
- reflections = state.get("reflections", [])
754
- reflections.append(new_reflection)
755
- state = safe_state_update(state, {"reflections": reflections})
756
 
757
- return state, db.get_all_reflections()
758
- except Exception as e:
759
- print(f"Error saving reflection: {e}")
760
- return state, state.get("reflections", [])
761
-
762
- def save_reflection_fixed(text, state):
763
- if not text.strip():
764
- return state, []
765
-
766
- try:
767
- current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
768
- name = state.get("user_name", "익명")
769
 
770
- if text_analyzer:
771
- sentiment = text_analyzer(text)[0]
772
- sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
773
- else:
774
- sentiment_text = "분석 불가"
775
-
776
- # DB에 저장
777
- db.save_reflection(name, text, sentiment_text)
778
-
779
- # 화면에 표시할 데이터 형식으로 변환
780
- display_data = []
781
- all_reflections = db.get_all_reflections()
782
- for ref in all_reflections:
783
- display_data.append([
784
- ref["timestamp"],
785
- ref["reflection"],
786
- ref["sentiment"]
787
- ])
788
-
789
- # 상태 업데이트
790
- state = safe_state_update(state, {"reflections": display_data})
791
 
792
- return state, display_data
793
-
794
  except Exception as e:
795
- print(f"Error saving reflection: {e}")
796
- return state, []
797
-
 
 
 
 
798
 
799
- def handle_save_wish(text, state):
800
- if not text.strip():
801
- return "소원을 입력해주세요.", []
 
802
 
803
- try:
804
- name = state.get("user_name", "익명")
805
- db.save_wish(name, text)
806
- wishes = db.get_all_wishes()
807
- wish_display_data = [
808
- [wish["timestamp"], wish["wish"], wish["name"]]
809
- for wish in wishes
810
- ]
811
- return "소원이 저장되었습니다.", wish_display_data
812
- except Exception as e:
813
- print(f"Error saving wish: {e}")
814
- return "오류가 발생했습니다.", []
815
 
816
  # 이벤트 연결
817
- name_submit_btn.click(
818
- fn=handle_name_submit,
819
- inputs=[name_input, state],
820
- outputs=[welcome_section, story_section, blessing_section, entry_guide_section, state]
821
- )
822
-
823
- continue_btn.click(
824
- fn=handle_continue,
825
- outputs=[story_section, welcome_section, blessing_section, entry_guide_section]
 
826
  )
827
 
828
  set_baseline_btn.click(
829
  fn=handle_blessing_complete,
830
- inputs=[baseline_audio, state],
831
  outputs=[state, baseline_status, blessing_section, entry_guide_section]
832
  )
833
 
834
- enter_btn.click(
835
- fn=handle_enter,
836
- outputs=[tabs]
837
- )
838
-
839
- play_music_btn.click(
840
- fn=lambda: "assets/main_music.mp3",
841
- outputs=[audio]
842
- )
843
-
844
- save_btn.click(
845
- fn=save_reflection_fixed, # handle_save_reflection -> save_reflection_fixed
846
- inputs=[reflection_input, state],
847
- outputs=[state, reflections_display]
848
- )
849
-
850
- clear_btn.click(
851
- fn=lambda: None,
852
- outputs=[voice_input]
853
- )
854
-
855
- analyze_btn.click(
856
- fn=analyze_voice,
857
- inputs=[voice_input, state],
858
- outputs=[state, transcribed_text, voice_emotion, text_emotion, final_prompt]
859
- )
860
-
861
- generate_btn.click(
862
- fn=generate_image_from_prompt,
863
- inputs=[final_prompt],
864
- outputs=[result_image]
865
- )
866
-
867
- save_final_btn.click(
868
- fn=handle_save_wish,
869
- inputs=[final_reflection, state],
870
- outputs=[baseline_status, wishes_display]
871
- )
872
  return app
873
 
874
- # Gradio 인터페이스 생성 함수
875
- def create_gradio_interface():
876
- demo = create_interface()
877
- return demo
878
-
879
- # Flask 라우트 추가
880
- @app.route('/')
881
- def root():
882
- return render_template('index.html')
883
-
884
- @app.route('/manifest.json')
885
- def manifest():
886
- return send_from_directory('static', 'manifest.json')
887
-
888
- @app.route('/service-worker.js')
889
- def service_worker():
890
- return send_from_directory('static', 'service-worker.js')
891
-
892
- @app.route('/static/icons/<path:path>')
893
- def serve_icons(path):
894
- return send_from_directory('static/icons', path)
895
-
896
- @app.route('/assets/<path:path>')
897
- def serve_custom_assets(path):
898
- return send_from_directory('assets', path)
899
-
900
- if __name__ == "__main__":
901
- # 필요한 디렉토리 생성
902
- for directory in ['static/icons', 'assets', 'templates']:
903
- os.makedirs(directory, exist_ok=True)
904
-
905
- # PWA 파일 존재 확인 및 생성
906
- if not os.path.exists('templates/index.html'):
907
- with open('templates/index.html', 'w', encoding='utf-8') as f:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908
  f.write('''<!DOCTYPE html>
909
  <html lang="ko">
910
  <head>
@@ -955,15 +771,23 @@ if __name__ == "__main__":
955
  </body>
956
  </html>''')
957
 
 
 
 
 
 
 
 
 
958
  # Gradio 앱 생성
959
- demo = create_gradio_interface()
960
 
961
  # Flask 앱에 Gradio 마운트
962
- app = gr.mount_gradio_app(app, demo, path="/gradio")
963
 
964
  # Flask 서버 실행
965
  app.run(
966
  host="0.0.0.0",
967
  port=7860,
968
  debug=True
969
- )
 
1
+ """
2
+ 디지털 굿판 (Digital Gut-pan) - 도시 속 디지털 의례 공간
3
+ - 음성 기반 감정 분석
4
+ - 소원 아카이브 시스템
5
+ - PWA 지원
6
+ """
7
+
8
+ import os
9
+ import json
10
  import numpy as np
11
  import librosa
 
12
  from datetime import datetime
 
13
  from flask import Flask, send_from_directory, render_template
14
+ import gradio as gr
15
+ from transformers import pipeline
16
  import requests
 
17
  from dotenv import load_dotenv
18
 
19
+ # Flask 앱 초기화
20
  app = Flask(__name__)
21
 
 
 
 
 
 
 
 
 
 
22
  # 환경변수 로드
23
  load_dotenv()
24
 
 
42
  온천천 온천장역에서 장전역까지 걸으며 더 깊은 체험이 가능합니다.
43
  """
44
 
45
+ # 경로 설정
46
+ PATHS = {
47
+ 'data': 'data',
48
+ 'assets': 'assets',
49
+ 'static': 'static',
50
+ 'templates': 'templates',
51
+ 'generated_images': 'generated_images',
52
+ 'wishes': 'data/wishes'
53
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
+ # 디렉토리 생성
56
+ for path in PATHS.values():
57
+ os.makedirs(path, exist_ok=True)
 
 
58
 
59
  # API 설정
60
  HF_API_TOKEN = os.getenv("roots", "")
 
79
  speech_recognizer = None
80
  text_analyzer = None
81
 
82
+ # Flask 라우트
83
+ @app.route('/static/<path:path>')
84
+ def serve_static(path):
85
+ return send_from_directory('static', path)
86
+
87
+ @app.route('/assets/<path:path>')
88
+ def serve_assets(path):
89
+ return send_from_directory('assets', path)
90
+
91
+ @app.route('/wishes/<path:path>')
92
+ def serve_wishes(path):
93
+ return send_from_directory('data/wishes', path)
94
+
95
+ # CSS 스타일
96
+ GRADIO_CSS = """
97
+ @media (max-width: 600px) {
98
+ .container { padding: 10px !important; }
99
+ .gradio-row {
100
+ flex-direction: column !important;
101
+ gap: 10px !important;
102
+ }
103
+ .gradio-button {
104
+ width: 100% !important;
105
+ margin: 5px 0 !important;
106
+ min-height: 44px !important;
107
+ touch-action: manipulation !important;
108
+ user-select: none !important;
109
+ }
110
+ .gradio-textbox { width: 100% !important; }
111
+ .gradio-audio { width: 100% !important; }
112
+ .gradio-image { width: 100% !important; }
113
+ #audio-recorder { width: 100% !important; }
114
+ #result-image { width: 100% !important; }
115
+ .gradio-dataframe {
116
+ overflow-x: auto !important;
117
+ max-width: 100% !important;
118
+ }
119
+ }
120
+
121
+ /* UI 개선 */
122
+ .gradio-button {
123
+ transition: all 0.3s ease;
124
+ }
125
+ .gradio-button:active {
126
+ transform: scale(0.98);
127
+ }
128
+
129
+ /* 모바일 최적화 */
130
+ * {
131
+ -webkit-tap-highlight-color: transparent;
132
+ -webkit-touch-callout: none;
133
+ }
134
+
135
+ .record-button {
136
+ position: relative !important;
137
+ touch-action: manipulation !important;
138
+ }
139
+ """
140
+
141
+ # 초기 상태 정의
142
+ INITIAL_STATE = {
143
+ "user_name": "",
144
+ "baseline_features": None,
145
+ "current_tab": 0,
146
+ "last_emotion": None
147
+ }
148
 
149
  # 음성 분석 관련 함수들
150
  def calculate_baseline_features(audio_data):
151
+ """기존 음성 분석 함수 유지"""
152
  try:
153
  if isinstance(audio_data, tuple):
154
  sr, y = audio_data
 
176
  return None
177
 
178
  def map_acoustic_to_emotion(features, baseline_features=None):
179
+ """기존 감정 매핑 함수 유지"""
180
  if features is None:
181
  return {
182
  "primary": "알 수 없음",
 
253
 
254
  return emotions
255
 
256
+ class WishArchive:
257
+ """소원 아카이브 관리 클래스"""
258
+
259
+ def __init__(self):
260
+ """아카이브 초기화"""
261
+ self.archive_path = os.path.join(PATHS['wishes'], 'wish_archive.json')
262
+ os.makedirs(os.path.dirname(self.archive_path), exist_ok=True)
263
+ self.wishes = self._load_archive()
264
+
265
+ def _load_archive(self) -> list:
266
+ """아카이브 파일 로드"""
267
+ if not os.path.exists(self.archive_path):
268
+ return []
269
+ try:
270
+ with open(self.archive_path, 'r', encoding='utf-8') as f:
271
+ return json.load(f)
272
+ except Exception as e:
273
+ print(f"Error loading wish archive: {e}")
274
+ return []
275
+
276
+ def add_wish(self, wish_data: dict) -> bool:
277
+ """새로운 소원 추가"""
278
+ try:
279
+ self.wishes.append(wish_data)
280
+ with open(self.archive_path, 'w', encoding='utf-8') as f:
281
+ json.dump(self.wishes, f, ensure_ascii=False, indent=2)
282
+ return True
283
+ except Exception as e:
284
+ print(f"Error saving wish: {e}")
285
+ return False
286
+
287
+ def get_display_data(self) -> list:
288
+ """화면 표시용 데이터"""
289
+ return [
290
+ [wish["timestamp"], wish["wish"], wish["name"]]
291
+ for wish in sorted(self.wishes, key=lambda x: x["timestamp"], reverse=True)
292
+ ]
293
+
294
+
295
  def analyze_voice(audio_data, state):
296
+ """기존 음성 분석 함수 유지"""
297
  if audio_data is None:
298
  return state, "음성을 먼저 녹음해주세요.", "", "", ""
299
 
 
357
  return state, f"오류 발생: {str(e)}", "", "", ""
358
 
359
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  def create_interface():
361
+ """Gradio 인터페이스 생성"""
362
+ wish_archive = WishArchive()
363
 
364
  initial_state = {
365
  "user_name": "",
366
  "baseline_features": None,
367
+ "current_tab": 0,
368
+ "last_emotion": None,
369
+ "analysis_complete": False
 
 
370
  }
371
 
372
+ with gr.Blocks(theme=gr.themes.Soft(), css=GRADIO_CSS, analytics_enabled=False, title="디지털 굿판") as app:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  state = gr.State(value=initial_state)
374
 
375
  gr.Markdown("# 디지털 굿판")
 
391
  interactive=True
392
  )
393
  name_submit_btn = gr.Button("굿판 시작하기", variant="primary")
394
+
395
  # 2단계: 세계관 설명
396
  story_section = gr.Column(visible=False)
397
  with story_section:
398
  gr.Markdown(ONCHEON_STORY)
399
  continue_btn = gr.Button("준비하기", variant="primary")
400
+
401
+ # 3단계: 축원 의식 (수정된 부분)
402
  blessing_section = gr.Column(visible=False)
403
  with blessing_section:
404
  gr.Markdown("### 축원의식을 시작하겠습니다")
405
  gr.Markdown("'명짐 복짐 짊어지고 안가태평하시기를 비도발원 축원 드립니다'")
406
+ with gr.Column() as recording_section:
407
+ baseline_audio = gr.Audio(
408
+ label="축원 문장 녹음하기",
409
+ sources=["microphone"],
410
+ type="numpy",
411
+ streaming=False
412
+ )
413
+ analysis_status = gr.Markdown(
414
+ "",
415
+ visible=False
416
+ )
417
+
418
+ with gr.Column(visible=False) as baseline_results:
419
+ baseline_status = gr.Markdown("")
420
+ set_baseline_btn = gr.Button(
421
+ "축원 마치기",
422
+ variant="primary",
423
+ visible=False
424
+ )
425
 
426
  # 4단계: 굿판 입장 안내
427
  entry_guide_section = gr.Column(visible=False)
 
434
  """)
435
  enter_btn = gr.Button("청신 의식 시작하기", variant="primary")
436
 
437
+ # 청신 탭
438
  with gr.TabItem("청신") as tab_listen:
439
  gr.Markdown("## 청신 - 소리로 정화하기")
440
  gr.Markdown("""
 
453
  show_download_button=True,
454
  visible=True
455
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
 
457
  # 기원 탭
458
  with gr.TabItem("기원") as tab_wish:
 
465
  type="numpy",
466
  streaming=False
467
  )
468
+ voice_analysis_status = gr.Markdown(
469
+ "",
470
+ visible=False
471
+ )
472
  with gr.Row():
473
  clear_btn = gr.Button("녹음 지우기", variant="secondary")
474
  analyze_btn = gr.Button("소원 분석하기", variant="primary")
 
489
 
490
  # 송신 탭
491
  with gr.TabItem("송신") as tab_send:
492
+ gr.Markdown("## 송신 - 소지(소원지)를 남겨주세요")
 
 
 
 
 
 
 
 
 
 
 
493
  gr.Markdown("## 온천천에 전하고 싶은 소원을 남겨주세요")
494
  final_reflection = gr.Textbox(
495
  label="소원",
 
498
  )
499
  save_final_btn = gr.Button("소원 전하기", variant="primary")
500
  gr.Markdown("""
501
+ 💫 여러분의 소원은 11월 25일 온천천 벽면에 설치될 소원나무에 전시될 예정입니다.
502
+ 따뜻한 마음을 담아 작성해주세요.
503
+ """)
504
  wishes_display = gr.Dataframe(
505
  headers=["시간", "소원", "이름"],
506
  label="기록된 소원들",
507
+ value=wish_archive.get_display_data(),
508
  interactive=False,
509
  wrap=True
510
  )
511
 
512
  # 이벤트 핸들러들
513
+ def handle_baseline_recording(audio, state):
514
+ """기준 음성 분석"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
  if audio is None:
516
+ return {
517
+ "state": state,
518
+ "analysis": gr.update(visible=True, value="음성을 먼저 녹음해주세요."),
519
+ "results": gr.update(visible=False),
520
+ "status": "",
521
+ "button": gr.update(visible=False)
522
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
523
 
524
  try:
525
+ # 분석 중임을 표시
526
+ analysis_update = gr.update(visible=True, value="음성을 분석하고 있습니다...")
527
+ yield {
528
+ "state": state,
529
+ "analysis": analysis_update,
530
+ "results": gr.update(visible=False),
531
+ "status": "",
532
+ "button": gr.update(visible=False)
533
+ }
 
 
 
 
534
 
535
+ features = calculate_baseline_features(audio)
536
+ if features is None:
537
+ return {
538
+ "state": state,
539
+ "analysis": gr.update(visible=True, value="음성 분석에 실패했습니다. 다시 시도해주세요."),
540
+ "results": gr.update(visible=False),
541
+ "status": "",
542
+ "button": gr.update(visible=False)
543
+ }
 
 
 
544
 
545
+ # 분석 성공 시
546
+ new_state = {**state, "baseline_features": features, "analysis_complete": True}
547
+ return {
548
+ "state": new_state,
549
+ "analysis": gr.update(visible=False),
550
+ "results": gr.update(visible=True),
551
+ "status": "축원 문장이 정상적으로 인식되었습니다.",
552
+ "button": gr.update(visible=True)
553
+ }
 
 
 
 
 
 
 
 
 
 
 
 
554
 
 
 
555
  except Exception as e:
556
+ return {
557
+ "state": state,
558
+ "analysis": gr.update(visible=True, value=f"오류가 발생했습니다: {str(e)}"),
559
+ "results": gr.update(visible=False),
560
+ "status": "",
561
+ "button": gr.update(visible=False)
562
+ }
563
 
564
+ def handle_blessing_complete(state):
565
+ """축원 완료 처리"""
566
+ if not state.get("analysis_complete"):
567
+ return state, "음성 분석이 완료되지 않았습니다.", gr.update(visible=True), gr.update(visible=False)
568
 
569
+ return state, "축원이 완료되었습니다.", gr.update(visible=False), gr.update(visible=True)
 
 
 
 
 
 
 
 
 
 
 
570
 
571
  # 이벤트 연결
572
+ baseline_audio.change(
573
+ fn=handle_baseline_recording,
574
+ inputs=[baseline_audio, state],
575
+ outputs=[
576
+ state,
577
+ analysis_status,
578
+ baseline_results,
579
+ baseline_status,
580
+ set_baseline_btn
581
+ ]
582
  )
583
 
584
  set_baseline_btn.click(
585
  fn=handle_blessing_complete,
586
+ inputs=[state],
587
  outputs=[state, baseline_status, blessing_section, entry_guide_section]
588
  )
589
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
590
  return app
591
 
592
+ def create_pwa_files():
593
+ """PWA 필요 파일들 생성"""
594
+ # manifest.json 생성
595
+ manifest_path = 'static/manifest.json'
596
+ if not os.path.exists(manifest_path):
597
+ manifest_data = {
598
+ "name": "디지털 굿판",
599
+ "short_name": "디지털 굿판",
600
+ "description": "현대 도시 속 디지털 의례 공간",
601
+ "start_url": "/",
602
+ "display": "standalone",
603
+ "background_color": "#ffffff",
604
+ "theme_color": "#000000",
605
+ "orientation": "portrait",
606
+ "icons": [
607
+ {
608
+ "src": "/static/icons/icon-72x72.png",
609
+ "sizes": "72x72",
610
+ "type": "image/png",
611
+ "purpose": "any maskable"
612
+ },
613
+ {
614
+ "src": "/static/icons/icon-96x96.png",
615
+ "sizes": "96x96",
616
+ "type": "image/png",
617
+ "purpose": "any maskable"
618
+ },
619
+ {
620
+ "src": "/static/icons/icon-128x128.png",
621
+ "sizes": "128x128",
622
+ "type": "image/png",
623
+ "purpose": "any maskable"
624
+ },
625
+ {
626
+ "src": "/static/icons/icon-144x144.png",
627
+ "sizes": "144x144",
628
+ "type": "image/png",
629
+ "purpose": "any maskable"
630
+ },
631
+ {
632
+ "src": "/static/icons/icon-152x152.png",
633
+ "sizes": "152x152",
634
+ "type": "image/png",
635
+ "purpose": "any maskable"
636
+ },
637
+ {
638
+ "src": "/static/icons/icon-192x192.png",
639
+ "sizes": "192x192",
640
+ "type": "image/png",
641
+ "purpose": "any maskable"
642
+ },
643
+ {
644
+ "src": "/static/icons/icon-384x384.png",
645
+ "sizes": "384x384",
646
+ "type": "image/png",
647
+ "purpose": "any maskable"
648
+ },
649
+ {
650
+ "src": "/static/icons/icon-512x512.png",
651
+ "sizes": "512x512",
652
+ "type": "image/png",
653
+ "purpose": "any maskable"
654
+ }
655
+ ]
656
+ }
657
+ with open(manifest_path, 'w', encoding='utf-8') as f:
658
+ json.dump(manifest_data, f, ensure_ascii=False, indent=2)
659
+
660
+ # service-worker.js 생성
661
+ sw_path = 'static/service-worker.js'
662
+ if not os.path.exists(sw_path):
663
+ with open(sw_path, 'w', encoding='utf-8') as f:
664
+ f.write('''
665
+ // 캐시 이름 설정
666
+ const CACHE_NAME = 'digital-gutpan-v1';
667
+
668
+ // 캐시할 파일 목록
669
+ const urlsToCache = [
670
+ '/',
671
+ '/static/icons/icon-72x72.png',
672
+ '/static/icons/icon-96x96.png',
673
+ '/static/icons/icon-128x128.png',
674
+ '/static/icons/icon-144x144.png',
675
+ '/static/icons/icon-152x152.png',
676
+ '/static/icons/icon-192x192.png',
677
+ '/static/icons/icon-384x384.png',
678
+ '/static/icons/icon-512x512.png',
679
+ '/assets/main_music.mp3'
680
+ ];
681
+
682
+ // 서비스 워커 설치 시
683
+ self.addEventListener('install', event => {
684
+ event.waitUntil(
685
+ caches.open(CACHE_NAME)
686
+ .then(cache => cache.addAll(urlsToCache))
687
+ .then(() => self.skipWaiting())
688
+ );
689
+ });
690
+
691
+ // 서비스 워커 활성화 시
692
+ self.addEventListener('activate', event => {
693
+ event.waitUntil(
694
+ caches.keys().then(cacheNames => {
695
+ return Promise.all(
696
+ cacheNames.map(cacheName => {
697
+ if (cacheName !== CACHE_NAME) {
698
+ return caches.delete(cacheName);
699
+ }
700
+ })
701
+ );
702
+ }).then(() => self.clients.claim())
703
+ );
704
+ });
705
+
706
+ // 네트워크 요청 처리
707
+ self.addEventListener('fetch', event => {
708
+ event.respondWith(
709
+ caches.match(event.request)
710
+ .then(response => {
711
+ if (response) {
712
+ return response;
713
+ }
714
+ return fetch(event.request);
715
+ })
716
+ );
717
+ });
718
+ '''.strip())
719
+
720
+ # index.html 생성
721
+ index_path = 'templates/index.html'
722
+ if not os.path.exists(index_path):
723
+ with open(index_path, 'w', encoding='utf-8') as f:
724
  f.write('''<!DOCTYPE html>
725
  <html lang="ko">
726
  <head>
 
771
  </body>
772
  </html>''')
773
 
774
+ if __name__ == "__main__":
775
+ # 필요한 디렉토리 생성
776
+ for directory in ['static/icons', 'assets', 'templates', 'data', 'generated_images']:
777
+ os.makedirs(directory, exist_ok=True)
778
+
779
+ # PWA 파일 생성
780
+ create_pwa_files()
781
+
782
  # Gradio 앱 생성
783
+ demo = create_interface()
784
 
785
  # Flask 앱에 Gradio 마운트
786
+ app = gr.mount_gradio_app(app, demo, path="/")
787
 
788
  # Flask 서버 실행
789
  app.run(
790
  host="0.0.0.0",
791
  port=7860,
792
  debug=True
793
+ )