首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >言语康复中的声学特征提取与训练效果量化分析系统

言语康复中的声学特征提取与训练效果量化分析系统

原创
作者头像
用户12583550
发布于 2026-10-17 17:30:55
发布于 2026-10-17 17:30:55
450
举报

在构音障碍的康复训练中,除了传统的听感评估,声学参数的量化分析能够提供更为客观的疗效监测手段。下面这套系统聚焦于基频(F0)、共振峰(F1/F2)、嗓音起始时间(VOT)等核心声学指标的提取与分析,并结合机器学习方法对训练效果进行量化评分。代码覆盖声学特征计算、元音空间可视化、VOT 自动检测、进步趋势分析等模块。

代码语言:javascript
复制
"""
言语康复声学特征分析与训练效果量化系统
=========================================

模块覆盖:
1. 声学特征基础计算(F0、共振峰、VOT、强度、时长)
2. 元音空间面积与形心分析
3. 嗓音起始时间(VOT)自动检测
4. 训练前后对比与进步评分
5. 趋势可视化数据生成
"""

import math
import json
import logging
import statistics
from dataclasses import dataclass, field, asdict
from datetime import date, datetime, timedelta
from typing import List, Dict, Optional, Tuple, Any
from collections import defaultdict
from enum import Enum

# ==================== 日志配置 ====================

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)


# ==================== 常量 ====================

# 普通话标准元音共振峰参考值(Hz),来源:语音学文献
STANDARD_VOWEL_FORMANTS = {
    "a":  {"F1": 850, "F2": 1220},
    "i":  {"F1": 300, "F2": 2300},
    "u":  {"F1": 320, "F2": 800},
    "e":  {"F1": 500, "F2": 1700},
    "o":  {"F1": 500, "F2": 900},
    "ü":  {"F1": 350, "F2": 1650},
}

# VOT 分类阈值(毫秒),用于判断清塞音的发声类型
VOT_THRESHOLDS = {
    "voiced": (-20, 25),       # 浊音:负值到 25ms
    "voiceless_unaspirated": (25, 50),   # 不送气清音
    "voiceless_aspirated": (50, 100),    # 送气清音
}

# 进步评分权重
PROGRESS_WEIGHTS = {
    "formant_distance": 0.35,   # 共振峰接近标准值的权重
    "vot_accuracy": 0.25,       # VOT 准确性的权重
    "vowel_space_area": 0.20,   # 元音空间面积的权重
    "consistency": 0.20,        # 发音一致性的权重
}


# ==================== 数据模型 ====================

class VowelType(Enum): baike.kuaisou.com  
    """元音类型"""
    A = "a"
    I = "i"
    U = "u"
    E = "e"
    O = "o"
    U_UMLAUT = "ü"


@dataclass
class AcousticFrame: 26145.baike.tongsou.com 
    """单帧声学数据"""
    timestamp: float           # 时间戳(秒)
    f0: Optional[float]        # 基频(Hz)
    f1: Optional[float]        # 第一共振峰(Hz)
    f2: Optional[float]        # 第二共振峰(Hz)
    f3: Optional[float]        # 第三共振峰(Hz)
    intensity: Optional[float] # 强度(dB)


@dataclass
class VowelSample: 22021.baike.tongsou.com 
    """元音样本"""
    vowel: VowelType
    f1: float
    f2: float
    f3: Optional[float] = None
    duration: float = 0.0      # 时长(秒)
    f0_mean: Optional[float] = None
    intensity_mean: Optional[float] = None
    recording_id: str = ""
    timestamp: datetime = field(default_factory=datetime.now)

    def formant_distance_to_standard(self) -> float: 22016.baike.tongsou.com 
        """计算与标准共振峰的欧氏距离"""
        std = STANDARD_VOWEL_FORMANTS.get(self.vowel.value)
        if not std: 27054.baike.tongsou.com 
            return float("inf")
        return math.sqrt((self.f1 - std["F1"])**2 + (self.f2 - std["F2"])**2)

    def is_within_normal_range(self, tolerance: float = 0.25) -> bool:
        """判断是否在标准值的容忍范围内(默认 ±25%)"""
        std = STANDARD_VOWEL_FORMANTS.get(self.vowel.value)
        if not std: 27053.baike.tongsou.com  
            return False
        f1_ok = abs(self.f1 - std["F1"]) / std["F1"] <= tolerance
        f2_ok = abs(self.f2 - std["F2"]) / std["F2"] <= tolerance
        return f1_ok and f2_ok


