chariscait commited on
Commit
7795a36
·
verified ·
1 Parent(s): a851012

Update app.py - custom webcam component, no WebRTC

Browse files
Files changed (1) hide show
  1. app.py +72 -124
app.py CHANGED
@@ -36,12 +36,22 @@ try:
36
  except ImportError:
37
  HAS_PIL = False
38
 
39
- try:
40
- from streamlit_webrtc import webrtc_streamer, WebRtcMode
41
- import av
42
- HAS_WEBRTC = True
43
- except ImportError:
44
- HAS_WEBRTC = False
 
 
 
 
 
 
 
 
 
 
45
 
46
  from models import EmotionLabel, EMOTION_LABELS, CulturalRegion, EMOTION_EMOJI
47
  from face_detector import FaceEmotionDetector
@@ -657,10 +667,7 @@ def show_demo():
657
  return
658
 
659
  # ── Primary: Live Streaming ──────────────────────────────────────
660
- if HAS_WEBRTC:
661
- _show_live_session(processor, remaining, start)
662
- else:
663
- st.warning("Live streaming requires streamlit-webrtc. Use video upload below.")
664
 
665
  # ── Secondary: Video Upload ──────────────────────────────────────
666
  st.divider()
@@ -696,97 +703,54 @@ def show_demo():
696
 
697
 
698
  def _show_live_session(processor, remaining, start):
699
- """Live session using streamlit-webrtc. Only ONE Start button (WebRTC's built-in)."""
700
 
701
- # ── Stop button + timer (above the stream) ──────────────────────
702
- col_stop, col_timer, col_report = st.columns([1, 1, 1])
703
-
704
- with col_stop:
705
- if processor.is_active:
706
- if st.button("⏹ Stop Session", type="primary", use_container_width=True):
707
- processor.stop_session()
708
- st.session_state.show_report = True
709
- st.rerun()
710
 
711
- with col_timer:
712
- if processor.is_active:
713
- elapsed = processor.elapsed_seconds
714
- session_remaining = max(0, 60 - elapsed)
715
- st.markdown(
716
- '<div style="text-align: center; padding: 8px;">'
717
- '<span style="color: #FF4444; font-size: 14px; font-weight: 700;">● LIVE</span>'
718
- '<span style="color: #00D4FF; margin-left: 12px; font-weight: 700; font-size: 16px;">'
719
- '{:.0f}s remaining</span>'
720
- '</div>'.format(session_remaining),
721
- unsafe_allow_html=True,
722
- )
723
- else:
724
- st.markdown(
725
- '<div style="text-align: center; padding: 8px; color: #6B7B9D;">'
726
- 'Click <strong>START</strong> on the video to begin'
727
- '</div>',
728
- unsafe_allow_html=True,
729
- )
730
-
731
- with col_report:
732
- if processor.is_active:
733
- if st.button("📊 View Report", use_container_width=True):
734
- processor.stop_session()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
735
  st.session_state.show_report = True
736
  st.rerun()
737
 
738
- # ── WebRTC Stream + Results side by side ─────────────────────────
739
- col_video, col_results = st.columns([1, 1])
740
-
741
- with col_video:
742
- # The WebRTC component provides its own START/STOP button
743
- webrtc_ctx = webrtc_streamer(
744
- key="emosphere-live",
745
- mode=WebRtcMode.SENDRECV,
746
- video_frame_callback=processor.video_frame_callback,
747
- audio_frame_callback=processor.audio_frame_callback,
748
- media_stream_constraints={
749
- "video": {"width": {"ideal": 640}, "height": {"ideal": 480}},
750
- "audio": True,
751
- },
752
- rtc_configuration={
753
- "iceServers": [
754
- {"urls": ["stun:stun.l.google.com:19302"]},
755
- {"urls": ["stun:stun1.l.google.com:19302"]},
756
- {"urls": ["stun:stun2.l.google.com:19302"]},
757
- {"urls": ["stun:stun3.l.google.com:19302"]},
758
- {"urls": ["stun:stun4.l.google.com:19302"]},
759
- {
760
- "urls": [
761
- "turn:openrelay.metered.ca:80",
762
- "turn:openrelay.metered.ca:80?transport=tcp",
763
- "turn:openrelay.metered.ca:443",
764
- "turns:openrelay.metered.ca:443",
765
- ],
766
- "username": "openrelayproject",
767
- "credential": "openrelayproject",
768
- },
769
- ]
770
- },
771
- async_processing=True,
772
- )
773
-
774
- # Auto-start processing when WebRTC connects
775
- if webrtc_ctx.state.playing and not processor.is_active:
776
- processor.start_session()
777
- st.rerun()
778
-
779
- # Auto-stop when WebRTC disconnects
780
- if not webrtc_ctx.state.playing and processor.is_active:
781
- processor.stop_session()
782
- st.session_state.show_report = True
783
- st.rerun()
784
-
785
- # Auto-stop after 60 seconds of session
786
- if processor.is_active and processor.elapsed_seconds >= 60:
787
- processor.stop_session()
788
- st.session_state.show_report = True
789
- st.rerun()
790
 
