Spaces:
Runtime error
Runtime error
Claude commited on
fix: 이미지 다운로드 txt 버그 수정 + 한글 폰트 즉시 적용
Browse files- 이미지 bytes를 session_state에 캐시하여 첫 클릭에 바로 PNG 다운로드
- Dockerfile에 fonts-nanum 설치 추가 (베이스 이미지 재빌드 없이 즉시 적용)
- 교사 대시보드 이미지 다운로드도 동일 방식으로 통일
https://claude.ai/code/session_016DE3uHLsVmA5Uv7gZGGaKT
- Dockerfile +3 -0
- components/opinion_change.py +36 -18
- components/teacher_dashboard.py +7 -14
Dockerfile
CHANGED
|
@@ -3,6 +3,9 @@ FROM ghcr.io/violetwizard-lim/talk-trace-ai:base
|
|
| 3 |
WORKDIR /app
|
| 4 |
COPY . .
|
| 5 |
|
|
|
|
|
|
|
|
|
|
| 6 |
EXPOSE 7860
|
| 7 |
|
| 8 |
CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
|
|
|
|
| 3 |
WORKDIR /app
|
| 4 |
COPY . .
|
| 5 |
|
| 6 |
+
RUN apt-get update && apt-get install -y --no-install-recommends fonts-nanum \
|
| 7 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 8 |
+
|
| 9 |
EXPOSE 7860
|
| 10 |
|
| 11 |
CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
|
components/opinion_change.py
CHANGED
|
@@ -1,9 +1,8 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
|
| 3 |
from config import AI_MODEL_NAME, LIVE_BOARD_FETCH_LIMIT
|
| 4 |
-
from utils import create_analysis_image
|
| 5 |
from db import (
|
| 6 |
-
fetch_debate_status,
|
| 7 |
fetch_live_messages,
|
| 8 |
fetch_opinion_change,
|
| 9 |
opinion_changes_available,
|
|
@@ -42,9 +41,9 @@ def render_post_opinion_section(supabase, room_name, student_name, act_type, cur
|
|
| 42 |
return
|
| 43 |
|
| 44 |
row = fetch_opinion_change(supabase, room_name, student_name)
|
| 45 |
-
pre_opinion
|
| 46 |
post_opinion = (row or {}).get("post_opinion") or ""
|
| 47 |
-
ai_analysis
|
| 48 |
|
| 49 |
st.subheader("🔄 토론 후 생각 변화 기록")
|
| 50 |
|
|
@@ -78,26 +77,42 @@ def render_post_opinion_section(supabase, room_name, student_name, act_type, cur
|
|
| 78 |
if ai_analysis:
|
| 79 |
st.info("🤖 **AI 배움 분석**")
|
| 80 |
st.markdown(ai_analysis.replace("\n", "\n\n"))
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
st.download_button(
|
| 87 |
-
"🖼️ 분석 결과 이미지로 저장",
|
| 88 |
-
data=img_bytes,
|
| 89 |
-
file_name=filename,
|
| 90 |
-
mime="image/png",
|
| 91 |
-
use_container_width=True,
|
| 92 |
-
)
|
| 93 |
-
except Exception:
|
| 94 |
-
pass
|
| 95 |
else:
|
| 96 |
if st.button("🤖 AI 배움 분석 받기", use_container_width=True):
|
| 97 |
_trigger_analysis(supabase, room_name, student_name, act_type, current_topic, pre_opinion, post_opinion)
|
| 98 |
st.rerun()
|
| 99 |
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
def _trigger_analysis(supabase, room_name, student_name, act_type, current_topic, pre_opinion, post_opinion):
|
| 102 |
df_all = fetch_live_messages(supabase, room_name, LIVE_BOARD_FETCH_LIMIT)
|
| 103 |
if not df_all.empty:
|
|
@@ -122,6 +137,9 @@ def _trigger_analysis(supabase, room_name, student_name, act_type, current_topic
|
|
| 122 |
)
|
| 123 |
if res_text:
|
| 124 |
save_opinion_analysis(supabase, room_name, student_name, res_text)
|
|
|
|
|
|
|
|
|
|
| 125 |
st.toast("✅ AI 분석 완료!", icon="🎉")
|
| 126 |
else:
|
| 127 |
st.toast("🚨 AI 분석에 실패했습니다.", icon="❌")
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
|
| 3 |
from config import AI_MODEL_NAME, LIVE_BOARD_FETCH_LIMIT
|
| 4 |
+
from utils import create_analysis_image
|
| 5 |
from db import (
|
|
|
|
| 6 |
fetch_live_messages,
|
| 7 |
fetch_opinion_change,
|
| 8 |
opinion_changes_available,
|
|
|
|
| 41 |
return
|
| 42 |
|
| 43 |
row = fetch_opinion_change(supabase, room_name, student_name)
|
| 44 |
+
pre_opinion = (row or {}).get("pre_opinion") or ""
|
| 45 |
post_opinion = (row or {}).get("post_opinion") or ""
|
| 46 |
+
ai_analysis = (row or {}).get("ai_analysis") or ""
|
| 47 |
|
| 48 |
st.subheader("🔄 토론 후 생각 변화 기록")
|
| 49 |
|
|
|
|
| 77 |
if ai_analysis:
|
| 78 |
st.info("🤖 **AI 배움 분석**")
|
| 79 |
st.markdown(ai_analysis.replace("\n", "\n\n"))
|
| 80 |
+
_render_image_download(
|
| 81 |
+
student_name, current_topic, pre_opinion, post_opinion, ai_analysis,
|
| 82 |
+
session_key=f"img_{room_name}_{student_name}",
|
| 83 |
+
btn_key="dl_analysis_student",
|
| 84 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
else:
|
| 86 |
if st.button("🤖 AI 배움 분석 받기", use_container_width=True):
|
| 87 |
_trigger_analysis(supabase, room_name, student_name, act_type, current_topic, pre_opinion, post_opinion)
|
| 88 |
st.rerun()
|
| 89 |
|
| 90 |
|
| 91 |
+
def _render_image_download(student_name, topic, pre_opinion, post_opinion, ai_analysis,
|
| 92 |
+
session_key, btn_key):
|
| 93 |
+
"""이미지 bytes를 session_state에 캐시 후 download_button 렌더링."""
|
| 94 |
+
cache_key = session_key + "_bytes"
|
| 95 |
+
if cache_key not in st.session_state:
|
| 96 |
+
try:
|
| 97 |
+
st.session_state[cache_key] = create_analysis_image(
|
| 98 |
+
student_name, topic, pre_opinion, post_opinion, ai_analysis
|
| 99 |
+
)
|
| 100 |
+
except Exception:
|
| 101 |
+
st.session_state[cache_key] = None
|
| 102 |
+
|
| 103 |
+
img_bytes = st.session_state.get(cache_key)
|
| 104 |
+
if img_bytes:
|
| 105 |
+
filename = f"배움분석_{student_name}.png"
|
| 106 |
+
st.download_button(
|
| 107 |
+
"🖼️ 분석 결과 이미지로 저장",
|
| 108 |
+
data=img_bytes,
|
| 109 |
+
file_name=filename,
|
| 110 |
+
mime="image/png",
|
| 111 |
+
use_container_width=True,
|
| 112 |
+
key=btn_key,
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
def _trigger_analysis(supabase, room_name, student_name, act_type, current_topic, pre_opinion, post_opinion):
|
| 117 |
df_all = fetch_live_messages(supabase, room_name, LIVE_BOARD_FETCH_LIMIT)
|
| 118 |
if not df_all.empty:
|
|
|
|
| 137 |
)
|
| 138 |
if res_text:
|
| 139 |
save_opinion_analysis(supabase, room_name, student_name, res_text)
|
| 140 |
+
# 캐시 초기화 — 새 분석 결과로 이미지 재생성
|
| 141 |
+
cache_key = f"img_{room_name}_{student_name}_bytes"
|
| 142 |
+
st.session_state.pop(cache_key, None)
|
| 143 |
st.toast("✅ AI 분석 완료!", icon="🎉")
|
| 144 |
else:
|
| 145 |
st.toast("🚨 AI 분석에 실패했습니다.", icon="❌")
|
components/teacher_dashboard.py
CHANGED
|
@@ -4,7 +4,8 @@ import plotly.express as px
|
|
| 4 |
import streamlit as st
|
| 5 |
|
| 6 |
from db import destroy_room_data, fetch_all_opinion_changes, fetch_debate_status, fetch_live_messages, opinion_changes_available, session_control_available, set_debate_status
|
| 7 |
-
from utils import create_analysis_image
|
|
|
|
| 8 |
from validators import with_fallback_author_role
|
| 9 |
from utils import log_audit
|
| 10 |
from config import DASHBOARD_FETCH_LIMIT, ROOM_DESTROY_ENABLED, UI_FONT_FAMILY
|
|
@@ -69,19 +70,11 @@ def render_teacher_dashboard(supabase, room_name, user_role, student_name, curre
|
|
| 69 |
if ai:
|
| 70 |
st.caption("🤖 AI 배움 분석")
|
| 71 |
st.markdown(ai.replace("\n", "\n\n"))
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
data=img_bytes,
|
| 78 |
-
file_name=filename,
|
| 79 |
-
mime="image/png",
|
| 80 |
-
use_container_width=True,
|
| 81 |
-
key="oc_img_download",
|
| 82 |
-
)
|
| 83 |
-
except Exception:
|
| 84 |
-
pass
|
| 85 |
else:
|
| 86 |
st.caption("AI 분석이 아직 없습니다.")
|
| 87 |
|
|
|
|
| 4 |
import streamlit as st
|
| 5 |
|
| 6 |
from db import destroy_room_data, fetch_all_opinion_changes, fetch_debate_status, fetch_live_messages, opinion_changes_available, session_control_available, set_debate_status
|
| 7 |
+
from utils import create_analysis_image
|
| 8 |
+
from components.opinion_change import _render_image_download
|
| 9 |
from validators import with_fallback_author_role
|
| 10 |
from utils import log_audit
|
| 11 |
from config import DASHBOARD_FETCH_LIMIT, ROOM_DESTROY_ENABLED, UI_FONT_FAMILY
|
|
|
|
| 70 |
if ai:
|
| 71 |
st.caption("🤖 AI 배움 분석")
|
| 72 |
st.markdown(ai.replace("\n", "\n\n"))
|
| 73 |
+
_render_image_download(
|
| 74 |
+
selected, current_topic, pre, post, ai,
|
| 75 |
+
session_key=f"img_teacher_{room_name}_{selected}",
|
| 76 |
+
btn_key="dl_analysis_teacher",
|
| 77 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
else:
|
| 79 |
st.caption("AI 분석이 아직 없습니다.")
|
| 80 |
|