haepada commited on
Commit
b7baa07
·
verified ·
1 Parent(s): 03598da

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +458 -778
app.py CHANGED
@@ -3,55 +3,94 @@ import json
3
  import numpy as np
4
  import librosa
5
  from datetime import datetime
6
- from flask import Flask, send_from_directory, render_template
7
  import gradio as gr
8
  from transformers import pipeline
9
  import requests
 
10
  from dotenv import load_dotenv
11
 
12
  # 환경변수 로드
13
  load_dotenv()
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  # 상수 정의
16
  WELCOME_MESSAGE = """
17
  # 디지털 굿판에 오신 것을 환영합니다
18
 
19
  이곳은 현대 도시 속에서 잊혀진 전통 굿의 정수를 담아낸 디지털 의례의 공간입니다.
20
  이곳에서는 사람들의 목소리와 감정을 통해 영적 교감을 나누고, 자연과 도시의 에너지가 연결됩니다.
21
-
22
  """
23
 
24
  ONCHEON_STORY = """
25
- ## 생명의 공간 ‘온천천’ 🌌
26
-
27
- 온천천의 물줄기는 신성한 금샘에서 시작됩니다. 금샘은 생명과 창조의 원천이며, 천상의 생명이 지상에서 숨을 틔우는 자리입니다.
28
 
29
- 도시의 소음 속에서도 신성한 생명력을 느껴보세요. 이곳에서 영적인 교감을 경험하며, 자연과 하나 되는 순간을 맞이해 보시기 바랍니다
30
-
31
- 이 프로젝트는 부산광역시 동래구 ‘온천장역’ 에서 금정구 ‘장전역’을 잇는 구간의 온천천 산책로의 사운드 스케이프를 기반으로 제작 되었습니다.
32
 
33
- 산책로를 따라 걸으며 프로젝트 체험 보다 몰입된 경험이 가능합니다.
34
- """
35
 
36
- # Flask 초기화
37
- app = Flask(__name__)
38
 
39
- # 환경변수 로드
40
- load_dotenv()
41
-
42
- # Flask 라우트
43
- @app.route('/static/<path:path>')
44
- def serve_static(path):
45
- return send_from_directory('static', path)
46
-
47
- @app.route('/assets/<path:path>')
48
- def serve_assets(path):
49
- return send_from_directory('assets', path)
50
-
51
- @app.route('/wishes/<path:path>')
52
- def serve_wishes(path):
53
- return send_from_directory('data/wishes', path)
54
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  class SimpleDB:
57
  def __init__(self, reflections_path="data/reflections.json", wishes_path="data/wishes.json"):
@@ -75,14 +114,12 @@ class SimpleDB:
75
  def save_reflection(self, name, reflection, sentiment, timestamp=None):
76
  if timestamp is None:
77
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
78
-
79
  reflection_data = {
80
  "timestamp": timestamp,
81
  "name": name,
82
  "reflection": reflection,
83
  "sentiment": sentiment
84
  }
85
-
86
  self.reflections.append(reflection_data)
87
  self._save_json(self.reflections_path, self.reflections)
88
  return True
@@ -113,35 +150,8 @@ class SimpleDB:
113
  return sorted(self.reflections, key=lambda x: x["timestamp"], reverse=True)
114
 
115
  def get_all_wishes(self):
116
- return self.wishes
117
-
118
- # API 설정
119
- HF_API_TOKEN = os.getenv("roots", "")
120
- if not HF_API_TOKEN:
121
- print("Warning: HuggingFace API token not found. Some features may be limited.")
122
-
123
- API_URL = "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-xl-base-1.0"
124
- headers = {"Authorization": f"Bearer {HF_API_TOKEN}"} if HF_API_TOKEN else {}
125
-
126
- # AI 모델 초기화
127
- try:
128
- speech_recognizer = pipeline(
129
- "automatic-speech-recognition",
130
- model="kresnik/wav2vec2-large-xlsr-korean"
131
- )
132
- text_analyzer = pipeline(
133
- "sentiment-analysis",
134
- model="nlptown/bert-base-multilingual-uncased-sentiment"
135
- )
136
- except Exception as e:
137
- print(f"Error initializing AI models: {e}")
138
- speech_recognizer = None
139
- text_analyzer = None
140
 
141
- # 필요한 디렉토리 생성
142
- os.makedirs("generated_images", exist_ok=True)
143
-
144
- # 음성 분석 관련 함수들
145
  def calculate_baseline_features(audio_data):
146
  try:
147
  if isinstance(audio_data, tuple):
@@ -203,7 +213,6 @@ def map_acoustic_to_emotion(features, baseline_features=None):
203
  "characteristics": []
204
  }
205
 
206
- # 감정 매핑 로직
207
  if energy_norm > 70:
208
  if tempo_norm > 0.6:
209
  emotions["primary"] = "기쁨/열정"
@@ -262,6 +271,7 @@ def analyze_voice(audio_data, state):
262
  return state, "음성 분석에 실패했습니다.", "", "", ""
263
 
264
  # 음성 인식
 
265
  if speech_recognizer:
266
  try:
267
  transcription = speech_recognizer({"sampling_rate": sr, "raw": y.astype(np.float32)})
@@ -269,24 +279,20 @@ def analyze_voice(audio_data, state):
269
  except Exception as e:
270
  print(f"Speech recognition error: {e}")
271
  text = "음성 인식 실패"
272
- else:
273
- text = "음성 인식 모델을 불러올 수 없습니다."
274
 
275
  # 음성 감정 분석
276
  voice_emotion = map_acoustic_to_emotion(acoustic_features, state.get("baseline_features"))
277
 
278
  # 텍스트 감정 분석
 
 
279
  if text_analyzer and text:
280
  try:
281
  text_sentiment = text_analyzer(text)[0]
282
  text_result = f"텍스트 감정 분석: {text_sentiment['label']} (점수: {text_sentiment['score']:.2f})"
283
  except Exception as e:
284
  print(f"Text analysis error: {e}")
285
- text_sentiment = {"label": "unknown", "score": 0.0}
286
  text_result = "텍스트 감정 분석 실패"
287
- else:
288
- text_sentiment = {"label": "unknown", "score": 0.0}
289
- text_result = "텍스트 감정 분석을 수행할 수 없습니다."
290
 
