鸿蒙AI语音翻译便签应用设计与实现

鸿蒙AI语音翻译便签应用设计与实现

一、系统架构设计

基于HarmonyOS的AI能力和分布式技术,我们设计了一个语音翻译便签应用,能够实时将语音输入转换为文字并进行翻译,最终生成多语言便签,支持跨设备同步。

https://example.com/ai-voice-translator-arch.png

系统包含三个核心模块:

  1. ​语音识别模块​​ - 使用@ohos.multimedia.audio和AI语音识别服务
  2. ​翻译服务模块​​ - 集成云端翻译API
  3. ​分布式便签同步模块​​ - 通过@ohos.distributedData实现多设备同步

二、核心代码实现

1. 语音识别服务(ArkTS)

// VoiceRecognitionService.ets
import audio from '@ohos.multimedia.audio';
import plugin from '@ohos.ai.nlp';

const TRANSLATION_PLUGIN_NAME = 'com.example.translation';

class VoiceRecognitionService {
  private static instance: VoiceRecognitionService = null;
  private audioCapturer: audio.AudioCapturer;
  private translationPlugin: plugin.NlpPlugin;
  private listeners: VoiceRecognitionListener[] = [];
  private isRecognizing: boolean = false;
  
  private constructor() {
    this.initAudioCapturer();
    this.initTranslationPlugin();
  }
  
  public static getInstance(): VoiceRecognitionService {
    if (!VoiceRecognitionService.instance) {
      VoiceRecognitionService.instance = new VoiceRecognitionService();
    }
    return VoiceRecognitionService.instance;
  }
  
  private initAudioCapturer(): void {
    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 = audio.createAudioCapturer({
      audioStreamInfo: audioStreamInfo,
      audioCapturerInfo: audioCapturerInfo
    });
  }
  
  private async initTranslationPlugin(): Promise {
    try {
      this.translationPlugin = await plugin.getNlpPlugin(TRANSLATION_PLUGIN_NAME);
    } catch (err) {
      console.error('初始化翻译插件失败:', JSON.stringify(err));
    }
  }
  
  public async startRecognition(): Promise {
    if (this.isRecognizing) return false;
    
    try {
      await this.audioCapturer.start();
      this.isRecognizing = true;
      
      // 开始读取音频数据并识别
      this.processAudioData();
      return true;
    } catch (err) {
      console.error('开始语音识别失败:', JSON.stringify(err));
      return false;
    }
  }
  
  public async stopRecognition(): Promise {
    if (!this.isRecognizing) return;
    
    try {
      await this.audioCapturer.stop();
      this.isRecognizing = false;
    } catch (err) {
      console.error('停止语音识别失败:', JSON.stringify(err));
    }
  }
  
  private async processAudioData(): Promise {
    while (this.isRecognizing) {
      const buffer = await this.audioCapturer.read();
      if (buffer && buffer.byteLength > 0) {
        // 实际应用中这里应该调用语音识别API
        const text = await this.mockSpeechToText(buffer);
        this.notifyRecognitionResult(text);
      }
    }
  }
  
  private async mockSpeechToText(audioData: ArrayBuffer): Promise {
    // 模拟语音识别,实际应用中应调用真实API
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve("这是模拟的语音识别结果");
      }, 500);
    });
  }
  
  public async translate(text: string, targetLang: string): Promise {
    if (!this.translationPlugin) {
      console.error('翻译插件未初始化');
      return text;
    }
    
    try {
      const result = await this.translationPlugin.translate({
        text: text,
        targetLang: targetLang,
        sourceLang: 'auto'
      });
      return result.translatedText;
    } catch (err) {
      console.error('翻译失败:', JSON.stringify(err));
      return text;
    }
  }
  
  public addListener(listener: VoiceRecognitionListener): void {
    if (!this.listeners.includes(listener)) {
      this.listeners.push(listener);
    }
  }
  
  public removeListener(listener: VoiceRecognitionListener): void {
    this.listeners = this.listeners.filter(l => l !== listener);
  }
  
  private notifyRecognitionResult(text: string): void {
    this.listeners.forEach(listener => {
      listener.onRecognitionResult(text);
    });
  }
  
  private notifyRecognitionError(error: string): void {
    this.listeners.forEach(listener => {
      listener.onRecognitionError(error);
    });
  }
}