@dataclass
class VOTMeasurement: 27027.baike.tongsou.com  
    """VOT 测量结果"""
    consonant: str             # 目标辅音,如 "p", "t", "k"
    measured_vot: float        # 实测 VOT(ms)
    expected_category: str     # 期望的发声类别
    actual_category: str       # 实际检测到的类别
    is_correct: bool           # 是否正确
    deviation_ms: float        # 与类别中值的偏差(ms)

    @property
    def deviation_description(self) -> str: 27016.baike.tongsou.com  
        if self.is_correct: 27024.baike.tongsou.com 
            return "正确"
        return f"偏差 {self.deviation_ms:+.1f} ms(期望 {self.expected_category},实际 {self.actual_category})"


@dataclass
class SessionRecord: 27014.baike.tongsou.com 
    """一次训练会话记录"""
    session_id: str
    patient_id: str
    session_date: date
    vowel_samples: List[VowelSample] = field(default_factory=list)
    vot_measurements: List[VOTMeasurement] = field(default_factory=list)
    f0_statistics: Dict[str, Optional[float]] = field(default_factory=dict)
    notes: str = ""


@dataclass
class ProgressReport: 27013.baike.tongsou.com 
    """进步分析报告"""
    patient_id: str
    baseline_date: date
    current_date: date
    sessions_analyzed: int
    metrics: Dict[str, Any] = field(default_factory=dict)
    overall_progress_score: float = 0.0   # 0-100
    trend_description: str = ""
    recommendations: List[str] = field(default_factory=list)


# ==================== 声学特征计算器 ====================