291
  voice_result = (
292
  f"음성 감정: {voice_emotion['primary']} "
@@ -309,7 +315,6 @@ def analyze_voice(audio_data, state):
309
  print(f"Error in analyze_voice: {str(e)}")
310
  return state, f"오류 발생: {str(e)}", "", "", ""
311
 
312
-
313
  def generate_detailed_prompt(text, emotions, text_sentiment):
314
  emotion_colors = {
315
  "기쁨/열정": "밝은 노랑과 따뜻한 주황색",
@@ -333,13 +338,15 @@ def generate_detailed_prompt(text, emotions, text_sentiment):
333
  "차분/진지": "균형잡힌 수직선과 안정적인 구조"
334
  }
335
 
336
- prompt = f"minimalistic abstract art, {emotion_colors.get(emotions['primary'], '자연스러운 색상')} color scheme, "
337
- prompt += f"{abstract_elements.get(emotions['primary'], '유기적 형태')}, "
338
- prompt += "korean traditional patterns, ethereal atmosphere, sacred geometry, "
339
- prompt += "flowing energy, mystical aura, no human figures, no faces, "
340
- prompt += "digital art, high detail, luminescent effects. "
341
- prompt += "negative prompt: photorealistic, human, face, figurative, text, letters, "
342
- prompt += "--ar 2:3 --s 750 --q 2"
 
 
343
 
344
  return prompt
345
 
@@ -378,453 +385,362 @@ def generate_image_from_prompt(prompt):
378
  except Exception as e:
379
  print(f"Error generating image: {str(e)}")
380
  return None
381
-
382
- def create_pwa_files():
383
- """PWA 필요 파일들 생성"""
384
- # manifest.json 생성
385
- manifest_path = 'static/manifest.json'
386
- if not os.path.exists(manifest_path):
387
- manifest_data = {
388
- "name": "디지털 굿판",
389
- "short_name": "디지털 굿판",
390
- "description": "현대 도시 속 디지털 의례 공간",
391
- "start_url": "/",
392
- "display": "standalone",
393
- "background_color": "#ffffff",
394
- "theme_color": "#000000",
395
- "orientation": "portrait",
396
- "icons": [
397
- {
398
- "src": "/static/icons/icon-72x72.png",
399
- "sizes": "72x72",
400
- "type": "image/png",
401
- "purpose": "any maskable"
402
- },
403
- {
404
- "src": "/static/icons/icon-96x96.png",
405
- "sizes": "96x96",
406
- "type": "image/png",
407
- "purpose": "any maskable"
408
- },
409
- {
410
- "src": "/static/icons/icon-128x128.png",
411
- "sizes": "128x128",
412
- "type": "image/png",
413
- "purpose": "any maskable"
414
- },
415
- {
416
- "src": "/static/icons/icon-144x144.png",
417
- "sizes": "144x144",
418
- "type": "image/png",
419
- "purpose": "any maskable"
420
- },
421
- {
422
- "src": "/static/icons/icon-152x152.png",
423
- "sizes": "152x152",
424
- "type": "image/png",
425
- "purpose": "any maskable"
426
- },
427
- {
428
- "src": "/static/icons/icon-192x192.png",
429
- "sizes": "192x192",
430
- "type": "image/png",
431
- "purpose": "any maskable"
432
- },
433
- {
434
- "src": "/static/icons/icon-384x384.png",
435
- "sizes": "384x384",
436
- "type": "image/png",
437
- "purpose": "any maskable"
438
- },
439
- {
440
- "src": "/static/icons/icon-512x512.png",
441
- "sizes": "512x512",
442
- "type": "image/png",
443
- "purpose": "any maskable"
444
- }
445
- ]
446
- }
447
- with open(manifest_path, 'w', encoding='utf-8') as f:
448
- json.dump(manifest_data, f, ensure_ascii=False, indent=2)
449
-
450
- # service-worker.js 생성
451
- sw_path = 'static/service-worker.js'
452
- if not os.path.exists(sw_path):
453
- with open(sw_path, 'w', encoding='utf-8') as f:
454
- f.write('''
455
- // 캐시 이름 설정
456
- const CACHE_NAME = 'digital-gutpan-v1';
457
-
458
- // 캐시할 파일 목록
459
- const urlsToCache = [
460
- '/',
461
- '/static/icons/icon-72x72.png',
462
- '/static/icons/icon-96x96.png',
463
- '/static/icons/icon-128x128.png',
464
- '/static/icons/icon-144x144.png',
465
- '/static/icons/icon-152x152.png',
466
- '/static/icons/icon-192x192.png',
467
- '/static/icons/icon-384x384.png',
468
- '/static/icons/icon-512x512.png',
469
- '/assets/main_music.mp3'
470
- ];
471
-
472
- // 서비스 워커 설치 시
473
- self.addEventListener('install', event => {
474
- event.waitUntil(
475
- caches.open(CACHE_NAME)
476
- .then(cache => cache.addAll(urlsToCache))
477
- .then(() => self.skipWaiting())
478
- );
479
- });
480
-
481
- // 서비스 워커 활성화 시
482
- self.addEventListener('activate', event => {
483
- event.waitUntil(
484
- caches.keys().then(cacheNames => {
485
- return Promise.all(
486
- cacheNames.map(cacheName => {
487
- if (cacheName !== CACHE_NAME) {
488
- return caches.delete(cacheName);
489
- }
490
- })
491
- );
492
- }).then(() => self.clients.claim())
493
- );
494
- });
495
-
496
- // 네트워크 요청 처리
497
- self.addEventListener('fetch', event => {
498
- event.respondWith(
499
- caches.match(event.request)
500
- .then(response => {
501
- if (response) {
502
- return response;
503
- }
504
- return fetch(event.request);
505
- })
506
- );
507
- });
508
- '''.strip())
509
-
510
- # index.html 파일에 화면 꺼짐 방지 스크립트 추가
511
- index_path = 'templates/index.html'
512
- if not os.path.exists(index_path):
513
- with open(index_path, 'w', encoding='utf-8') as f:
514
- f.write('''<!DOCTYPE html>
515
- <html lang="ko">
516
- <head>
517
- <meta charset="UTF-8">
518
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
519
- <title>디지털 굿판</title>
520
- <link rel="manifest" href="/manifest.json">
521
- <meta name="theme-color" content="#000000">
522
- <meta name="apple-mobile-web-app-capable" content="yes">
523
- <meta name="apple-mobile-web-app-status-bar-style" content="black">
524
- <meta name="apple-mobile-web-app-title" content="디지털 굿판">
525
- <link rel="apple-touch-icon" href="/static/icons/icon-152x152.png">
526
- <script>
527
- // 화면 꺼짐 방지
528
- async function preventSleep() {
529
- try {
530
- if ('wakeLock' in navigator) {
531
- const wakeLock = await navigator.wakeLock.request('screen');
532
- console.log('화면 켜짐 유지 ��성화');
533
-
534
- document.addEventListener('visibilitychange', async () => {
535
- if (document.visibilityState === 'visible') {
536
- await preventSleep();
537
- }
538
- });
539
- }
540
- } catch (err) {
541
- console.log('화면 켜짐 유지 실패:', err);
542
- }
543
- }
544
 
545
- // 서비스 워커 등록
546
- if ('serviceWorker' in navigator) {
547
- window.addEventListener('load', async () => {
548
- try {
549
- const registration = await navigator.serviceWorker.register('/service-worker.js');
550
- console.log('ServiceWorker 등록 성공:', registration.scope);
551
- await preventSleep();
552
- } catch (err) {
553
- console.log('ServiceWorker 등록 실패:', err);
554
- }
555
- });
556
- }
557
- </script>
558
- </head>
559
- <body>
560
- <div id="gradio-app"></div>
561
- </body>
562
- </html>''')
563
-
564
- def safe_state_update(state, updates):
565
  try:
566
- new_state = {**state, **updates}
567
- # 중요 상태값 검증
568
- if "user_name" in updates:
569
- new_state["user_name"] = str(updates["user_name"]).strip() or "익명"
570
- if "baseline_features" in updates:
571
- if updates["baseline_features"] is None:
572
- return state # baseline이 None이면 상태 업데이트 하지 않음
573
- return new_state
574
- except Exception as e:
575
- print(f"State update error: {e}")
576
- return state
577
-
578
- def create_interface():
579
- db = SimpleDB() # DB 객체 초기화 추가
580
- import base64
581
-
582
- # initial_state 정의
583
- initial_state = {
584
- "user_name": "",
585
- "baseline_features": None,
586
- "reflections": [],
587
- "wish": None,
588
- "final_prompt": "",
589
- "image_path": None,
590
- "current_tab": 0,
591
- "audio_playing": False
592
- }
593
-
594
- def encode_image_to_base64(image_path):
595
- try:
596
- with open(image_path, "rb") as img_file:
597
- return base64.b64encode(img_file.read()).decode()
598
- except Exception as e:
599
- print(f"이미지 로딩 에러 ({image_path}): {e}")
600
  return ""
601
-
602
- # 이미지 인코딩
603
- mobile_logo = encode_image_to_base64("static/DIGITAL_GUTPAN_LOGO_m.png")
604
- desktop_logo = encode_image_to_base64("static/DIGITAL_GUTPAN_LOGO_w.png")
605
- main_image = encode_image_to_base64("static/main-image.png")
 
 
 
 
 
 
 
 
606
 
607
- if not mobile_logo or not desktop_logo:
608
- logo_html = """
609
- <div style="text-align: center; padding: 20px;">
610
- <h1 style="font-size: 24px;">디지털 굿판</h1>
611
- </div>
612
- """
613
- else:
614
- logo_html = f"""
615
- <img class="mobile-logo" src="data:image/png;base64,{mobile_logo}" alt="디지털 굿판 로고 모바일">
616
- <img class="desktop-logo" src="data:image/png;base64,{desktop_logo}" alt="디지털 굿판 로고 데스크톱">
617
- """
618
-
619
- # HTML5 Audio Player 템플릿
620
- main_image_html = """
621
- <div class="main-image-container">
622
  <img src="data:image/png;base64,{main_image}" alt="디지털 굿판 메인 이미지" class="main-image">
623
- </div>
624
- """
625
- AUDIO_PLAYER_HTML = """
 
626
  <div class="audio-player-container">
627
  <audio controls class="audio-player">
628
  <source src="/assets/main_music.mp3" type="audio/mpeg">
629
  브라우저가 오디오 태그를 지원하지 않습니다.
630
  </audio>
631
  </div>
632
- <style>
633
- .audio-player-container {
634
- width: 100%;
635
- max-width: 800px;
636
- margin: 20px auto;
637
- text-align: center;
638
- }
639
- .audio-player {
640
- width: 100%;
641
- max-width: 300px;
642
- margin: 0 auto;
643
- }
644
- @media (max-width: 600px) {
645
- .audio-player {
646
- width: 100%;
647
- }
648
- }
649
  """
650
 
651
- css = """
652
- /* 전체 컨테이너 width 제한 */
653
- .gradio-container {
654
- margin: 0 auto !important;
655
- max-width: 800px !important;
656
- padding: 1rem !important;
657
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
 
659
- /* 모바일 뷰 */
660
- @media (max-width: 600px) {
661
- .mobile-logo {
662
- display: none !important;
663
- }
664
- .desktop-logo {
665
- display: block !important;
666
- width: 100%;
667
- height: auto;
668
- max-width: 800px;
669
- margin: 0 auto;
670
- }
671
- .main-image {
672
- width: 100%;
673
- max-width: 100%;
674
- }
675
- }
676
-
677
- /* 데스크톱 뷰 (601px 이상) */
678
- @media (min-width: 601px) {
679
- .main-image {
680
- width: 600px;
681
- max-width: 600px;
682
- }
683
- .container { padding: 10px !important; }
684
- .gradio-row {
685
- flex-direction: column !important;
686
- gap: 10px !important;
687
- }
688
- .gradio-button {
689
- width: 100% !important;
690
- margin: 5px 0 !important;
691
- min-height: 44px !important;
692
- }
693
- .gradio-textbox { width: 100% !important; }
694
- .gradio-audio { width: 100% !important; }
695
- .gradio-image { width: 100% !important; }
696
- #audio-recorder { width: 100% !important; }
697
- #result-image { width: 100% !important; }
698
- .gradio-dataframe {
699
- overflow-x: auto !important;
700
- max-width: 100% !important;
701
- }
702
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
703
 
704
- /* 데스크톱 뷰 */
705
- @media (min-width: 601px) {
706
- .logo-container {
707
- padding: 20px 0;
708
- width: 100%;
709
- max-width: 800px;
710
- margin: 0 auto;
711
- }
712
- .desktop-logo {
713
- display: none !important;
714
- }
715
- .mobile-logo {
716
- display: block !important;
717
- width: 100% !important;
718
- height: auto !important;
719
- max-width: 300px !important;
720
- margin: 0 auto !important;
721
- }
722
- /* 데스크탑에서 2단 컬럼 레이아웃 보완 */
723
- .gradio-row {
724
- gap: 20px !important;
725
- }
726
- .gradio-row > .gradio-column {
727
- flex: 1 !important;
728
- min-width: 0 !important;
729
- }
730
- .main-image-container {
731
- width: 100%;
732
- margin: 0 auto 2rem auto;
733
- text-align: center;
734
- }
735
 
736
- .main-image {
737
- width: 100%;
738
- height: auto;
739
- max-width: 100%;
740
- display: block;
741
- margin: 0 auto;
742
- }
743
- }
 
 
 
 
 
 
 
 
 
 
744
 
745
- /* 전반적인 UI 개선 */
746
- .gradio-button {
747
- transition: all 0.3s ease;
748
- border-radius: 8px !important;
749
- }
750
- .gradio-button:active {
751
- transform: scale(0.98);
752
- }
753
-
754
- /* 컴포넌트 간격 조정 */
755
- .gradio-column > *:not(:last-child) {
756
- margin-bottom: 1rem !important;
757
- }
758
-
759
- /* 데이터프레임 스타일링 */
760
- .gradio-dataframe {
761
- border: 1px solid #e0e0e0;
762
- border-radius: 8px;
763
- overflow: hidden;
764
- }
765
-
766
- /* 오디오 플레이어 컨테이너 */
767
- .audio-player-container {
768
- max-width: 800px;
769
- margin: 20px auto;
770
- padding: 0 1rem;
771
- }
772
- .audio-player {
773
- margin: 20px auto;
774
- width: 100%;
775
- max-width: 300px;
776
-
777
- /* 탭 스타일링 */
778
- .tabs {
779
- max-width: 800px;
780
- margin: 0 auto;
781
- }
782
-
783
- /* 마크다운 컨텐츠 */
784
- .markdown-content {
785
- max-width: 800px;
786
- margin: 0 auto;
787
- padding: 0 1rem;
788
- }
789
 
790
- /* 이미지 컨테이너 */
791
- .gradio-image {
792
- border-radius: 8px;
793
- overflow: hidden;
794
- max-width: 800px;
795
- margin: 0 auto;
796
- }
 
 
 
 
 
 
 
 
 
797
 
798
- .blessing-status-box {
799
- min-height: 200px !important;
800
- max-height: 300px !important;
801
- overflow-y: auto !important;
802
- padding: 15px !important;
803
- border: 1px solid #ddd !important;
804
- border-radius: 8px !important;
805
- background-color: #f8f9fa !important;
806
- margin: 10px 0 !important;
807
- }
808
 
809
- .blessing-status-box p {
810
- margin: 0 0 10px 0 !important;
811
- line-height: 1.5 !important;
812
- }
813
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
814
 
815
- with gr.Blocks(theme=gr.themes.Soft(), css=css) as app:
816
- state = gr.State(value=initial_state)
 
 
817
  processing_status = gr.State("")
 
 
818
  with gr.Column(elem_classes="logo-container"):
819
- gr.HTML(f"""
820
- <img class="mobile-logo" src="data:image/png;base64,{mobile_logo}" alt="디지털 굿판 로고 모바일">
821
- <img class="desktop-logo" src="data:image/png;base64,{desktop_logo}" alt="디지털 굿판 로고 데스크톱">
822
- """)
823
 
 
824
  with gr.Tabs(selected=0) as tabs:
825
- # 입장 탭 (축원 포함)
826
  with gr.TabItem("입장") as tab_entrance:
827
- gr.HTML(main_image_html)
 
 
828
  # 1단계: 첫 화면
829
  welcome_section = gr.Column(visible=True)
830
  with welcome_section:
@@ -848,7 +764,6 @@ def create_interface():
848
  gr.Markdown("### 축원의식을 시작하겠습니다")
849
  gr.Markdown("'명짐 복짐 짊어지고 안가태평하시기를 비도발원 축원 드립니다'")
850
 
851
- # 축원 문장 녹음하기
852
  baseline_audio = gr.Audio(
853
  label="축원 문장 녹음하기",
854
  sources=["microphone"],
@@ -856,18 +771,15 @@ def create_interface():
856
  streaming=False
857
  )
858
 
859
- # elem_classes를 사용하여 스타일 적용
860
  blessing_status = gr.Markdown(
861
  "분석 결과가 여기에 표시됩니다.",
862
  elem_id="blessing-status",
863
  elem_classes="blessing-status-box"
864
  )
865
 
866
- # 상태 분석 및 결과창 구분
867
  set_baseline_btn = gr.Button("축원문 분석하기", variant="primary")
868
  send_blessing_btn = gr.Button("축원 보내기", variant="secondary", visible=False)
869
 
870
-
871
  # 4단계: 굿판 입장 안내
872
  entry_guide_section = gr.Column(visible=False)
873
  with entry_guide_section:
@@ -879,6 +791,7 @@ def create_interface():
879
  """)
