haepada commited on
Commit
33cb0be
·
verified ·
1 Parent(s): 143adfd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +409 -423
app.py CHANGED
@@ -1,24 +1,13 @@
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,19 +31,67 @@ ONCHEON_STORY = """
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,76 +116,11 @@ except Exception as e:
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,7 +148,6 @@ def calculate_baseline_features(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,47 +224,7 @@ def map_acoustic_to_emotion(features, baseline_features=None):
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,19 +288,128 @@ def analyze_voice(audio_data, state):
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,37 +431,26 @@ def create_interface():
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,7 +463,6 @@ def create_interface():
434
  """)
435
  enter_btn = gr.Button("청신 의식 시작하기", variant="primary")
436
 
437
- # 청신 탭
438
  with gr.TabItem("청신") as tab_listen:
439
  gr.Markdown("## 청신 - 소리로 정화하기")
440
  gr.Markdown("""
@@ -453,6 +481,21 @@ def create_interface():
453
  show_download_button=True,
454
  visible=True
455
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
 
457
  # 기원 탭
458
  with gr.TabItem("기원") as tab_wish:
@@ -465,10 +508,6 @@ def create_interface():
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,7 +528,18 @@ def create_interface():
489
 
490
  # 송신 탭
491
  with gr.TabItem("송신") as tab_send:
492
- gr.Markdown("## 송신 - 소지(소원지)를 남겨주세요")
 
 
 
 
 
 
 
 
 
 
 
493
  gr.Markdown("## 온천천에 전하고 싶은 소원을 남겨주세요")
494
  final_reflection = gr.Textbox(
495
  label="소원",
@@ -498,292 +548,228 @@ def create_interface():
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>
727
- <meta charset="UTF-8">
728
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
729
- <title>디지털 굿판</title>
730
- <link rel="manifest" href="/manifest.json">
731
- <meta name="theme-color" content="#000000">
732
- <meta name="apple-mobile-web-app-capable" content="yes">
733
- <meta name="apple-mobile-web-app-status-bar-style" content="black">
734
- <meta name="apple-mobile-web-app-title" content="디지털 굿판">
735
- <link rel="apple-touch-icon" href="/static/icons/icon-152x152.png">
736
- <script>
737
- // 화면 꺼짐 방지
738
- async function preventSleep() {
739
- try {
740
- if ('wakeLock' in navigator) {
741
- const wakeLock = await navigator.wakeLock.request('screen');
742
- console.log('화면 켜짐 유지 활성화');
743
-
744
- document.addEventListener('visibilitychange', async () => {
745
- if (document.visibilityState === 'visible') {
746
- await preventSleep();
747
- }
748
- });
749
- }
750
- } catch (err) {
751
- console.log('화면 켜짐 유지 실패:', err);
752
- }
753
- }
754
 
755
- // 서비스 워커 등록
756
- if ('serviceWorker' in navigator) {
757
- window.addEventListener('load', async () => {
758
- try {
759
- const registration = await navigator.serviceWorker.register('/service-worker.js');
760
- console.log('ServiceWorker 등록 성공:', registration.scope);
761
- await preventSleep();
762
- } catch (err) {
763
- console.log('ServiceWorker 등록 실패:', err);
764
- }
765
- });
766
- }
767
- </script>
768
- </head>
769
- <body>
770
- <div id="gradio-app"></div>
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
  demo.launch(
 
 
785
  server_name="0.0.0.0",
786
  server_port=7860,
787
- share=True,
788
- debug=True
789
- )
 
 
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
  import requests
8
+ import json
9
  from dotenv import load_dotenv
10
 
 
 
 
11
  # 환경변수 로드
12
  load_dotenv()
13
 
 
31
  온천천 온천장역에서 장전역까지 걸으며 더 깊은 체험이 가능합니다.
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
+
57
+ reflection_data = {
58
+ "timestamp": timestamp,
59
+ "name": name,
60
+ "reflection": reflection,
61
+ "sentiment": sentiment
62
+ }
63
+
64
+ self.reflections.append(reflection_data)
65
+ self._save_json(self.reflections_path, self.reflections)
66
+ return True
67
+
68
+ def save_wish(self, name, wish, emotion_data=None, timestamp=None):
69
+ if timestamp is None:
70
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
71
+ wish_data = {
72
+ "name": name,
73
+ "wish": wish,
74
+ "emotion": emotion_data,
75
+ "timestamp": timestamp
76
+ }
77
+ self.wishes.append(wish_data)
78
+ self._save_json(self.wishes_path, self.wishes)
79
+ return True
80
+
81
+ def _save_json(self, file_path, data):
82
+ try:
83
+ with open(file_path, 'w', encoding='utf-8') as f:
84
+ json.dump(data, f, ensure_ascii=False, indent=2)
85
+ return True
86
+ except Exception as e:
87
+ print(f"Error saving to {file_path}: {e}")
88
+ return False
89
+
90
+ def get_all_reflections(self):
91
+ return sorted(self.reflections, key=lambda x: x["timestamp"], reverse=True)
92
+
93
+ def get_all_wishes(self):
94
+ return self.wishes
95
 
96
  # API 설정
97
  HF_API_TOKEN = os.getenv("roots", "")
 
116
  speech_recognizer = None
117
  text_analyzer = None
118
 
119
+ # 필요한 디렉토리 생성
120
+ os.makedirs("generated_images", exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
  # 음성 분석 관련 함수들
123
  def calculate_baseline_features(audio_data):
 
124
  try:
125
  if isinstance(audio_data, tuple):
126
  sr, y = audio_data
 
148
  return None
149
 
150
  def map_acoustic_to_emotion(features, baseline_features=None):
 
151
  if features is None:
152
  return {
153
  "primary": "알 수 없음",
 
224
 
225
  return emotions
226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  def analyze_voice(audio_data, state):
 
228
  if audio_data is None:
229
  return state, "음성을 먼저 녹음해주세요.", "", "", ""
230
 
 
288
  return state, f"오류 발생: {str(e)}", "", "", ""
289
 
290
 
291
+ def generate_detailed_prompt(text, emotions, text_sentiment):
292
+ emotion_colors = {
293
+ "기쁨/열정": "밝은 노랑과 따뜻한 주황색",
294
+ "분노/강조": "강렬한 빨강과 짙은 검정",
295
+ "놀람/흥분": "선명한 파랑과 밝은 보라",
296
+ "관심/호기심": "연한 하늘색과 민트색",
297
+ "슬픔/우울": "어두운 파랑과 회색",
298
+ "피로/무기력": "탁한 갈색과 짙은 회색",
299
+ "평온/안정": "부드러운 초록과 베이지",
300
+ "차분/진지": "차분한 남색과 깊은 보라"
301
+ }
302
+
303
+ if emotions["intensity"] > 70:
304
+ visual_style = "역동적인 붓질과 강한 대비"
305
+ elif emotions["intensity"] > 40:
306
+ visual_style = "균형잡힌 구도와 중간 톤의 조화"
307
+ else:
308
+ visual_style = "부드러운 그라데이션과 차분한 톤"
309
+
310
+ prompt = f"한국 전통 민화 스타일의 추상화, {emotion_colors.get(emotions['primary'], '자연스러운 색상')} 기반. "
311
+ prompt += f"{visual_style}로 표현된 {emotions['primary']}의 감정. "
312
+ prompt += f"음성의 특징({', '.join(emotions['characteristics'])})을 화면의 동적 요소로 표현. "
313
+ prompt += f"발화 내용 '{text}'에서 느껴지는 감정({text_sentiment['label']} - 점수: {text_sentiment['score']:.2f})을 은유적 이미지로 담아내기."
314
+
315
+ return prompt
316
+
317
+ def generate_image_from_prompt(prompt):
318
+ if not prompt:
319
+ print("No prompt provided")
320
+ return None
321
+
322
+ try:
323
+ response = requests.post(
324
+ API_URL,
325
+ headers=headers,
326
+ json={
327
+ "inputs": prompt,
328
+ "parameters": {
329
+ "negative_prompt": "ugly, blurry, poor quality, distorted",
330
+ "num_inference_steps": 30,
331
+ "guidance_scale": 7.5
332
+ }
333
+ }
334
+ )
335
+
336
+ if response.status_code == 200:
337
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
338
+ image_path = f"generated_images/image_{timestamp}.png"
339
+ os.makedirs("generated_images", exist_ok=True)
340
+
341
+ with open(image_path, "wb") as f:
342
+ f.write(response.content)
343
+
344
+ return image_path
345
+ else:
346
+ print(f"Error: {response.status_code}")
347
+ print(f"Response: {response.text}")
348
+ return None
349
+ except Exception as e:
350
+ print(f"Error generating image: {str(e)}")
351
+ return None
352
+
353
+ def safe_state_update(state, updates):
354
+ try:
355
+ new_state = {**state, **updates}
356
+ # 중요 상태값 검증
357
+ if "user_name" in updates:
358
+ new_state["user_name"] = str(updates["user_name"]).strip() or "익명"
359
+ if "baseline_features" in updates:
360
+ if updates["baseline_features"] is None:
361
+ return state # baseline이 None이면 상태 업데이트 하지 않음
362
+ return new_state
363
+ except Exception as e:
364
+ print(f"State update error: {e}")
365
+ return state
366
+
367
  def create_interface():
368
+ db = SimpleDB()
 
369
 
370
  initial_state = {
371
  "user_name": "",
372
  "baseline_features": None,
373
+ "reflections": [],
374
+ "wish": None,
375
+ "final_prompt": "",
376
+ "image_path": None,
377
+ "current_tab": 0
378
  }
379
 
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
+ }
402
+
403
+ /* 전반적인 UI 개선 */
404
+ .gradio-button {
405
+ transition: all 0.3s ease;
406
+ }
407
+ .gradio-button:active {
408
+ transform: scale(0.98);
409
+ }
410
+ """
411
+
412
+ with gr.Blocks(theme=gr.themes.Soft(), css=css) as app:
413
  state = gr.State(value=initial_state)
414
 
415
  gr.Markdown("# 디지털 굿판")
 
431
  interactive=True
432
  )
