mramirez2001 commited on
Commit
2ad5fa9
verified
1 Parent(s): 05b2dea

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -120
app.py CHANGED
@@ -22,7 +22,6 @@ print("Loading Whisper for transcription...")
22
  whisper_model = whisper.load_model("base", device="cpu")
23
  print("Whisper model loaded.")
24
 
25
-
26
  # --- 1. DEFINICI脫N DE PROMPTS PARA LA IA ---
27
 
28
  # Prompt para mantener la conversaci贸n
@@ -34,162 +33,138 @@ A student will speak to you. Your task is to keep a natural, simple conversation
34
  3. Your entire response must be a single, short paragraph in natural, conversational English. DO NOT use JSON.
35
  """
36
 
37
- # Prompt para la evaluaci贸n final
38
  FINAL_EVALUATION_SYSTEM_PROMPT = """
39
  You are an expert English language examiner providing a final report. Analyze the entire conversation history provided.
40
-
41
  Your task is to return a single, valid JSON object with the following structure. Do not include any text outside this JSON object.
42
-
43
  JSON Output Structure:
44
  {
45
  "cefr_level": "string (e.g., A2, B1)",
46
- "feedback_en": {
47
- "strengths": "string (A paragraph summarizing the student's strong points in pronunciation, vocabulary, and fluency.)",
48
- "areas_for_improvement": "string (A paragraph detailing the main patterns of error and what to focus on.)",
49
- "word_by_word_feedback": [
50
- {"word": "string", "feedback": "string (Specific phonetic or usage feedback.)"}
51
- ]
52
- },
53
- "feedback_es": {
54
- "fortalezas": "string (Un p谩rrafo resumiendo los puntos fuertes del estudiante en pronunciaci贸n, vocabulario y fluidez.)",
55
- "areas_a_mejorar": "string (Un p谩rrafo detallando los patrones de error principales y en qu茅 enfocarse.)",
56
- "feedback_por_palabra": [
57
- {"palabra": "string", "feedback": "string (Retroalimentaci贸n fon茅tica o de uso espec铆fica.)"}
58
- ]
59
- }
60
  }
