Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| from huggingface_hub import InferenceClient | |
| import os | |
| import tempfile | |
| # ---------------------------------------------------------------------- | |
| # Configuration | |
| # ---------------------------------------------------------------------- | |
| HF_TOKEN = os.getenv("HF_TOKEN") | |
| if HF_TOKEN is None: | |
| print("โ ๏ธ HF_TOKEN not set. Some models may not be accessible. Please add your token as a secret.") | |
| MODEL_MAP = { | |
| "GPT-4o (Multilingual)": "mistralai/Mistral-7B-Instruct-v0.2", | |
| "Claude (Translation)": "mistralai/Mixtral-8x7B-Instruct-v0.1", | |
| "Gemini (Job Coach)": "google/gemma-2-9b-it", | |
| } | |
| DEFAULT_MODEL = "GPT-4o (Multilingual)" | |
| JOB_SEEKER_PROFILE = { | |
| "name": "John Tan", | |
| "native_language": "English", | |
| "target_job": "Software Engineer (Mandarinโrequired)", | |
| "current_skills": "Python, JavaScript, 3 years experience", | |
| "target_location": "Kuala Lumpur / Singapore", | |
| } | |
| QUICK_ACTIONS = { | |
| "en": [ | |
| "Translate 'I have 5 years of experience in marketing' to Mandarin", | |
| "Common interview questions for a software engineer (with Mandarin answers)", | |
| "How to write a cover letter in Mandarin?", | |
| "What are the key phrases for a Mandarin resume?", | |
| ], | |
| "ms": [ | |
| "Terjemahkan 'Saya ada 5 tahun pengalaman dalam pemasaran' ke Mandarin", | |
| "Soalan temuduga biasa untuk jurutera perisian (dengan jawapan Mandarin)", | |
| "Bagaimana menulis surat lamaran dalam Mandarin?", | |
| "Apakah frasa utama untuk resume Mandarin?", | |
| ] | |
| } | |
| SYSTEM_PROMPT_TEMPLATE = """You are a friendly assistant helping a nonโMandarin speaker apply for jobs that require Mandarin. | |
| User profile: | |
| - Name: {name} | |
| - Native language: {native_language} | |
| - Target job: {target_job} | |
| - Current skills: {current_skills} | |
| - Target location: {target_location} | |
| Your role: | |
| 1. Translate between the user's native language and Mandarin (simplified Chinese). | |
| 2. Help draft resumes, cover letters, and emails in Mandarin. | |
| 3. Provide common interview questions and sample answers in Mandarin. | |
| 4. Explain cultural nuances and workplace etiquette in Mandarinโspeaking companies. | |
| 5. Practice conversations: you can speak in Mandarin and ask the user to respond. | |
| Always respond in the same language as the user's question, except when translation is requested. | |
| When providing Mandarin text, include pinyin pronunciation in parentheses if helpful. | |
| Be encouraging and patient โ the user is learning.""" | |
| ACCENT_EMOJI = { | |
| "american": "๐บ๐ธ", "british": "๐ฌ๐ง", "australian": "๐ฆ๐บ", "canadian": "๐จ๐ฆ", | |
| "irish": "๐ฎ๐ช", "scottish": "๐ด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ", "indian": "๐ฎ๐ณ", "south-african": "๐ฟ๐ฆ", | |
| "new-zealand": "๐ณ๐ฟ", "spanish": "๐ช๐ธ", "french": "๐ซ๐ท", "german": "๐ฉ๐ช", | |
| "italian": "๐ฎ๐น", "portuguese": "๐ต๐น", "brazilian": "๐ง๐ท", "mexican": "๐ฒ๐ฝ", | |
| "argentinian": "๐ฆ๐ท", "japanese": "๐ฏ๐ต", "chinese": "๐จ๐ณ", "korean": "๐ฐ๐ท", | |
| "russian": "๐ท๐บ", "arabic": "๐ธ๐ฆ", "dutch": "๐ณ๐ฑ", "swedish": "๐ธ๐ช", | |
| "norwegian": "๐ณ๐ด", "danish": "๐ฉ๐ฐ", "finnish": "๐ซ๐ฎ", "polish": "๐ต๐ฑ", | |
| "turkish": "๐น๐ท", "greek": "๐ฌ๐ท", | |
| } | |
| GENDER_ICON = { | |
| "male": "โ๏ธ", "female": "โ๏ธ", "non-binary": "โง๏ธ", "other": "๐น" | |
| } | |
| # ---------------------------------------------------------------------- | |
| # Helper functions | |
| # ---------------------------------------------------------------------- | |
| def get_client(token=None): | |
| if token: | |
| return InferenceClient(token=token) | |
| if HF_TOKEN: | |
| return InferenceClient(token=HF_TOKEN) | |
| return InferenceClient() | |
| def transcribe_audio(audio_path, token=None): | |
| if audio_path is None: | |
| return "" | |
| client = get_client(token) | |
| try: | |
| result = client.automatic_speech_recognition(audio_path, model="openai/whisper-large-v3") | |
| return result["text"] | |
| except Exception as e: | |
| return f"[STT Error: {str(e)}]" | |
| def synthesize_speech(text, token=None, accent="chinese", gender="female", age=30): | |
| if not text: | |
| return None | |
| client = get_client(token) | |
| age_desc = f"{age} year old" if age else "" | |
| voice_desc = f"{age_desc} {gender} {accent} speaker".strip() | |
| prompt = f"[{voice_desc}] {text}" | |
| try: | |
| audio_bytes = client.text_to_speech(prompt, model="suno/bark") | |
| with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f: | |
| f.write(audio_bytes) | |
| return f.name | |
| except Exception as e: | |
| print(f"TTS error: {e}") | |
| return None | |
| def user_message(message, history): | |
| """Append user message to history and clear input.""" | |
| if not message.strip(): | |
| return history, "" | |
| history.append((message, None)) | |
| return history, "" | |
| def bot_response(history, model_name, profile, native_lang, token): | |
| """Stream assistant response and update history.""" | |
| if not history or history[-1][1] is not None: | |
| yield history | |
| return | |
| user_msg = history[-1][0] | |
| client = get_client(token) | |
| system_prompt = SYSTEM_PROMPT_TEMPLATE.format(**profile) | |
| messages = [{"role": "system", "content": system_prompt}] | |
| for u, a in history[:-1]: | |
| if u: | |
| messages.append({"role": "user", "content": u}) | |
| if a: | |
| messages.append({"role": "assistant", "content": a}) | |
| messages.append({"role": "user", "content": user_msg}) | |
| model_id = MODEL_MAP.get(model_name, MODEL_MAP[DEFAULT_MODEL]) | |
| try: | |
| stream = client.chat_completion( | |
| messages=messages, | |
| model=model_id, | |
| max_tokens=1024, | |
| stream=True, | |
| ) | |
| partial = "" | |
| for chunk in stream: | |
| if chunk.choices and chunk.choices[0].delta.content: | |
| partial += chunk.choices[0].delta.content | |
| new_history = history.copy() | |
| new_history[-1] = (user_msg, partial) | |
| yield new_history | |
| except Exception as e: | |
| error_msg = f"Error: {str(e)}" | |
| if "401" in str(e) or "Authorization" in str(e): | |
| error_msg = "โ ๏ธ Authentication failed. Please provide a valid Hugging Face token." | |
| new_history = history.copy() | |
| new_history[-1] = (user_msg, error_msg) | |
| yield new_history | |
| def voice_input(audio, history, token): | |
| """Transcribe audio and return updated history + transcript.""" | |
| if audio is None: | |
| return history, "", None | |
| transcript = transcribe_audio(audio, token) | |
| if transcript.startswith("[STT Error"): | |
| history.append((transcript, None)) | |
| return history, "", None | |
| return history, transcript, audio | |
| def quick_action_send(action, history, model, profile, lang, token): | |
| """Handle quick action button: append message and stream response.""" | |
| if not action.strip(): | |
| yield history | |
| return | |
| history.append((action, None)) | |
| yield history # show user message immediately | |
| # Then stream bot response | |
| client = get_client(token) | |
| system_prompt = SYSTEM_PROMPT_TEMPLATE.format(**profile) | |
| messages = [{"role": "system", "content": system_prompt}] | |
| for u, a in history[:-1]: | |
| if u: | |
| messages.append({"role": "user", "content": u}) | |
| if a: | |
| messages.append({"role": "assistant", "content": a}) | |
| messages.append({"role": "user", "content": action}) | |
| model_id = MODEL_MAP.get(model, MODEL_MAP[DEFAULT_MODEL]) | |
| try: | |
| stream = client.chat_completion( | |
| messages=messages, | |
| model=model_id, | |
| max_tokens=1024, | |
| stream=True, | |
| ) | |
| partial = "" | |
| for chunk in stream: | |
| if chunk.choices and chunk.choices[0].delta.content: | |
| partial += chunk.choices[0].delta.content | |
| new_history = history.copy() | |
| new_history[-1] = (action, partial) | |
| yield new_history | |
| except Exception as e: | |
| error_msg = f"Error: {str(e)}" | |
| if "401" in str(e) or "Authorization" in str(e): | |
| error_msg = "โ ๏ธ Authentication failed. Please provide a valid Hugging Face token." | |
| new_history = history.copy() | |
| new_history[-1] = (action, error_msg) | |
| yield new_history | |
| def update_profile(name, native_lang, target_job, skills, location): | |
| return { | |
| "name": name, | |
| "native_language": native_lang, | |
| "target_job": target_job, | |
| "current_skills": skills, | |
| "target_location": location, | |
| } | |
| def toggle_ui_language(lang): | |
| en_visible = lang == "en" | |
| ms_visible = lang == "ms" | |
| return ( | |
| gr.update(visible=en_visible), gr.update(visible=en_visible), | |
| gr.update(visible=en_visible), gr.update(visible=en_visible), | |
| gr.update(visible=ms_visible), gr.update(visible=ms_visible), | |
| gr.update(visible=ms_visible), gr.update(visible=ms_visible) | |
| ) | |
| def clear_chat(): | |
| return [], None | |
| def speak_last_response(history, token, accent, gender, age): | |
| if not history or not history[-1][1]: | |
| return None | |
| text = history[-1][1] | |
| return synthesize_speech(text, token, accent, gender, age) | |
| # ---------------------------------------------------------------------- | |
| # Gradio Interface | |
| # ---------------------------------------------------------------------- | |
| with gr.Blocks(title="Mandarin Job Application Assistant") as demo: | |
| gr.Markdown(""" | |
| # ๐ฃ๏ธ Mandarin Job Application Assistant | |
| **For nonโMandarin speakers applying to jobs that require Mandarin.** | |
| Use voice or text to get translations, interview practice, and resume help. | |
| """) | |
| # States | |
| chat_state = gr.State([]) | |
| profile_state = gr.State(JOB_SEEKER_PROFILE) | |
| token_state = gr.State(HF_TOKEN) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| # Token input (optional) | |
| with gr.Group(): | |
| gr.Markdown("### ๐ Hugging Face Token (optional)") | |
| token_input = gr.Textbox( | |
| label="HF Token", | |
| type="password", | |
| placeholder="Enter your token if you have one", | |
| value=HF_TOKEN if HF_TOKEN else "" | |
| ) | |
| token_btn = gr.Button("Set Token") | |
| # Profile | |
| with gr.Group(): | |
| gr.Markdown("### ๐ค Your Profile") | |
| name_input = gr.Textbox(label="Name", value=JOB_SEEKER_PROFILE["name"]) | |
| native_lang_input = gr.Dropdown( | |
| choices=["English", "Malay", "Tamil", "Other"], | |
| value=JOB_SEEKER_PROFILE["native_language"], | |
| label="Native Language" | |
| ) | |
| target_job_input = gr.Textbox(label="Target Job (Mandarinโrequired)", value=JOB_SEEKER_PROFILE["target_job"]) | |
| skills_input = gr.Textbox(label="Your Skills / Experience", value=JOB_SEEKER_PROFILE["current_skills"], lines=2) | |
| location_input = gr.Textbox(label="Target Location", value=JOB_SEEKER_PROFILE["target_location"]) | |
| update_profile_btn = gr.Button("Update Profile") | |
| # UI language | |
| ui_language = gr.Radio( | |
| choices=[("English", "en"), ("Bahasa Malaysia", "ms")], | |
| value="en", | |
| label="Interface Language", | |
| interactive=True | |
| ) | |
| # Model | |
| model = gr.Dropdown( | |
| choices=list(MODEL_MAP.keys()), | |
| value=DEFAULT_MODEL, | |
| label="AI Model", | |
| interactive=True | |
| ) | |
| # Voice Configuration | |
| gr.Markdown("### ๐๏ธ Voice Settings (for spoken responses)") | |
| with gr.Row(): | |
| accent = gr.Dropdown( | |
| choices=list(ACCENT_EMOJI.keys()), | |
| value="chinese", | |
| label="Accent" | |
| ) | |
| gender = gr.Radio( | |
| choices=["male", "female", "non-binary", "other"], | |
| value="female", | |
| label="Gender" | |
| ) | |
| age = gr.Slider(minimum=18, maximum=80, value=30, step=1, label="Age") | |
| # Quick actions | |
| gr.Markdown("### โก Quick Actions") | |
| with gr.Row(): | |
| btn_en1 = gr.Button(QUICK_ACTIONS["en"][0], visible=True) | |
| btn_en2 = gr.Button(QUICK_ACTIONS["en"][1], visible=True) | |
| with gr.Row(): | |
| btn_en3 = gr.Button(QUICK_ACTIONS["en"][2], visible=True) | |
| btn_en4 = gr.Button(QUICK_ACTIONS["en"][3], visible=True) | |
| btn_ms1 = gr.Button(QUICK_ACTIONS["ms"][0], visible=False) | |
| btn_ms2 = gr.Button(QUICK_ACTIONS["ms"][1], visible=False) | |
| btn_ms3 = gr.Button(QUICK_ACTIONS["ms"][2], visible=False) | |
| btn_ms4 = gr.Button(QUICK_ACTIONS["ms"][3], visible=False) | |
| # Voice input | |
| gr.Markdown("### ๐ค Voice Input (Speak in your native language)") | |
| audio_input = gr.Audio(sources=["microphone"], type="filepath", label="Record your question") | |
| voice_btn = gr.Button("Transcribe and Send") | |
| with gr.Column(scale=2): | |
| chatbot = gr.Chatbot(label="Conversation", height=500) | |
| with gr.Row(): | |
| msg = gr.Textbox(label="Your message", placeholder="Type your question here...", scale=4) | |
| send_btn = gr.Button("Send", variant="primary", scale=1) | |
| with gr.Row(): | |
| clear_btn = gr.Button("Clear Chat") | |
| tts_btn = gr.Button("๐ Speak Last Response (with selected voice)") | |
| audio_output = gr.Audio(label="Spoken Response", type="filepath", autoplay=True, visible=False) | |
| # ------------------------------------------------------------------ | |
| # Event handlers | |
| # ------------------------------------------------------------------ | |
| # Set token | |
| def set_token(token_val): | |
| return token_val | |
| token_btn.click(set_token, inputs=[token_input], outputs=[token_state]) | |
| # Update profile | |
| update_profile_btn.click( | |
| update_profile, | |
| inputs=[name_input, native_lang_input, target_job_input, skills_input, location_input], | |
| outputs=[profile_state] | |
| ).then( | |
| lambda: gr.Info("Profile updated!"), None, None | |
| ) | |
| # Send message (text input) | |
| send_btn.click( | |
| user_message, inputs=[msg, chat_state], outputs=[chatbot, msg] | |
| ).then( | |
| bot_response, | |
| inputs=[chat_state, model, profile_state, native_lang_input, token_state], | |
| outputs=chatbot | |
| ) | |
| # Voice input | |
| voice_btn.click( | |
| voice_input, inputs=[audio_input, chat_state, token_state], outputs=[chatbot, msg, audio_input] | |
| ).then( | |
| user_message, inputs=[msg, chat_state], outputs=[chatbot, msg] | |
| ).then( | |
| bot_response, | |
| inputs=[chat_state, model, profile_state, native_lang_input, token_state], | |
| outputs=chatbot | |
| ) | |
| # Quick actions | |
| all_quick_btns = [btn_en1, btn_en2, btn_en3, btn_en4, btn_ms1, btn_ms2, btn_ms3, btn_ms4] | |
| for btn in all_quick_btns: | |
| btn.click( | |
| quick_action_send, | |
| inputs=[btn, chat_state, model, profile_state, native_lang_input, token_state], | |
| outputs=chatbot | |
| ).then( | |
| lambda: "", None, msg # clear message box | |
| ) | |
| # UI language toggle | |
| ui_language.change( | |
| toggle_ui_language, | |
| inputs=ui_language, | |
| outputs=[btn_en1, btn_en2, btn_en3, btn_en4, btn_ms1, btn_ms2, btn_ms3, btn_ms4] | |
| ) | |
| # TTS for last response | |
| tts_btn.click( | |
| speak_last_response, | |
| inputs=[chat_state, token_state, accent, gender, age], | |
| outputs=[audio_output] | |
| ).then( | |
| lambda: gr.update(visible=True), None, audio_output | |
| ) | |
| # Clear chat | |
| clear_btn.click(clear_chat, outputs=[chatbot, audio_output]) | |
| # ---------------------------------------------------------------------- | |
| # Launch | |
| # ---------------------------------------------------------------------- | |
| if __name__ == "__main__": | |
| demo.launch(theme=gr.themes.Soft()) |