zlwu преди 1 година
родител
ревизия
396b4b6f6d
променени са 6 файла, в които са добавени 138 реда и са изтрити 50 реда
  1. 18 1
      backend/README.md
  2. 82 37
      backend/ai_service.py
  3. 1 1
      backend/config.py
  4. 6 5
      frontend/src/components/AnimatedCharacter.tsx
  5. 30 5
      frontend/src/components/WaveformVisualizer.tsx
  6. 1 1
      frontend/src/utils/audioPlayer.ts

+ 18 - 1
backend/README.md

@@ -6,7 +6,24 @@ python -m pip install -r requirements.txt
 ```
 
 ## 配置
-复制 `.env.example` 为 `.env`,配置API Key。
+环境变量配置(通过 `.env` 文件或直接设置):
+
+### 必需配置
+- `DASHSCOPE_API_KEY`: 阿里云 DashScope API Key
+
+### AI 模型配置
+- `LLM_MODEL`: 默认 `qwen-flash`
+- `TTS_MODEL`: 默认 `qwen3-tts-flash`
+- `TTS_VOICE`: 默认 `Cherry` (可选: `Stella`)
+- `TTS_SPEECH_RATE`: 默认 `1.0` (语速倍率)
+
+### 学习者配置
+- `LEARNER_PROFICIENCY`: 学习者水平,默认 `A1-A2`
+- `AGE_TAG`: 年龄组,默认 `Adult`
+- `TOPIC_TAG`: 对话主题,默认 `Daily Conversation`
+- `EXPERIMENT_TAG`: 实验模式
+  - `control`: 对照组(不启用纠错反馈)
+  - `experiment`: 实验组(启用纠错反馈策略)
 
 ## 运行
 ```bash

+ 82 - 37
backend/ai_service.py

@@ -67,20 +67,10 @@ class AIService:
             print(f"用户输入: {user_text}")
             
             # 构建System Prompt
-            system_prompt = """You are a friendly English learning assistant.
-Please respond to the user in English to help them practice.
-Be encouraging and supportive.
-
-At the end of your response, on a new line, add ONLY the emotion tag in this exact format:
-[EMOTION:emotion_name]
-
-Available emotions: neutral, happy, sad, excited, angry, surprised, thoughtful, encouraging
-
-Example response:
-Great job! That's the correct pronunciation. Keep practicing and you'll get even better!
-[EMOTION:encouraging]"""
+            system_prompt = self._build_system_prompt()
             
             print(f"调用 LLM 流式: {settings.llm_model}")
