Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,11 +1,9 @@
|
|
| 1 |
-
#
|
| 2 |
"""
|
| 3 |
Voice→Place Recommender (Streamlit / Hugging Face Spaces)
|
| 4 |
- 日本語音声感情認識:S3PRL(HuBERT base) + HFの下流(.ckpt)を用いてJTES(4感情)推定
|
| 5 |
-
-
|
| 6 |
-
-
|
| 7 |
-
- apt.txt: ffmpeg, (任意で)fonts-ipaexfont, fonts-noto-cjk
|
| 8 |
-
- requirements.txt: streamlit-audiorecorder, s3prl==0.4.17, torch==2.0.1, torchaudio==2.0.2 など
|
| 9 |
"""
|
| 10 |
|
| 11 |
# ===== 基本インポート =====
|
|
@@ -13,6 +11,8 @@ import io, base64, os, random
|
|
| 13 |
import numpy as np
|
| 14 |
import soundfile as sf
|
| 15 |
from pydub import AudioSegment
|
|
|
|
|
|
|
| 16 |
|
| 17 |
import streamlit as st
|
| 18 |
from audiorecorder import audiorecorder
|
|
@@ -31,6 +31,10 @@ import torch.nn as nn
|
|
| 31 |
from huggingface_hub import HfApi, hf_hub_download
|
| 32 |
from s3prl.nn import S3PRLUpstream, Featurizer
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
# ===== フォント設定(日本語) =====
|
| 35 |
jp_candidates = ["IPAexGothic", "IPAGothic", "Noto Sans CJK JP", "Noto Sans CJK"]
|
| 36 |
for name in jp_candidates:
|
|
@@ -287,6 +291,41 @@ def audio_player_bytes(b: bytes, mime="audio/wav"):
|
|
| 287 |
unsafe_allow_html=True,
|
| 288 |
)
|
| 289 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
# ===== フォールバック(簡易特徴量) =====
|
| 291 |
def extract_features(y, sr):
|
| 292 |
abs_y = np.abs(y)
|
|
@@ -480,6 +519,49 @@ def plot_emotion_map(emotion_label, scores, method="AI"):
|
|
| 480 |
fontsize=14, fontweight='bold')
|
| 481 |
plt.tight_layout(); return fig
|
| 482 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
# ===== メイン =====
|
| 484 |
def main():
|
| 485 |
st.set_page_config(page_title="Voice→Place Recommender", page_icon="🎙️", layout="centered")
|
|
@@ -506,7 +588,7 @@ def main():
|
|
| 506 |
st.session_state["wav_bytes"] = buf.getvalue()
|
| 507 |
audio_player_bytes(st.session_state["wav_bytes"], mime="audio/wav")
|
| 508 |
st.caption(f"録音サイズ: {len(st.session_state['wav_bytes']) / 1024:.1f} KB")
|
| 509 |
-
if st.button("🧹 クリアして新しく録音",
|
| 510 |
for k in ["wav_bytes","recs","feat","emotion_label","scores","method"]:
|
| 511 |
st.session_state[k] = None
|
| 512 |
st.session_state["rec_key"] += 1; st.rerun()
|
|
@@ -533,7 +615,7 @@ def main():
|
|
| 533 |
|
| 534 |
analysis_method = st.radio("分析方法", ["AIモデル(推奨)", "音声特徴量ベース"], horizontal=True)
|
| 535 |
|
| 536 |
-
if st.button("🔍 推定 & レコメンド", type="primary",
|
| 537 |
disabled=(st.session_state["wav_bytes"] is None)):
|
| 538 |
with st.spinner('感情を分析中...'):
|
| 539 |
raw_bytes = st.session_state["wav_bytes"]
|
|
@@ -575,30 +657,42 @@ def main():
|
|
| 575 |
st.subheader("感情分析結果")
|
| 576 |
fig = plot_emotion_map(emotion_label, scores, method)
|
| 577 |
st.pyplot(fig, clear_figure=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
|
| 579 |
st.subheader("3) おすすめ(上位4件)")
|
| 580 |
cols = st.columns(4)
|
| 581 |
for i, p in enumerate(recs[:4]):
|
| 582 |
with cols[i % 4]:
|
| 583 |
-
if "image" in p: st.image(p["image"],
|
| 584 |
st.markdown(f"**{p['name']}**"); st.caption(f"タグ: {', '.join(p['tags'])}")
|
| 585 |
|
| 586 |
st.subheader("4) 評価")
|
| 587 |
choice_name = st.selectbox("第一候補を選んでください", [p["name"] for p in recs[:4]])
|
| 588 |
rating_like = st.slider("行ってみたい度(★)", 1, 5, 4)
|
| 589 |
rating_vibe = st.slider("気分に合う度(🎯)", 1, 5, 4)
|
| 590 |
-
reasons = st.multiselect("理由タグ(1
|
| 591 |
comment = st.text_input("ひとことコメント(任意・20字)", max_chars=20)
|
| 592 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
consent_research = (consent == "匿名で保存する")
|
| 594 |
if not consent_research: st.info("体験のみモードです。研究ログは保存しません。")
|
| 595 |
else: st.success("保存機能は開発中です。")
|
| 596 |
|
| 597 |
st.divider()
|
| 598 |
-
if st.button("▶ 次の人を録音する(状態をクリア)",
|
| 599 |
for k in ["wav_bytes","recs","emotion_label","scores","method"]:
|
| 600 |
st.session_state[k] = None
|
| 601 |
st.session_state["rec_key"] += 1; st.rerun()
|
| 602 |
|
| 603 |
if __name__ == "__main__":
|
| 604 |
-
main()
|
|
|
|
| 1 |
+
# app_updated.py
|
| 2 |
"""
|
| 3 |
Voice→Place Recommender (Streamlit / Hugging Face Spaces)
|
| 4 |
- 日本語音声感情認識:S3PRL(HuBERT base) + HFの下流(.ckpt)を用いてJTES(4感情)推定
|
| 5 |
+
- 音声波形表示機能を追加
|
| 6 |
+
- SNS共有ボタンを追加
|
|
|
|
|
|
|
| 7 |
"""
|
| 8 |
|
| 9 |
# ===== 基本インポート =====
|
|
|
|
| 11 |
import numpy as np
|
| 12 |
import soundfile as sf
|
| 13 |
from pydub import AudioSegment
|
| 14 |
+
import urllib.parse
|
| 15 |
+
from datetime import datetime
|
| 16 |
|
| 17 |
import streamlit as st
|
| 18 |
from audiorecorder import audiorecorder
|
|
|
|
| 31 |
from huggingface_hub import HfApi, hf_hub_download
|
| 32 |
from s3prl.nn import S3PRLUpstream, Featurizer
|
| 33 |
|
| 34 |
+
# Librosa for waveform
|
| 35 |
+
import librosa
|
| 36 |
+
import librosa.display
|
| 37 |
+
|
| 38 |
# ===== フォント設定(日本語) =====
|
| 39 |
jp_candidates = ["IPAexGothic", "IPAGothic", "Noto Sans CJK JP", "Noto Sans CJK"]
|
| 40 |
for name in jp_candidates:
|
|
|
|
| 291 |
unsafe_allow_html=True,
|
| 292 |
)
|
| 293 |
|
| 294 |
+
# ===== 音声波形表示機能を追加 =====
|
| 295 |
+
def create_waveform_visualization(audio_bytes):
|
| 296 |
+
"""音声波形を可視化"""
|
| 297 |
+
if audio_bytes is None:
|
| 298 |
+
return None
|
| 299 |
+
|
| 300 |
+
try:
|
| 301 |
+
# バイトデータから音声を読み込み
|
| 302 |
+
y, sr = sf.read(io.BytesIO(audio_bytes), dtype="float32")
|
| 303 |
+
|
| 304 |
+
# 図の作成
|
| 305 |
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 6), dpi=100)
|
| 306 |
+
|
| 307 |
+
# 波形表示
|
| 308 |
+
librosa.display.waveshow(y, sr=sr, ax=ax1, color='#4169E1', alpha=0.8)
|
| 309 |
+
ax1.set_title('Audio Waveform', fontsize=14, fontweight='bold')
|
| 310 |
+
ax1.set_xlabel('Time (s)')
|
| 311 |
+
ax1.set_ylabel('Amplitude')
|
| 312 |
+
ax1.grid(True, alpha=0.3)
|
| 313 |
+
|
| 314 |
+
# スペクトログラム
|
| 315 |
+
D = librosa.stft(y)
|
| 316 |
+
DB = librosa.amplitude_to_db(abs(D), ref=np.max)
|
| 317 |
+
img = librosa.display.specshow(DB, sr=sr, x_axis='time', y_axis='hz', ax=ax2)
|
| 318 |
+
ax2.set_title('Spectrogram', fontsize=14, fontweight='bold')
|
| 319 |
+
fig.colorbar(img, ax=ax2, format='%+2.0f dB')
|
| 320 |
+
|
| 321 |
+
plt.tight_layout()
|
| 322 |
+
|
| 323 |
+
return fig
|
| 324 |
+
|
| 325 |
+
except Exception as e:
|
| 326 |
+
st.error(f"波形表示エラー: {e}")
|
| 327 |
+
return None
|
| 328 |
+
|
| 329 |
# ===== フォールバック(簡易特徴量) =====
|
| 330 |
def extract_features(y, sr):
|
| 331 |
abs_y = np.abs(y)
|
|
|
|
| 519 |
fontsize=14, fontweight='bold')
|
| 520 |
plt.tight_layout(); return fig
|
| 521 |
|
| 522 |
+
# ===== SNS共有ボタン機能を追加 =====
|
| 523 |
+
def create_share_buttons(emotion_label, place_name):
|
| 524 |
+
"""SNS共有ボタンを生成"""
|
| 525 |
+
# 共有用のテキスト
|
| 526 |
+
share_text = f"Voice × Place Labで感情「{emotion_label}」と診断されました!おすすめの場所は「{place_name}」です。"
|
| 527 |
+
encoded_text = urllib.parse.quote(share_text)
|
| 528 |
+
|
| 529 |
+
# 現在のページURL(実際のデプロイURLに置き換える必要があります)
|
| 530 |
+
page_url = "https://your-app-url.com"
|
| 531 |
+
encoded_url = urllib.parse.quote(page_url)
|
| 532 |
+
|
| 533 |
+
# Twitter共有リンク
|
| 534 |
+
twitter_url = f"https://twitter.com/intent/tweet?text={encoded_text}&url={encoded_url}"
|
| 535 |
+
|
| 536 |
+
# Facebook共有リンク
|
| 537 |
+
facebook_url = f"https://www.facebook.com/sharer/sharer.php?u={encoded_url}"
|
| 538 |
+
|
| 539 |
+
# LINE共有リンク
|
| 540 |
+
line_url = f"https://line.me/R/msg/text/?{encoded_text}%20{encoded_url}"
|
| 541 |
+
|
| 542 |
+
# ボタンのHTML
|
| 543 |
+
share_html = f"""
|
| 544 |
+
<div style='display: flex; gap: 10px; margin: 20px 0;'>
|
| 545 |
+
<a href='{twitter_url}' target='_blank' style='text-decoration: none;'>
|
| 546 |
+
<div style='background: #1DA1F2; color: white; padding: 10px 20px; border-radius: 5px; display: inline-block;'>
|
| 547 |
+
🐦 Twitterで共有
|
| 548 |
+
</div>
|
| 549 |
+
</a>
|
| 550 |
+
<a href='{facebook_url}' target='_blank' style='text-decoration: none;'>
|
| 551 |
+
<div style='background: #4267B2; color: white; padding: 10px 20px; border-radius: 5px; display: inline-block;'>
|
| 552 |
+
📘 Facebookで共有
|
| 553 |
+
</div>
|
| 554 |
+
</a>
|
| 555 |
+
<a href='{line_url}' target='_blank' style='text-decoration: none;'>
|
| 556 |
+
<div style='background: #00B900; color: white; padding: 10px 20px; border-radius: 5px; display: inline-block;'>
|
| 557 |
+
💬 LINEで共有
|
| 558 |
+
</div>
|
| 559 |
+
</a>
|
| 560 |
+
</div>
|
| 561 |
+
"""
|
| 562 |
+
|
| 563 |
+
return share_html
|
| 564 |
+
|
| 565 |
# ===== メイン =====
|
| 566 |
def main():
|
| 567 |
st.set_page_config(page_title="Voice→Place Recommender", page_icon="🎙️", layout="centered")
|
|
|
|
| 588 |
st.session_state["wav_bytes"] = buf.getvalue()
|
| 589 |
audio_player_bytes(st.session_state["wav_bytes"], mime="audio/wav")
|
| 590 |
st.caption(f"録音サイズ: {len(st.session_state['wav_bytes']) / 1024:.1f} KB")
|
| 591 |
+
if st.button("🧹 クリアして新しく録音", key="clear_rec"):
|
| 592 |
for k in ["wav_bytes","recs","feat","emotion_label","scores","method"]:
|
| 593 |
st.session_state[k] = None
|
| 594 |
st.session_state["rec_key"] += 1; st.rerun()
|
|
|
|
| 615 |
|
| 616 |
analysis_method = st.radio("分析方法", ["AIモデル(推奨)", "音声特徴量ベース"], horizontal=True)
|
| 617 |
|
| 618 |
+
if st.button("🔍 推定 & レコメンド", type="primary",
|
| 619 |
disabled=(st.session_state["wav_bytes"] is None)):
|
| 620 |
with st.spinner('感情を分析中...'):
|
| 621 |
raw_bytes = st.session_state["wav_bytes"]
|
|
|
|
| 657 |
st.subheader("感情分析結果")
|
| 658 |
fig = plot_emotion_map(emotion_label, scores, method)
|
| 659 |
st.pyplot(fig, clear_figure=True)
|
| 660 |
+
|
| 661 |
+
# 音声波形の表示
|
| 662 |
+
st.subheader("音声波形分析")
|
| 663 |
+
waveform_fig = create_waveform_visualization(st.session_state["wav_bytes"])
|
| 664 |
+
if waveform_fig:
|
| 665 |
+
st.pyplot(waveform_fig, clear_figure=True)
|
| 666 |
|
| 667 |
st.subheader("3) おすすめ(上位4件)")
|
| 668 |
cols = st.columns(4)
|
| 669 |
for i, p in enumerate(recs[:4]):
|
| 670 |
with cols[i % 4]:
|
| 671 |
+
if "image" in p: st.image(p["image"], use_column_width=True)
|
| 672 |
st.markdown(f"**{p['name']}**"); st.caption(f"タグ: {', '.join(p['tags'])}")
|
| 673 |
|
| 674 |
st.subheader("4) 評価")
|
| 675 |
choice_name = st.selectbox("第一候補を選んでください", [p["name"] for p in recs[:4]])
|
| 676 |
rating_like = st.slider("行ってみたい度(★)", 1, 5, 4)
|
| 677 |
rating_vibe = st.slider("気分に合う度(🎯)", 1, 5, 4)
|
| 678 |
+
reasons = st.multiselect("理由タグ(1—3個)", REASON_TAGS, max_selections=3)
|
| 679 |
comment = st.text_input("ひとことコメント(任意・20字)", max_chars=20)
|
| 680 |
+
|
| 681 |
+
# SNS共有ボタンの表示
|
| 682 |
+
st.subheader("5) SNSで共有")
|
| 683 |
+
share_html = create_share_buttons(display_emotion, choice_name)
|
| 684 |
+
st.markdown(share_html, unsafe_allow_html=True)
|
| 685 |
+
|
| 686 |
+
if st.button("💾 ログ保存", key="save_log"):
|
| 687 |
consent_research = (consent == "匿名で保存する")
|
| 688 |
if not consent_research: st.info("体験のみモードです。研究ログは保存しません。")
|
| 689 |
else: st.success("保存機能は開発中です。")
|
| 690 |
|
| 691 |
st.divider()
|
| 692 |
+
if st.button("▶ 次の人を録音する(状態をクリア)", key="next_person"):
|
| 693 |
for k in ["wav_bytes","recs","emotion_label","scores","method"]:
|
| 694 |
st.session_state[k] = None
|
| 695 |
st.session_state["rec_key"] += 1; st.rerun()
|
| 696 |
|
| 697 |
if __name__ == "__main__":
|
| 698 |
+
main()
|