791
  with col_results:
792
  if processor.is_active:
@@ -794,29 +758,25 @@ def _show_live_session(processor, remaining, start):
794
  else:
795
  st.markdown(
796
  '<div class="glass-card" style="text-align: center; padding: 40px;">'
797
- '<span style="font-size: 48px;">🎬</span>'
798
  '<h3 style="margin: 12px 0 8px; color: #B0BCD0 !important;">Ready to Stream</h3>'
799
  '<p style="color: #6B7B9D; margin: 0; font-size: 13px;">'
800
- 'Click the <strong>START</strong> button on the left to begin '
801
- 'your 60-second live emotion analysis session.</p>'
802
  '<div style="margin-top: 16px; padding: 12px; background: rgba(0,212,255,0.06); '
803
  'border-radius: 8px; border: 1px solid rgba(0,212,255,0.15);">'
804
  '<p style="color: #00D4FF; font-size: 12px; margin: 0;">'
805
- '🧑 Face &bull; 🎙 Voice &bull; 💬 Speech &bull; 🧍 Posture<br/>'
806
  'All fused with fuzzy logic in real-time.</p>'
807
  '</div>'
808
  '</div>',
809
  unsafe_allow_html=True,
810
  )
811
 
812
- # Refresh while active to update results
813
- if processor.is_active:
814
- _schedule_rerun_fast()
815
-
816
 
817
- @st.fragment(run_every=1.5)
818
  def _render_live_results(processor):
819
- """Auto-updating display of live emotion results."""
820
  fused = processor.get_latest_fused()
821
  face = processor.get_latest_face()
822
  voice = processor.get_latest_voice()
@@ -829,7 +789,7 @@ def _render_live_results(processor):
829
  if fused is None:
