ayaka68 commited on
Commit
755eb09
·
verified ·
1 Parent(s): 5cae9df

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +106 -12
app.py CHANGED
@@ -1,11 +1,9 @@
1
- # app.py
2
  """
3
  Voice→Place Recommender (Streamlit / Hugging Face Spaces)
4
  - 日本語音声感情認識:S3PRL(HuBERT base) + HFの下流(.ckpt)を用いてJTES(4感情)推定
5
- - Spaces → Settings → Secrets に HF_TOKEN(Read権限)を設定
6
- - 可能なら KUSHINADA_FILENAME で ckpt を明示指定(例: s3prl/result/downstream/.../dev-best.ckpt)
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("🧹 クリアして新しく録音", width="stretch"):
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", width="stretch",
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"], width="stretch")
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("理由タグ(13個)", REASON_TAGS, max_selections=3)
591
  comment = st.text_input("ひとことコメント(任意・20字)", max_chars=20)
592
- if st.button("💾 ログ保存", width="stretch"):
 
 
 
 
 
 
593
  consent_research = (consent == "匿名で保存する")
594
  if not consent_research: st.info("体験のみモードです。研究ログは保存しません。")
595
  else: st.success("保存機能は開発中です。")
596
 
597
  st.divider()
598
- if st.button("▶ 次の人を録音する(状態をクリア)", width="stretch"):
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("理由タグ(13個)", 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()