Spaces:
Running
Running
File size: 38,774 Bytes
1d9a214 9e263c9 a425180 1d9a214 9e263c9 ca849a5 9e263c9 1d9a214 7795a36 ca849a5 1d9a214 7f331f4 1d9a214 aad250a 1708417 aad250a 1708417 1d9a214 aad250a 1708417 aad250a 1708417 1d9a214 b3db27c 1d9a214 363d43a 1d9a214 17352b9 1d9a214 17352b9 1d9a214 363d43a 1d9a214 a425180 4816605 1d9a214 363d43a 1d9a214 7f331f4 a425180 1d9a214 b3db27c 1d9a214 b3db27c 50fd653 b3db27c 1d9a214 3e2a599 1d9a214 363d43a 1d9a214 363d43a 1d9a214 fc1537d 363d43a 1d9a214 052447e 1d9a214 b3db27c 50fd653 b3db27c 1d9a214 363d43a 1d9a214 363d43a 1d9a214 363d43a 1d9a214 363d43a 1d9a214 9901521 1d9a214 9e263c9 1d9a214 2225933 1d9a214 2225933 1d9a214 a425180 1d9a214 9e263c9 ca849a5 1d9a214 4816605 1d9a214 ca849a5 9e263c9 ca849a5 a425180 9e263c9 83c9af6 1d9a214 9e263c9 9901521 9e263c9 ca849a5 a425180 ca849a5 9901521 1d9a214 9e263c9 a425180 038a5fa a425180 9e263c9 4816605 ca849a5 7795a36 9e263c9 ca849a5 4816605 ca849a5 4816605 ca849a5 9e263c9 2225933 ca849a5 2225933 ca849a5 7795a36 ca849a5 7795a36 637afa6 4816605 7795a36 f9fa4ec 7795a36 f9fa4ec 7795a36 052447e 7795a36 f9fa4ec ca849a5 4816605 7795a36 a425180 ca849a5 7795a36 ca849a5 7795a36 ca849a5 5120efd ca849a5 a425180 5120efd f9fa4ec a425180 7795a36 ca849a5 7795a36 ca849a5 a425180 ca849a5 7795a36 ca849a5 a425180 ca849a5 7795a36 ca849a5 a425180 ca849a5 a425180 ca849a5 a425180 ca849a5 7795a36 ca849a5 4816605 9e263c9 4816605 9e263c9 ca849a5 4816605 7f331f4 1d9a214 49c409b 1d9a214 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 | """EmoSphere -- HuggingFace Spaces Demo (Streamlit).
Trial-gated multimodal emotion recognition demo with fuzzy fusion.
Run: streamlit run app.py
Flow:
1. Landing page with email registration
2. 6-digit code verification
3. Demo: Upload video OR camera+mic quick capture
4. Full multimodal analysis (face, voice, speech, posture)
5. Session report with emotion timeline
6. Trial-ended screen with contact info
"""
from __future__ import annotations
import time
import io
from datetime import datetime
from collections import deque
from pathlib import Path
import numpy as np
import streamlit as st
try:
import cv2
HAS_CV2 = True
except ImportError:
HAS_CV2 = False
try:
from PIL import Image
HAS_PIL = True
except ImportError:
HAS_PIL = False
import base64
import streamlit.components.v1 as components_lib
import os as _os
# Custom webcam component (no WebRTC needed)
_WEBCAM_DIR = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "webcam_component")
_webcam_component_func = None
if _os.path.isdir(_WEBCAM_DIR):
_webcam_component_func = components_lib.declare_component("webcam_capture", path=_WEBCAM_DIR)
def webcam_capture(key="webcam"):
"""Custom webcam component β captures video frames + audio, sends to Python."""
if _webcam_component_func is None:
st.error("Webcam component not found.")
return None
return _webcam_component_func(key=key, default=None)
from models import EmotionLabel, EMOTION_LABELS, CulturalRegion, EMOTION_EMOJI
from face_detector import FaceEmotionDetector
from text_detector import TextEmotionDetector
from posture_detector import PostureEmotionDetector
from auth import (
validate_email,
is_email_used,
mark_email_used,
generate_code,
send_code_email,
smtp_configured,
get_remaining_seconds,
is_trial_expired,
TRIAL_DURATION_SECONDS,
VIDEO_MAX_DURATION_SECONDS,
CAMERA_MAX_DURATION_SECONDS,
)
# =====================================================================
# Page Config
# =====================================================================
st.set_page_config(
page_title="EmoSphere",
page_icon="\U0001f300",
layout="wide",
initial_sidebar_state="collapsed",
)
# =====================================================================
# Brand Constants
# =====================================================================
EMOTION_COLORS = {
EmotionLabel.JOY: "#FFD700",
EmotionLabel.SADNESS: "#4A90D9",
EmotionLabel.SURPRISE: "#FF8C00",
EmotionLabel.FEAR: "#8B5CF6",
EmotionLabel.DISGUST: "#10B981",
EmotionLabel.NEUTRAL: "#94A3B8",
EmotionLabel.LOVE: "#F472B6",
EmotionLabel.CALM: "#67E8F9",
}
CULTURAL_OPTIONS = {
"universal": "Universal",
"western": "Western",
"east_asian": "East Asian",
"south_asian": "South Asian",
"middle_eastern": "Middle Eastern",
"african": "African",
"latin_american": "Latin American",
}
# =====================================================================
# Global CSS
# =====================================================================
GLOBAL_CSS = """
<style>
.stApp {
background: linear-gradient(180deg, #0A0A1A 0%, #0D0D2B 50%, #0F0F2E 100%);
color: #B0BCD0;
}
section[data-testid="stSidebar"] {
background: #0F0F2E;
border-right: 1px solid rgba(255,255,255,0.08);
}
h1 {
background: linear-gradient(90deg, #E948A0, #9B6FCE, #00D4FF);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 800;
letter-spacing: 2px;
}
h2, h3 { color: #00D4FF !important; }
.glass-card {
background: linear-gradient(135deg, rgba(22,22,64,0.8), rgba(30,30,82,0.6));
border: 1px solid rgba(255,255,255,0.1);
border-radius: 16px;
padding: 20px;
margin: 10px 0;
backdrop-filter: blur(10px);
}
.emotion-bar {
background: rgba(255,255,255,0.05);
border-radius: 8px;
height: 28px;
margin: 4px 0;
overflow: hidden;
position: relative;
}
.emotion-fill {
height: 100%;
border-radius: 8px;
display: flex;
align-items: center;
padding-left: 10px;
font-weight: 600;
font-size: 13px;
color: white;
transition: width 0.5s ease;
}
div[data-testid="stMetric"] {
background: rgba(22,22,64,0.6);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 12px;
padding: 12px;
}
div[data-testid="stMetric"] label { color: #6B7B9D !important; }
div[data-testid="stMetric"] div[data-testid="stMetricValue"] { color: #00D4FF !important; }
.stButton > button,
.stFormSubmitButton > button,
button[kind="primary"],
button[kind="primaryFormSubmit"],
.stButton > button[kind="primary"],
.stFormSubmitButton > button[kind="primaryFormSubmit"],
div[data-testid="stFormSubmitButton"] > button,
div[data-testid="stButton"] > button {
background: linear-gradient(90deg, #9B6FCE, #6C63FF) !important;
background-color: #9B6FCE !important;
color: white !important;
border: none !important;
border-radius: 12px !important;
font-weight: 700 !important;
letter-spacing: 0.5px !important;
padding: 0.5rem 2rem !important;
}
.stButton > button:hover,
.stFormSubmitButton > button:hover,
button[kind="primary"]:hover,
button[kind="primaryFormSubmit"]:hover,
div[data-testid="stFormSubmitButton"] > button:hover,
div[data-testid="stButton"] > button:hover {
background: linear-gradient(90deg, #7C3AED, #5B4FCE) !important;
background-color: #7C3AED !important;
}
.stTabs [data-baseweb="tab"] { color: #6B7B9D; font-weight: 600; }
.stTabs [aria-selected="true"] {
color: #00D4FF !important;
border-bottom-color: #00D4FF !important;
}
.stTextArea textarea, .stTextInput input {
background: rgba(22,22,64,0.6) !important;
border: 1px solid rgba(255,255,255,0.1) !important;
color: white !important;
border-radius: 12px !important;
}
.stSelectbox > div > div {
background: rgba(22,22,64,0.6) !important;
border: 1px solid rgba(255,255,255,0.1) !important;
color: white !important;
}
#MainMenu { visibility: hidden; }
footer { visibility: hidden; }
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
100% { transform: translateY(0px); }
}
.floating-logo {
animation: float 6s ease-in-out infinite;
display: block;
margin: 0 auto;
}
.auth-container {
max-width: 500px;
margin: 60px auto;
text-align: center;
}
.countdown-bar {
position: fixed;
top: 0; left: 0; right: 0;
height: 4px;
background: rgba(255,255,255,0.1);
z-index: 9999;
}
.countdown-fill {
height: 100%;
background: linear-gradient(90deg, #00D4FF, #E948A0);
transition: width 1s linear;
}
.countdown-text {
position: fixed;
top: 8px;
right: 16px;
font-size: 13px;
font-weight: 700;
color: #00D4FF;
z-index: 9999;
background: rgba(10,10,26,0.8);
padding: 4px 12px;
border-radius: 8px;
border: 1px solid rgba(0,212,255,0.3);
}
.trial-ended {
max-width: 560px;
margin: 80px auto;
text-align: center;
}
</style>
<script>
// Auto-focus iframe so clicks work immediately
window.focus();
document.addEventListener('DOMContentLoaded', function() { window.focus(); });
// Also focus on any mouse movement over the page
document.addEventListener('mouseover', function() { window.focus(); }, {once: true});
</script>
"""
st.markdown(GLOBAL_CSS, unsafe_allow_html=True)
# =====================================================================
# Session State Initialization
# =====================================================================
# Secret bypass: ?bypass=cait2026 skips auth
_qp = st.query_params
if "auth_stage" not in st.session_state:
if _qp.get("bypass") == "cait2026":
st.session_state.auth_stage = "demo"
st.session_state.demo_start_time = time.time() + 99999
else:
st.session_state.auth_stage = "email" # email | code | demo | ended
if "auth_email" not in st.session_state:
st.session_state.auth_email = ""
if "auth_code" not in st.session_state:
st.session_state.auth_code = ""
if "auth_code_shown" not in st.session_state:
st.session_state.auth_code_shown = False
if "auth_email_pending" not in st.session_state:
st.session_state.auth_email_pending = False
if "demo_start_time" not in st.session_state:
st.session_state.demo_start_time = None
if "history" not in st.session_state:
st.session_state.history = deque(maxlen=50)
if "show_report" not in st.session_state:
st.session_state.show_report = False
if "video_processing" not in st.session_state:
st.session_state.video_processing = False
# ββ Background email send (runs after rerun so UI stays responsive) ββ
if st.session_state.get("auth_email_pending") and st.session_state.auth_stage == "code":
st.session_state.auth_email_pending = False
_email = st.session_state.auth_email
_code = st.session_state.auth_code
print(f"[Auth] Attempting to send code to {_email}...")
try:
sent, _msg = send_code_email(_email, _code)
print(f"[Auth] send_code_email returned: sent={sent}, msg={_msg}")
if sent:
st.session_state.auth_email_sent = True
else:
st.session_state.auth_email_error = _msg
except Exception as _exc:
print(f"[Auth] send_code_email exception: {_exc}")
st.session_state.auth_email_error = str(_exc)
# =====================================================================
# Model Loader (cached)
# =====================================================================
@st.cache_resource
def load_face_detector():
det = FaceEmotionDetector()
det.load()
return det
@st.cache_resource
def load_text_detector():
det = TextEmotionDetector()
det.load()
return det
@st.cache_resource
def load_posture_detector():
det = PostureEmotionDetector()
det.load()
return det
@st.cache_resource
def load_live_processor():
from live_processor import LiveSessionProcessor
proc = LiveSessionProcessor()
proc.initialize()
return proc
# =====================================================================
# Helper: Render Functions
# =====================================================================
def render_emotion_bars(scores):
sorted_emotions = sorted(scores.items(), key=lambda x: -x[1])
html_parts = []
for label, score in sorted_emotions:
color = EMOTION_COLORS.get(label, "#94A3B8")
emoji = EMOTION_EMOJI.get(label, "")
pct = max(score * 100, 2)
html_parts.append(
'<div class="emotion-bar">'
'<div class="emotion-fill" style="width: {}%; background: {};">'
'{} {} - {:.1f}%'
'</div></div>'.format(pct, color, emoji, label.value, score * 100)
)
st.markdown("".join(html_parts), unsafe_allow_html=True)
def render_emotion_bubble(label, score):
color = EMOTION_COLORS.get(label, "#94A3B8")
emoji = EMOTION_EMOJI.get(label, "")
st.markdown(
'<div style="text-align: center; padding: 20px;">'
'<div style="display: inline-flex; width: 140px; height: 140px; border-radius: 50%;'
' background: radial-gradient(circle, {}30, {}10);'
' border: 2px solid {}80; align-items: center; justify-content: center;'
' flex-direction: column; margin: 0 auto; box-shadow: 0 0 30px {}40;">'
'<span style="font-size: 48px; line-height: 1.2;">{}</span>'
'<span style="font-size: 22px; font-weight: 700; color: {};">{:.0f}%</span>'
'</div>'
'<div style="margin-top: 10px; font-size: 16px; font-weight: 700; color: {};'
' letter-spacing: 2px; text-transform: uppercase;">{}</div>'
'</div>'.format(
color, color, color, color, emoji, color, score * 100, color, label.value
),
unsafe_allow_html=True,
)
def render_countdown_bar(remaining):
pct = (remaining / TRIAL_DURATION_SECONDS) * 100
if remaining > 15:
color = "#00D4FF"
elif remaining > 5:
color = "#FF4444"
else:
color = "#FF0000"
mins = int(remaining) // 60
secs = int(remaining) % 60
st.markdown(
'<div class="countdown-bar">'
'<div class="countdown-fill" style="width: {}%; background: linear-gradient(90deg, {}, #E948A0);"></div>'
'</div>'
'<div class="countdown-text" style="color: {};">'
'Trial: {}:{:02d}'
'</div>'.format(pct, color, color, mins, secs),
unsafe_allow_html=True,
)
# =====================================================================
# AUTH GATE: Stage 1 -- Email Input
# =====================================================================
def _get_logo_b64():
"""Load logo as base64 for HTML embedding."""
import base64
logo_path = Path(__file__).parent / "logo.png"
if logo_path.exists():
return base64.b64encode(logo_path.read_bytes()).decode()
return None
def show_landing_page():
logo_b64 = _get_logo_b64()
if logo_b64:
st.markdown(
f'<div style="text-align:center; padding: 20px 0;">'
f'<img src="data:image/png;base64,{logo_b64}" class="floating-logo" '
f'style="width: 300px; height: 300px; object-fit: contain;"/>'
f'</div>',
unsafe_allow_html=True,
)
st.markdown(
'<p style="text-align:center; color:#6B7B9D; font-size:16px; max-width:500px; margin:0 auto 32px;">'
'Multimodal & cultural-aware emotion recognition — AI detection of emotions from face, text, posture and voice.'
'</p>',
unsafe_allow_html=True,
)
col_l, col_c, col_r = st.columns([1, 2, 1])
with col_c:
st.markdown(
'<div class="glass-card" style="text-align: center;">'
'<h3 style="margin-top: 0;">Request Demo Access</h3>'
'<p style="color: #6B7B9D; font-size: 13px;">'
'Enter your email to receive a one-time access code.<br/>'
'Each email grants a single 60-second demo session.'
'</p></div>',
unsafe_allow_html=True,
)
with st.form("email_form", clear_on_submit=False):
email = st.text_input(
"Email address",
placeholder="you@example.com",
key="email_input",
)
submitted = st.form_submit_button("Send Access Code", use_container_width=True, type="primary")
if submitted:
if not email or not email.strip():
st.error("Please enter an email address.")
elif not validate_email(email):
st.error("Please enter a valid email address.")
elif is_email_used(email):
st.error(
"This email has already been used for a demo session. "
"Contact info@caitcore.com for full access."
)
else:
code = generate_code()
st.session_state.auth_email = email.strip().lower()
st.session_state.auth_code = code
st.session_state.auth_stage = "code"
st.session_state.auth_email_pending = True
st.rerun()
st.markdown(
'<div style="text-align: center; margin-top: 40px; color: #6B7B9D; font-size: 12px;">'
'<p>EmoSphere v1.0 by '
'<a href="mailto:info@caitcore.com" style="color: #00D4FF;">CAIT</a></p>'
'<p>No surveillance • No medical screening • No data storage</p>'
'</div>',
unsafe_allow_html=True,
)
# =====================================================================
# AUTH GATE: Stage 2 -- Code Verification
# =====================================================================
def show_code_verification():
logo_b64 = _get_logo_b64()
if logo_b64:
st.markdown(
f'<div style="text-align:center; padding: 10px 0;">'
f'<img src="data:image/png;base64,{logo_b64}" class="floating-logo" '
f'style="width: 200px; height: 200px; object-fit: contain;"/>'
f'</div>',
unsafe_allow_html=True,
)
col_l, col_c, col_r = st.columns([1, 2, 1])
with col_c:
st.markdown(
'<div class="glass-card" style="text-align: center;">'
'<h3 style="margin-top: 0;">Enter Access Code</h3>'
'<p style="color: #6B7B9D; font-size: 13px;">'
'A 6-digit code was sent to '
'<strong style="color: #00D4FF;">{}</strong>'
'</p></div>'.format(st.session_state.auth_email),
unsafe_allow_html=True,
)
with st.form("code_form", clear_on_submit=False):
code_input = st.text_input(
"6-digit code",
placeholder="123456",
max_chars=6,
key="code_input",
)
verified = st.form_submit_button("Verify & Start Demo", use_container_width=True, type="primary")
if verified:
if code_input.strip() == st.session_state.auth_code:
mark_email_used(st.session_state.auth_email)
st.session_state.demo_start_time = time.time()
st.session_state.auth_stage = "demo"
st.rerun()
else:
st.error("Invalid code. Please try again.")
if st.button("Back", use_container_width=True):
st.session_state.auth_stage = "email"
st.session_state.auth_code = ""
st.rerun()
# =====================================================================
# AUTH GATE: Stage 4 -- Trial Ended
# =====================================================================
def show_trial_ended():
st.markdown(
'<div class="trial-ended">'
'<div style="font-size: 64px; margin-bottom: 16px;">🌀</div>'
'</div>',
unsafe_allow_html=True,
)
st.markdown(
'<div style="text-align:center;">'
'<img src="https://caitcore.com/images/emosphere-logo.png" style="width:120px; height:120px; border-radius:16px; margin-bottom:16px;" />'
'</div>',
unsafe_allow_html=True,
)
col_l, col_c, col_r = st.columns([1, 2, 1])
with col_c:
st.markdown(
'<div class="glass-card" style="text-align: center; padding: 40px;">'
'<div style="font-size: 48px; margin-bottom: 16px;">⏰</div>'
'<h2 style="margin-top: 0;">Trial Complete</h2>'
'<p style="color: #B0BCD0; font-size: 15px; margin: 16px 0;">'
'Your 60-second demo session has ended.<br/>'
'Thank you for trying EmoSphere.'
'</p>'
'<div style="margin: 24px 0; padding: 20px; background: rgba(0,212,255,0.08);'
' border-radius: 12px; border: 1px solid rgba(0,212,255,0.2);">'
'<p style="color: #00D4FF; font-weight: 700; font-size: 16px; margin: 0 0 8px;">'
'Want full access?'
'</p>'
'<p style="color: #B0BCD0; font-size: 14px; margin: 0;">'
'Contact us for enterprise licensing, API access, and custom integrations.'
'</p>'
'<a href="mailto:info@caitcore.com"'
' style="display: inline-block; margin-top: 16px; padding: 10px 28px;'
' background: linear-gradient(90deg, #E948A0, #9B6FCE);'
' color: white; text-decoration: none; border-radius: 12px;'
' font-weight: 700; font-size: 14px;">'
'Contact info@caitcore.com'
'</a>'
'</div>'
'<p style="color: #6B7B9D; font-size: 12px; margin-top: 20px;">'
'EmoSphere v1.0 • Multimodal Emotion AI for Psychotherapy'
'</p>'
'</div>',
unsafe_allow_html=True,
)
# =====================================================================
# AUTH GATE: Stage 3 -- Demo (time-limited)
# =====================================================================
def show_demo():
"""Show the demo, guarded by a 60-second timer."""
start = st.session_state.demo_start_time
# Allow report viewing even after trial expires
has_report = st.session_state.get("show_report", False)
if start is None:
st.session_state.auth_stage = "ended"
st.rerun()
return
if is_trial_expired(start) and not has_report:
st.session_state.auth_stage = "ended"
st.rerun()
return
remaining = get_remaining_seconds(start)
if not has_report:
render_countdown_bar(remaining)
# Load the live processor
processor = load_live_processor()
with st.sidebar:
st.markdown("### EmoSphere Demo")
mins = int(remaining) // 60
secs = int(remaining) % 60
st.markdown("**Time remaining:** {}:{:02d}".format(mins, secs))
st.divider()
st.markdown("### How it works")
st.markdown(
"1. Click **START** on the video stream\n"
"2. All 4 modalities analyzed in real-time\n"
"3. Fused with **Mamdani fuzzy logic**\n"
"4. Click **Stop & View Report** when done"
)
st.divider()
st.markdown("### Modalities")
st.markdown(
"π§ **Face** β ViT expression \n"
"π **Voice** β Wav2Vec2 prosody \n"
"π¬ **Speech** β DistilRoBERTa NLP \n"
"π§ **Posture/Gesture** β MediaPipe pose + hands"
)
# Header
st.markdown(
'<div style="text-align:center;">'
'<img src="https://caitcore.com/images/emosphere-logo.png" '
'style="width:80px; height:80px; border-radius:12px; margin-bottom:8px;" />'
'<h2 style="margin:0;">EmoSphere β Live Emotion Analysis</h2>'
'<p style="color:#6B7B9D; font-size:14px; margin-top:4px;">'
'Multimodal AI emotion detection with fuzzy fusion '
'β face, voice, speech & posture</p>'
'</div>',
unsafe_allow_html=True,
)
# Show report if ready
if st.session_state.get("show_report"):
from session_report import render_session_report
render_session_report(processor)
if st.button("Done β End Trial", use_container_width=True, type="primary"):
st.session_state.show_report = False
st.session_state.auth_stage = "ended"
st.rerun()
return
# Show video processing screen
if st.session_state.get("video_processing"):
_show_video_processing(processor, start)
return
# ββ Primary: Live Streaming ββββββββββββββββββββββββββββββββββββββ
_show_live_session(processor, remaining, start)
# ββ Secondary: Video Upload ββββββββββββββββββββββββββββββββββββββ
st.divider()
st.markdown(
'<div style="text-align:center;">'
'<h3 style="margin: 0;">Or Upload a Video</h3>'
'<p style="color: #6B7B9D; font-size: 13px;">Upload a short video (MP4, max 60s) for full multimodal analysis.</p>'
'</div>',
unsafe_allow_html=True,
)
uploaded_video = st.file_uploader(
"Choose video file",
type=["mp4", "webm", "avi", "mov", "mkv"],
key="video_upload",
label_visibility="collapsed",
)
if uploaded_video is not None:
st.video(uploaded_video)
if st.button("π Analyze Video", type="primary", use_container_width=True):
video_bytes = uploaded_video.read()
st.session_state.video_bytes = video_bytes
st.session_state.video_processing = True
st.rerun()
# Check trial expiry (but allow report viewing)
if is_trial_expired(start) and not st.session_state.get("show_report", False):
if processor.is_active:
processor.stop_session()
# Auto-generate report instead of going to "ended"
st.session_state.show_report = True
st.rerun()
else:
st.session_state.auth_stage = "ended"
st.rerun()
def _show_live_session(processor, remaining, start):
"""Live session using custom webcam component (no WebRTC needed)."""
# Video + Results side by side
col_video, col_results = st.columns([1.6, 1])
with col_video:
# Custom webcam component with built-in START/STOP + timer
component_value = webcam_capture(key="webcam_live")
# Handle component messages
if component_value and isinstance(component_value, dict):
msg_type = component_value.get("type")
if msg_type == "started":
if not processor.is_active:
processor.start_session()
st.session_state["session_started"] = True
elif msg_type == "frame":
if not processor.is_active:
processor.start_session()
st.session_state["session_started"] = True
# Decode base64 JPEG and process
data_url = component_value.get("data", "")
if "," in data_url:
try:
img_b64 = data_url.split(",", 1)[1]
img_bytes = base64.b64decode(img_b64)
processor.process_image(img_bytes)
except Exception as e:
print(f"[App] Frame decode error: {e}")
elif msg_type == "audio":
data_url = component_value.get("data", "")
if "," in data_url:
try:
audio_b64 = data_url.split(",", 1)[1]
audio_bytes = base64.b64decode(audio_b64)
whisper_lang = st.session_state.get("whisper_language", None)
processor.process_audio_bytes(audio_bytes, language=whisper_lang)
except Exception as e:
print(f"[App] Audio decode error: {e}")
elif msg_type == "stopped":
if processor.is_active:
processor.stop_session()
st.session_state["session_started"] = False
st.session_state.show_report = True
st.rerun()
elif msg_type == "error":
st.error("Camera/mic error: " + component_value.get("message", "unknown"))
with col_results:
if processor.is_active:
_render_live_results(processor)
else:
st.markdown(
'<div class="glass-card" style="text-align: center; padding: 40px;">'
'<span style="font-size: 48px;">🎥</span>'
'<h3 style="margin: 12px 0 8px; color: #B0BCD0 !important;">Ready to Stream</h3>'
'<p style="color: #6B7B9D; margin: 0; font-size: 13px;">'
'Click <strong>START SESSION</strong> on the left to begin '
'your 60-second live emotion analysis.</p>'
'<div style="margin-top: 16px; padding: 12px; background: rgba(0,212,255,0.06); '
'border-radius: 8px; border: 1px solid rgba(0,212,255,0.15);">'
'<p style="color: #00D4FF; font-size: 12px; margin: 0;">'
'🧑 Face • 🎙 Voice • 💬 Speech • 🧍 Posture/Gesture<br/>'
'All fused with fuzzy logic in real-time.</p>'
'</div>'
'</div>',
unsafe_allow_html=True,
)
# Language selector under Ready to Stream
st.markdown(
'<p style="font-weight:600; color:#B0BCD0; margin-top:16px; margin-bottom:4px; font-size:14px;">'
'Select your speech language:</p>',
unsafe_allow_html=True,
)
lang_options = {
"English": "en",
"Greek": "el",
"Spanish": "es",
"French": "fr",
"German": "de",
"Italian": "it",
"Portuguese": "pt",
"Dutch": "nl",
"Russian": "ru",
"Chinese": "zh",
"Japanese": "ja",
"Korean": "ko",
"Arabic": "ar",
"Turkish": "tr",
"Polish": "pl",
"Swedish": "sv",
"Romanian": "ro",
"Bulgarian": "bg",
"Serbian": "sr",
"Croatian": "hr",
}
selected_lang = st.selectbox(
"Speech language",
options=list(lang_options.keys()),
index=0,
key="speech_language",
label_visibility="collapsed",
)
st.session_state["whisper_language"] = lang_options[selected_lang]
# Stop button β below the columns, always visible during session
if processor.is_active or st.session_state.get("session_started", False):
st.markdown("") # spacer
if st.button("βΉ Stop Session & View Report", type="primary", use_container_width=True, key="stop_session_btn"):
if processor.is_active:
processor.stop_session()
st.session_state["session_started"] = False
st.session_state.show_report = True
st.rerun()
@st.fragment(run_every=2.0)
def _render_live_results(processor):
"""Auto-updating display of live emotion results. Refreshes every 2s."""
fused = processor.get_latest_fused()
face = processor.get_latest_face()
voice = processor.get_latest_voice()
text = processor.get_latest_text()
posture = processor.get_latest_posture()
stats = processor.get_stats()
transcript = processor.get_transcript()
topics = processor.get_topics()
if fused is None:
st.markdown(
'<div class="glass-card" style="text-align: center; padding: 20px;">'
'<span style="font-size: 36px;">🔮</span>'
'<p style="color: #6B7B9D; margin-top: 8px;">'
'Analyzing... Speak, move, or express yourself.</p>'
'</div>',
unsafe_allow_html=True,
)
return
# Dominant emotion bubble
render_emotion_bubble(fused.dominant, fused.dominant_score)
# Fused emotion bars
fused_scores = {s.label: s.score for s in fused.scores}
render_emotion_bars(fused_scores)
# Modality signals
st.markdown("#### Modality Signals")
mod_data = [
("🧑 Face", face),
("🎙 Voice", voice),
("💬 Speech", text),
("🧍 Posture", posture),
]
mod_colors = ["#E948A0", "#FFD700", "#00D4FF", "#10B981"]
for (mod_label, mod_result), color in zip(mod_data, mod_colors):
if mod_result is not None:
dom = mod_result.dominant
emoji = EMOTION_EMOJI.get(dom, "")
conf = mod_result.confidence * 100
st.markdown(
'<div style="display: flex; align-items: center; margin: 4px 0; font-size: 13px;">'
'<span style="width: 100px; flex-shrink: 0;">{}</span>'
'<span style="font-size: 16px; margin-right: 6px;">{}</span>'
'<span style="color: {}; font-weight: 600; width: 70px;">{}</span>'
'<div style="flex: 1; background: rgba(255,255,255,0.05); border-radius: 4px; height: 8px; overflow: hidden;">'
'<div style="width: {:.0f}%; height: 100%; background: {}; border-radius: 4px;"></div>'
'</div>'
'<span style="color: #6B7B9D; margin-left: 8px; font-size: 11px;">{:.0f}%</span>'
'</div>'.format(mod_label, emoji, color, dom.value, conf, color, conf),
unsafe_allow_html=True,
)
else:
st.markdown(
'<div style="display: flex; align-items: center; margin: 4px 0; font-size: 13px;">'
'<span style="width: 100px; flex-shrink: 0;">{}</span>'
'<span style="color: #6B7B9D; font-style: italic;">waiting...</span>'
'</div>'.format(mod_label),
unsafe_allow_html=True,
)
# Live transcript
if transcript:
st.markdown("#### Live Transcript")
recent = transcript[-5:]
html_parts = ['<div class="glass-card" style="max-height: 180px; overflow-y: auto; padding: 10px;">']
for seg in recent:
emoji = EMOTION_EMOJI.get(seg.emotion, "") if seg.emotion else ""
mins = int(seg.timestamp) // 60
secs = int(seg.timestamp) % 60
html_parts.append(
'<div style="padding: 4px 0; border-bottom: 1px solid rgba(255,255,255,0.05); font-size: 13px;">'
'<span style="color: #6B7B9D;">{}:{:02d}</span> '
'<span>{}</span> '
'<span style="color: #B0BCD0;">{}</span>'
'</div>'.format(mins, secs, emoji, seg.text)
)
html_parts.append('</div>')
st.markdown("".join(html_parts), unsafe_allow_html=True)
# Topics
if topics:
topic_html = " ".join(
'<span style="display: inline-block; background: rgba(0,212,255,0.12); '
'border: 1px solid rgba(0,212,255,0.25); border-radius: 16px; '
'padding: 2px 10px; margin: 2px; font-size: 11px; color: #00D4FF;">{}</span>'.format(
t.replace("_", " ").title()
)
for t in topics
)
st.markdown(
'<div style="margin-top: 8px;">'
'<span style="color: #6B7B9D; font-size: 12px; font-weight: 600;">Topics: </span>'
'{}</div>'.format(topic_html),
unsafe_allow_html=True,
)
# Stats
st.markdown(
'<div style="color: #6B7B9D; font-size: 11px; margin-top: 8px; text-align: right;">'
'Frames: {} • Audio: {} • Transcript: {}'
'</div>'.format(
stats.get("video_frames", 0),
stats.get("audio_chunks", 0),
stats.get("transcript_segments", 0),
),
unsafe_allow_html=True,
)
def _show_video_processing(processor, start):
"""Process an uploaded video and show results."""
video_bytes = st.session_state.get("video_bytes")
if not video_bytes:
st.session_state.video_processing = False
st.rerun()
return
st.markdown(
'<div class="glass-card" style="text-align: center; padding: 24px;">'
'<span style="font-size: 42px;">βοΈ</span>'
'<h3 style="margin: 8px 0;">Analyzing Video...</h3>'
'<p style="color: #6B7B9D; font-size: 13px;">'
'Processing all 4 modalities (face, voice, speech, posture) with fuzzy fusion.</p>'
'</div>',
unsafe_allow_html=True,
)
progress_bar = st.progress(0, text="Initializing...")
def update_progress(pct):
progress_bar.progress(min(int(pct * 100), 100), text=f"Processing... {int(pct * 100)}%")
summary = processor.process_video_file(video_bytes, progress_callback=update_progress)
progress_bar.progress(100, text="Complete!")
# Clean up
st.session_state.video_processing = False
st.session_state.pop("video_bytes", None)
if summary is not None:
st.session_state.show_report = True
st.rerun()
else:
st.error("Video processing failed. Please try a different video format (MP4 recommended).")
if st.button("Back", use_container_width=True):
st.rerun()
def _schedule_rerun(remaining):
"""Schedule automatic page reload to update countdown and enforce expiry."""
if remaining <= 0:
return
interval = min(5.0, remaining)
try:
import streamlit.components.v1 as components
js_code = (
'<script>'
'setTimeout(function() { window.location.reload(); }, '
+ str(int(interval * 1000))
+ ');</script>'
)
components.html(js_code, height=0)
except Exception:
pass
# =====================================================================
# Main Router
# =====================================================================
def main():
stage = st.session_state.auth_stage
if stage == "demo" and st.session_state.demo_start_time:
if is_trial_expired(st.session_state.demo_start_time):
st.session_state.auth_stage = "ended"
stage = "ended"
if stage == "email":
show_landing_page()
elif stage == "code":
show_code_verification()
elif stage == "demo":
show_demo()
elif stage == "ended":
show_trial_ended()
else:
show_landing_page()
if __name__ == "__main__":
main()
|