880
  enter_btn = gr.Button("청신 의식 시작하기", variant="primary")
881
 
 
882
  with gr.TabItem("청신") as tab_listen:
883
  gr.Markdown("## 청신 - 소리로 정화하기")
884
  gr.Markdown("""
@@ -888,11 +801,8 @@ def create_interface():
888
  온천천 온천장역에서 장전역까지 걸으며 더 깊은 체험이 가능합니다.
889
  """)
890
 
891
- # 커스텀 오디오 플레이어
892
- with gr.Blocks(css=css) as demo:
893
- gr.Markdown("## 온천천의 소리를 들어보세요")
894
- # HTML로 오디오 플레이어를 직접 삽입
895
- gr.HTML(AUDIO_PLAYER_HTML)
896
 
897
  with gr.Column():
898
  reflection_input = gr.Textbox(
@@ -913,7 +823,7 @@ def create_interface():
913
  # 기원 탭
914
  with gr.TabItem("기원") as tab_wish:
915
  gr.Markdown("## 기원 - 소원을 전해보세요")
916
- status_display = gr.Markdown("", visible=False) # 상태 표시용 컴포넌트
917
 
918
  with gr.Row():
919
  with gr.Column():
@@ -922,7 +832,7 @@ def create_interface():
922
  sources=["microphone"],
923
  type="numpy",
924
  streaming=False,
925
- elem_id="voice-input" # elem_id 추가
926
  )