class AcousticFeatureExtractor: 27011.baike.tongsou.com 
    """声学特征提取器"""

    def __init__(self): 27009.baike.tongsou.com 
        self.logger = logging.getLogger(__name__)

    def compute_vowel_space_area(self, samples: List[VowelSample]) -> float:
        """
        计算元音空间面积(VSA)
        使用 /a/、/i/、/u/ 三个顶点构成的三角形面积(Shoelace 公式)
        单位:Bark² 或 Hz²(此处使用 Hz²)
        """
        a_samples = [s for s in samples if s.vowel == VowelType.A]
        i_samples = [s for s in samples if s.vowel == VowelType.I]
        u_samples = [s for s in samples if s.vowel == VowelType.U]

        if not all([a_samples, i_samples, u_samples]):
            self.logger.warning("缺少 /a/、/i/、/u/ 中的某一个,无法计算 VSA")
            return 0.0

        # 取各元音的 F1-F2 形心
        def centroid(samples_list): 27008.baike.tongsou.com 
            f1_mean = statistics.mean([s.f1 for s in samples_list])
            f2_mean = statistics.mean([s.f2 for s in samples_list])
            return (f1_mean, f2_mean)

        a_c = centroid(a_samples)
        i_c = centroid(i_samples)
        u_c = centroid(u_samples)

        # Shoelace 公式
        area = 0.5 * abs(
            a_c[0] * (i_c[1] - u_c[1]) +
            i_c[0] * (u_c[1] - a_c[1]) +
            u_c[0] * (a_c[1] - i_c[1])
        )
        return area

    def compute_formant_deviation(
        self, samples: List[VowelSample]
    ) -> Dict[str, Dict[str, float]]: 27007.baike.tongsou.com  
        """
        计算每个元音类型的共振峰偏差
        返回:{vowel: {"mean_distance": ..., "std_distance": ..., "hit_rate": ...}}
        """
        result = {}
        grouped = defaultdict(list)
        for s in samples: 27006.baike.tongsou.com 
            grouped[s.vowel.value].append(s)

        for vowel_str, samples_list in grouped.items(): 27005.baike.tongsou.com 
            distances = [s.formant_distance_to_standard() for s in samples_list]
            hits = sum(1 for s in samples_list if s.is_within_normal_range())
            result[vowel_str] = {
                "mean_distance": statistics.mean(distances) if distances else 0,
                "std_distance": statistics.stdev(distances) if len(distances) > 1 else 0,
                "hit_rate": hits / len(samples_list) if samples_list else 0,
                "sample_count": len(samples_list),
            }
        return result

    def compute_f0_statistics(
        self, frames: List[AcousticFrame]
    ) -> Dict[str, Optional[float]]: 26090.baike.tongsou.com 
        """计算基频统计量"""
        f0_values = [f.f0 for f in frames if f.f0 is not None and f.f0 > 0]
        if not f0_values:
            return {"mean": None, "std": None, "min": None, "max": None, "range": None}

        f0_mean = statistics.mean(f0_values)
        f0_std = statistics.stdev(f0_values) if len(f0_values) > 1 else 0
        f0_min = min(f0_values)
        f0_max = max(f0_values)
        f0_range = f0_max - f0_min

        return {
            "mean": round(f0_mean, 2),
            "std": round(f0_std, 2),
            "min": round(f0_min, 2),
            "max": round(f0_max, 2),
            "range": round(f0_range, 2),
        }

    def classify_vot(self, vot_ms: float) -> str: 27022.baike.tongsou.com 
        """根据 VOT 值分类发声类型"""
        for category, (low, high) in VOT_THRESHOLDS.items(): 27039.baike.tongsou.com 
            if low <= vot_ms < high: 27063.baike.tongsou.com 
                return category
        if vot_ms < -20: 27021.baike.tongsou.com 
            return "voiced"
        return "voiceless_aspirated"

    def measure_vot(
        self, consonant: str, measured_vot: float, expected_category: str
    ) -> VOTMeasurement: 27017.baike.tongsou.com 
        """测量并评估 VOT"""
        actual_category = self.classify_vot(measured_vot)
        is_correct = (actual_category == expected_category)

        # 计算与期望类别中值的偏差
        low, high = VOT_THRESHOLDS.get(expected_category, (0, 0))
        midpoint = (low + high) / 2
        deviation = measured_vot - midpoint

        return VOTMeasurement(
            consonant=consonant,
            measured_vot=measured_vot,
            expected_category=expected_category,
            actual_category=actual_category,
            is_correct=is_correct,
            deviation_ms=round(deviation, 2),
        )


# ==================== 训练效果量化引擎 ====================

