roots / app.py
haepada's picture
Update app.py
2277770 verified
raw
history blame
17.1 kB
import gradio as gr
import numpy as np
import librosa
from transformers import pipeline
from datetime import datetime
import os
import requests
import json
import time
# 데이터 저장을 위한 간단한 파일 기반 DB
class SimpleDB:
def __init__(self, file_path="wishes.json"):
self.file_path = file_path
self.wishes = self._load_wishes()
def _load_wishes(self):
try:
if os.path.exists(self.file_path):
with open(self.file_path, 'r', encoding='utf-8') as f:
return json.load(f)
return []
except Exception as e:
print(f"Error loading wishes: {e}")
return []
def save_wish(self, name, wish, timestamp=None):
if timestamp is None:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
wish_data = {
"name": name,
"wish": wish,
"timestamp": timestamp
}
self.wishes.append(wish_data)
try:
with open(self.file_path, 'w', encoding='utf-8') as f:
json.dump(self.wishes, f, ensure_ascii=False, indent=2)
return True
except Exception as e:
print(f"Error saving wish: {e}")
return False
# 환경변수 설정
HF_API_TOKEN = os.getenv("roots")
if not HF_API_TOKEN:
raise ValueError("roots token not found in environment variables")
# API 설정
API_URL = "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-xl-base-1.0"
headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
# AI 모델 초기화
speech_recognizer = pipeline(
"automatic-speech-recognition",
model="kresnik/wav2vec2-large-xlsr-korean"
)
text_analyzer = pipeline(
"sentiment-analysis",
model="nlptown/bert-base-multilingual-uncased-sentiment"
)
# 상수 정의
IMAGE_DISPLAY_TIME = 30 # 이미지 표시 시간 (초)
WELCOME_MESSAGE = """
# 디지털 굿판에 오신 것을 환영합니다
디지털 굿판은 현대 도시 속에서 잊혀진 전통 굿의 정수를 담아낸 **디지털 의례의 공간**입니다.
이곳에서는 사람들의 목소리와 감정을 통해 **영적 교감**을 나누고, **자연과 도시의 에너지가 연결**됩니다.
이제, 평온함과 치유의 여정을 시작해보세요.
"""
WORLDVIEW_MESSAGE = """
## 굿판의 세계관 🌌
온천천의 물줄기는 신성한 금샘에서 시작됩니다. 금샘은 생명과 창조의 원천이며,
천상의 생명이 지상에서 숨을 틔우는 자리입니다. 도시의 소음 속에서도 신성한 생명력을 느껴보세요.
이곳에서 영적인 교감을 경험하며, 자연과 하나 되는 순간을 맞이해 보시기 바랍니다.
이 앱은 온천천의 사운드스케이프를 녹음하여 제작되었으며,
온천천 온천장역에서 장전역까지 걸으며 더 깊은 체험이 가능합니다.
"""
# Part 2/3 - Core Functions and Image Generation
def generate_image_from_prompt(prompt):
"""이미지 생성 함수"""
print(f"Generating image with prompt: {prompt}")
try:
if not prompt:
print("No prompt provided")
return None
response = requests.post(
API_URL,
headers=headers,
json={
"inputs": prompt,
"parameters": {
"negative_prompt": "ugly, blurry, poor quality, distorted",
"num_inference_steps": 30,
"guidance_scale": 7.5
}
}
)
if response.status_code == 200:
print("Image generated successfully")
# 이미지 저장 로직 추가
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
image_path = f"generated_images/{timestamp}.png"
os.makedirs("generated_images", exist_ok=True)
with open(image_path, "wb") as f:
f.write(response.content)
return response.content, image_path
else:
print(f"Error: {response.status_code}")
print(f"Response: {response.text}")
return None, None
except Exception as e:
print(f"Error generating image: {str(e)}")
return None, None
def analyze_voice_with_retry(audio_path, state, max_retries=3):
"""음성 분석 함수 (재시도 로직 포함)"""
for attempt in range(max_retries):
try:
y, sr = librosa.load(audio_path, sr=16000)
acoustic_features = {
"energy": float(np.mean(librosa.feature.rms(y=y))),
"tempo": float(librosa.beat.tempo(y)[0]),
"pitch": float(np.mean(librosa.feature.zero_crossing_rate(y))),
"volume": float(np.mean(np.abs(y)))
}
transcription = speech_recognizer(y)
text = transcription["text"]
emotions = map_acoustic_to_emotion(acoustic_features, state.get("baseline_features"))
text_sentiment = text_analyzer(text)[0]
return {
"text": text,
"emotions": emotions,
"sentiment": text_sentiment,
"features": acoustic_features
}
except Exception as e:
if attempt == max_retries - 1:
raise e
print(f"Attempt {attempt + 1} failed, retrying...")
continue
def generate_detailed_prompt(text, emotions, text_sentiment):
"""감정 기반 상세 프롬프트 생성"""
emotion_colors = {
"기쁨/열정": "밝은 노랑과 따뜻한 주황색",
"분노/강조": "강렬한 빨강과 짙은 검정",
"놀람/흥분": "선명한 파랑과 밝은 보라",
"관심/호기심": "연한 하늘색과 민트색",
"슬픔/우울": "어두운 파랑과 회색",
"피로/무기력": "탁한 갈색과 짙은 회색",
"평온/안정": "부드러운 초록과 베이지",
"차분/진지": "차분한 남색과 깊은 보라"
}
if emotions["intensity"] > 70:
visual_style = "역동적인 붓질과 강한 대비"
elif emotions["intensity"] > 40:
visual_style = "균형잡힌 구도와 중간 톤의 조화"
else:
visual_style = "부드러운 그라데이션과 차분한 톤"
prompt = f"한국 전통 민화 스타일의 추상화, {emotion_colors.get(emotions['primary'], '자연스러운 색상')} 기반. "
prompt += f"{visual_style}로 표현된 {emotions['primary']}의 감정. "
prompt += f"음성의 특징({', '.join(emotions['characteristics'])})을 화면의 동적 요소로 표현. "
prompt += f"발화 내용 '{text}'에서 느껴지는 감정(강도: {text_sentiment['score']}/5)을 은유적 이미지로 담아내기."
return prompt
def update_final_prompt(state):
"""청신의 감상들을 종합하여 최종 프롬프트 업데이트"""
combined_prompt = "한국 전통 민화 스타일의 추상화, 온천천에서의 감상과 소원을 담아내기:\n\n"
if state.get("reflections"):
combined_prompt += "청신의 감상들:\n"
for time, text, sentiment in state["reflections"]:
combined_prompt += f"- {time}: {text} ({sentiment})\n"
if state.get("wish"):
combined_prompt += f"\n소원:\n{state['wish']}"
return combined_prompt
def handle_image_timeout(result_image, delay=IMAGE_DISPLAY_TIME):
"""이미지 자동 사라짐 처리"""
time.sleep(delay)
return gr.update(value=None)
def save_reflection(text, state):
"""감상 저장"""
if not text.strip():
return state, state["reflections"]
try:
current_time = datetime.now().strftime("%H:%M:%S")
sentiment = text_analyzer(text)[0]
new_reflection = [current_time, text, f"{sentiment['label']} ({sentiment['score']:.2f})"]
if "reflections" not in state:
state["reflections"] = []
state["reflections"].append(new_reflection)
return state, state["reflections"]
except Exception as e:
print(f"Error in save_reflection: {str(e)}")
return state, []
# Part 3/3 - Interface and Main
def create_interface():
# DB 초기화
db = SimpleDB()
with gr.Blocks(theme=gr.themes.Soft()) as app:
state = gr.State({
"user_name": "",
"baseline_features": None,
"reflections": [],
"wish": None,
"final_prompt": "",
"image_path": None
})
# 헤더
header = gr.Markdown("# 디지털 굿판")
user_display = gr.Markdown("")
# 탭 구성
with gr.Tabs() as tabs:
# 입장
with gr.Tab("입장", id="intro"):
gr.Markdown(WELCOME_MESSAGE)
name_input = gr.Textbox(
label="이름을 알려주세요",
placeholder="이름을 입력해주세요"
)
worldview_display = gr.Markdown(visible=False)
start_btn = gr.Button("여정 시작하기")
continue_btn = gr.Button("다음 단계로", visible=False)
# 기준 설정
with gr.Tab("기준 설정", id="baseline"):
gr.Markdown("""
### 축원의 문장을 평온한 마음으로 읽어주세요
먼저, 평온한 마음으로 축원의 문장을 읽어주세요.
이 축원은 당신에게 평화와 안정을 불러일으키며,
감정을 정확히 이해하기 위한 **기준점**이 될 것입니다.
""")
gr.Markdown("'당신의 건강과 행복이 늘 가득하기를'")
baseline_audio = gr.Audio(
label="축원 문장 녹음하기",
sources=["microphone"]
)
set_baseline_btn = gr.Button("기준점 설정 완료")
baseline_status = gr.Markdown("")
# 청신
with gr.Tab("청신", id="cleansing"):
gr.Markdown("""
## 청신 - 소리로 정화하기
온천천의 물소리에 귀 기울이며 **30분간 마음을 정화**해보세요.
장전역까지 이어지는 이 여정을 함께하며,
차분히 자연의 소리에 마음을 기울여보세요.
""")
play_music_btn = gr.Button("온천천의 소리 듣기")
with gr.Row():
audio = gr.Audio(
value=None,
type="filepath",
label="온천천의 소리",
interactive=False,
autoplay=False
)
with gr.Column():
reflection_input = gr.Textbox(
label="지금 이 순간의 감상을 자유롭게 적어보세요",
lines=3
)
save_btn = gr.Button("감상 저장하기")
reflections_display = gr.Dataframe(
headers=["시간", "감상", "감정 분석"],
label="기록된 감상들"
)
# 기원
with gr.Tab("기원", id="prayer"):
gr.Markdown("""
## 기원 - 소원을 전해보세요
당신의 소원을 온전히 담아 이곳에 전해주세요.
당신의 목소리와 감정이 영적 메시지가 되어 전해지며,
이 기원이 당신에게 평화와 안정을 불러오기를 바랍니다.
""")
with gr.Row():
with gr.Column():
voice_input = gr.Audio(
label="소원을 나누고 싶은 마음을 말해주세요",
sources=["microphone"]
)
analyze_btn = gr.Button("소원 분석하기")
with gr.Column():
transcribed_text = gr.Textbox(
label="인식된 텍스트",
interactive=False
)
voice_emotion = gr.Textbox(
label="음성 감정 분석",
interactive=False
)
text_emotion = gr.Textbox(
label="텍스트 감정 분석",
interactive=False
)
# 송신
with gr.Tab("송신", id="sending"):
gr.Markdown("""
## 송신 - 마음의 그림을 남기고, 보내기
당신의 마음을 시각화하여 그려봅니다.
자연과 영적 교감을 통해 얻은 평온과 치유의 흔적을,
하나의 그림으로 담아보세요.
""")
final_prompt = gr.Textbox(
label="생성된 프롬프트",
interactive=False,
lines=3
)
with gr.Row():
generate_btn = gr.Button("마음의 그림 그리기")
save_image_btn = gr.Button("이미지 저장하기")
result_image = gr.Image(label="생성된 이미지")
image_timer = gr.Markdown(
"이미지는 30초 후 자동으로 사라집니다...",
visible=False
)
# 최종 감상
gr.Markdown("""
## 마지막 감상을 남겨주세요
이제 당신의 여정이 마무리되었습니다.
마지막으로 느낀 감상을 한 줄로 남겨주세요.
""")
final_reflection = gr.Textbox(
label="마지막 감상",
placeholder="한 줄로 남겨주세요..."
)
save_final_btn = gr.Button("감상 남기기")
# 이벤트 핸들러
def start_journey(name):
if not name.strip():
return "이름을 입력해주세요", gr.update(), gr.update()
state = {"user_name": name}
return (
WORLDVIEW_MESSAGE,
gr.update(visible=True),
gr.update(visible=True)
)
def save_wish_to_db(text, state):
if text and state.get("user_name"):
db.save_wish(state["user_name"], text)
return "소원이 안전하게 저장되었습니다."
return "저장에 실패했습니다."
def handle_image_generation(prompt):
image_content, image_path = generate_image_from_prompt(prompt)
if image_content:
gr.update(visible=True) # 타이머 표시
return image_content
return None
# 이벤트 연결
start_btn.click(
fn=start_journey,
inputs=[name_input],
outputs=[worldview_display, continue_btn, tabs]
)
set_baseline_btn.click(
fn=lambda x, s: ({"baseline_features": calculate_baseline_features(x)}, "기준점이 설정되었습니다."),
inputs=[baseline_audio, state],
outputs=[state, baseline_status]
)
save_btn.click(
fn=save_reflection,
inputs=[reflection_input, state],
outputs=[state, reflections_display]
)
analyze_btn.click(
fn=analyze_voice,
inputs=[voice_input, state],
outputs=[transcribed_text, voice_emotion, text_emotion, state]
)
generate_btn.click(
fn=handle_image_generation,
inputs=[final_prompt],
outputs=[result_image]
)
save_final_btn.click(
fn=save_wish_to_db,
inputs=[final_reflection, state],
outputs=[gr.Markdown("")] # 저장 상태 메시지
)
# 이미지 자동 사라짐 설정
result_image.change(
fn=lambda: gr.update(value=None),
inputs=[],
outputs=[result_image],
_js=f"() => setTimeout(() => {{document.querySelector('#result_image image').src = ''}}, {IMAGE_DISPLAY_TIME*1000})"
)
return app
if __name__ == "__main__":
demo = create_interface()
demo.launch(debug=True)