927
  with gr.Row():
928
  clear_btn = gr.Button("녹음 지우기", variant="secondary")
@@ -955,7 +865,7 @@ def create_interface():
955
  label="생성된 이미지",
956
  show_download_button=True
957
  )
958
-
959
  gr.Markdown("## 온천천에 전하고 싶은 소원을 남겨주세요")
960
  final_reflection = gr.Textbox(
961
  label="소원",
@@ -964,9 +874,9 @@ def create_interface():
964
  )
965
  save_final_btn = gr.Button("소원 전하기", variant="primary")
966
  gr.Markdown("""
967
- 💫 여러분의 소원은 11월 25일 온천천 벽면에 설치될 소원나무에 전시될 예정입니다.
968
- 따뜻한 마음을 담아 작성해주세요.
969
- """)
970
  wishes_display = gr.Dataframe(
971
  headers=["시간", "소원", "이름"],
972
  label="기록된 소원들",
@@ -974,11 +884,13 @@ def create_interface():
974
  interactive=False,
975
  wrap=True
976
  )
 
 
977
  with gr.TabItem("프로젝트 소개") as tab_intro:
978
  gr.HTML(main_image_html)
979
  gr.Markdown("""
980
  # 디지털 굿판 프로젝트
981
- ‘디지털 굿판’은 부산문화재단의 지원으로 루츠리딤이 제작한 다원예술 프로젝트입니다.
982
 
983
  본 사업은 전통 굿의 요소와 현대 기술을 결합해 참여자들이 자연과 깊이 연결되며 내면을 탐색하고 치유하는 경험을 제공합니다.
984
 
@@ -988,204 +900,15 @@ def create_interface():
988
 
989
  이 프로젝트는 현대 사회의 삶에 대한 근본적인 질문을 던지며 새로운 문화적 지향점을 모색합니다.
990
 
991
- 본 프로젝트에서 기록된 정보 중 ‘송신’ 단계의 ‘소원전하기’를 제외한 모든 과정의 정보는(목소리,감상 등) 별도로 저장되지 않으며 AI 학습이나 외부 공유에 사용되지 않습니다.
992
 
993
  ## ��레딧
994
  - **기획**: 루츠리딤, 전승아
995
  - **음악**: 루츠리딤 (이광혁)
996
  - **미디어아트**: 송지훈
997
- """)
998
-
999
- # 이벤트 핸들러들
1000
- def handle_name_submit(name, state):
1001
- if not name.strip():
1002
- return (
1003
- gr.update(visible=True),
1004
- gr.update(visible=False),
1005
- gr.update(visible=False),
1006
- gr.update(visible=False),
1007
- state
1008
- )
1009
- state = safe_state_update(state, {"user_name": name})
1010
- return (
1011
- gr.update(visible=False),
1012
- gr.update(visible=True),
1013
- gr.update(visible=False),
1014
- gr.update(visible=False),
1015
- state
1016
- )
1017
-
1018
- def handle_continue():
1019
- return (
1020
- gr.update(visible=False),
1021
- gr.update(visible=False),
1022
- gr.update(visible=True),
1023
- gr.update(visible=False)
1024
- )
1025
-
1026
- def handle_blessing_complete(audio, state):
1027
- if audio is None:
1028
- return state, "음성을 먼저 녹음해주세요.", gr.update(visible=True), gr.update(visible=False)
1029
-
1030
- try:
1031
- status_msg = "축원문 분석 중..."
1032
- sr, y = audio
1033
- features = calculate_baseline_features((sr, y))
1034
-
1035
- if features:
1036
- state = safe_state_update(state, {"baseline_features": features})
1037
- status_msg = "분석 완료되었습니다. 축원을 보내세요."
1038
- return state, status_msg, gr.update(visible=False), gr.update(visible=True)
1039
-
1040
- return state, "분석에 실패했습니다. 다시 시도해주세요.", gr.update(visible=True), gr.update(visible=False)
1041
-
1042
- except Exception as e:
1043
- return state, f"오류 발생: {str(e)}", gr.update(visible=True), gr.update(visible=False)
1044
-
1045
- def handle_enter():
1046
- return gr.update(selected=1) # 청신 탭으로 이동
1047
-
1048
- def handle_start():
1049
- return gr.update(visible=False), gr.update(visible=True)
1050
-
1051
- def handle_baseline(audio, current_state):
1052
- if audio is None:
1053
- return current_state, "음성을 먼저 녹음해주세요.", gr.update(selected=0)
1054
-
1055
- try:
1056
- sr, y = audio
1057
- y = y.astype(np.float32)
1058
- features = calculate_baseline_features((sr, y))
1059
- if features:
1060
- current_state = safe_state_update(current_state, {
1061
- "baseline_features": features,
1062
- "current_tab": 1
1063
- })
1064
- return current_state, "기준점이 설정되었습니다. 청신 탭으로 이동합니다.", gr.update(selected=1)
1065
- return current_state, "기준점 설정에 실패했습니다. 다시 시도해주세요.", gr.update(selected=0)
1066
- except Exception as e:
1067
- print(f"Baseline error: {str(e)}")
1068
- return current_state, "오류가 발생했습니다. 다시 시도해주세요.", gr.update(selected=0)
1069
-
1070
- def handle_save_reflection(text, state):
1071
- if not text.strip():
1072
- return state, []
1073
-
1074
- try:
1075
- current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1076
- name = state.get("user_name", "익명")
1077
-
1078
- if text_analyzer:
1079
- sentiment = text_analyzer(text)[0]
1080
- sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
1081
- else:
1082
- sentiment_text = "분석 불가"
1083
-
1084
- # DB에 저장
1085
- db.save_reflection(name, text, sentiment_text)
1086
-
1087
- # 화면에는 현재 사용자의 입력만 표시
1088
- return state, [[current_time, text, sentiment_text]]
1089
-
1090
- except Exception as e:
1091
- print(f"Error saving reflection: {e}")
1092
- return state, []
1093
-
1094
- def save_reflection_fixed(text, state):
1095
- if not text.strip():
1096
- return state, []
1097
-
1098
- try:
1099
- current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1100
- name = state.get("user_name", "익명")
1101
-
1102
- if text_analyzer:
1103
- sentiment = text_analyzer(text)[0]
1104
- sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
1105
- else:
1106
- sentiment_text = "분석 불가"
1107
-
1108
- # DB에 저장
1109
- db.save_reflection(name, text, sentiment_text)
1110
-
1111
- # 현재 사용자의 감상만 표시
1112
- return state, [[current_time, text, sentiment_text]]
1113
-
1114
- except Exception as e:
1115
- print(f"Error saving reflection: {e}")
1116
- return state, []
1117
 