+            print(f"学习者水平: {settings.LEARNER_PROFICIENCY}, 年龄组: {settings.AGE_TAG}, 主题: {settings.TOPIC_TAG}, 模式: {settings.EXPERIMENT_TAG}")
             
             # 调用LLM流式API
             response = self.openai_client.chat.completions.create(
@@ -190,21 +180,11 @@ Great job! That's the correct pronunciation. Keep practicing and you'll get even
             print(f"\n========== LLM 开始 ==========")
             print(f"用户输入: {user_text}")
             
-            # 构建System Prompt,要求返回情感标签
-            system_prompt = """You are a friendly English learning assistant.
-Please respond to the user in English to help them practice.
-Be encouraging and supportive.
-
-At the end of your response, on a new line, add ONLY the emotion tag in this exact format:
-[EMOTION:emotion_name]
-
-Available emotions: neutral, happy, sad, excited, angry, surprised, thoughtful, encouraging
-
-Example response:
-Great job! That's the correct pronunciation. Keep practicing and you'll get even better!
-[EMOTION:encouraging]"""
+            # 构建System Prompt
+            system_prompt = self._build_system_prompt()
             
             print(f"调用 LLM: {settings.llm_model}")
+            print(f"学习者水平: {settings.LEARNER_PROFICIENCY}, 年龄组: {settings.AGE_TAG}, 主题: {settings.TOPIC_TAG}, 模式: {settings.EXPERIMENT_TAG}")
             
             # 调用LLM
             response = self.openai_client.chat.completions.create(
@@ -362,18 +342,83 @@ Great job! That's the correct pronunciation. Keep practicing and you'll get even
     
     def _get_voice_for_emotion(self, emotion: EmotionType) -> str:
         """根据情感选择音色"""
-        # 情感到音色的映射(统一使用 Elias 男声)
-        voice_map = {
-            "neutral": "Elias",
-            "happy": "Elias",
-            "sad": "Elias",
-            "excited": "Elias",
-            "angry": "Elias",
-            "surprised": "Elias",
-            "thoughtful": "Elias",
-            "encouraging": "Elias",
-        }
-        return voice_map.get(emotion, settings.tts_voice)
+        # 所有情感统一使用配置文件中的音色
+        return settings.tts_voice
+    
+    def _build_system_prompt(self) -> str:
+        """构建System Prompt"""
+        learner_proficiency = settings.LEARNER_PROFICIENCY
+        age_tag = settings.AGE_TAG
+        topic_tag = settings.TOPIC_TAG
+        experiment_tag = settings.EXPERIMENT_TAG
+        
+        # 基础提示词
+        prompt = f"""**[Role]** 
+You are **KELLY**, a friendly and supportive English conversation partner based in London.
+
+**[Objective]** 
+Help Chinese EFL learners practice **spoken English**.
+
+**[Target Audience]**
+- Language Level: **{learner_proficiency}**
+- Age Group: **{age_tag}**
+- Conversation Topic: **{topic_tag}**
+
+**[Response Guidelines]**
+1. **Lead the Conversation**: Don't just follow the learner's questions—take initiative. Gently guide the conversation forward by introducing related ideas, asking engaging questions, or adding playful input.
+2. **Stay Focused**: Keep the conversation on the topic {topic_tag}, and make it suitable for {age_tag} learners.
+3. **Be Concise**: Respond in 2–3 short, natural sentences—like small talk with a friend.
+4. **Use Simple Language**: Stick to A1-A2 level (very easy) vocabulary and grammar. No idioms, no complex tenses. Use high-frequency, easy words.
+5. **Adapt to Learner**: If learners give very short or confused answers, simplify even more: use shorter, clearer sentences.
+6. **Spark Real Conversation**: Ask easy follow-up questions that feel personal, imaginative, or reflective (e.g., "What if you could go anywhere?", "How do you feel when...?", "Why do you like it?"). Let the conversation feel playful, personal, and curious.
+7. **Transition Smoothly**: If the topic feels finished or the learner seems less engaged, gently move to a related or new topic in a natural way. avoid overly specific or unusual questions (e.g., "What type of fish do you like?" or "What shape is your favorite lamp?"), as these can feel awkward or hard to answer. Instead, use broader, everyday topics that are easier to talk about, like: "That reminds me — do you usually cook at home or eat out?", "Speaking of weekends, do you like relaxing or doing something active?"
+8. **Being Empathetic**: Acknowledge the learner's feelings and experiences, but avoid overdoing it. 
+9. **Seek Clarification**: When meaning is unclear, use context and ask the learner to clarify. After requesting clarification, pause and wait for their response before continuing the conversation.
+10. **Handle Misunderstandings**: 
+ - If the learner says "Sorry?" → Rephrase your last sentence in simpler English.
+ - If they ask "Do you mean…?" → Confirm their guess and briefly explain in basic terms.
+"""
+        
+        # 如果是实验组,添加纠错反馈策略
+        if experiment_tag == "experiment":
+            correction_rule = """
+**[Corrective Feedback Strategy]**
+Goal: 
+Prioritising conversation flow while helping learners notice and improve their grammar.
+
+Tone & Guidelines: 
+(1) Use a kind and supportive tone: correct gently, and always reinforce progress;
+(2) Focus ONLY on grammar errors, ignore vocabulary issues;
+(3) Correct ONLY ONE error at a time;
+(4) Praise self-corrections (e.g. "I do… I did it last time") — encourage the effort, don't go back to correct the earlier mistake.
+(5) Reinforcement: Praise learners' correct usage to build their confidence. Examples: "That's right!", "Good job with the past tense!"
+(6) Avoid disrupting the conversation flow — don't say things like 'let's fix it', 'it should be...', 'you made a mistake', or 'you should say...' 
+
+Correction Strategy:
+- For **grammar errors** (e.g., tense, agreement): Use **recasts** as the default - restate with correct form naturally; If meaning is unclear: use a clarification check; If repeated 2+ times: use explicit correction in simple and friendly English.
+- For **vocabulary errors** (e.g., inappropriate word choice): ONLY use recasts; NEVER explain or point out!
+
+Correction Techniques:
+- **Recast** (PREFERRED): For minor grammar mistakes, restate the learner's sentence naturally with the correct form — without pointing out the error or saying "like you said, but…". Example: Learner: "Yesterday I go to restaurant.", You: "Yesterday you **went** to the restaurant? That's great!"
+- **Clarification Check**: Ask a confirming question when meaning is unclear. After asking the question, DO NOT continue asking other follow-up questions; instead, wait for the student's clarification. Example: "Do you mean 'you **went** to the restaurant'?" 
+- **Explicit Correction**: Only when a grammar mistake repeats many times, you explain the correct form clearly in basic English. After providing an Explicit Correction, DO NOT continue asking the follow-up question; instead, wait for the student's self-correction. If they try to self-correct, respond ONLY to the grammar part and don't focus on vocabulary. Example: "We say 'went' because it's past tense. So it's: 'I went to the restaurant.' Can you try it again?"
+"""
+            prompt += correction_rule
+        
+        # 添加情感标签要求
+        prompt += """
+
+**[Emotion Tag]**
+At the end of your response, on a new line, add ONLY the emotion tag in this exact format:
+[EMOTION:emotion_name]
+
+Available emotions: neutral, happy, sad, excited, angry, surprised, thoughtful, encouraging
+
+Example response:
+That sounds like fun! What did you do there?
+[EMOTION:happy]"""
+        
+        return prompt
 
 
 # 全局AI服务实例

+ 1 - 1
backend/config.py

@@ -35,7 +35,7 @@ class Settings(BaseSettings):
     
     # TTS配置
     tts_model: str = os.getenv("TTS_MODEL", "qwen3-tts-flash")
-    tts_voice: str = os.getenv("TTS_VOICE", "Jennifer")  # 女声
+    tts_voice: str = os.getenv("TTS_VOICE", "Cherry")  # 女声(支持:Cherry, Stella)
     tts_speech_rate: float = float(os.getenv("TTS_SPEECH_RATE", "1.0"))  # 语速:1.0 = 正常速度
     
     # 豆包API超时配置

+ 6 - 5
frontend/src/components/AnimatedCharacter.tsx

@@ -145,12 +145,13 @@ export const AnimatedCharacter: React.FC<AnimatedCharacterProps> = ({ emotion, s
       {/* 状态指示器 */}
       <div style={{
         position: 'absolute',
-        top: '20px',
+        top: '-30px',
         padding: '8px 16px',
         backgroundColor: 'rgba(255, 255, 255, 0.2)',
         borderRadius: '20px',
         fontSize: '0.9rem',
         color: 'white',
+        zIndex: 10,
       }}>
         {statusText}
       </div>
@@ -162,10 +163,9 @@ export const AnimatedCharacter: React.FC<AnimatedCharacterProps> = ({ emotion, s
         display: 'flex',
         alignItems: 'center',
         justifyContent: 'center',
-        background: 'rgba(255, 255, 255, 0.15)',
-        borderRadius: '50%',
-        boxShadow: '0 8px 20px rgba(0, 0, 0, 0.2)',
-        marginBottom: '30px',
+        marginBottom: '40px',
+        position: 'relative',
+        zIndex: 5,
       }}>
         <div 
           className="emoji-character"
@@ -192,6 +192,7 @@ export const AnimatedCharacter: React.FC<AnimatedCharacterProps> = ({ emotion, s
         textAlign: 'center',
         fontSize: '14px',
         lineHeight: '1.5',
+        zIndex: 10,
       }}>
         <div style={{
           position: 'absolute',

+ 30 - 5
frontend/src/components/WaveformVisualizer.tsx

@@ -29,8 +29,16 @@ export const WaveformVisualizer: React.FC<WaveformVisualizerProps> = ({ audioDat
       ctx.fillRect(0, 0, width, height);
 
       if (!isActive || !audioData) {
-        // 静止状态:显示平线
-        ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+        // 静止状态:显示多彩平线
+        const gradient = ctx.createLinearGradient(0, 0, width, 0);
+        gradient.addColorStop(0, 'rgba(255, 107, 107, 0.4)');
+        gradient.addColorStop(0.2, 'rgba(255, 184, 77, 0.4)');
+        gradient.addColorStop(0.4, 'rgba(255, 234, 167, 0.4)');
+        gradient.addColorStop(0.6, 'rgba(106, 255, 179, 0.4)');
+        gradient.addColorStop(0.8, 'rgba(118, 184, 255, 0.4)');
+        gradient.addColorStop(1, 'rgba(178, 118, 255, 0.4)');
+        
+        ctx.strokeStyle = gradient;
         ctx.lineWidth = 2;
         ctx.beginPath();
         ctx.moveTo(0, height / 2);
@@ -39,8 +47,17 @@ export const WaveformVisualizer: React.FC<WaveformVisualizerProps> = ({ audioDat
         return;
       }
 
+      // 创建多彩渐变 - 从左到右
+      const gradient = ctx.createLinearGradient(0, 0, width, 0);
+      gradient.addColorStop(0, '#ff6b6b');    // 红色
+      gradient.addColorStop(0.2, '#ffb84d');  // 橙色
+      gradient.addColorStop(0.4, '#ffeaa7');  // 黄色
+      gradient.addColorStop(0.6, '#6affb3');  // 绿色
+      gradient.addColorStop(0.8, '#76b8ff');  // 蓝色
+      gradient.addColorStop(1, '#b276ff');    // 紫色
+
       // 绘制波形
-      ctx.strokeStyle = '#4ade80';
+      ctx.strokeStyle = gradient;
       ctx.lineWidth = 2;
       ctx.beginPath();
 
@@ -63,11 +80,19 @@ export const WaveformVisualizer: React.FC<WaveformVisualizerProps> = ({ audioDat
       ctx.lineTo(width, height / 2);
       ctx.stroke();
 
-      // 绘制填充
+      // 绘制填充 - 使用半透明渐变
+      const fillGradient = ctx.createLinearGradient(0, 0, width, 0);
+      fillGradient.addColorStop(0, 'rgba(255, 107, 107, 0.3)');
+      fillGradient.addColorStop(0.2, 'rgba(255, 184, 77, 0.3)');
+      fillGradient.addColorStop(0.4, 'rgba(255, 234, 167, 0.3)');
+      fillGradient.addColorStop(0.6, 'rgba(106, 255, 179, 0.3)');
+      fillGradient.addColorStop(0.8, 'rgba(118, 184, 255, 0.3)');
+      fillGradient.addColorStop(1, 'rgba(178, 118, 255, 0.3)');
+      
       ctx.lineTo(width, height);
       ctx.lineTo(0, height);
       ctx.closePath();
-      ctx.fillStyle = 'rgba(74, 222, 128, 0.2)';
+      ctx.fillStyle = fillGradient;
       ctx.fill();
     };
 

+ 1 - 1
frontend/src/utils/audioPlayer.ts

@@ -14,7 +14,7 @@ export class AudioPlayer {
   private currentSources: AudioBufferSourceNode[] = []; // 当前播放的音频源
   
   // 播放速度控制
-  private playbackRate = 1.2; // 播放速度:1.2 = 加快 20%
+  private playbackRate = 1.0; // 播放速度:1.0 = 正常速度(保持原音)
 
   async initialize(): Promise<void> {
     // 创建 AudioContext,采样率 24000Hz(与后端 TTS 一致)