433
  name_submit_btn = gr.Button("굿판 시작하기", variant="primary")
434
+
435
  # 2단계: 세계관 설명
436
  story_section = gr.Column(visible=False)
437
  with story_section:
438
  gr.Markdown(ONCHEON_STORY)
439
  continue_btn = gr.Button("준비하기", variant="primary")
440
+
441
+ # 3단계: 축원 의식
442
  blessing_section = gr.Column(visible=False)
443
  with blessing_section:
444
  gr.Markdown("### 축원의식을 시작하겠습니다")
445
  gr.Markdown("'명짐 복짐 짊어지고 안가태평하시기를 비도발원 축원 드립니다'")
446
+ baseline_audio = gr.Audio(
447
+ label="축원 문장 녹음하기",
448
+ sources=["microphone"],
449
+ type="numpy",
450
+ streaming=False
451
+ )
452
+ set_baseline_btn = gr.Button("축원 마치기", variant="primary")
453
+ baseline_status = gr.Markdown("")
 
 
 
 
 
 
 
 
 
 
 
454
 
455
  # 4단계: 굿판 입장 안내
456
  entry_guide_section = gr.Column(visible=False)
 
463
  """)
464
  enter_btn = gr.Button("청신 의식 시작하기", variant="primary")
465
 
 
466
  with gr.TabItem("청신") as tab_listen:
467
  gr.Markdown("## 청신 - 소리로 정화하기")
468
  gr.Markdown("""
 