interface VoiceRecognitionListener {
  onRecognitionResult(text: string): void;
  onRecognitionError(error: string): void;
}

export const voiceService = VoiceRecognitionService.getInstance();

2. 便签管理服务(ArkTS)

// NoteService.ets
import distributedData from '@ohos.distributedData';

const NOTE_SYNC_CHANNEL = 'translation_notes';

class NoteService {
  private static instance: NoteService = null;
  private dataManager: distributedData.DataManager;
  private listeners: NoteListener[] = [];
  private notes: TranslationNote[] = [];
  
  private constructor() {
    this.initDataManager();
  }
  
  public static getInstance(): NoteService {
    if (!NoteService.instance) {
      NoteService.instance = new NoteService();
    }
    return NoteService.instance;
  }
  
  private initDataManager(): void {
    this.dataManager = distributedData.createDataManager({
      bundleName: 'com.example.voicenotes',
      area: distributedData.Area.GLOBAL
    });
    
    this.dataManager.registerDataListener(NOTE_SYNC_CHANNEL, (data) => {
      this.handleSyncData(data);
    });
  }
  
  public async createNote(originalText: string, translations: { [lang: string]: string }): Promise {
    const note: TranslationNote = {
      id: Date.now().toString(),
      originalText: originalText,
      translations: translations,
      createdAt: Date.now(),
      updatedAt: Date.now(),
      deviceId: this.getDeviceId()
    };
    
    this.notes.unshift(note);
    this.syncNote(note);
    return note;
  }
  
  public async updateNote(noteId: string, updates: Partial): Promise {
    const index = this.notes.findIndex(n => n.id === noteId);
    if (index === -1) return false;
    
    this.notes[index] = {
      ...this.notes[index],
      ...updates,
      updatedAt: Date.now()
    };
    
    this.syncNote(this.notes[index]);
    return true;
  }
  
  public async deleteNote(noteId: string): Promise {
    const index = this.notes.findIndex(n => n.id === noteId);
    if (index === -1) return false;
    
    this.notes.splice(index, 1);
    this.syncDelete(noteId);
    return true;
  }
  
  public getNotes(): TranslationNote[] {
    return [...this.notes];
  }
  
  private syncNote(note: TranslationNote): void {
    this.dataManager.syncData(NOTE_SYNC_CHANNEL, {
      type: 'updateNote',
      note: note
    });
  }
  
  private syncDelete(noteId: string): void {
    this.dataManager.syncData(NOTE_SYNC_CHANNEL, {
      type: 'deleteNote',
      noteId: noteId
    });
  }
  
  private handleSyncData(data: any): void {
    if (!data) return;
    
    switch (data.type) {
      case 'updateNote':
        this.handleNoteUpdate(data.note);
        break;
      case 'deleteNote':
        this.handleNoteDelete(data.noteId);
        break;
    }
  }
  
  private handleNoteUpdate(note: TranslationNote): void {
    if (note.deviceId === this.getDeviceId()) return;
    
    const index = this.notes.findIndex(n => n.id === note.id);
    if (index === -1) {
      this.notes.unshift(note);
    } else {
      this.notes[index] = note;
    }
    
    this.notifyNotesUpdated();
  }
  
  private handleNoteDelete(noteId: string): void {
    if (this.notes.some(n => n.id === noteId && n.deviceId !== this.getDeviceId())) {
      this.notes = this.notes.filter(n => n.id !== noteId);
      this.notifyNotesUpdated();
    }
  }
  
  private notifyNotesUpdated(): void {
    this.listeners.forEach(listener => {
      listener.onNotesChanged(this.getNotes());
    });
  }
  
  public addListener(listener: NoteListener): void {
    if (!this.listeners.includes(listener)) {
      this.listeners.push(listener);
    }
  }
  
  public removeListener(listener: NoteListener): void {
    this.listeners = this.listeners.filter(l => l !== listener);
  }
  
  private getDeviceId(): string {
    // 实际实现需要获取设备ID
    return 'local_device';
  }
}

interface TranslationNote {
  id: string;
  originalText: string;
  translations: { [lang: string]: string };
  createdAt: number;
  updatedAt: number;
  deviceId: string;
}

interface NoteListener {
  onNotesChanged(notes: TranslationNote[]): void;
}