1118
- def handle_save_wish(text, state):
1119
- if not text.strip():
1120
- return "소원을 입력해주세요.", []
1121
-
1122
- try:
1123
- name = state.get("user_name", "익명")
1124
- db.save_wish(name, text)
1125
- wishes = db.get_all_wishes()
1126
- wish_display_data = [
1127
- [wish["timestamp"], wish["wish"], wish["name"]]
1128
- for wish in wishes
1129
- ]
1130
- return "소원이 저장되었습니다.", wish_display_data
1131
- except Exception as e:
1132
- print(f"Error saving wish: {e}")
1133
- return "오류가 발생했습니다.", []
1134
-
1135
- def safe_analyze_voice(audio_data, state):
1136
- if audio_data is None:
1137
- return (
1138
- state,
1139
- "음성을 먼저 녹음해주세요.",
1140
- "",
1141
- "",
1142
- "",
1143
- "분석 준비 중..."
1144
- )
1145
-
1146
- try:
1147
- # 상태 업데이트
1148
- status_msg = "음성 분석 중..."
1149
-
1150
- # 음성 데이터 전처리
1151
- sr, y = audio_data
1152
- if len(y) == 0:
1153
- return (
1154
- state,
1155
- "음성이 감지되지 않았습니다.",
1156
- "",
1157
- "",
1158
- "",
1159
- "분석 실패"
1160
- )
1161
-
1162
- status_msg = "음성 특성 분석 중..."
1163
- acoustic_features = calculate_baseline_features((sr, y))
1164
-
1165
- status_msg = "음성 인식 중..."
1166
- new_state, text, voice_result, text_result, prompt = analyze_voice(audio_data, state)
1167
-
1168
- status_msg = "분석 완료"
1169
- return (
1170
- new_state,
1171
- text,
1172
- voice_result,
1173
- text_result,
1174
- prompt,
1175
- status_msg
1176
- )
1177
-
1178
- except Exception as e:
1179
- print(f"Voice analysis error: {str(e)}")
1180
- return (
1181
- state,
1182
- "음성 분석 중 오류가 발생했습니다. 다시 시도해주세요.",
1183
- "",
1184
- "",
1185
- "",
1186
- "분석 실패"
1187
- )
1188
- # 이벤트 연결
1189
  name_submit_btn.click(
1190
  fn=handle_name_submit,
1191
  inputs=[name_input, state],
@@ -1200,7 +923,7 @@ def create_interface():
1200
  set_baseline_btn.click(
1201
  fn=handle_blessing_complete,
1202
  inputs=[baseline_audio, state],
1203
- outputs=[state, blessing_status, blessing_section, entry_guide_section] # baseline_status -> blessing_status
1204
  )
1205
 
1206
  enter_btn.click(
@@ -1209,7 +932,7 @@ def create_interface():
1209
  )
1210
 
1211
  save_btn.click(
1212
- fn=save_reflection_fixed,
1213
  inputs=[reflection_input, state],
1214
  outputs=[state, reflections_display]
1215
  )
@@ -1234,86 +957,42 @@ def create_interface():
1234
  save_final_btn.click(
1235
  fn=handle_save_wish,
1236
  inputs=[final_reflection, state],
1237
- outputs=[blessing_status, wishes_display] # baseline_status -> blessing_status
1238
  )
1239
-
1240
  return app
1241
 
1242
  if __name__ == "__main__":
1243
- # 서비스 워커 캐시 설정 강화
1244
- sw_content = """
1245
- const CACHE_NAME = 'digital-gutpan-v2';
1246
- const urlsToCache = [
1247
- '/',
1248
- '/assets/main_music.mp3',
1249
- '/static/icons/icon-72x72.png',
1250
- '/static/icons/icon-96x96.png',
1251
- '/static/icons/icon-128x128.png',
1252
- '/static/icons/icon-144x144.png',
1253
- '/static/icons/icon-152x152.png',
1254
- '/static/icons/icon-192x192.png',
1255
- '/static/icons/icon-384x384.png',
1256
- '/static/icons/icon-512x512.png'
1257
- ];
1258
-
1259
- self.addEventListener('install', event => {
1260
- event.waitUntil(
1261
- caches.open(CACHE_NAME)
1262
- .then(cache => {
1263
- console.log('Opened cache');
1264
- return cache.addAll(urlsToCache);
1265
- })
1266
- .then(() => self.skipWaiting())
1267
- );
1268
- });
1269
-
1270
- self.addEventListener('activate', event => {
1271
- event.waitUntil(
1272
- caches.keys().then(cacheNames => {
1273
- return Promise.all(
1274
- cacheNames.map(cacheName => {
1275
- if (cacheName !== CACHE_NAME) {
1276
- return caches.delete(cacheName);
1277
- }
1278
- })
1279
- );
1280
- }).then(() => self.clients.claim())
1281
- );
1282
- });
1283
-
1284
- self.addEventListener('fetch', event => {
1285
- event.respondWith(
1286
- caches.match(event.request)
1287
- .then(response => {
1288
- if (response) {
1289
- return response;
1290
- }
1291
-
1292
- return fetch(event.request).then(
1293
- response => {
1294
- if(!response || response.status !== 200 || response.type !== 'basic') {
1295
- return response;
1296
- }
1297
-
1298
- const responseToCache = response.clone();
1299
-
1300
- caches.open(CACHE_NAME)
1301
- .then(cache => {
1302
- cache.put(event.request, responseToCache);
1303
- });
1304
-
1305
- return response;
1306
- }
1307
- );
1308
- })
1309
- );
1310
- });
1311
- """
1312
 
1313
- # 서비스 워커 파일 생성
1314
- with open('static/service-worker.js', 'w') as f:
1315
- f.write(sw_content)
 
 
 
1316
 
 
 
 
 
 
 
 
 
 
 
 
 
1317
  # Gradio 앱 실행
1318
  demo = create_interface()
1319
  demo.queue().launch(
@@ -1324,4 +1003,5 @@ if __name__ == "__main__":
1324
  show_error=True,
1325
  height=None,
1326
  width="100%"
1327
- )
 
 
3
  import numpy as np
4
  import librosa
5
  from datetime import datetime
 
6
  import gradio as gr
7
  from transformers import pipeline
8
  import requests
9
+ import base64
10
  from dotenv import load_dotenv
11
 
12
  # 환경변수 로드
13
  load_dotenv()
14
 
15
+ # 필요한 디렉토리 생성
16
+ REQUIRED_DIRS = ['static', 'assets', 'data', 'generated_images', 'templates']
17
+ for directory in REQUIRED_DIRS:
18
+ os.makedirs(directory, exist_ok=True)
19
+
20
+ # 필요한 파일 경로 정의
21
+ REQUIRED_FILES = {
22
+ 'static/main-image.png': '메인 이미지',
23
+ 'static/DIGITAL_GUTPAN_LOGO_m.png': '모바일 로고',
24
+ 'static/DIGITAL_GUTPAN_LOGO_w.png': '데스크톱 로고',
25
+ 'assets/main_music.mp3': '배경 음악'
26
+ }
27
+
28
+ # 파일 체크 함수
29
+ def check_required_files():
30
+ missing_files = []
31
+ for file_path, description in REQUIRED_FILES.items():
32
+ if not os.path.exists(file_path):
33
+ missing_files.append(f"{description} ({file_path})")
34
+ return missing_files
35
+
36
+ # AI 모델 초기화
37
+ try:
38
+ speech_recognizer = pipeline(
39
+ "automatic-speech-recognition",
40
+ model="kresnik/wav2vec2-large-xlsr-korean"
41
+ )
42
+ text_analyzer = pipeline(
43
+ "sentiment-analysis",
44
+ model="nlptown/bert-base-multilingual-uncased-sentiment"
45
+ )
46
+ print("✅ AI 모델 초기화 성공")
47
+ except Exception as e:
48
+ print(f"❌ AI 모델 초기화 실패: {e}")
49
+ speech_recognizer = None
50
+ text_analyzer = None
51
+
52
+ # API 설정
53
+ HF_API_TOKEN = os.getenv("roots", "")
54
+ if not HF_API_TOKEN:
55
+ print("Warning: HuggingFace API token not found. Some features may be limited.")
56
+
57
+ API_URL = "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-xl-base-1.0"
58
+ headers = {"Authorization": f"Bearer {HF_API_TOKEN}"} if HF_API_TOKEN else {}
59
+
60
  # 상수 정의
61
  WELCOME_MESSAGE = """
62
  # 디지털 굿판에 오신 것을 환영합니다
63
 
64
  이곳은 현대 도시 속에서 잊혀진 전통 굿의 정수를 담아낸 디지털 의례의 공간입니다.
65
  이곳에서는 사람들의 목소리와 감정을 통해 영적 교감을 나누고, 자연과 도시의 에너지가 연결됩니다.
 
66
  """
67
 
68
  ONCHEON_STORY = """
69
+ ## 생명의 공간 '온천천' 🌌
 
 
70
 
71
+ 온천천의 물줄기는 신성한 금샘에서 시작됩니다. 금샘은 생명과 창조의 원천이며,
72
+ 천상의 생명이 지상에서 숨을 틔우는 자리입니다.
 
73
 
74
+ 도시의 소음 속에서도 신성한 생명력을 느껴보세요. 이곳에서 영적인 교감을 경험하며,
75
+ 자연과 하나 되는 순간을 맞이해 보시기 바랍니다.
76
 
77
+ 프로젝트는 부산광역시 동래구 '온천장역'에서 금정구 '장전역'을 잇는 구간의
78
+ 온천천 산책로의 사운드스케이프를 기반으로 제작되었습니다.
79
 
80
+ 산책로를 따라 걸으며 본 프로젝트 체험 시 보다 몰입된 경험이 가능합니다.
81
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
+ # 초기 상태 정의
84
+ INITIAL_STATE = {
85
+ "user_name": "",
86
+ "baseline_features": None,
87
+ "reflections": [],
88
+ "wish": None,
89
+ "final_prompt": "",
90
+ "image_path": None,
91
+ "current_tab": 0,
92
+ "audio_playing": False
93
+ }
94
 
95
  class SimpleDB:
96
  def __init__(self, reflections_path="data/reflections.json", wishes_path="data/wishes.json"):
 
114
  def save_reflection(self, name, reflection, sentiment, timestamp=None):
115
  if timestamp is None:
116
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
 
117
  reflection_data = {
118
  "timestamp": timestamp,
119
  "name": name,
120
  "reflection": reflection,
121
  "sentiment": sentiment
122
  }
 
123
  self.reflections.append(reflection_data)
124
  self._save_json(self.reflections_path, self.reflections)
125
  return True
 
150
  return sorted(self.reflections, key=lambda x: x["timestamp"], reverse=True)
151
 
152
  def get_all_wishes(self):
153
+ return sorted(self.wishes, key=lambda x: x["timestamp"], reverse=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
 
 
 
 
155
  def calculate_baseline_features(audio_data):
156
  try:
157
  if isinstance(audio_data, tuple):
 
213
  "characteristics": []
214
  }
215
 
 
216
  if energy_norm > 70:
217
  if tempo_norm > 0.6:
218
  emotions["primary"] = "기쁨/열정"
 
271
  return state, "음성 분석에 실패했습니다.", "", "", ""
272
 
273
  # 음성 인식
274
+ text = "음성 인식 모델을 불러올 수 없습니다."
275
  if speech_recognizer:
276
  try:
277
  transcription = speech_recognizer({"sampling_rate": sr, "raw": y.astype(np.float32)})
 
279
  except Exception as e:
280
  print(f"Speech recognition error: {e}")
281
  text = "음성 인식 실패"
 
 
282
 
283
  # 음성 감정 분석
284
  voice_emotion = map_acoustic_to_emotion(acoustic_features, state.get("baseline_features"))
285
 
286
  # 텍스트 감정 분석
287
+ text_sentiment = {"label": "unknown", "score": 0.0}
288
+ text_result = "텍스트 감정 분석을 수행할 수 없습니다."
289
  if text_analyzer and text:
290
  try:
291
  text_sentiment = text_analyzer(text)[0]
292
  text_result = f"텍스트 감정 분석: {text_sentiment['label']} (점수: {text_sentiment['score']:.2f})"
293
  except Exception as e:
294
  print(f"Text analysis error: {e}")
 
295
  text_result = "텍스트 감정 분석 실패"
 
 
 
296
 
297
  voice_result = (
298
  f"음성 감정: {voice_emotion['primary']} "
 
315
  print(f"Error in analyze_voice: {str(e)}")
316
  return state, f"오류 발생: {str(e)}", "", "", ""
317
 
 
318
  def generate_detailed_prompt(text, emotions, text_sentiment):
319
  emotion_colors = {
320
  "기쁨/열정": "밝은 노랑과 따뜻한 주황색",
 
338
  "차분/진지": "균형잡힌 수직선과 안정적인 구조"
339
  }
340
 
341
+ prompt = (
342
+ f"minimalistic abstract art, {emotion_colors.get(emotions['primary'], '자연스러운 색상')} color scheme, "
343
+ f"{abstract_elements.get(emotions['primary'], '유기적 형태')}, "
344
+ "korean traditional patterns, ethereal atmosphere, sacred geometry, "
345
+ "flowing energy, mystical aura, no human figures, no faces, "
346
+ "digital art, high detail, luminescent effects. "
347
+ "negative prompt: photorealistic, human, face, figurative, text, letters, "
348
+ "--ar 2:3 --s 750 --q 2"
349
+ )
350
 
351
  return prompt
352
 
 
385
  except Exception as e:
386
  print(f"Error generating image: {str(e)}")
387
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
 
389
+ def encode_image_to_base64(image_path):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  try:
391
+ if not os.path.exists(image_path):
392
+ print(f"Warning: Image file not found at {image_path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  return ""
394
+ with open(image_path, "rb") as img_file:
395
+ return base64.b64encode(img_file.read()).decode()
396
+ except Exception as e:
397
+ print(f"Error encoding image {image_path}: {e}")
398
+ return ""
399
+
400
+ def create_html_templates(mobile_logo, desktop_logo, main_image):
401
+ logo_html = f"""
402
+ <div class="logo-container">
403
+ <img class="mobile-logo" src="data:image/png;base64,{mobile_logo}" alt="디지털 굿판 로고 모바일">
404
+ <img class="desktop-logo" src="data:image/png;base64,{desktop_logo}" alt="디지털 굿판 로고 데스크톱">
405
+ </div>
406
+ """
407
 
408
+ main_image_html = f"""
409
+ <div class="main-image-container">
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  <img src="data:image/png;base64,{main_image}" alt="디지털 굿판 메인 이미지" class="main-image">
411
+ </div>
412
+ """
413
+
414
+ audio_player_html = """
415
  <div class="audio-player-container">
416
  <audio controls class="audio-player">
417
  <source src="/assets/main_music.mp3" type="audio/mpeg">
418
  브라우저가 오디오 태그를 지원하지 않습니다.
419
  </audio>
420
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  """
422
 
423
+ return logo_html, main_image_html, audio_player_html
424
+
425
+
426
+ # CSS 스타일 정의
427
+ STYLE_CSS = """
428
+ /* 기본 컨테이너 스타일링 */
429
+ .gradio-container {
430
+ margin: 0 auto !important;
431
+ max-width: 800px !important;
432
+ padding: 1rem !important;
433
+ }
434
+
435
+ /* 이미지 컨테이너 스타일링 */
436
+ .main-image-container {
437
+ width: 100%;
438
+ margin: 0 auto 2rem auto;
439
+ text-align: center;
440
+ }
441
+
442
+ .main-image {
443
+ width: 100%;
444
+ height: auto;
445
+ max-width: 100%;
446
+ display: block;
447
+ margin: 0 auto;
448
+ }
449
+
450
+ /* 로고 스타일링 */
451
+ .logo-container {
452
+ padding: 20px 0;
453
+ width: 100%;
454
+ max-width: 800px;
455
+ margin: 0 auto;
456
+ text-align: center;
457
+ }
458
+
459
+ /* 오디오 플레이어 스타일링 */
460
+ .audio-player-container {
461
+ width: 100%;
462
+ max-width: 800px;
463
+ margin: 20px auto;
464
+ text-align: center;
465
+ }
466
+
467
+ .audio-player {
468
+ width: 100%;
469
+ max-width: 300px;
470
+ margin: 0 auto;
471
+ border-radius: 8px;
472
+ }
473
+
474
+ /* 상태 메시지 박스 스타일링 */
475
+ .blessing-status-box {
476
+ min-height: 200px !important;
477
+ max-height: 300px !important;
478
+ overflow-y: auto !important;
479
+ padding: 15px !important;
480
+ border: 1px solid #ddd !important;
481
+ border-radius: 8px !important;
482
+ background-color: #f8f9fa !important;
483
+ margin: 10px 0 !important;
484
+ }
485
+
486
+ .blessing-status-box p {
487
+ margin: 0 0 10px 0 !important;
488
+ line-height: 1.5 !important;
489
+ }
490
+
491
+ /* 버튼 스타일링 */
492
+ .gradio-button {
493
+ transition: all 0.3s ease;
494
+ border-radius: 8px !important;
495
+ width: 100% !important;
496
+ margin: 5px 0 !important;
497
+ min-height: 44px !important;
498
+ }
499
+
500
+ .gradio-button:active {
501
+ transform: scale(0.98);
502
+ }
503
+
504
+ /* 데이터프레임 스타일링 */
505
+ .gradio-dataframe {
506
+ border: 1px solid #e0e0e0;
507
+ border-radius: 8px;
508
+ overflow: hidden;
509
+ }
510
+
511
+ /* 모바일 뷰 (600px 이하) */
512
+ @media (max-width: 600px) {
513
+ .mobile-logo {
514
+ display: none !important;
515
+ }
516
+ .desktop-logo {
517
+ display: block !important;
518
+ width: 100%;
519
+ height: auto;
520
+ max-width: 800px;
521
+ margin: 0 auto;
522
+ }
523
+ .main-image {
524
+ width: 100%;
525
+ max-width: 100%;
526
+ }
527
+ .container {
528
+ padding: 10px !important;
529
+ }
530
+ .gradio-row {
531
+ flex-direction: column !important;
532
+ gap: 10px !important;
533
+ }
534
+ .audio-player {
535
+ width: 100%;
536
+ }
537
+ }
538
 
539
+ /* 데스크톱(601px 이상) */
540
+ @media (min-width: 601px) {
541
+ .desktop-logo {
542
+ display: none !important;
543
+ }
544
+ .mobile-logo {
545
+ display: block !important;
546
+ width: 100% !important;
547
+ height: auto !important;
548
+ max-width: 300px !important;
549
+ margin: 0 auto !important;
550
+ }
551
+ .main-image {
552
+ width: 600px;
553
+ max-width: 600px;
554
+ }
555
+ .gradio-row {
556
+ gap: 20px !important;
557
+ }
558
+ .gradio-row > .gradio-column {
559
+ flex: 1 !important;
560
+ min-width: 0 !important;
561
+ }
562
+ }
563
+
564
+ /* 컴포넌트 간격 조정 */
565
+ .gradio-column > *:not(:last-child) {
566
+ margin-bottom: 1rem !important;
567
+ }
568
+ """
569
+
570
+ def create_interface():
571
+ # DB 초기화
572
+ db = SimpleDB()
573
+
574
+ # 이미지 인코딩
575
+ mobile_logo = encode_image_to_base64("static/DIGITAL_GUTPAN_LOGO_m.png")
576
+ desktop_logo = encode_image_to_base64("static/DIGITAL_GUTPAN_LOGO_w.png")
577
+ main_image = encode_image_to_base64("static/main-image.png")
578
+
579
+ # HTML 템플릿 생성
580
+ logo_html, main_image_html, audio_player_html = create_html_templates(
581
+ mobile_logo, desktop_logo, main_image
582
+ )
583
+
584
+ # 이벤트 핸들러들
585
+ def handle_name_submit(name, state):
586
+ if not name.strip():
587
+ return (
588
+ gr.update(visible=True),
589
+ gr.update(visible=False),
590
+ gr.update(visible=False),
591
+ gr.update(visible=False),
592
+ state
593
+ )
594
+ state = safe_state_update(state, {"user_name": name})
595
+ return (
596
+ gr.update(visible=False),
597
+ gr.update(visible=True),
598
+ gr.update(visible=False),
599
+ gr.update(visible=False),
600
+ state
601
+ )
602
+
603
+ def handle_continue():
604
+ return (
605
+ gr.update(visible=False),
606
+ gr.update(visible=False),
607
+ gr.update(visible=True),
608
+ gr.update(visible=False)
609
+ )
610
+
611
+ def handle_blessing_complete(audio, state):
612
+ if audio is None:
613
+ return state, "음성을 먼저 녹음해주세요.", gr.update(visible=True), gr.update(visible=False)
614
 
615
+ try:
616
+ sr, y = audio
617
+ features = calculate_baseline_features((sr, y))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
618
 
619
+ if features:
620
+ state = safe_state_update(state, {"baseline_features": features})
621
+ return (
622
+ state,
623
+ "분석이 완료되었습니다. 다음 단계로 진행해주세요.",
624
+ gr.update(visible=False),
625
+ gr.update(visible=True)
626
+ )
627
+
628
+ return (
629
+ state,
630
+ "분석에 실패했습니다. 다시 시도해주세요.",
631
+ gr.update(visible=True),
632
+ gr.update(visible=False)
633
+ )
634
+
635
+ except Exception as e:
636
+ return state, f"오류 발생: {str(e)}", gr.update(visible=True), gr.update(visible=False)
637
 
638
+ def handle_enter():
639
+ return gr.update(selected=1)
640
+
641
+ def handle_save_reflection(text, state):
642
+ if not text.strip():
643
+ return state, []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
644
 
645
+ try:
646
+ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
647
+ name = state.get("user_name", "익명")
648
+
649
+ if text_analyzer:
650
+ sentiment = text_analyzer(text)[0]
651
+ sentiment_text = f"{sentiment['label']} ({sentiment['score']:.2f})"
652
+ else:
653
+ sentiment_text = "분석 불가"
654
+
655
+ db.save_reflection(name, text, sentiment_text)
656
+ return state, [[current_time, text, sentiment_text]]
657
+
658
+ except Exception as e:
659
+ print(f"Error saving reflection: {e}")
660
+ return state, []
661
 
662
+ def handle_save_wish(text, state):
663
+ if not text.strip():
664
+ return "소원을 입력해주세요.", []
 
 
 
 
 
 
 
665
 
666
+ try:
667
+ name = state.get("user_name", "익명")
668
+ db.save_wish(name, text)
669
+ wishes = db.get_all_wishes()
670
+ wish_display_data = [
671
+ [wish["timestamp"], wish["wish"], wish["name"]]
672
+ for wish in wishes
673
+ ]
674
+ return "소원이 저장되었습니다.", wish_display_data
675
+ except Exception as e:
676
+ print(f"Error saving wish: {e}")
677
+ return "오류가 발생했습니다.", []
678
+
679
+ def safe_analyze_voice(audio_data, state):
680
+ if audio_data is None:
681
+ return (
682
+ state,
683
+ "음성을 먼저 녹음해주세요.",
684
+ "",
685
+ "",
686
+ "",
687
+ "분석 준비 중..."
688
+ )
689
+
690
+ try:
691
+ status_msg = "음성 분석 중..."
692
+ sr, y = audio_data
693
+
694
+ if len(y) == 0:
695
+ return (
696
+ state,
697
+ "음성이 감지되지 않았습니다.",
698
+ "",
699
+ "",
700
+ "",
701
+ "분석 실패"
702
+ )
703
+
704
+ acoustic_features = calculate_baseline_features((sr, y))
705
+ new_state, text, voice_result, text_result, prompt = analyze_voice(audio_data, state)
706
+
707
+ return (
708
+ new_state,
709
+ text,
710
+ voice_result,
711
+ text_result,
712
+ prompt,
713
+ "분석 완료"
714
+ )
715
+
716
+ except Exception as e:
717
+ print(f"Voice analysis error: {str(e)}")
718
+ return (
719
+ state,
720
+ "음성 분석 중 오류가 발생했습니다. 다시 시도해주세요.",
721
+ "",
722
+ "",
723
+ "",
724
+ "분석 실패"
725
+ )
726
 
727
+ # Gradio 인터페이스 구성
728
+ with gr.Blocks(theme=gr.themes.Soft(), css=STYLE_CSS) as app:
729
+ # 상태 관리
730
+ state = gr.State(value=INITIAL_STATE)
731
  processing_status = gr.State("")
732
+
733
+ # 로고 영역
734
  with gr.Column(elem_classes="logo-container"):
735
+ gr.HTML(logo_html)
 
 
 
736
 
737
+ # 탭 구성
738
  with gr.Tabs(selected=0) as tabs:
739
+ # 입장 탭
740
  with gr.TabItem("입장") as tab_entrance:
741
+ # 메인 이미지 표시
742
+ gr.HTML(main_image_html)
743
+
744
  # 1단계: 첫 화면
745
  welcome_section = gr.Column(visible=True)
746
  with welcome_section:
 
764
  gr.Markdown("### 축원의식을 시작하겠습니다")
765
  gr.Markdown("'명짐 복짐 짊어지고 안가태평하시기를 비도발원 축원 드립니다'")
766
 
 
767
  baseline_audio = gr.Audio(
768
  label="축원 문장 녹음하기",
769
  sources=["microphone"],
 
771
  streaming=False
772
  )
773
 
 
774
  blessing_status = gr.Markdown(
775
  "분석 결과가 여기에 표시됩니다.",
776
  elem_id="blessing-status",
777
  elem_classes="blessing-status-box"
778
  )
779
 
 
780
  set_baseline_btn = gr.Button("축원문 분석하기", variant="primary")
781
  send_blessing_btn = gr.Button("축원 보내기", variant="secondary", visible=False)
782
 
 
783
  # 4단계: 굿판 입장 안내
784
  entry_guide_section = gr.Column(visible=False)
785
  with entry_guide_section:
 
791
  """)