61
  """
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
  # --- 2. FUNCIONES L脫GICAS ---
65
 
66
- def transcribe_audio(audio_input):
67
- """Transcribe el audio usando la API de Whisper de OpenAI."""
68
- if audio_input is None:
69
- return ""
70
- sr, y = audio_input
71
- temp_audio_path = "temp_audio.wav"
72
- sf.write(temp_audio_path, y, sr)
73
- with open(temp_audio_path, "rb") as audio_file:
74
- transcript = client.audio.transcriptions.create(
75
- model="whisper-1",
76
- file=audio_file
77
- ).text
78
- return transcript
79
-
 
 
 
 
80
  def chat_interaction(audio_input, history_state):
81
- """
82
- Gestiona una vuelta de la conversaci贸n.
83
- """
84
  if not api_key_found: raise gr.Error("OpenAI API key not found.")
85
  if audio_input is None: return history_state, history_state, "", ""
86
-
87
- # 1. Transcribir el audio del usuario
88
- user_text = transcribe_audio(audio_input)
89
-
90
- # 2. Actualizar el historial con el mensaje del usuario
91
  history_state.append({"role": "user", "content": user_text})
92
-
93
- # Formatear para el chatbot de Gradio
94
- chat_display = []
95
- for i, msg in enumerate(history_state):
96
- if msg['role'] == 'user':
97
- chat_display.append((msg['content'], None))
98
- elif msg['role'] == 'assistant':
99
- if chat_display and chat_display[-1][1] is None:
100
- chat_display[-1] = (chat_display[-1][0], msg['content'])
101
 
102
- # 3. Decidir si continuar la conversaci贸n o dar el reporte final
103
- if len(history_state) < 9: # 1 system + 4 pares de user/assistant
104
- # --- Continuar conversaci贸n ---
105
  messages_to_send = [{"role": "system", "content": CONVERSATION_SYSTEM_PROMPT}] + history_state
106
-
107
- response = client.chat.completions.create(
108
- model="gpt-4o",
109
- messages=messages_to_send,
110
- temperature=0.7
111
- )
112
  ai_response = response.choices[0].message.content
113
  history_state.append({"role": "assistant", "content": ai_response})
114
  chat_display[-1] = (chat_display[-1][0], ai_response)
115
-
116
  return chat_display, history_state, gr.Markdown(visible=False), gr.Markdown(visible=False)
117
-
118
  else:
119
- # --- Generar evaluaci贸n final ---
120
- print("Generating final evaluation...")
121
- messages_to_send = [{"role": "system", "content": FINAL_EVALUATION_SYSTEM_PROMPT}] + history_state
122
-
123
- response = client.chat.completions.create(
124
- model="gpt-4o",
125
- response_format={"type": "json_object"},
126
- messages=messages_to_send
127
- )
128
-
129
  try:
130
  result = json.loads(response.choices[0].message.content)
131
-
132
- # Formatear el feedback en Ingl茅s
133
- fb_en = result.get('feedback_en', {})
134
- md_en = f"## Final Report (CEFR Level: {result.get('cefr_level', 'N/A')})\n"
135
- md_en += f"### Strengths\n{fb_en.get('strengths', '')}\n"
136
- md_en += f"### Areas for Improvement\n{fb_en.get('areas_for_improvement', '')}\n"
137
- md_en += "### Word-by-Word Feedback\n"
138
- for item in fb_en.get('word_by_word_feedback', []):
139
- md_en += f"- **{item['word']}**: {item['feedback']}\n"
140
-
141
- # Formatear el feedback en Espa帽ol
142
- fb_es = result.get('feedback_es', {})
143
- md_es = f"## Reporte Final (Nivel MCERL: {result.get('cefr_level', 'N/A')})\n"
144
- md_es += f"### Fortalezas\n{fb_es.get('fortalezas', '')}\n"
145
- md_es += f"### 脕reas a Mejorar\n{fb_es.get('areas_a_mejorar', '')}\n"
146
- md_es += "### Retroalimentaci贸n por Palabra\n"
147
- for item in fb_es.get('feedback_por_palabra', []):
148
- md_es += f"- **{item['palabra']}**: {item['feedback']}\n"
149
-
150
- # Mensaje final para el chat
151
- final_message = "Thank you for the conversation! Here is your final report."
152
- chat_display[-1] = (chat_display[-1][0], final_message)
153
-
154
  return chat_display, history_state, gr.Markdown(value=md_en, visible=True), gr.Markdown(value=md_es, visible=True)
155
-
156
  except (json.JSONDecodeError, KeyError) as e:
157
- print(f"Error parsing final report: {e}")
158
- return chat_display, history_state, gr.Markdown(value="Error generating report.", visible=True), gr.Markdown(visible=False)
159
 
160
- # --- 3. INTERFAZ DE GRADIO ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
162
  gr.Markdown("# 馃嚞馃嚙 AI English Speaking Practice & Assessment")
163
-
164
  with gr.Tabs():
 
165
  with gr.TabItem("Pr谩ctica Conversacional (Chat AI)"):
166
  with gr.Row():
167
  with gr.Column(scale=2):
168
- chatbot = gr.Chatbot(
169
- value=[(None, "Hi there! I'm Alex. How are you doing today?")],
170
- label="Conversation with your AI Tutor"
171
- )
172
  audio_in_chat = gr.Audio(sources=["microphone"], type="numpy", label="Record your response")
173
  with gr.Column(scale=1):
174
- gr.Markdown("### Final Report")
175
- feedback_en_out = gr.Markdown(label="English Feedback", visible=False)
176
- feedback_es_out = gr.Markdown(label="Retroalimentaci贸n en Espa帽ol", visible=False)
177
-
178
- # Estado para guardar el historial de la conversaci贸n
179
- history = gr.State([])
180
-
181
- audio_in_chat.stop_recording(
182
- fn=chat_interaction,
183
- inputs=[audio_in_chat, history],
184
- outputs=[chatbot, history, feedback_en_out, feedback_es_out]
185
- )
186
 
 
187
  with gr.TabItem("Evaluaci贸n por Frase"):
188
- gr.Markdown("This is a placeholder for the original sentence evaluation tool.")
189
- # Aqu铆 pegar铆as la interfaz de la herramienta anterior si quisieras combinar ambas.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
  if __name__ == "__main__":
192
- if not api_key_found:
193
- print("\nFATAL: OpenAI API key not found. Please set the OPENAI_API_KEY environment variable.")
194
- else:
195
- demo.launch(debug=True)
 
22
  whisper_model = whisper.load_model("base", device="cpu")
23
  print("Whisper model loaded.")
24
 
 
25
  # --- 1. DEFINICI脫N DE PROMPTS PARA LA IA ---
26
 
27
  # Prompt para mantener la conversaci贸n
 
33
  3. Your entire response must be a single, short paragraph in natural, conversational English. DO NOT use JSON.
34
  """