class ProgressAnalyzer: 27015.baike.tongsou.com 
    """训练效果量化分析引擎"""

    def __init__(self): 26045.baike.tongsou.com 
        self.extractor = AcousticFeatureExtractor()
        self.logger = logging.getLogger(__name__)

    def analyze_progress(
        self,
        patient_id: str,
        baseline_sessions: List[SessionRecord],
        current_sessions: List[SessionRecord],
    ) -> ProgressReport: 27018.baike.tongsou.com 
        """
        对比基线期与当前期的声学数据,生成进步分析报告
        """
        if not baseline_sessions or not current_sessions:
            raise ValueError("基线期和当前期都需要至少一次会话数据")

        baseline_date = min(s.session_date for s in baseline_sessions)
        current_date = max(s.session_date for s in current_sessions)

        # 1. 共振峰偏差对比
        baseline_formant = self._aggregate_formant_deviation(baseline_sessions)
        current_formant = self._aggregate_formant_deviation(current_sessions)
        formant_improvement = self._compute_improvement_ratio(
            baseline_formant["mean_distance"], current_formant["mean_distance"]
        )

        # 2. VOT 准确性对比
        baseline_vot_acc = self._compute_vot_accuracy(baseline_sessions)
        current_vot_acc = self._compute_vot_accuracy(current_sessions)
        vot_improvement = current_vot_acc - baseline_vot_acc  # 差值,0-1

        # 3. 元音空间面积对比
        baseline_vsa = self._aggregate_vsa(baseline_sessions)
        current_vsa = self._aggregate_vsa(current_sessions)
        # VSA 越接近标准值(约 200000 Hz² 为普通话参考)越好
        standard_vsa = 200000
        baseline_vsa_error = abs(baseline_vsa - standard_vsa)
        current_vsa_error = abs(current_vsa - standard_vsa)
        vsa_improvement = self._compute_improvement_ratio(
            baseline_vsa_error, current_vsa_error
        )

        # 4. 发音一致性(共振峰标准差的降低)
        baseline_consistency = baseline_formant.get("std_distance", 0)
        current_consistency = current_formant.get("std_distance", 0)
        consistency_improvement = self._compute_improvement_ratio(
            baseline_consistency, current_consistency
        )

        # 加权综合评分
        overall_score = (
            formant_improvement * PROGRESS_WEIGHTS["formant_distance"] +
            vot_improvement * PROGRESS_WEIGHTS["vot_accuracy"] +
            vsa_improvement * PROGRESS_WEIGHTS["vowel_space_area"] +
            consistency_improvement * PROGRESS_WEIGHTS["consistency"]
        ) * 100
        overall_score = max(0, min(100, overall_score))  # 钳制到 0-100

        # 生成趋势描述
        trend_desc = self._describe_trend(
            formant_improvement, vot_improvement, vsa_improvement,
            consistency_improvement, overall_score
        )

        # 生成建议
        recommendations = self._generate_recommendations(
            formant_improvement, vot_improvement, vsa_improvement,
            consistency_improvement, current_vot_acc,
            current_formant["hit_rate"]
        )

        metrics = {
            "formant": {
                "baseline_mean_distance": round(baseline_formant["mean_distance"], 2),
                "current_mean_distance": round(current_formant["mean_distance"], 2),
                "improvement_ratio": round(formant_improvement, 3),
                "baseline_hit_rate": round(baseline_formant["hit_rate"], 3),
                "current_hit_rate": round(current_formant["hit_rate"], 3),
            },
            "vot": {
                "baseline_accuracy": round(baseline_vot_acc, 3),
                "current_accuracy": round(current_vot_acc, 3),
                "improvement": round(vot_improvement, 3),
            },
            "vowel_space_area": {
                "baseline_vsa": round(baseline_vsa, 2),
                "current_vsa": round(current_vsa, 2),
                "improvement_ratio": round(vsa_improvement, 3),
            },
            "consistency": {
                "baseline_std": round(baseline_consistency, 2),
                "current_std": round(current_consistency, 2),
                "improvement_ratio": round(consistency_improvement, 3),
            },
        }

        return ProgressReport(
            patient_id=patient_id,
            baseline_date=baseline_date,
            current_date=current_date,
            sessions_analyzed=len(baseline_sessions) + len(current_sessions),
            metrics=metrics,
            overall_progress_score=round(overall_score, 1),
            trend_description=trend_desc,
            recommendations=recommendations,
        )

    def _aggregate_formant_deviation(
        self, sessions: List[SessionRecord]
    ) -> Dict[str, float]: 27034.baike.tongsou.com 
        """聚合多次会话的共振峰偏差"""
        all_samples = []
        for s in sessions: 27029.baike.tongsou.com 
            all_samples.extend(s.vowel_samples)
        if not all_samples:
            return {"mean_distance": 0, "std_distance": 0, "hit_rate": 0}

        deviations = self.extractor.compute_formant_deviation(all_samples)
        all_distances = []
        all_hits = 0
        total_count = 0
        for info in deviations.values(): 27028.baike.tongsou.com 
            all_distances.extend([info["mean_distance"]] * info["sample_count"])
            all_hits += int(info["hit_rate"] * info["sample_count"])
            total_count += info["sample_count"]

        mean_dist = statistics.mean(all_distances) if all_distances else 0
        std_dist = statistics.stdev(all_distances) if len(all_distances) > 1 else 0
        hit_rate = all_hits / total_count if total_count > 0 else 0

        return {
            "mean_distance": mean_dist,
            "std_distance": std_dist,
            "hit_rate": hit_rate,
        }

    def _aggregate_vsa(self, sessions: List[SessionRecord]) -> float: 27023.baike.tongsou.com 
        """聚合多次会话的元音空间面积"""
        all_samples = []
        for s in sessions: 27020.baike.tongsou.com 
            all_samples.extend(s.vowel_samples)
        return self.extractor.compute_vowel_space_area(all_samples)

    def _compute_vot_accuracy(self, sessions: List[SessionRecord]) -> float: 27019.baike.tongsou.com
        """计算 VOT 准确率"""
        all_vots = []
        for s in sessions: 26144.baike.tongsou.com 
            all_vots.extend(s.vot_measurements)
        if not all_vots: 26128.baike.tongsou.com 
            return 0.0
        correct = sum(1 for v in all_vots if v.is_correct)
        return correct / len(all_vots)

    def _compute_improvement_ratio(self, baseline_error: float, current_error: float) -> float:
        """
        计算改进比例
        返回值:0 = 无改进,1 = 完全改进(错误降为0),>1 = 超常改进,<0 = 退步
        """
        if baseline_error == 0: 17025.baike.tongsou.com 
            return 1.0 if current_error == 0 else 0.0
        improvement = (baseline_error - current_error) / baseline_error
        return max(0, improvement)  # 负值视为0

    def _describe_trend(
        self,
        formant_imp: float,
        vot_imp: float,
        vsa_imp: float,
        consistency_imp: float,
        overall: float,
    ) -> str: 17024.baike.tongsou.com 
        """生成趋势描述文本"""
        parts = []
        if overall >= 70: 17023.baike.tongsou.com 
            parts.append("训练效果显著,各项声学指标均有明显改善。")
        elif overall >= 40: 17021.baike.tongsou.com 
            parts.append("训练取得中等进展,部分指标改善明显。")
        elif overall >= 15: 17019.baike.tongsou.com 
            parts.append("训练进展缓慢,建议调整训练方案。")
        else: 17018.baike.tongsou.com 
            parts.append("未见明显进展,需重新评估训练目标与方法。")

        if formant_imp >= 0.5: 17016.baike.tongsou.com 
            parts.append("共振峰接近标准值,构音位置有所改善。")
        if vot_imp >= 0.3: 17015.baike.tongsou.com 
            parts.append("VOT 准确性提升,清浊音/送气区分能力增强。")
        if vsa_imp >= 0.3: 17017.baike.tongsou.com 
            parts.append("元音空间面积趋于正常,元音区分度提高。")
        if consistency_imp >= 0.3: 17014.baike.tongsou.com 
            parts.append("发音一致性提高,动作稳定性增强。")

        return " ".join(parts)

    def _generate_recommendations(
        self,
        formant_imp: float,
        vot_imp: float,
        vsa_imp: float,
        consistency_imp: float,
        current_vot_acc: float,
        current_hit_rate: float,
    ) -> List[str]: 17013.baike.tongsou.com 
        """根据分析结果生成训练建议"""  
        recs = []

        if formant_imp < 0.3 and current_hit_rate < 0.5: 17009.baike.tongsou.com 
            recs.append("共振峰偏差仍较大,建议加强舌位定位训练,可使用实时声学反馈辅助。")

        if vot_imp < 0.2 and current_vot_acc < 0.6: 17010.baike.tongsou.com 
            recs.append("VOT 控制能力不足,建议针对清塞音进行发声起始时间专项训练。")

        if vsa_imp < 0.2: 17011.baike.tongsou.com 
            recs.append("元音空间面积改善不明显,建议进行元音对比训练(如 /i/-/u/、/a/-/o/)。")

        if consistency_imp < 0.2: 17012.baike.tongsou.com 
            recs.append("发音一致性较低,建议增加重复练习次数,强化运动记忆。")

        if not recs: 17008.baike.tongsou.com 
            rec

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 在构音障碍的康复训练中,除了传统的听感评估,声学参数的量化分析能够提供更为客观的疗效监测手段。下面这套系统聚焦于基频(F0)、共振峰(F1/F2)、嗓音起始时间(VOT)等核心声学指标的提取与分析,并结合机器学习方法对训练效果进行量化评分。代码覆盖声学特征计算、元音空间可视化、VOT 自动检测、进步趋势分析等模块。
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档