Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,10 +1,8 @@
|
|
| 1 |
"""
|
| 2 |
🎙️ AI Interview Agent — Reem
|
| 3 |
Bilingual (English / Arabic) first-round interview assistant for HR.
|
| 4 |
-
|
| 5 |
Every turn (questions and answers) is written to SQLite as it happens, so the
|
| 6 |
full transcript is available to HR afterwards for assessment.
|
| 7 |
-
|
| 8 |
Secrets / environment:
|
| 9 |
GROQ_API_KEY required (speech-to-text + LLM)
|
| 10 |
HR_PASSWORD required to open the HR portal (portal stays locked if unset)
|
|
@@ -94,6 +92,8 @@ NOTICE = {
|
|
| 94 |
"Please do not share sensitive personal information (for example health details, religion, "
|
| 95 |
"or ID numbers) in your answers.",
|
| 96 |
"You may stop at any time. Contact HR to ask for access to, or deletion of, your data.",
|
|
|
|
|
|
|
| 97 |
], "I have read the above and agree to take this interview."),
|
| 98 |
"ar": ("قبل أن نبدأ", [
|
| 99 |
"تتم هذه المقابلة بواسطة مساعد ذكاء اصطناعي وليس بواسطة موظف.",
|
|
@@ -104,6 +104,7 @@ NOTICE = {
|
|
| 104 |
"القرارات الوظيفية يتخذها أشخاص وليس الذكاء الاصطناعي.",
|
| 105 |
"يرجى عدم ذكر معلومات شخصية حساسة (مثل التفاصيل الصحية أو الدين أو أرقام الهوية) في إجاباتكم.",
|
| 106 |
"يمكنكم التوقف في أي وقت، ويمكنكم التواصل مع الموارد البشرية لطلب الاطلاع على بياناتكم أو حذفها.",
|
|
|
|
| 107 |
], "قرأتُ ما سبق وأوافق على إجراء هذه المقابلة."),
|
| 108 |
}
|
| 109 |
|
|
@@ -536,13 +537,16 @@ function stop(){c.classList.remove('speaking');r.classList.remove('active');s.te
|
|
| 536 |
a.addEventListener('ended',stop);a.addEventListener('pause',stop);
|
| 537 |
function p(){var x=a.play();if(x&&x.catch)x.catch(function(){t.style.display='inline-block';s.textContent='Tap to hear';});}
|
| 538 |
t.addEventListener('click',function(){a.play().catch(function(){});});
|
| 539 |
-
if(
|
|
|
|
| 540 |
|
| 541 |
|
| 542 |
-
def render_avatar(img_b64: str, name: str, audio_b64: str = "") -> str:
|
|
|
|
| 543 |
img = (f'<img src="data:image/jpeg;base64,{img_b64}" alt="{html.escape(name)}">'
|
| 544 |
if img_b64 else '<div class="ph">👩💼</div>')
|
| 545 |
-
script = AUDIO_JS.replace("__AUDIO__", audio_b64)
|
|
|
|
| 546 |
return (AVATAR_TEMPLATE.replace("__IMG__", img)
|
| 547 |
.replace("__NAME__", html.escape(name)).replace("__SCRIPT__", script))
|
| 548 |
|
|
@@ -578,6 +582,7 @@ def begin_interview(inv, pos):
|
|
| 578 |
"questions": questions, "q_index": 0, "followups_used": 0, "done": False,
|
| 579 |
"stage": "ready"}
|
| 580 |
ss.pending_audio = tts_b64(greeting, lang)
|
|
|
|
| 581 |
st.rerun()
|
| 582 |
|
| 583 |
|
|
@@ -592,6 +597,7 @@ def resume_interview(inv, pos, row):
|
|
| 592 |
"questions": questions, "q_index": min(row["q_index"], len(questions) - 1),
|
| 593 |
"followups_used": row["followups_used"], "done": False, "stage": stage}
|
| 594 |
ss.pending_audio = tts_b64(last[-1]["content"], lang) if last else ""
|
|
|
|
| 595 |
st.rerun()
|
| 596 |
|
| 597 |
|
|
@@ -723,9 +729,10 @@ def interview_screen():
|
|
| 723 |
|
| 724 |
with col1:
|
| 725 |
audio = ss.get("pending_audio", "")
|
|
|
|
| 726 |
if "_photo" not in ss:
|
| 727 |
ss["_photo"] = load_b64(PHOTO_PATH)
|
| 728 |
-
st.components.v1.html(render_avatar(ss["_photo"], AGENT_NAME[lang], audio), height=480)
|
| 729 |
ss.pending_audio = ""
|
| 730 |
|
| 731 |
with col2:
|
|
|
|
| 1 |
"""
|
| 2 |
🎙️ AI Interview Agent — Reem
|
| 3 |
Bilingual (English / Arabic) first-round interview assistant for HR.
|
|
|
|
| 4 |
Every turn (questions and answers) is written to SQLite as it happens, so the
|
| 5 |
full transcript is available to HR afterwards for assessment.
|
|
|
|
| 6 |
Secrets / environment:
|
| 7 |
GROQ_API_KEY required (speech-to-text + LLM)
|
| 8 |
HR_PASSWORD required to open the HR portal (portal stays locked if unset)
|
|
|
|
| 92 |
"Please do not share sensitive personal information (for example health details, religion, "
|
| 93 |
"or ID numbers) in your answers.",
|
| 94 |
"You may stop at any time. Contact HR to ask for access to, or deletion of, your data.",
|
| 95 |
+
"Please turn your volume on. After you start, tap the 🔊 button next to the avatar once "
|
| 96 |
+
"to hear the first message.",
|
| 97 |
], "I have read the above and agree to take this interview."),
|
| 98 |
"ar": ("قبل أن نبدأ", [
|
| 99 |
"تتم هذه المقابلة بواسطة مساعد ذكاء اصطناعي وليس بواسطة موظف.",
|
|
|
|
| 104 |
"القرارات الوظيفية يتخذها أشخاص وليس الذكاء الاصطناعي.",
|
| 105 |
"يرجى عدم ذكر معلومات شخصية حساسة (مثل التفاصيل الصحية أو الدين أو أرقام الهوية) في إجاباتكم.",
|
| 106 |
"يمكنكم التوقف في أي وقت، ويمكنكم التواصل مع الموارد البشرية لطلب الاطلاع على بياناتكم أو حذفها.",
|
| 107 |
+
"يرجى تشغيل الصوت. بعد البدء، اضغطوا على زر 🔊 بجانب الصورة مرة واحدة لسماع الرسالة الأولى.",
|
| 108 |
], "قرأتُ ما سبق وأوافق على إجراء هذه المقابلة."),
|
| 109 |
}
|
| 110 |
|
|
|
|
| 537 |
a.addEventListener('ended',stop);a.addEventListener('pause',stop);
|
| 538 |
function p(){var x=a.play();if(x&&x.catch)x.catch(function(){t.style.display='inline-block';s.textContent='Tap to hear';});}
|
| 539 |
t.addEventListener('click',function(){a.play().catch(function(){});});
|
| 540 |
+
if(__MANUAL__){t.style.display='inline-block';s.textContent='Tap to hear';}
|
| 541 |
+
else if(document.readyState==='complete')p();else window.addEventListener('load',p);})();"""
|
| 542 |
|
| 543 |
|
| 544 |
+
def render_avatar(img_b64: str, name: str, audio_b64: str = "", manual: bool = False) -> str:
|
| 545 |
+
"""manual=True skips autoplay and shows the tap button (browsers block the first autoplay)."""
|
| 546 |
img = (f'<img src="data:image/jpeg;base64,{img_b64}" alt="{html.escape(name)}">'
|
| 547 |
if img_b64 else '<div class="ph">👩💼</div>')
|
| 548 |
+
script = (AUDIO_JS.replace("__AUDIO__", audio_b64)
|
| 549 |
+
.replace("__MANUAL__", "true" if manual else "false")) if audio_b64 else ""
|
| 550 |
return (AVATAR_TEMPLATE.replace("__IMG__", img)
|
| 551 |
.replace("__NAME__", html.escape(name)).replace("__SCRIPT__", script))
|
| 552 |
|
|
|
|
| 582 |
"questions": questions, "q_index": 0, "followups_used": 0, "done": False,
|
| 583 |
"stage": "ready"}
|
| 584 |
ss.pending_audio = tts_b64(greeting, lang)
|
| 585 |
+
ss.manual_audio = True # first audio of the session: browsers block autoplay, show tap button
|
| 586 |
st.rerun()
|
| 587 |
|
| 588 |
|
|
|
|
| 597 |
"questions": questions, "q_index": min(row["q_index"], len(questions) - 1),
|
| 598 |
"followups_used": row["followups_used"], "done": False, "stage": stage}
|
| 599 |
ss.pending_audio = tts_b64(last[-1]["content"], lang) if last else ""
|
| 600 |
+
ss.manual_audio = True # fresh page load: no prior user gesture, autoplay will be blocked
|
| 601 |
st.rerun()
|
| 602 |
|
| 603 |
|
|
|
|
| 729 |
|
| 730 |
with col1:
|
| 731 |
audio = ss.get("pending_audio", "")
|
| 732 |
+
manual = ss.pop("manual_audio", False)
|
| 733 |
if "_photo" not in ss:
|
| 734 |
ss["_photo"] = load_b64(PHOTO_PATH)
|
| 735 |
+
st.components.v1.html(render_avatar(ss["_photo"], AGENT_NAME[lang], audio, manual), height=480)
|
| 736 |
ss.pending_audio = ""
|
| 737 |
|
| 738 |
with col2:
|