35
 
36
+ # Prompt para la evaluaci贸n final de la conversaci贸n
37
  FINAL_EVALUATION_SYSTEM_PROMPT = """
38
  You are an expert English language examiner providing a final report. Analyze the entire conversation history provided.
 
39
  Your task is to return a single, valid JSON object with the following structure. Do not include any text outside this JSON object.
 
40
  JSON Output Structure:
41
  {
42
  "cefr_level": "string (e.g., A2, B1)",
43
+ "feedback_en": { "strengths": "string", "areas_for_improvement": "string", "word_by_word_feedback": [{"word": "string", "feedback": "string"}] },
44
+ "feedback_es": { "fortalezas": "string", "areas_a_mejorar": "string", "feedback_por_palabra": [{"palabra": "string", "feedback": "string"}] }
 
 
 
 
 
 
 
 
 
 
 
 
45
  }
46
  """
47
 
48
+ # Prompt para la evaluaci贸n de una sola frase
49
+ SENTENCE_EVALUATION_SYSTEM_PROMPT = """
50
+ You are an expert English language examiner specializing in phonetics. Your task is to provide a detailed, diagnostic assessment of a student's spoken English based on a reference sentence and detailed word-level audio analysis.
51
+ Input You Will Receive: A JSON object with `reference_transcript` and a list of `spoken_words` with timestamps and energy.
52
+ Your entire response MUST be in English. You must return a single, valid JSON object with the following structure. Do not include any text outside this JSON object.
53
+ JSON Output Structure:
54
+ {
55
+ "overall_score_100": integer,
56
+ "cefr_level": "string (A1, A2, B1, B2, C1, or C2)",
57
+ "holistic_feedback": { "strengths": "string", "areas_for_improvement": "string" },
58
+ "word_by_word_analysis": [ { "reference_word": "string", "spoken_word": "string", "word_score_100": integer, "correct_ipa": "string", "feedback": "string" } ]
59
+ }
60
+ """
61
 
62
  # --- 2. FUNCIONES L脫GICAS ---
63
 
64
+ # Funci贸n auxiliar para extracci贸n de caracter铆sticas
65
+ def extract_word_level_features(audio_path):
66
+ try:
67
+ y, sr = librosa.load(audio_path, sr=16000)
68
+ result = whisper_model.transcribe(audio_path, word_timestamps=True, fp16=False)
69
+ if not result["segments"] or 'words' not in result["segments"][0]: return []
70
+ word_segments = result["segments"][0]["words"]
71
+ features_list = []
72
+ for segment in word_segments:
73
+ start_sample = int(segment['start'] * sr); end_sample = int(segment['end'] * sr)
74
+ word_audio = y[start_sample:end_sample]
75
+ rms_energy = np.mean(librosa.feature.rms(y=word_audio)) if len(word_audio) > 0 else 0
76
+ features_list.append({"word": segment['word'].strip(), "start": round(segment['start'], 2), "end": round(segment['end'], 2), "energy": round(float(rms_energy), 4)})
77
+ return features_list
78
+ except Exception as e:
79
+ print(f"Error during feature extraction: {e}"); return []
80
+
81
+ # Funci贸n para la Pesta帽a "Pr谩ctica Conversacional"
82
  def chat_interaction(audio_input, history_state):
 
 
 
83
  if not api_key_found: raise gr.Error("OpenAI API key not found.")
84
  if audio_input is None: return history_state, history_state, "", ""
85
+ sr, y = audio_input; temp_audio_path = "temp_audio_chat.wav"; sf.write(temp_audio_path, y, sr)
86
+ user_text = client.audio.transcriptions.create(model="whisper-1", file=open(temp_audio_path, "rb")).text
 
 
 
87
  history_state.append({"role": "user", "content": user_text})