481
  show_download_button=True,
482
  visible=True
483
  )
484
+ with gr.Column():
485
+ reflection_input = gr.Textbox(
486
+ label="지금 이 순간의 감상을 자유롭게 적어보세요",
487
+ lines=3,
488
+ max_lines=5
489
+ )
490
+ save_btn = gr.Button("감상 저장하기", variant="secondary")
491
+ reflections_display = gr.Dataframe(
492
+ headers=["시간", "감상", "감정 분석"],
493
+ label="기록된 감상들",
494
+ value=[], # 초기값은 빈 리스트
495
+ interactive=False,
496
+ wrap=True,
497
+ row_count=(5, "dynamic") # 동적으로 행 수 조정
498
+ )
499
 
500
  # 기원 탭
501
  with gr.TabItem("기원") as tab_wish:
 
508
  type="numpy",
509
  streaming=False
510
  )
 
 
 
 
511
  with gr.Row():
512
  clear_btn = gr.Button("녹음 지우기", variant="secondary")
513
  analyze_btn = gr.Button("소원 분석하기", variant="primary")
 
528
 
529
  # 송신 탭
530
  with gr.TabItem("송신") as tab_send:
531
+ gr.Markdown("## 송신 - 소지(소원지)를 그려 날려 태워봅시다")
532
+ final_prompt = gr.Textbox(
533
+ label="생성된 프롬프트",
534
+ interactive=False,
535
+ lines=3
536
+ )
537
+ generate_btn = gr.Button("마음의 그림 그리기", variant="primary")
538
+ result_image = gr.Image(
539
+ label="생성된 이미지",
540
+ show_download_button=True
541
+ )
542
+
543
  gr.Markdown("## 온천천에 전하고 싶은 소원을 남겨주세요")