830
  st.markdown(
831
  '<div class="glass-card" style="text-align: center; padding: 20px;">'
832
- '<span style="font-size: 36px;">🔮</span>'
833
  '<p style="color: #6B7B9D; margin-top: 8px;">'
834
  'Analyzing... Speak, move, or express yourself.</p>'
835
  '</div>',
@@ -847,10 +807,10 @@ def _render_live_results(processor):
847
  # Modality signals
848
  st.markdown("#### Modality Signals")
849
  mod_data = [
850
- ("🧑 Face", face),
851
- ("🎙 Voice", voice),
852
- ("💬 Text", text),
853
- ("🧍 Posture", posture),
854
  ]
855
  mod_colors = ["#E948A0", "#FFD700", "#00D4FF", "#10B981"]
856
 
@@ -919,7 +879,7 @@ def _render_live_results(processor):
919
  # Stats
920
  st.markdown(
921
  '<div style="color: #6B7B9D; font-size: 11px; margin-top: 8px; text-align: right;">'
922
- 'Frames: {} &bull; Audio: {} &bull; Transcript: {}'
923
  '</div>'.format(
924
  stats.get("video_frames", 0),
925
  stats.get("audio_chunks", 0),
@@ -929,18 +889,6 @@ def _render_live_results(processor):
929
  )
930
 
931
 
932
- def _schedule_rerun_fast():
933
- """Schedule a fast rerun to keep live results updating."""
934
- try:
935
- import streamlit.components.v1 as components
936
- components.html(
937
- '<script>setTimeout(function() { window.location.reload(); }, 2000);</script>',
938
- height=0,
939
- )
940
- except Exception:
941
- pass
942
-
943
-
944
  def _show_video_processing(processor, start):
945
  """Process an uploaded video and show results."""
946
  video_bytes = st.session_state.get("video_bytes")
 
36
  except ImportError:
37
  HAS_PIL = False
38
 
39
+ import base64
40
+ import streamlit.components.v1 as components_lib
41
+ import os as _os
42
+
43
+ # Custom webcam component (no WebRTC needed)
44
+ _WEBCAM_DIR = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "webcam_component")
45
+ _webcam_component_func = None
46
+ if _os.path.isdir(_WEBCAM_DIR):
47
+ _webcam_component_func = components_lib.declare_component("webcam_capture", path=_WEBCAM_DIR)
48
+
49
+ def webcam_capture(key="webcam"):
50
+ """Custom webcam component — captures video frames + audio, sends to Python."""
51
+ if _webcam_component_func is None:
52
+ st.error("Webcam component not found.")
53
+ return None
54
+ return _webcam_component_func(key=key, default=None)
55
 
56
  from models import EmotionLabel, EMOTION_LABELS, CulturalRegion, EMOTION_EMOJI
57
  from face_detector import FaceEmotionDetector
 
667
  return
668
 
669
  # ── Primary: Live Streaming ──────────────────────────────────────
670
+ _show_live_session(processor, remaining, start)
 
 
 
671
 
672
  # ── Secondary: Video Upload ──────────────────────────────────────
673
  st.divider()
 
703
 
704
 
705
  def _show_live_session(processor, remaining, start):
706
+ """Live session using custom webcam component (no WebRTC needed)."""
707
 
708
+ # Video + Results side by side
709
+ col_video, col_results = st.columns([1.3, 1])
 
 
 
 
 
 
 
710
 
711
+ with col_video:
712
+ # Custom webcam component with built-in START/STOP + timer
713
+ component_value = webcam_capture(key="webcam_live")
714
+
715
+ # Handle component messages
716
+ if component_value and isinstance(component_value, dict):
717
+ msg_type = component_value.get("type")
718
+
719
+ if msg_type == "started":
720
+ if not processor.is_active:
721
+ processor.start_session()
722
+
723
+ elif msg_type == "frame":
724
+ if not processor.is_active:
725
+ processor.start_session()
726
+ # Decode base64 JPEG and process
727
+ data_url = component_value.get("data", "")
728
+ if "," in data_url:
729
+ try:
730
+ img_b64 = data_url.split(",", 1)[1]
731
+ img_bytes = base64.b64decode(img_b64)
732
+ processor.process_image(img_bytes)
733
+ except Exception as e:
734
+ print(f"[App] Frame decode error: {e}")
735
+
736
+ elif msg_type == "audio":
737
+ data_url = component_value.get("data", "")
738
+ if "," in data_url:
739
+ try:
740
+ audio_b64 = data_url.split(",", 1)[1]
741
+ audio_bytes = base64.b64decode(audio_b64)
742
+ processor.process_audio_bytes(audio_bytes)
743
+ except Exception as e:
744
+ print(f"[App] Audio decode error: {e}")
745
+
746
+ elif msg_type == "stopped":
747
+ if processor.is_active:
748
+ processor.stop_session()
749
  st.session_state.show_report = True
750
  st.rerun()
751
 
752
+ elif msg_type == "error":
753
+ st.error("Camera/mic error: " + component_value.get("message", "unknown"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
754
 
755
  with col_results:
756
  if processor.is_active:
 
758
  else:
759
  st.markdown(
760
  '<div class="glass-card" style="text-align: center; padding: 40px;">'
761
+ '<span style="font-size: 48px;">&#127909;</span>'
762
  '<h3 style="margin: 12px 0 8px; color: #B0BCD0 !important;">Ready to Stream</h3>'
763
  '<p style="color: #6B7B9D; margin: 0; font-size: 13px;">'
764
+ 'Click <strong>START SESSION</strong> on the left to begin '
765
+ 'your 60-second live emotion analysis.</p>'
766
  '<div style="margin-top: 16px; padding: 12px; background: rgba(0,212,255,0.06); '
767
  'border-radius: 8px; border: 1px solid rgba(0,212,255,0.15);">'
768
  '<p style="color: #00D4FF; font-size: 12px; margin: 0;">'
769
+ '&#129489; Face &#8226; &#127897; Voice &#8226; &#128172; Speech &#8226; &#129485; Posture<br/>'
770
  'All fused with fuzzy logic in real-time.</p>'
771
  '</div>'
772
  '</div>',
773
  unsafe_allow_html=True,
774
  )
775
 
 
 
 
 
776
 
777
+ @st.fragment(run_every=2.0)
778
  def _render_live_results(processor):
779
+ """Auto-updating display of live emotion results. Refreshes every 2s."""
780
  fused = processor.get_latest_fused()
781
  face = processor.get_latest_face()
782
  voice = processor.get_latest_voice()
 
789
  if fused is None:
790
  st.markdown(
791
  '<div class="glass-card" style="text-align: center; padding: 20px;">'
792
+ '<span style="font-size: 36px;">&#128302;</span>'
793
  '<p style="color: #6B7B9D; margin-top: 8px;">'
794
  'Analyzing... Speak, move, or express yourself.</p>'
795
  '</div>',
 
807
  # Modality signals
808
  st.markdown("#### Modality Signals")
809
  mod_data = [
810
+ ("&#129489; Face", face),
811
+ ("&#127897; Voice", voice),
812
+ ("&#128172; Speech", text),
813
+ ("&#129485; Posture", posture),
814
  ]
815
  mod_colors = ["#E948A0", "#FFD700", "#00D4FF", "#10B981"]
816
 
 
879
  # Stats
880
  st.markdown(
881
  '<div style="color: #6B7B9D; font-size: 11px; margin-top: 8px; text-align: right;">'
882
+ 'Frames: {} &#8226; Audio: {} &#8226; Transcript: {}'
883
  '</div>'.format(
884
  stats.get("video_frames", 0),
885
  stats.get("audio_chunks", 0),
 
889
  )
890
 
891
 
 
 
 
 
 
 
 
 
 
 
 
 
892
  def _show_video_processing(processor, start):
893
  """Process an uploaded video and show results."""
894
  video_bytes = st.session_state.get("video_bytes")