export const noteService = NoteService.getInstance();

3. 语音翻译便签主界面(ArkUI)

// VoiceTranslationApp.ets
import { voiceService, noteService } from './VoiceRecognitionService';

@Entry
@Component
struct VoiceTranslationApp {
  @State isRecognizing: boolean = false;
  @State recognizedText: string = '';
  @State translatedTexts: { [lang: string]: string } = {};
  @State targetLanguages: string[] = ['en', 'ja', 'ko', 'fr'];
  @State notes: TranslationNote[] = [];
  @State activeTab: 'record' | 'notes' = 'record';
  
  aboutToAppear() {
    noteService.addListener({
      onNotesChanged: (notes) => {
        this.notes = notes;
      }
    });
    
    this.notes = noteService.getNotes();
  }
  
  build() {
    Column() {
      // 顶部选项卡
      Tabs({ barPosition: BarPosition.Start }) {
        TabContent() {
          this.buildRecorderUI()
        }
        .tabBar('语音翻译')
        
        TabContent() {
          this.buildNotesUI()
        }
        .tabBar('便签列表')
      }
      .onChange((index: number) => {
        this.activeTab = index === 0 ? 'record' : 'notes';
      })
      .height('100%')
    }
    .width('100%')
    .height('100%')
  }
  
  @Builder
  buildRecorderUI() {
    Column() {
      // 语音识别状态指示器
      Row() {
        Text(this.isRecognizing ? '正在聆听...' : '点击开始录音')
          .fontSize(18)
        
        Circle()
          .width(20)
          .height(20)
          .fill(this.isRecognizing ? Color.Red : Color.Gray)
          .margin({ left: 10 })
      }
      .margin({ bottom: 30 })
      
      // 录音控制按钮
      Button(this.isRecognizing ? '停止录音' : '开始录音')
        .width(200)
        .height(60)
        .fontSize(20)
        .onClick(async () => {
          if (this.isRecognizing) {
            await voiceService.stopRecognition();
            this.isRecognizing = false;
          } else {
            this.isRecognizing = await voiceService.startRecognition();
          }
        })
        .margin({ bottom: 30 })
      
      // 识别结果
      if (this.recognizedText) {
        Text('识别结果:')
          .fontSize(16)
          .margin({ bottom: 10 })
        
        Text(this.recognizedText)
          .fontSize(18)
          .margin({ bottom: 30 })
          .padding(10)
          .backgroundColor('#F5F5F5')
          .borderRadius(8)
      }
      
      // 翻译结果
      if (Object.keys(this.translatedTexts).length > 0) {
        Text('翻译结果:')
          .fontSize(16)
          .margin({ bottom: 10 })
        
        ForEach(this.targetLanguages, (lang) => {
          Column() {
            Text(`${this.getLanguageName(lang)}:`)
              .fontSize(14)
              .fontColor('#666666')
            
            Text(this.translatedTexts[lang] || '翻译中...')
              .fontSize(16)
              .margin({ top: 4 })
              .padding(10)
              .backgroundColor('#F0F8FF')
              .borderRadius(8)
          }
          .margin({ bottom: 15 })
        })
      }
      
      // 保存按钮
      if (this.recognizedText) {
        Button('保存为便签')
          .width(200)
          .height(50)
          .margin({ top: 30 })
          .onClick(async () => {
            const note = await noteService.createNote(
              this.recognizedText,
              this.translatedTexts
            );
            promptAction.showToast({ message: '便签已保存', duration: 2000 });
            this.resetRecorder();
          })
      }
    }
    .padding(20)
    .width('100%')
    .height('100%')
  }
  
  @Builder
  buildNotesUI() {
    Column() {
      if (this.notes.length === 0) {
        Text('没有便签记录')
          .fontSize(16)
          .margin({ top: 50 })
      } else {
        List({ space: 10 }) {
          ForEach(this.notes, (note) => {
            ListItem() {
              this.buildNoteItem(note)
            }
          })
        }
        .layoutWeight(1)
      }
    }
    .padding(20)
    .width('100%')
    .height('100%')
  }
  
  @Builder
  buildNoteItem(note: TranslationNote) {
    Column() {
      // 原始文本
      Text(note.originalText)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 8 })
      