88
+ chat_display = [(history_state[i]['content'], history_state[i+1]['content']) for i in range(0, len(history_state)-1, 2)]
89
+ chat_display.append((user_text, None))
 
 
 
 
 
 
 
90
 
91
+ if len(history_state) < 9:
 
 
92
  messages_to_send = [{"role": "system", "content": CONVERSATION_SYSTEM_PROMPT}] + history_state
93
+ response = client.chat.completions.create(model="gpt-4o", messages=messages_to_send, temperature=0.7)
 
 
 
 
 
94
  ai_response = response.choices[0].message.content
95
  history_state.append({"role": "assistant", "content": ai_response})
96
  chat_display[-1] = (chat_display[-1][0], ai_response)
 
97
  return chat_display, history_state, gr.Markdown(visible=False), gr.Markdown(visible=False)
 
98
  else:
99
+ print("Generating final evaluation..."); messages_to_send = [{"role": "system", "content": FINAL_EVALUATION_SYSTEM_PROMPT}] + history_state
100
+ response = client.chat.completions.create(model="gpt-4o", response_format={"type": "json_object"}, messages=messages_to_send)
 
 
 
 
 
 
 
 
101
  try:
102
  result = json.loads(response.choices[0].message.content)
103
+ fb_en = result.get('feedback_en', {}); md_en = f"## Final Report (CEFR Level: {result.get('cefr_level', 'N/A')})\n### Strengths\n{fb_en.get('strengths', '')}\n### Areas for Improvement\n{fb_en.get('areas_for_improvement', '')}\n### Word-by-Word Feedback\n"
104
+ for item in fb_en.get('word_by_word_feedback', []): md_en += f"- **{item['word']}**: {item['feedback']}\n"
105
+ fb_es = result.get('feedback_es', {}); md_es = f"## Reporte Final (Nivel MCERL: {result.get('cefr_level', 'N/A')})\n### Fortalezas\n{fb_es.get('fortalezas', '')}\n### 脕reas a Mejorar\n{fb_es.get('areas_a_mejorar', '')}\n### Retroalimentaci贸n por Palabra\n"
106
+ for item in fb_es.get('feedback_por_palabra', []): md_es += f"- **{item['palabra']}**: {item['feedback']}\n"
107
+ chat_display[-1] = (chat_display[-1][0], "Thank you for the conversation! Here is your final report.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  return chat_display, history_state, gr.Markdown(value=md_en, visible=True), gr.Markdown(value=md_es, visible=True)
 
109
  except (json.JSONDecodeError, KeyError) as e:
110
+ print(f"Error parsing final report: {e}"); return chat_display, history_state, gr.Markdown(value="Error generating report.", visible=True), gr.Markdown(visible=False)
 
111
 
112
+ # Funci贸n para la Pesta帽a "Evaluaci贸n por Frase"
113
+ def run_sentence_evaluation(audio_input, reference_transcript):
114
+ if not api_key_found: raise gr.Error("OpenAI API key not found.")
115
+ if audio_input is None or not reference_transcript:
116
+ return 0, "N/A", "Please provide both an audio file and the reference text.", None
117
+ sr, y = audio_input; temp_audio_path = "temp_audio_sentence.wav"; sf.write(temp_audio_path, y, sr)
118
+ word_features = extract_word_level_features(temp_audio_path)
119
+ if not word_features:
120
+ return 0, "N/A", "Could not process the audio. Please try recording again.", None
121
+ prompt_data = {"reference_transcript": reference_transcript, "spoken_words": word_features}
122
+ print("Sending detailed data to GPT-4o for analysis...")
123
+ response = client.chat.completions.create(model="gpt-4o", response_format={"type": "json_object"}, messages=[{"role": "system", "content": SENTENCE_EVALUATION_SYSTEM_PROMPT}, {"role": "user", "content": json.dumps(prompt_data)}])
124
+ try:
125
+ result = json.loads(response.choices[0].message.content)
126
+ holistic_feedback_md = f"### Strengths\n{result['holistic_feedback']['strengths']}\n\n### Areas for Improvement\n{result['holistic_feedback']['areas_for_improvement']}"
127
+ word_analysis_df = pd.DataFrame(result['word_by_word_analysis'])
128
+ return (result.get("overall_score_100", 0), result.get("cefr_level", "N/A"), holistic_feedback_md, gr.DataFrame(value=word_analysis_df, headers=["Reference Word", "Spoken Word", "Score", "Correct IPA", "Feedback"], interactive=False))
129
+ except (json.JSONDecodeError, KeyError) as e:
130
+ print(f"Error processing API response: {e}"); error_msg = "The API response was not in the expected format. Please try again."
131
+ return 0, "Error", error_msg, None
132
+
133
+ # --- 3. INTERFAZ DE GRADIO CON PESTA脩AS ---
134
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
135
  gr.Markdown("# 馃嚞馃嚙 AI English Speaking Practice & Assessment")
 
136
  with gr.Tabs():
137
+ # --- PESTA脩A 1: CHAT AI ---
138
  with gr.TabItem("Pr谩ctica Conversacional (Chat AI)"):
139
  with gr.Row():
140
  with gr.Column(scale=2):
141
+ chatbot = gr.Chatbot(value=[(None, "Hi there! I'm Alex. How are you doing today?")], label="Conversation with your AI Tutor")
 
 
 
142
  audio_in_chat = gr.Audio(sources=["microphone"], type="numpy", label="Record your response")
143
  with gr.Column(scale=1):
144
+ gr.Markdown("### Final Report"); feedback_en_out = gr.Markdown(label="English Feedback", visible=False); feedback_es_out = gr.Markdown(label="Retroalimentaci贸n en Espa帽ol", visible=False)
145
+ history = gr.State([])
146
+ audio_in_chat.stop_recording(fn=chat_interaction, inputs=[audio_in_chat, history], outputs=[chatbot, history, feedback_en_out, feedback_es_out])
 
 
 
 
 
 
 
 
 
147
 
148
+ # --- PESTA脩A 2: EVALUACI脫N POR FRASE ---
149
  with gr.TabItem("Evaluaci贸n por Frase"):
150
+ TONGUE_TWISTERS = ["Peter Piper picked a peck of pickled peppers.", "She sells seashells by the seashore.", "How much wood would a woodchuck chuck if a woodchuck could chuck wood?", "Betty Botter bought some butter but she said the butter鈥檚 bitter.", "A proper copper coffee pot."]
151
+ gr.Markdown("Choose a tongue twister or write your own sentence. Record yourself, and our AI examiner will provide a detailed diagnostic report.")
152
+ tongue_twister_selector = gr.Dropdown(choices=TONGUE_TWISTERS, label="Or Choose a Tongue Twister to Practice")
153
+ with gr.Row():
154
+ with gr.Column(scale=1):
155
+ audio_in_sentence = gr.Audio(sources=["microphone"], type="numpy", label="1. Record Your Voice")
156
+ text_in_sentence = gr.Textbox(lines=3, label="2. Reference Sentence", value=TONGUE_TWISTERS[0])
157
+ submit_btn_sentence = gr.Button("Get Assessment", variant="primary")
158
+ with gr.Column(scale=2):
159
+ gr.Markdown("### Assessment Summary")
160
+ with gr.Row(): score_out_sentence = gr.Number(label="Overall Score (0-100)", interactive=False); level_out_sentence = gr.Textbox(label="Estimated CEFR Level", interactive=False)
161
+ holistic_feedback_out_sentence = gr.Markdown(label="Examiner's Feedback")
162
+ gr.Markdown("--- \n ### Detailed Word-by-Word Analysis")
163
+ word_analysis_out_sentence = gr.DataFrame(headers=["Reference Word", "Spoken Word", "Score", "Correct IPA", "Feedback"], label="Phonetic Breakdown", wrap=True)
164
+ def update_text(choice): return gr.Textbox(value=choice)
165
+ tongue_twister_selector.change(fn=update_text, inputs=tongue_twister_selector, outputs=text_in_sentence)
166
+ submit_btn_sentence.click(fn=run_sentence_evaluation, inputs=[audio_in_sentence, text_in_sentence], outputs=[score_out_sentence, level_out_sentence, holistic_feedback_out_sentence, word_analysis_out_sentence])
167
 
168
  if __name__ == "__main__":
169
+ if not api_key_found: print("\nFATAL: OpenAI API key not found.")
170
+ else: demo.launch(debug=True)