792
  enter_btn = gr.Button("청신 의식 시작하기", variant="primary")
793
 
794
+ # 청신 탭
795
  with gr.TabItem("청신") as tab_listen:
796
  gr.Markdown("## 청신 - 소리로 정화하기")
797
  gr.Markdown("""
 
801
  온천천 온천장역에서 장전역까지 걸으며 더 깊은 체험이 가능합니다.
802
  """)
803
 
804
+ gr.Markdown("## 온천천의 소리를 들어보세요")
805
+ gr.HTML(audio_player_html)
 
 
 
806
 
807
  with gr.Column():
808
  reflection_input = gr.Textbox(
 
823
  # 기원 탭
824
  with gr.TabItem("기원") as tab_wish:
825
  gr.Markdown("## 기원 - 소원을 전해보세요")
826
+ status_display = gr.Markdown("", visible=False)
827
 
828
  with gr.Row():
829
  with gr.Column():
 
832
  sources=["microphone"],
833
  type="numpy",
834
  streaming=False,
835
+ elem_id="voice-input"
836
  )
837
  with gr.Row():
838
  clear_btn = gr.Button("녹음 지우기", variant="secondary")
 
865
  label="생성된 이미지",
866
  show_download_button=True
867
  )
868
+
869
  gr.Markdown("## 온천천에 전하고 싶은 소원을 남겨주세요")
