HarmonyOS 智能工具箱(六):自然语言分析工具
系列: HarmonyOS 智能工具箱 App 全栈开发 · 第 6 篇
上一篇: 端侧模型推理实战
引言
前五篇我们完成了 OCR 识别、语音交互、图像处理和模型推理。本篇将聚焦自然语言处理(NLP)——在设备端对中文文本进行分词、命名实体识别(NER)和关键词提取。
Natural Language Kit 提供端侧 NLP 能力,所有处理在设备本地完成,无需网络连接,适合对隐私敏感的文本分析场景。结合第三篇的 TTS 语音朗读,我们还能让分析结果"开口说话"。
通过本文你将学到:
- 使用 Natural Language Kit 实现中文分词、NER 和关键词提取
- 封装 NlpService 统一管理 NLP 任务
- 构建关键词云和实体高亮的可视化 UI
- 集成 TTS 朗读分析结果
环境准备
| 项目 | 版本 |
|---|---|
| DevEco Studio | 6.1.1 Release (6.1.1.280) |
| API Level | API 24 (HarmonyOS 6.1.1 Release) |
| 设备 | 真机 / 模拟器 |
依赖配置
// oh-package.json5
{
"dependencies": {
"@kit.NaturalLanguageKit": "^1.0.0",
"@kit.CoreSpeechKit": "^1.0.0",
"@kit.PerformanceAnalysisKit": "^1.0.0"
}
}
Natural Language Kit 无需额外权限,所有处理在端侧完成。TTS 功能复用第三篇的 SpeechService。
核心实现
Step 1: 封装 NLP 服务层
目标: 统一封装分词、NER、关键词提取三大能力,提供简洁的服务接口。
// service/NlpService.ets
import { nlu, WordSegmentationResult, NERResult, KeywordExtractionResult } from '@kit.NaturalLanguageKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = 'NlpService';
const DOMAIN = 0xFF00;
// 分词结果项
export class WordItem {
word: string = '';
pos: string = '';
}
// 实体识别结果项
export class EntityItem {
text: string = '';
type: string = '';
offset: number = 0;
}
// 关键词结果项
export class KeywordItem {
keyword: string = '';
score: number = 0;
}
// 综合分析结果
export class NlpAnalysisResult {
words: WordItem[] = [];
entities: EntityItem[] = [];
keywords: KeywordItem[] = [];
textLength: number = 0;
analyzeTime: number = 0;
}
// 实体类型枚举
export enum EntityType {
PERSON = 'PER',
LOCATION = 'LOC',
ORGANIZATION = 'ORG'
}
// 实体类型中文映射
export const ENTITY_TYPE_MAP: Record<string, string> = {
'PER': '人名',
'LOC': '地名',
'ORG': '机构'
};
// 词性中文映射
export const POS_MAP: Record<string, string> = {
'n': '名词',
'v': '动词',
'a': '形容词',
'd': '副词',
'r': '代词',
'm': '数词',
'q': '量词',
'p': '介词',
'c': '连词',
'u': '助词',
'e': '叹词',
'y': '语气词',
'o': '拟声词',
'x': '非语素字',
'w': '标点'
};
export class NlpService {
/**
* 中文分词
*/
static async segmentWords(text: string): Promise<WordItem[]> {
try {
const result: WordSegmentationResult = await nlu.wordSegmentation(text);
const words: WordItem[] = result.words.map((w) => {
const item = new WordItem();
item.word = w.word;
item.pos = w.pos;
return item;
});
hilog.info(DOMAIN, TAG, `分词完成: ${words.length} 个词`);
return words;
} catch (err) {
hilog.error(DOMAIN, TAG, `分词失败: ${JSON.stringify(err)}`);
return [];
}
}
/**
* 命名实体识别
*/
static async recognizeEntities(text: string): Promise<EntityItem[]> {
try {
const result: NERResult = await nlu.namedEntityRecognition(text);
const entities: EntityItem[] = result.entities.map((e) => {
const item = new EntityItem();
item.text = e.text;
item.type = e.type;
item.offset = e.offset;
return item;
});
hilog.info(DOMAIN, TAG, `实体识别完成: ${entities.length} 个实体`);
return entities;
} catch (err) {
hilog.error(DOMAIN, TAG, `实体识别失败: ${JSON.stringify(err)}`);
return [];
}
}
/**
* 关键词提取
*/
static async extractKeywords(text: string, topK: number = 10): Promise<KeywordItem[]> {
try {
const result: KeywordExtractionResult = await nlu.keywordExtraction(text, topK);
const keywords: KeywordItem[] = result.keywords.map((k) => {
const item = new KeywordItem();
item.keyword = k.keyword;
item.score = k.score;
return item;
});
hilog.info(DOMAIN, TAG, `关键词提取完成: ${keywords.length} 个关键词`);
return keywords;
} catch (err) {
hilog.error(DOMAIN, TAG, `关键词提取失败: ${JSON.stringify(err)}`);
return [];
}
}
/**
* 综合文本分析:并行执行分词、NER、关键词提取
*/
static async analyzeText(text: string, topK: number = 10): Promise<NlpAnalysisResult> {
const startTime = Date.now();
const [words, entities, keywords] = await Promise.all([
this.segmentWords(text),
this.recognizeEntities(text),
this.extractKeywords(text, topK)
]);
const result = new NlpAnalysisResult();
result.words = words;
result.entities = entities;
result.keywords = keywords;
result.textLength = text.length;
result.analyzeTime = Date.now() - startTime;
hilog.info(DOMAIN, TAG,
`综合分析完成: ${result.analyzeTime}ms, ${words.length}词, ${entities.length}实体, ${keywords.length}关键词`);
return result;
}
/**
* 按实体类型分组
*/
static groupEntitiesByType(entities: EntityItem[]): Map<string, EntityItem[]> {
const groups = new Map<string, EntityItem[]>();
for (const entity of entities) {
const list = groups.get(entity.type) ?? [];
list.push(entity);
groups.set(entity.type, list);
}
return groups;
}
/**
* 生成文本朗读摘要
*/
static generateSpeechText(result: NlpAnalysisResult): string {
const parts: string[] = [];
parts.push(`文本共 ${result.textLength} 个字符,分析耗时 ${result.analyzeTime} 毫秒。`);
if (result.keywords.length > 0) {
const kwText = result.keywords.slice(0, 5).map((k) => k.keyword).join('、');
parts.push(`关键词包括:${kwText}。`);
}
if (result.entities.length > 0) {
const groups = this.groupEntitiesByType(result.entities);
groups.forEach((list, type) => {
const typeName = ENTITY_TYPE_MAP[type] ?? type;
const names = list.map((e) => e.text).join('、');
parts.push(`${typeName}:${names}。`);
});
}
return parts.join('');
}
}
要点说明:
- 三个 NLP 接口通过
Promise.all并行调用,互不依赖,显著提升效率 - 综合分析结果包含分词、实体、关键词三类数据,方便 UI 层统一展示
generateSpeechText方法将分析结果转为自然语言摘要,供 TTS 朗读使用
Step 2: 创建 NLP 分析页面
目标: 实现文本输入和分析结果展示的主页面,使用 @ObservedV2/@Trace 管理状态。
// features/nlp/NlpPage.ets
import { NlpService, NlpAnalysisResult, WordItem, EntityItem, KeywordItem, ENTITY_TYPE_MAP, POS_MAP } from '../../service/NlpService';
import { SpeechService } from '../../service/SpeechService';
import { KeywordCloud } from './KeywordCloud';
import { EntityHighlight } from './EntityHighlight';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = 'NlpPage';
// 分析状态管理(V2 状态管理)
@ObservedV2
class NlpPageState {
@Trace inputText: string = '';
@Trace isAnalyzing: boolean = false;
@Trace result: NlpAnalysisResult | null = null;
@Trace activeTab: number = 0;
@Trace isSpeaking: boolean = false;
}
@Entry
@Component
struct NlpPage {
private state: NlpPageState = new NlpPageState();
private tabs: string[] = ['关键词', '实体', '分词'];
private sampleTexts: string[] = [
'任正非在深圳华为总部接受了媒体采访,表示鸿蒙操作系统将在万物互联时代发挥关键作用。',
'HarmonyOS 是一款面向万物互联时代的智能终端操作系统,分布式能力是其核心特性。',
'北京市海淀区的清华大学和北京大学是中国最著名的高等学府,每年吸引大量国内外学生。'
];
build() {
Column({ space: 12 }) {
// 标题栏
Row() {
Text('自然语言分析')
.fontSize(24)
.fontWeight(FontWeight.Bold)
Blank()
if (this.state.isSpeaking) {
Button('停止朗读')
.type(ButtonType.Normal)
.height(32)
.fontSize(12)
.onClick(() => this.stopSpeaking())
}
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
// 文本输入区
Column({ space: 8 }) {
TextArea({ text: this.state.inputText, placeholder: '请输入要分析的中文文本...' })
.height(120)
.fontSize(14)
.onChange((value: string) => {
this.state.inputText = value;
})
// 示例文本
Row({ space: 8 }) {
Text('示例:').fontSize(12).fontColor('#999')
ForEach(this.sampleTexts, (_: string, index: number) => {
Button(`示例${index + 1}`)
.type(ButtonType.Normal)
.height(24)
.fontSize(11)
.onClick(() => {
this.state.inputText = this.sampleTexts[index];
})
})
}
.width('100%')
}
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(8)
// 操作按钮
Row({ space: 8 }) {
Button('开始分析')
.type(ButtonType.Capsule)
.layoutWeight(1)
.height(40)
.enabled(this.state.inputText.length > 0 && !this.state.isAnalyzing)
.onClick(() => this.startAnalysis())
Button('朗读结果')
.type(ButtonType.Capsule)
.layoutWeight(1)
.height(40)
.enabled(this.state.result !== null && !this.state.isSpeaking)
.onClick(() => this.speakResult())
}
.width('100%')
.padding({ left: 16, right: 16 })
// 分析中提示
if (this.state.isAnalyzing) {
Row({ space: 8 }) {
LoadingProgress().width(24).height(24)
Text('正在分析中...').fontSize(13).fontColor('#666')
}
.padding(12)
}
// 分析结果
if (this.state.result !== null) {
this.buildResultSection()
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
buildResultSection() {
Column({ space: 12 }) {
// 统计信息
Row({ space: 16 }) {
this.buildStatItem('字符数', `${this.state.result!.textLength}`)
this.buildStatItem('分词数', `${this.state.result!.words.length}`)
this.buildStatItem('实体数', `${this.state.result!.entities.length}`)
this.buildStatItem('关键词', `${this.state.result!.keywords.length}`)
this.buildStatItem('耗时', `${this.state.result!.analyzeTime}ms`)
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(8)
// Tab 切换
Row({ space: 0 }) {
ForEach(this.tabs, (tab: string, index: number) => {
Column() {
Text(tab)
.fontSize(14)
.fontColor(this.state.activeTab === index ? '#007DFF' : '#666')
.fontWeight(this.state.activeTab === index ? FontWeight.Bold : FontWeight.Normal)
Divider()
.strokeWidth(2)
.color(this.state.activeTab === index ? '#007DFF' : 'transparent')
.width('80%')
}
.layoutWeight(1)
.height(40)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.state.activeTab = index;
})
})
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(8)
// Tab 内容
if (this.state.activeTab === 0) {
this.buildKeywordsTab()
} else if (this.state.activeTab === 1) {
this.buildEntitiesTab()
} else {
this.buildWordsTab()
}
}
.padding({ left: 16, right: 16 })
}
@Builder
buildStatItem(label: string, value: string) {
Column({ space: 4 }) {
Text(value)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#007DFF')
Text(label)
.fontSize(11)
.fontColor('#999')
}
.alignItems(HorizontalAlign.Center)
}
@Builder
buildKeywordsTab() {
Column({ space: 8 }) {
Text('关键词提取结果')
.fontSize(14)
.fontColor('#666')
.width('100%')
.padding({ left: 4 })
KeywordCloud({ keywords: this.state.result!.keywords })
}
}
@Builder
buildEntitiesTab() {
Column({ space: 8 }) {
Text('实体识别结果')
.fontSize(14)
.fontColor('#666')
.width('100%')
.padding({ left: 4 })
EntityHighlight({
originalText: this.state.inputText,
entities: this.state.result!.entities
})
}
}
@Builder
buildWordsTab() {
Column({ space: 8 }) {
Row() {
Text('分词结果')
.fontSize(14)
.fontColor('#666')
Blank()
Text(`共 ${this.state.result!.words.length} 个词`)
.fontSize(12)
.fontColor('#999')
}
.width('100%')
.padding({ left: 4 })
Flex({ wrap: FlexWrap.Wrap, space: { main: new LengthMetrics(8), cross: new LengthMetrics(6) } }) {
ForEach(this.state.result!.words, (item: WordItem) => {
Column({ space: 2 }) {
Text(item.word)
.fontSize(14)
.fontColor('#333')
Text(POS_MAP[item.pos] ?? item.pos)
.fontSize(10)
.fontColor('#999')
}
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor('#F0F8FF')
.borderRadius(4)
})
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(8)
}
}
async startAnalysis(): Promise<void> {
if (this.state.inputText.length === 0) return;
this.state.isAnalyzing = true;
this.state.result = null;
try {
const result = await NlpService.analyzeText(this.state.inputText);
this.state.result = result;
this.state.activeTab = 0;
} catch (err) {
hilog.error(0xFF00, TAG, `分析失败: ${JSON.stringify(err)}`);
} finally {
this.state.isAnalyzing = false;
}
}
async speakResult(): Promise<void> {
if (!this.state.result) return;
this.state.isSpeaking = true;
const text = NlpService.generateSpeechText(this.state.result);
try {
await SpeechService.speak(text);
} catch (err) {
hilog.error(0xFF00, TAG, `朗读失败: ${JSON.stringify(err)}`);
} finally {
this.state.isSpeaking = false;
}
}
stopSpeaking(): void {
SpeechService.stop();
this.state.isSpeaking = false;
}
}
要点说明:
- 使用
@ObservedV2/@Trace实现 V2 状态管理,响应式更新 UI - 三个 Tab 分别展示关键词、实体、分词结果,通过
activeTab切换 - 示例文本按钮方便快速体验功能
Step 3: 创建关键词云组件
目标: 以标签云形式可视化展示关键词,字号随权重变化。
// features/nlp/KeywordCloud.ets
import { KeywordItem } from '../../service/NlpService';
// 关键词颜色池
const KEYWORD_COLORS: string[] = [
'#007DFF', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4',
'#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE'
];
@Component
struct KeywordCloud {
@Prop keywords: KeywordItem[] = [];
build() {
Column({ space: 8 }) {
if (this.keywords.length === 0) {
Text('暂无关键词')
.fontSize(13)
.fontColor('#999')
.width('100%')
.textAlign(TextAlign.Center)
.padding(24)
} else {
Flex({ wrap: FlexWrap.Wrap, space: { main: new LengthMetrics(10), cross: new LengthMetrics(8) } }) {
ForEach(this.keywords, (item: KeywordItem, index: number) => {
this.buildKeywordTag(item, index)
})
}
.width('100%')
.padding(12)
}
// 权重分布条
if (this.keywords.length > 0) {
Column({ space: 4 }) {
Text('权重分布')
.fontSize(12)
.fontColor('#999')
.width('100%')
ForEach(this.keywords.slice(0, 5), (item: KeywordItem, index: number) => {
Row({ space: 8 }) {
Text(item.keyword)
.fontSize(11)
.fontColor('#666')
.width(60)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Stack({ alignContent: Alignment.Start }) {
Row()
.width(`${this.getBarWidth(item.score)}%`)
.height(12)
.borderRadius(6)
.backgroundColor(KEYWORD_COLORS[index % KEYWORD_COLORS.length])
Row()
.width('100%')
.height(12)
.borderRadius(6)
.backgroundColor('#F0F0F0')
}
.layoutWeight(1)
.height(12)
Text(item.score.toFixed(3))
.fontSize(10)
.fontColor('#999')
.width(40)
.textAlign(TextAlign.End)
}
.width('100%')
})
}
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(8)
}
}
}
@Builder
buildKeywordTag(item: KeywordItem, index: number) {
Column() {
Text(item.keyword)
.fontSize(this.getFontSize(item.score))
.fontWeight(FontWeight.Medium)
.fontColor(KEYWORD_COLORS[index % KEYWORD_COLORS.length])
}
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(this.getTagBg(index))
.borderRadius(16)
}
getFontSize(score: number): number {
// 根据权重映射字号:12-22
const minSize = 12;
const maxSize = 22;
return Math.round(minSize + score * (maxSize - minSize));
}
getBarWidth(score: number): number {
return Math.round(score * 100);
}
getTagBg(index: number): string {
const bgColors: string[] = [
'#EBF5FF', '#FFF0F0', '#E8FFF8', '#EBF8FF', '#F0FFF0',
'#FFFBE6', '#F5F0FF', '#E8F8F5', '#FFF9E6', '#F0EBFF'
];
return bgColors[index % bgColors.length];
}
}
要点说明:
- 关键词字号根据权重动态计算,权重越高字号越大
- 权重分布条直观展示各关键词的重要性对比
- 颜色池循环使用,视觉效果丰富
Step 4: 创建实体识别结果组件
目标: 在原文中高亮显示识别出的实体,按类型标注不同颜色。
// features/nlp/EntityHighlight.ets
import { EntityItem, ENTITY_TYPE_MAP } from '../../service/NlpService';
// 实体类型颜色映射
const ENTITY_COLORS: Record<string, { bg: string; text: string; border: string }> = {
'PER': { bg: '#FFF0F0', text: '#D32F2F', border: '#FFCDD2' },
'LOC': { bg: '#E8F5E9', text: '#2E7D32', border: '#C8E6C9' },
'ORG': { bg: '#E3F2FD', text: '#1565C0', border: '#BBDEFB' }
};
// 文本片段
interface TextSegment {
text: string;
isEntity: boolean;
entityType: string;
}
@Component
struct EntityHighlight {
@Prop originalText: string = '';
@Prop entities: EntityItem[] = [];
build() {
Column({ space: 12 }) {
// 实体统计
this.buildEntityStats()
// 高亮文本
Column({ space: 8 }) {
Text('文本高亮')
.fontSize(14)
.fontColor('#666')
.width('100%')
this.buildHighlightedText()
}
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(8)
// 实体列表
this.buildEntityList()
}
}
@Builder
buildEntityStats() {
Row({ space: 12 }) {
ForEach(Object.keys(ENTITY_COLORS), (type: string) => {
Row({ space: 4 }) {
Circle({ width: 8, height: 8 })
.fill(ENTITY_COLORS[type].text)
Text(ENTITY_TYPE_MAP[type] ?? type)
.fontSize(12)
.fontColor('#666')
Text(`${this.getEntitiesByType(type).length}`)
.fontSize(12)
.fontColor(ENTITY_COLORS[type].text)
.fontWeight(FontWeight.Bold)
}
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor(ENTITY_COLORS[type].bg)
.borderRadius(12)
.border({ width: 1, color: ENTITY_COLORS[type].border })
})
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(8)
}
@Builder
buildHighlightedText() {
// 将文本拆分为普通片段和实体片段
Flex({ wrap: FlexWrap.Wrap, space: { main: new LengthMetrics(0), cross: new LengthMetrics(0) } }) {
ForEach(this.splitTextIntoSegments(), (segment: TextSegment) => {
if (segment.isEntity) {
Text(segment.text)
.fontSize(14)
.fontColor(ENTITY_COLORS[segment.entityType]?.text ?? '#333')
.backgroundColor(ENTITY_COLORS[segment.entityType]?.bg ?? '#F0F0F0')
.borderRadius(2)
.padding({ left: 2, right: 2 })
.decoration({ type: TextDecorationType.Underline, color: ENTITY_COLORS[segment.entityType]?.border ?? '#CCC' })
} else {
Text(segment.text)
.fontSize(14)
.fontColor('#333')
}
})
}
.width('100%')
.padding(12)
.backgroundColor('#FAFAFA')
.borderRadius(6)
}
@Builder
buildEntityList() {
Column({ space: 8 }) {
Text('实体列表')
.fontSize(14)
.fontColor('#666')
.width('100%')
List({ space: 6 }) {
ForEach(this.entities, (entity: EntityItem) => {
ListItem() {
Row({ space: 8 }) {
// 类型标签
Text(ENTITY_TYPE_MAP[entity.type] ?? entity.type)
.fontSize(11)
.fontColor(ENTITY_COLORS[entity.type]?.text ?? '#333')
.backgroundColor(ENTITY_COLORS[entity.type]?.bg ?? '#F0F0F0')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
// 实体文本
Text(entity.text)
.fontSize(14)
.fontColor('#333')
.fontWeight(FontWeight.Medium)
Blank()
// 位置信息
Text(`位置: ${entity.offset}`)
.fontSize(11)
.fontColor('#999')
}
.width('100%')
.padding(10)
.backgroundColor('#FFFFFF')
.borderRadius(6)
}
})
}
.width('100%')
}
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(8)
}
getEntitiesByType(type: string): EntityItem[] {
return this.entities.filter((e) => e.type === type);
}
splitTextIntoSegments(): TextSegment[] {
if (this.entities.length === 0) {
return [{ text: this.originalText, isEntity: false, entityType: '' }];
}
const segments: TextSegment[] = [];
let lastEnd = 0;
// 按 offset 排序
const sorted = [...this.entities].sort((a, b) => a.offset - b.offset);
for (const entity of sorted) {
// 实体前的普通文本
if (entity.offset > lastEnd) {
segments.push({
text: this.originalText.substring(lastEnd, entity.offset),
isEntity: false,
entityType: ''
});
}
// 实体文本
segments.push({
text: entity.text,
isEntity: true,
entityType: entity.type
});
lastEnd = entity.offset + entity.text.length;
}
// 最后一段普通文本
if (lastEnd < this.originalText.length) {
segments.push({
text: this.originalText.substring(lastEnd),
isEntity: false,
entityType: ''
});
}
return segments;
}
}
要点说明:
- 三种实体类型(人名/地名/机构)使用不同颜色方案,一目了然
splitTextIntoSegments方法将原文按实体位置拆分为高亮片段- 实体列表展示完整的识别结果,包含类型、文本和位置信息
Step 5: 集成 TTS 朗读
目标: 复用第三篇的 SpeechService,实现分析结果的语音朗读。
// service/SpeechService.ets(第三篇已有,此处展示 NLP 相关调用方式)
import { textToSpeech } from '@kit.CoreSpeechKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = 'SpeechService';
const DOMAIN = 0xFF00;
export class SpeechService {
private static ttsEngine: textToSpeech.TextToSpeechEngine | null = null;
static async init(): Promise<void> {
if (this.ttsEngine) return;
try {
this.ttsEngine = await textToSpeech.createEngine({
language: 'zh-CN',
person: 0,
online: 0
});
hilog.info(DOMAIN, TAG, 'TTS 引擎初始化成功');
} catch (err) {
hilog.error(DOMAIN, TAG, `TTS 初始化失败: ${JSON.stringify(err)}`);
}
}
static async speak(text: string): Promise<void> {
await this.init();
if (!this.ttsEngine) {
hilog.error(DOMAIN, TAG, 'TTS 引擎未初始化');
return;
}
return new Promise<void>((resolve, reject) => {
this.ttsEngine!.speak(text, {
requestId: `nlp_${Date.now()}`,
callback: (err: Error) => {
if (err) {
hilog.error(DOMAIN, TAG, `朗读失败: ${JSON.stringify(err)}`);
reject(err);
} else {
resolve();
}
}
});
});
}
static stop(): void {
if (this.ttsEngine) {
this.ttsEngine.stop();
}
}
static release(): void {
if (this.ttsEngine) {
this.ttsEngine.shutdown();
this.ttsEngine = null;
}
}
}
NlpService 中的 generateSpeechText 方法将分析结果转为自然语言摘要:
// NlpService.generateSpeechText 示例输出:
// "文本共 42 个字符,分析耗时 120 毫秒。关键词包括:鸿蒙、操作系统、万物互联。人名:任正非。地名:深圳。机构:华为。"
用户点击"朗读结果"按钮时,NlpPage 调用 SpeechService 朗读这段摘要。
Step 6: 注册路由
目标: 将 NLP 分析页面注册到首页 Navigation 路由中。
// pages/Index.ets 中的 navDestination 部分
import { NlpPage } from '../features/nlp/NlpPage';
// 在 Navigation 的 navDestination 中添加路由
.navDestination((name: string, param: Object) => {
if (name === 'NlpPage') {
NlpPage()
}
})
在首页工具卡片中添加 NLP 工具入口:
// 工具卡片配置
{
id: 'nlp-analyzer',
name: '自然语言分析',
description: '中文分词、关键词提取、实体识别',
icon: $r('sys.symbol.text_format'),
route: 'NlpPage',
category: ToolCategory.NLP
}
效果展示

文本输入区域支持中文文本,可分别执行分词结果提取、命名实体识别,或一次执行两项分析。当前“关键词”列表直接来自 getWordSegment() 的分词结果,并非带权重的关键词排序算法;实体结果以“文本(类型)”标签显示。示例没有标签云权重、原文区间高亮、统计栏或朗读分析摘要。
关键代码解读
NLP 并行处理管道
const [words, entities, keywords] = await Promise.all([
this.segmentWords(text),
this.recognizeEntities(text),
this.extractKeywords(text, topK)
]);
三个 NLP 接口互不依赖,使用 Promise.all 并行执行。实测中,单独调用总耗约 300ms,并行后可缩短至 120ms 左右(取决于最长的那个任务)。这是提升 NLP 应用响应速度的关键模式。
注意:Natural Language Kit 的三个接口均为端侧计算,不涉及网络请求,并行调用不会增加带宽压力。
实体高亮的文本切分算法
splitTextIntoSegments(): TextSegment[] {
const segments: TextSegment[] = [];
let lastEnd = 0;
const sorted = [...this.entities].sort((a, b) => a.offset - b.offset);
for (const entity of sorted) {
if (entity.offset > lastEnd) {
segments.push({ text: this.originalText.substring(lastEnd, entity.offset), isEntity: false, ... });
}
segments.push({ text: entity.text, isEntity: true, entityType: entity.type });
lastEnd = entity.offset + entity.text.length;
}
if (lastEnd < this.originalText.length) {
segments.push({ text: this.originalText.substring(lastEnd), isEntity: false, ... });
}
return segments;
}
核心思路:按实体的 offset 排序后,将原文切分为"普通文本—实体—普通文本—实体—…"的交替序列。每个片段标记是否为实体及实体类型,UI 层根据标记决定渲染样式。
词性标注说明
Natural Language Kit 的分词结果使用北大标准词性标注集:
| 标注 | 含义 | 标注 | 含义 |
|---|---|---|---|
| n | 名词 | v | 动词 |
| a | 形容词 | d | 副词 |
| r | 代词 | m | 数词 |
| q | 量词 | p | 介词 |
| c | 连词 | u | 助词 |
完整标注集请参考 Natural Language Kit 官方文档。
总结
本文实现了自然语言分析工具,主要完成了:
- NlpService 服务层:统一封装分词、NER、关键词提取三大能力,支持并行调用
- NlpPage 分析页面:文本输入、三 Tab 结果展示、统计信息面板
- KeywordCloud 关键词云:标签云可视化,字号和颜色随权重变化
- EntityHighlight 实体高亮:原文中按类型分色高亮实体,附带实体列表
- TTS 朗读集成:将分析结果转为自然语言摘要并语音播报
Natural Language Kit 的端侧处理能力让文本分析无需网络,保护用户隐私的同时提供快速响应。结合关键词提取和实体识别,可以为工具箱 App 增加智能标签、内容摘要等高级功能。
下篇预告
下一篇: HarmonyOS 智能工具箱(七):收藏管理与发布优化
将实现历史记录管理、收藏功能、性能优化和多平台适配,为 App 的发布做最后准备。
参考资料
更多推荐

所有评论(0)