544
  final_reflection = gr.Textbox(
545
  label="소원",
 
548
  )
549
  save_final_btn = gr.Button("소원 전하기", variant="primary")
550
  gr.Markdown("""
551
+ 💫 여러분의 소원은 11월 25일 온천천 벽면에 설치될 소원나무에 전시될 예정입니다.
552
+ 따뜻한 마음을 담아 작성해주세요.
553
+ """)
554
  wishes_display = gr.Dataframe(
555
  headers=["시간", "소원", "이름"],
556
  label="기록된 소원들",
557
+ value=[],
558
  interactive=False,
559
  wrap=True
560
  )
561
 
562
  # 이벤트 핸들러들
563
+ def handle_name_submit(name, state):
564
+ if not name.strip():
565
+ return (
566
+ gr.update(visible=True),
567
+ gr.update(visible=False),
568
+ gr.update(visible=False),
569
+ gr.update(visible=False),
570
+ state
571
+ )
572
+ state = safe_state_update(state, {"user_name": name})
573
+ return (
574
+ gr.update(visible=False),
575
+ gr.update(visible=True),
576
+ gr.update(visible=False),
577
+ gr.update(visible=False),
578
+ state
579
+ )
580
+
581
+ def handle_continue():
582
+ return (
583
+ gr.update(visible=False),
584
+ gr.update(visible=False),
585
+ gr.update(visible=True),
586
+ gr.update(visible=False)
587
+ )
588
+
589
+ def handle_blessing_complete(audio, state):
590
  if audio is None:
591
+ return state, "음성을 먼저 녹음해주세요.", gr.update(visible=True), gr.update(visible=False)
 
 
 
 
 
 
592
 
593
  try:
594
+ # ... (기존 음성 처리 코드)
595
+ return (
596
+ state,
597
+ "축원이 완료되었습니다.",
598
+ gr.update(visible=False),
599
+ gr.update(visible=True)
600
+ )
601
+ except Exception as e:
602
+ return state, f"오류가 발생했습니다: {str(e)}", gr.update(visible=True), gr.update(visible=False)
603
+
604
+ def handle_enter():
605
+ return gr.update(selected=1) # 청신 탭으로 이동
606
+
607
+ def handle_start():
608
+ return gr.update(visible=False), gr.update(visible=True)
609
+
610
+ def handle_baseline(audio, current_state):
611
+ if audio is None:
612
+ return current_state, "음성을 먼저 녹음해주세요.", gr.update(selected=0)
613
+
614
+ try:
615
+ sr, y = audio
616
+ y = y.astype(np.float32)
617
+ features = calculate_baseline_features((sr, y))
618
+ if features:
619
+ current_state = safe_state_update(current_state, {
620
+ "baseline_features": features,
621
+ "current_tab": 1
622
+ })
623
+ return current_state, "기준점이 설정되었습니다. 청신 탭으로 이동합니다.", gr.update(selected=1)
624
+ return current_state, "기준점 설정에 실패했습니다. 다시 시도해주세요.", gr.update(selected=0)
625
+ except Exception as e:
626
+ print(f"Baseline error: {str(e)}")
627
+ return current_state, "오류가 발생했습니다. 다시 시도해주세요.", gr.update(selected=0)
628
+
629
+ def handle_save_reflection(text, state):
630
+ if not text.strip():
631
+ return state, []
632
+
633
+ try:
634
+ current_time = datetime.now().strftime("%H:%M:%S")
635
+ if text_analyzer:
636
+ sentiment = text_analyzer(text)[0]
637
+ sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
638
+ db.save_reflection(state.get("user_name", "익명"), text, sentiment)
639
+ else:
640
+ sentiment_text = "분석 불가"
641
+ db.save_reflection(state.get("user_name", "익명"), text, {"label": "unknown", "score": 0.0})
642
+
643
+ new_reflection = [current_time, text, sentiment_text]
644
+ reflections = state.get("reflections", [])
645
+ reflections.append(new_reflection)
646
+ state = safe_state_update(state, {"reflections": reflections})
647
 