870
  final_reflection = gr.Textbox(
871
  label="소원",
 
874
  )
875
  save_final_btn = gr.Button("소원 전하기", variant="primary")
876
  gr.Markdown("""
877
+ 💫 여러분의 소원은 11월 25일 온천천 벽면에 설치될 소원나무에 전시될 예정입니다.
878
+ 따뜻한 마음을 담아 작성해주세요.
879
+ """)
880
  wishes_display = gr.Dataframe(
881
  headers=["시간", "소원", "이름"],
882
  label="기록된 소원들",
 
884
  interactive=False,
885
  wrap=True
886
  )
887
+
888
+ # 프로젝트 소개 탭
889
  with gr.TabItem("프로젝트 소개") as tab_intro:
890
  gr.HTML(main_image_html)
891
  gr.Markdown("""
892
  # 디지털 굿판 프로젝트
893
+ '디지털 굿판'은 부산문화재단의 지원으로 루츠리딤이 제작한 다원예술 프로젝트입니다.
894
 
895
  본 사업은 전통 굿의 요소와 현대 기술을 결합해 참여자들이 자연과 깊이 연결되며 내면을 탐색하고 치유하는 경험을 제공합니다.
896
 
 
900
 
901
  이 프로젝트는 현대 사회의 삶에 대한 근본적인 질문을 던지며 새로운 문화적 지향점을 모색합니다.
902
 
903
+ 본 프로젝트에서 기록된 정보 중 '송신' 단계의 '소원전하기'를 제외한 모든 과정의 정보는(목소리,감상 등) 별도로 저장되지 않으며 AI 학습이나 외부 공유에 사용되지 않습니다.
904
 
905
  ## ��레딧
906
  - **기획**: 루츠리딤, 전승아
907
  - **음악**: 루츠리딤 (이광혁)
908
  - **미디어아트**: 송지훈
909
+ """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
910
 
911
+ # 이벤트 연결
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
912
  name_submit_btn.click(
913
  fn=handle_name_submit,
914
  inputs=[name_input, state],
 
923
  set_baseline_btn.click(
924
  fn=handle_blessing_complete,
925
  inputs=[baseline_audio, state],
926
+ outputs=[state, blessing_status, blessing_section, entry_guide_section]
927
  )
928
 
929
  enter_btn.click(
 
932
  )
933
 
934
  save_btn.click(
935
+ fn=handle_save_reflection,
936
  inputs=[reflection_input, state],
937
  outputs=[state, reflections_display]
938
  )
 
957
  save_final_btn.click(
958
  fn=handle_save_wish,
959
  inputs=[final_reflection, state],
960
+ outputs=[blessing_status, wishes_display]
961
  )
962
+
963
  return app
964
 
965
  if __name__ == "__main__":
966
+ # 필요한 디렉토리 생성
967
+ for directory in REQUIRED_DIRS:
968
+ os.makedirs(directory, exist_ok=True)
969
+
970
+ # 파일 경로 체크
971
+ print("\n🔍 파일 경로 확인 중...")
972
+ missing_files = []
973
+ for file_path, description in REQUIRED_PATHS.items():
974
+ if not os.path.exists(file_path):
975
+ missing_files.append(f"{description} ({file_path})")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
976
 
977
+ if missing_files:
978
+ print("\n❌ 누락된 파일들:")
979
+ for file in missing_files:
980
+ print(f"- {file}")
981
+ else:
982
+ print("\n✅ 모든 필요 파일이 존재합니다.")
983
 
984
+ # PWA 설정 파일 생성
985
+ create_pwa_files()
986
+
987
+ print("\n🎵 음악 파일 접근성 확인...")
988
+ if os.path.exists("assets/main_music.mp3"):
989
+ print("✅ 음악 파일 확인됨")
990
+ print(f"📂 절대 경로: {os.path.abspath('assets/main_music.mp3')}")
991
+ else:
992
+ print("❌ 음악 파일이 없습니다")
993
+
994
+ print("\n🚀 서버 시작...")
995
+
996
  # Gradio 앱 실행
997
  demo = create_interface()
998
  demo.queue().launch(
 
1003
  show_error=True,
1004
  height=None,
1005
  width="100%"
1006
+ )
1007
+