
鸿蒙跨端外语发音评分系统开发指南 原创
鸿蒙跨端外语发音评分系统开发指南
一、项目概述
本文基于HarmonyOS的语音识别和音频分析能力,结合分布式技术,开发一款外语发音评分系统。该系统能够实时评估用户外语发音准确度,并将评分结果同步到多设备,借鉴了《鸿蒙跨端U同步》中多设备数据同步的技术原理。
二、系统架构
±--------------------+ ±--------------------+ ±--------------------+
主设备 <-----> 分布式数据总线 <-----> 从设备
(手机/平板) (Distributed Bus) (智能手表/电脑)
±---------±---------+ ±---------±---------+ ±---------±---------+
±---------v----------+ ±---------v----------+ ±---------v----------+
语音分析模块 发音评估模块 数据同步模块
(Speech Analysis) (Pronunciation) (Data Sync)
±--------------------+ ±--------------------+ ±--------------------+
三、核心代码实现
发音评估服务
// src/main/ets/service/PronunciationService.ts
import { distributedData } from ‘@ohos.data.distributedData’;
import { BusinessError } from ‘@ohos.base’;
import { audio } from ‘@ohos.multimedia.audio’;
import { speechRecognizer } from ‘@ohos.multimedia.speech’;
import { ai } from ‘@ohos.ai’;
interface PronunciationScore {
accuracy: number; // 发音准确度(0-100)
fluency: number; // 流畅度(0-100)
completeness: number; // 完整度(0-100)
overall: number; // 综合评分(0-100)
timestamp: number;
interface EvaluationResult {
text: string; // 原文
pronunciation: string; // 用户发音
scores: PronunciationScore;
feedback: string[]; // 改进建议
export class PronunciationService {
private static instance: PronunciationService;
private kvStore: distributedData.KVStore | null = null;
private readonly STORE_ID = ‘pronunciation_store’;
private audioCapturer: audio.AudioCapturer | null = null;
private speechRecognizer: speechRecognizer.SpeechRecognizer | null = null;
private currentResult: EvaluationResult | null = null;
private referenceTexts: string[] = [
“Hello world”,
“How are you”,
“I love programming”,
“HarmonyOS is great”
];
private constructor() {
this.initKVStore();
this.initAudioCapturer();
this.initSpeechRecognizer();
public static getInstance(): PronunciationService {
if (!PronunciationService.instance) {
PronunciationService.instance = new PronunciationService();
return PronunciationService.instance;
private async initKVStore(): Promise<void> {
try {
const options: distributedData.KVManagerConfig = {
bundleName: 'com.example.pronunciation',
userInfo: {
userId: '0',
userType: distributedData.UserType.SAME_USER_ID
};
const kvManager = distributedData.createKVManager(options);
this.kvStore = await kvManager.getKVStore({
storeId: this.STORE_ID,
options: {
createIfMissing: true,
encrypt: false,
backup: false,
autoSync: true,
kvStoreType: distributedData.KVStoreType.SINGLE_VERSION
});
this.kvStore.on('dataChange', distributedData.SubscribeType.SUBSCRIBE_TYPE_REMOTE, (data) => {
data.insertEntries.forEach((entry: distributedData.Entry) => {
if (entry.key === 'evaluation_result') {
this.notifyResultChange(entry.value.value as EvaluationResult);
});
});
catch (e) {
console.error(Failed to initialize KVStore. Code: {e.code}, message: {e.message});
}
private async initAudioCapturer(): Promise<void> {
try {
const audioStreamInfo: audio.AudioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000,
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
};
const audioCapturerInfo: audio.AudioCapturerInfo = {
source: audio.SourceType.SOURCE_TYPE_MIC,
capturerFlags: 0
};
this.audioCapturer = await audio.createAudioCapturer({
streamInfo: audioStreamInfo,
capturerInfo: audioCapturerInfo
});
catch (e) {
console.error(Failed to initialize audio capturer. Code: {e.code}, message: {e.message});
}
private async initSpeechRecognizer(): Promise<void> {
try {
this.speechRecognizer = await speechRecognizer.createSpeechRecognizer();
const config: speechRecognizer.SpeechRecognizerConfig = {
language: 'en-US', // 英语(美国)
grammar: 'default'
};
await this.speechRecognizer.setConfig(config);
this.speechRecognizer.on('result', (result) => {
this.handleRecognitionResult(result);
});
catch (e) {
console.error(Failed to initialize speech recognizer. Code: {e.code}, message: {e.message});
}
private async initAITextGenerator(): Promise<void> {
try {
this.aiTextGenerator = await ai.createTextGenerator();
catch (e) {
console.error(Failed to initialize AI text generator. Code: {e.code}, message: {e.message});
}
public async startEvaluation(referenceText?: string): Promise<void> {
if (!this.audioCapturer || !this.speechRecognizer) return;
const text = referenceText || this.getRandomReferenceText();
this.currentResult = {
text,
pronunciation: '',
scores: {
accuracy: 0,
fluency: 0,
completeness: 0,
overall: 0,
timestamp: Date.now()
},
feedback: []
};
try {
await this.audioCapturer.start();
await this.speechRecognizer.start();
catch (e) {
console.error(Failed to start evaluation. Code: {e.code}, message: {e.message});
}
private getRandomReferenceText(): string {
const index = Math.floor(Math.random() * this.referenceTexts.length);
return this.referenceTexts[index];
private handleRecognitionResult(result: speechRecognizer.SpeechRecognizerResult): void {
if (!this.currentResult) return;
this.currentResult.pronunciation = result.text;
this.evaluatePronunciation();
private evaluatePronunciation(): void {
if (!this.currentResult) return;
const { text, pronunciation } = this.currentResult;
// 简化的评估逻辑 (实际应用中应使用更复杂的算法)
const accuracy = this.calculateAccuracy(text, pronunciation);
const fluency = this.calculateFluency(pronunciation);
const completeness = this.calculateCompleteness(text, pronunciation);
this.currentResult.scores = {
accuracy,
fluency,
completeness,
overall: Math.floor((accuracy + fluency + completeness) / 3),
timestamp: Date.now()
};
this.generateFeedback();
this.syncResult();
private calculateAccuracy(reference: string, pronunciation: string): number {
// 基于编辑距离的准确度计算
const distance = this.levenshteinDistance(reference.toLowerCase(), pronunciation.toLowerCase());
const maxLength = Math.max(reference.length, pronunciation.length);
return Math.max(0, 100 - (distance / maxLength * 100));
private levenshteinDistance(a: string, b: string): number {
const matrix = Array(a.length + 1).fill(null).map(() => Array(b.length + 1).fill(null));
for (let i = 0; i <= a.length; i++) matrix[i][0] = i;
for (let j = 0; j <= b.length; j++) matrix[0][j] = j;
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + cost
);
}
return matrix[a.length][b.length];
private calculateFluency(pronunciation: string): number {
// 基于单词数量和停顿的流畅度评估
const words = pronunciation.split(' ');
if (words.length === 0) return 0;
const wordLengths = words.map(word => word.length);
const avgLength = wordLengths.reduce((sum, len) => sum + len, 0) / wordLengths.length;
const lengthScore = Math.min(100, 100 - Math.abs(avgLength - 4.5) * 10);
const countScore = Math.min(100, words.length * 20);
return Math.floor((lengthScore + countScore) / 2);
private calculateCompleteness(reference: string, pronunciation: string): number {
// 基于覆盖率的完整度评估
const refWords = new Set(reference.toLowerCase().split(' '));
const pronWords = new Set(pronunciation.toLowerCase().split(' '));
let matched = 0;
refWords.forEach(word => {
if (pronWords.has(word)) matched++;
});
return Math.floor(matched / refWords.size * 100);
private generateFeedback(): void {
if (!this.currentResult) return;
const { scores, text, pronunciation } = this.currentResult;
this.currentResult.feedback = [];
if (scores.accuracy < 70) {
this.currentResult.feedback.push('注意单词发音准确性');
if (scores.fluency < 60) {
this.currentResult.feedback.push('尝试说得更流畅一些');
if (scores.completeness < 80) {
const missingWords = this.findMissingWords(text, pronunciation);
if (missingWords.length > 0) {
this.currentResult.feedback.push(遗漏了单词: ${missingWords.join(', ')});
}
if (scores.overall > 85) {
this.currentResult.feedback.push('发音很棒!继续保持');
}
private findMissingWords(reference: string, pronunciation: string): string[] {
const refWords = reference.toLowerCase().split(’ ‘);
const pronWords = new Set(pronunciation.toLowerCase().split(’ '));
return refWords.filter(word => !pronWords.has(word));
private async syncResult(): Promise<void> {
if (this.kvStore && this.currentResult) {
try {
await this.kvStore.put('evaluation_result', { value: this.currentResult });
catch (e) {
console.error(Failed to sync result. Code: {e.code}, message: {e.message});
}
public async stopEvaluation(): Promise<void> {
try {
if (this.audioCapturer) {
await this.audioCapturer.stop();
if (this.speechRecognizer) {
await this.speechRecognizer.stop();
} catch (e) {
console.error(Failed to stop evaluation. Code: {e.code}, message: {e.message});
}
public async getCurrentResult(): Promise<EvaluationResult | null> {
if (!this.kvStore) return this.currentResult;
try {
const entry = await this.kvStore.get('evaluation_result');
return entry?.value || this.currentResult;
catch (e) {
console.error(Failed to get evaluation result. Code: {e.code}, message: {e.message});
return this.currentResult;
}
public async destroy(): Promise<void> {
if (this.kvStore) {
this.kvStore.off(‘dataChange’);
if (this.audioCapturer) {
await this.audioCapturer.release();
if (this.speechRecognizer) {
await this.speechRecognizer.destroy();
}
发音评分组件
// src/main/ets/components/PronunciationCard.ets
@Component
export struct PronunciationCard {
private pronService = PronunciationService.getInstance();
@State evaluationResult: EvaluationResult | null = null;
@State isEvaluating: boolean = false;
@State referenceText: string = ‘’;
aboutToAppear(): void {
this.loadReferenceText();
private loadReferenceText(): void {
this.referenceText = this.pronService.getRandomReferenceText();
build() {
Column() {
// 参考文本
Text('请朗读以下句子:')
.fontSize(16)
.margin({ bottom: 10 });
Text(this.referenceText)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Center)
.margin({ bottom: 30 });
// 评分结果
if (this.evaluationResult) {
this.buildScoreDisplay();
// 控制按钮
Row() {
Button(this.isEvaluating ? '停止评估' : '开始评估')
.type(ButtonType.Capsule)
.width('40%')
.backgroundColor(this.isEvaluating ? '#F44336' : '#4CAF50')
.fontColor('#FFFFFF')
.onClick(() => {
this.toggleEvaluation();
});
if (!this.isEvaluating && !this.evaluationResult) {
Button('换一句')
.type(ButtonType.Capsule)
.width('40%')
.backgroundColor('#2196F3')
.fontColor('#FFFFFF')
.margin({ left: 20 })
.onClick(() => {
this.loadReferenceText();
});
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 30 });
.width(‘100%’)
.height('100%')
.padding(20);
@Builder
private buildScoreDisplay() {
Column() {
// 综合评分
Row() {
Text(‘综合评分:’)
.fontSize(18)
.margin({ right: 10 });
Text(${this.evaluationResult?.scores.overall || 0})
.fontSize(36)
.fontColor(this.getScoreColor(this.evaluationResult?.scores.overall || 0));
.width(‘100%’)
.justifyContent(FlexAlign.Start)
.margin({ bottom: 30 });
// 详细评分
Grid() {
GridItem() {
this.buildMetricCard('准确度', this.evaluationResult?.scores.accuracy || 0);
GridItem() {
this.buildMetricCard('流畅度', this.evaluationResult?.scores.fluency || 0);
GridItem() {
this.buildMetricCard('完整度', this.evaluationResult?.scores.completeness || 0);
}
.columnsTemplate('1fr 1fr')
.rowsTemplate('1fr')
.columnsGap(10)
.rowsGap(10)
.width('100%')
.height(100)
.margin({ bottom: 20 });
// 用户发音
Text(你的发音: ${this.evaluationResult?.pronunciation || ''})
.fontSize(16)
.margin({ bottom: 20 });
// 反馈建议
if (this.evaluationResult?.feedback && this.evaluationResult.feedback.length > 0) {
Column() {
Text('改进建议:')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 });
ForEach(this.evaluationResult.feedback, (item) => {
Text(• ${item})
.fontSize(14)
.margin({ bottom: 5 });
})
.width(‘100%’)
.padding(15)
.backgroundColor('#FFF3E0')
.borderRadius(10);
}
@Builder
private buildMetricCard(title: string, score: number) {
Column() {
Text(title)
.fontSize(14)
.fontColor(‘#666666’);
Row() {
Text(${score})
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ right: 5 });
Text('/100')
.fontSize(12)
.fontColor('#999999');
.margin({ top: 5 });
Progress({ value: score, total: 100, type: ProgressType.Linear })
.width('100%')
.height(6)
.margin({ top: 5 })
.color('#4CAF50');
.width(‘100%’)
.height('100%')
.padding(10)
.backgroundColor('#FFFFFF')
.borderRadius(10)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center);
private async toggleEvaluation(): Promise<void> {
if (this.isEvaluating) {
await this.pronService.stopEvaluation();
this.evaluationResult = await this.pronService.getCurrentResult();
else {
await this.pronService.startEvaluation(this.referenceText);
this.isEvaluating = !this.isEvaluating;
private getScoreColor(score: number): string {
if (score >= 85) return '#4CAF50';
if (score >= 70) return '#FFC107';
return '#F44336';
}
主界面实现
// src/main/ets/pages/PronunciationPage.ets
import { PronunciationService } from ‘…/service/PronunciationService’;
import { PronunciationCard } from ‘…/components/PronunciationCard’;
@Entry
@Component
struct PronunciationPage {
@State activeTab: number = 0;
@State deviceList: string[] = [];
private pronService = PronunciationService.getInstance();
build() {
Column() {
// 标题
Text(‘外语发音评分’)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 });
// 标签页
Tabs({ barPosition: BarPosition.Start }) {
TabContent() {
// 发音评估标签页
PronunciationCard()
.tabBar(‘发音评估’);
TabContent() {
// 历史记录标签页
this.buildHistoryTab()
.tabBar(‘历史记录’);
TabContent() {
// 设备管理标签页
this.buildDevicesTab()
.tabBar(‘设备管理’);
.barWidth(‘100%’)
.barHeight(50)
.width('100%')
.height('80%')
.width(‘100%’)
.height('100%')
.padding(20)
.onAppear(() => {
// 模拟获取设备列表
setTimeout(() => {
this.deviceList = ['我的手机', '智能手表', '平板电脑'];
}, 1000);
});
@Builder
private buildHistoryTab() {
Column() {
Text(‘历史评分记录’)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 });
// 模拟历史数据
List({ space: 15 }) {
ListItem() {
this.buildHistoryItem('Hello world', 85, '10分钟前');
ListItem() {
this.buildHistoryItem('How are you', 76, '30分钟前');
ListItem() {
this.buildHistoryItem('I love programming', 92, '1小时前');
}
.width('100%')
.layoutWeight(1);
.width(‘100%’)
.height('100%')
.padding(10);
@Builder
private buildHistoryItem(text: string, score: number, time: string) {
Row() {
Column() {
Text(text)
.fontSize(16)
.fontWeight(FontWeight.Bold);
Text(time)
.fontSize(12)
.fontColor('#666666')
.margin({ top: 5 });
.layoutWeight(1);
Text(${score})
.fontSize(24)
.fontColor(this.getScoreColor(score));
.width(‘100%’)
.padding(15)
.backgroundColor('#FFFFFF')
.borderRadius(10);
@Builder
private buildDevicesTab() {
Column() {
Text(‘已连接设备’)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 });
if (this.deviceList.length > 0) {
List({ space: 15 }) {
ForEach(this.deviceList, (device) => {
ListItem() {
Row() {
Image($r('app.media.ic_device'))
.width(40)
.height(40)
.margin({ right: 15 });
Text(device)
.fontSize(16)
.layoutWeight(1);
if (device === '我的手机') {
Text('主设备')
.fontSize(14)
.fontColor('#4CAF50');
}
.width('100%')
.padding(15)
.backgroundColor('#FFFFFF')
.borderRadius(10)
})
.width(‘100%’)
.layoutWeight(1);
else {
Text('没有连接的设备')
.fontSize(16)
.fontColor('#666666')
.margin({ top: 50 });
Button(‘添加设备’)
.type(ButtonType.Capsule)
.width('80%')
.margin({ top: 30 })
.backgroundColor('#2196F3')
.fontColor('#FFFFFF');
.width(‘100%’)
.height('100%')
.padding(10);
private getScoreColor(score: number): string {
if (score >= 85) return '#4CAF50';
if (score >= 70) return '#FFC107';
return '#F44336';
}
四、与游戏同步技术的结合点
分布式状态同步:借鉴游戏中多玩家状态同步机制,实现发音评分结果的跨设备同步
实时音频流处理:类似游戏中的实时数据流,处理音频数据
设备角色分配:类似游戏中的主机/客户端角色,确定主评分设备和从属设备
时间同步机制:确保多设备间的时间戳一致,类似游戏中的时间同步
数据压缩传输:优化音频和评分数据的传输效率,类似游戏中的网络优化
五、关键特性实现
语音识别配置:
const config: speechRecognizer.SpeechRecognizerConfig = {
language: 'en-US', // 英语(美国)
grammar: 'default'
};
发音准确度计算:
private calculateAccuracy(reference: string, pronunciation: string): number {
const distance = this.levenshteinDistance(reference.toLowerCase(), pronunciation.toLowerCase());
const maxLength = Math.max(reference.length, pronunciation.length);
return Math.max(0, 100 - (distance / maxLength * 100));
流畅度评估:
private calculateFluency(pronunciation: string): number {
const words = pronunciation.split(' ');
const wordLengths = words.map(word => word.length);
const avgLength = wordLengths.reduce((sum, len) => sum + len, 0) / wordLengths.length;
const lengthScore = Math.min(100, 100 - Math.abs(avgLength - 4.5) * 10);
const countScore = Math.min(100, words.length * 20);
return Math.floor((lengthScore + countScore) / 2);
数据同步:
private async syncResult(): Promise<void> {
if (this.kvStore && this.currentResult) {
await this.kvStore.put('evaluation_result', { value: this.currentResult });
}
六、性能优化策略
音频采样率优化:
const audioStreamInfo: audio.AudioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000, // 16kHz采样率
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE
};
批量数据处理:
private scheduleSync(): void {
if (this.syncTimer) clearTimeout(this.syncTimer);
this.syncTimer = setTimeout(() => {
this.syncResult();
this.syncTimer = null;
}, 2000); // 2秒内多次更新只同步一次
本地缓存优先:
public async getCurrentResult(): Promise<EvaluationResult | null> {
// 先返回本地缓存
const cachedResult = this.currentResult;
// 异步从分布式存储获取最新结果
if (this.kvStore) {
this.kvStore.get('evaluation_result').then((entry) => {
if (entry?.value) {
this.currentResult = entry.value;
});
return cachedResult;
资源释放管理:
public async destroy(): Promise<void> {
if (this.audioCapturer) {
await this.audioCapturer.release();
if (this.speechRecognizer) {
await this.speechRecognizer.destroy();
}
七、项目扩展方向
多语言支持:扩展支持更多语言的发音评估
发音示范:提供标准发音示范音频
长期进步追踪:记录用户发音进步曲线
社交功能:分享评分结果和进步
发音训练课程:基于薄弱环节推荐训练内容
八、总结
本外语发音评分系统实现了以下核心功能:
基于HarmonyOS语音识别能力的发音评估
多维度评分体系(准确度、流畅度、完整度)
个性化改进建议生成
多设备间的评分结果同步
通过借鉴游戏中的多设备同步技术,我们构建了一个专业的外语发音训练工具。该项目展示了HarmonyOS在语音处理和分布式技术方面的强大能力,为开发者提供了教育类应用开发的参考方案。