648
+ return state, db.get_all_reflections()
649
+ except Exception as e:
650
+ print(f"Error saving reflection: {e}")
651
+ return state, state.get("reflections", [])
652
+
653
+ def save_reflection_fixed(text, state):
654
+ if not text.strip():
655
+ return state, []
656
+
657
+ try:
658
+ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
659
+ name = state.get("user_name", "익명")
660
 
661
+ if text_analyzer:
662
+ sentiment = text_analyzer(text)[0]
663
+ sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
664
+ else:
665
+ sentiment_text = "분석 불가"
666
+
667
+ # DB에 저장
668
+ db.save_reflection(name, text, sentiment_text)
 
669
 
670
+ # 화면에 표시할 데이터 형식으로 변환
671
+ display_data = []
672
+ all_reflections = db.get_all_reflections()
673
+ for ref in all_reflections:
674
+ display_data.append([
675
+ ref["timestamp"],
676
+ ref["reflection"],
677
+ ref["sentiment"]
678
+ ])
679
+
680
+ # 상태 업데이트
681
+ state = safe_state_update(state, {"reflections": display_data})
682
+
683
+ return state, display_data
684
+
685
  except Exception as e:
686
+ print(f"Error saving reflection: {e}")
687
+ return state, []
688
+
 
 
 
 
689
 
690
+ def handle_save_wish(text, state):
691
+ if not text.strip():
692
+ return "소원을 입력해주세요.", []
 
693
 
694
+ try:
695
+ name = state.get("user_name", "익명")
696
+ db.save_wish(name, text)
697
+ wishes = db.get_all_wishes()
698
+ wish_display_data = [
699
+ [wish["timestamp"], wish["wish"], wish["name"]]
700
+ for wish in wishes
701
+ ]
702
+ return "소원이 저장되었습니다.", wish_display_data
703
+ except Exception as e:
704
+ print(f"Error saving wish: {e}")
705
+ return "오류가 발생했습니다.", []
706
 
707
  # 이벤트 연결
708
+ name_submit_btn.click(
709
+ fn=handle_name_submit,
710
+ inputs=[name_input, state],
711
+ outputs=[welcome_section, story_section, blessing_section, entry_guide_section, state]
712
+ )
713
+
714
+ continue_btn.click(
715
+ fn=handle_continue,
716
+ outputs=[story_section, welcome_section, blessing_section, entry_guide_section]
 
717
  )
718
 
719
  set_baseline_btn.click(
720
  fn=handle_blessing_complete,
721
+ inputs=[baseline_audio, state],
722
  outputs=[state, baseline_status, blessing_section, entry_guide_section]
723
  )
724
 
725
+ enter_btn.click(
726
+ fn=handle_enter,
727
+ outputs=[tabs]
728
+ )
729
 
730
+ play_music_btn.click(
731
+ fn=lambda: "assets/main_music.mp3",
732
+ outputs=[audio]
733
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
734
 
735
+ save_btn.click(
736
+ fn=save_reflection_fixed, # handle_save_reflection -> save_reflection_fixed
737
+ inputs=[reflection_input, state],
738
+ outputs=[state, reflections_display]
739
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
740
 
741
+ clear_btn.click(
742
+ fn=lambda: None,
743
+ outputs=[voice_input]
744
+ )
745
 
746
+ analyze_btn.click(
747
+ fn=analyze_voice,
748
+ inputs=[voice_input, state],
749
+ outputs=[state, transcribed_text, voice_emotion, text_emotion, final_prompt]
750
+ )
751
+
752
+ generate_btn.click(
753
+ fn=generate_image_from_prompt,
754
+ inputs=[final_prompt],
755
+ outputs=[result_image]
756
+ )
757
+
758
+ save_final_btn.click(
759
+ fn=handle_save_wish,
760
+ inputs=[final_reflection, state],
761
+ outputs=[baseline_status, wishes_display]
762
+ )
763
+ return app
764
 
765
+ if __name__ == "__main__":
766
  demo = create_interface()
767
  demo.launch(
768
+ debug=True,
769
+ share=True,
770
  server_name="0.0.0.0",
771
  server_port=7860,
772
+ show_error=True,
773
+ height=None,
774
+ width="100%"
775
+ )