      // 翻译文本
      ForEach(Object.entries(note.translations), ([lang, text]) => {
        Row() {
          Text(`${this.getLanguageName(lang)}:`)
            .fontSize(12)
            .fontColor('#666666')
            .width(60)
          
          Text(text)
            .fontSize(14)
            .layoutWeight(1)
        }
        .margin({ bottom: 4 })
      })
      
      // 时间和操作按钮
      Row() {
        Text(this.formatTime(note.createdAt))
          .fontSize(12)
          .fontColor('#999999')
          .layoutWeight(1)
        
        Button('删除')
          .width(80)
          .height(30)
          .fontSize(12)
          .onClick(async () => {
            await noteService.deleteNote(note.id);
            promptAction.showToast({ message: '便签已删除', duration: 1000 });
          })
      }
      .margin({ top: 8 })
    }
    .padding(15)
    .width('100%')
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
    .shadow({ radius: 2, color: '#000000', offsetX: 1, offsetY: 1 })
  }
  
  private async setupVoiceRecognition(): Promise {
    voiceService.addListener({
      onRecognitionResult: async (text) => {
        this.recognizedText = text;
        
        // 开始翻译
        for (const lang of this.targetLanguages) {
          this.translatedTexts[lang] = await voiceService.translate(text, lang);
        }
      },
      onRecognitionError: (error) => {
        promptAction.showToast({ message: `识别错误: ${error}`, duration: 2000 });
        this.isRecognizing = false;
      }
    });
  }
  
  private resetRecorder(): void {
    this.recognizedText = '';
    this.translatedTexts = {};
  }
  
  private getLanguageName(code: string): string {
    const languages: { [code: string]: string } = {
      'en': '英语',
      'ja': '日语',
      'ko': '韩语',
      'fr': '法语',
      'zh': '中文'
    };
    return languages[code] || code;
  }
  
  private formatTime(timestamp: number): string {
    const date = new Date(timestamp);
    return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${date.getMinutes()}`;
  }
}

三、项目配置

1. 权限配置

// module.json5
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "录制语音输入"
      },
      {
        "name": "ohos.permission.DISTRIBUTED_DATASYNC",
        "reason": "跨设备同步便签"
      },
      {
        "name": "ohos.permission.INTERNET",
        "reason": "访问翻译API"
      },
      {
        "name": "ohos.permission.ACCESS_NETWORK_STATE",
        "reason": "检查网络连接状态"
      }
    ],
    "abilities": [
      {
        "name": "MainAbility",
        "type": "page",
        "visible": true
      }
    ],
    "distributedNotification": {
      "scenarios": [
        {
          "name": "translation_notes",
          "value": "note_sync"
        }
      ]
    }
  }
}

2. 资源文件


{
  "string": [
    {
      "name": "app_name",
      "value": "语音翻译便签"
    },
    {
      "name": "recording_tab",
      "value": "语音翻译"
    },
    {
      "name": "notes_tab",
      "value": "便签列表"
    },
    {
      "name": "listening",
      "value": "正在聆听..."
    },
    {
      "name": "tap_to_record",
      "value": "点击开始录音"
    },
    {
      "name": "start_recording",
      "value": "开始录音"
    },
    {
      "name": "stop_recording",
      "value": "停止录音"
    },
    {
      "name": "recognition_result",
      "value": "识别结果:"
    },
    {
      "name": "translation_result",
      "value": "翻译结果:"
    },
    {
      "name": "translating",
      "value": "翻译中..."
    },
    {
      "name": "save_note",
      "value": "保存为便签"
    },
    {
      "name": "note_saved",
      "value": "便签已保存"
    },
    {
      "name": "no_notes",
      "value": "没有便签记录"
    },
    {
      "name": "delete",
      "value": "删除"
    },
    {
      "name": "note_deleted",
      "value": "便签已删除"
    },
    {
      "name": "recognition_error",
      "value": "识别错误"
    }
  ]
}

四、功能扩展

1. 离线翻译支持

// 在VoiceRecognitionService中添加离线翻译
class VoiceRecognitionService {
  private async getOfflineTranslation(text: string, targetLang: string): Promise {
    try {
      // 加载离线翻译模型
      const model = await plugin.loadNlpModel({
        modelName: `translation_${targetLang}`,
        modelPath: 'resources/rawfile/models/'
      });
      
      // 执行离线翻译
      const result = await model.translate({ text });
      return result.translatedText;
    } catch (err) {
      console.error('离线翻译失败:', JSON.stringify(err));
      return text;
    }
  }
  
  public async translate(text: string, targetLang: string): Promise {
    if (navigator.onLine) {
      // 优先使用在线翻译
      try {
        return await this.getOnlineTranslation(text, targetLang);
      } catch (err) {
        console.error('在线翻译失败,尝试离线翻译:', JSON.stringify(err));
      }
    }
    
    // 回退到离线翻译
    return await this.getOfflineTranslation(text, targetLang);
  }
  
  private async getOnlineTranslation(text: string, targetLang: string): Promise {
    // 实现在线翻译逻辑
    // ...
  }
}

2. 语音合成输出

// 添加语音合成服务
import audio from '@ohos.multimedia.audio';
import tts from '@ohos.multimedia.tts';

class TextToSpeechService {
  private audioRenderer: audio.AudioRenderer;
  private ttsEngine: tts.TtsEngine;
  
  constructor() {
    this.initAudioRenderer();
    this.initTtsEngine();
  }
  
  private initAudioRenderer(): void {
    const audioStreamInfo: audio.AudioStreamInfo = {
      samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
      channels: audio.AudioChannel.CHANNEL_2,
      sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
      encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
    };
    
    this.audioRenderer = audio.createAudioRenderer({
      audioStreamInfo: audioStreamInfo
    });
  }
  
  private async initTtsEngine(): Promise {
    this.ttsEngine = await tts.createTtsEngine();
    await this.ttsEngine.init({
      volume: 0.8,
      speed: 1.0,
      pitch: 1.0
    });
  }
  
  public async speak(text: string, lang: string): Promise {
    try {
      await this.ttsEngine.setLanguage(lang);
      await this.ttsEngine.speak(text);
    } catch (err) {
      console.error('语音合成失败:', JSON.stringify(err));
    }
  }
  
  public async stop(): Promise {
    await this.ttsEngine.stop();
  }
}

export const ttsService = new TextToSpeechService();

// 在UI中添加播放按钮
@Builder
buildNoteItem(note: TranslationNote) {
  // ...其他代码
  
  Button('朗读')
    .width(80)
    .height(30)
    .fontSize(12)
    .margin({ left: 10 })
    .onClick(async () => {
      await ttsService.speak(note.originalText, 'zh');
    })
}

3. 便签分类管理

// 在NoteService中添加分类支持
class NoteService {
  private categories: string[] = ['工作', '个人', '学习', '其他'];
  
  public getCategories(): string[] {
    return [...this.categories];
  }
  
  public async addCategory(name: string): Promise {
    if (!this.categories.includes(name)) {
      this.categories.push(name);
    }
  }
  
  public async createNote(
    originalText: string, 
    translations: { [lang: string]: string },
    category?: string
  ): Promise {
    const note: TranslationNote = {
      // ...其他字段
      category: category || '未分类'
    };
    // ...
  }
  
  public getNotesByCategory(category: string): TranslationNote[] {
    return this.notes.filter(note => note.category === category);
  }
}

// 在UI中添加分类筛选
@Builder
buildNotesUI() {
  Column() {
    // 分类筛选
    Picker({
      selected: this.selectedCategory,
      range: noteService.getCategories()
    })
      .onChange((value: string) => {
        this.selectedCategory = value;
      })
      .margin({ bottom: 15 })
    
    // 便签列表
    // ...
  }
}

五、总结

通过这个AI语音翻译便签应用的实现,我们学习了:

  1. 使用HarmonyOS音频服务进行语音录制
  2. 集成AI语音识别和翻译服务
  3. 设计多语言便签数据结构
  4. 实现跨设备便签同步
  5. 构建直观的用户界面

系统特点:

  • 实时语音识别和翻译
  • 多语言支持
  • 跨设备同步
  • 离线功能支持
  • 可扩展的分类管理

这个应用可以进一步扩展为功能更完善的语音助手,如:

  • 添加更多语言支持
  • 实现便签智能分类
  • 添加图片和富文本支持
  • 开发浏览器插件和桌面客户端
  • 集成日历和提醒功能

你可能感兴趣的:(ui,ArKUI-X,wpf,物联网,HarmonyOS5,仓颉)