ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于FastAPI与本地AI模型构建隐私优先的语音日记应用

基于FastAPI与本地AI模型构建隐私优先的语音日记应用 1. 背景与核心概念为什么我们需要私密的AI语音日记在日常开发和学习中我们常常需要记录灵感、复盘问题或整理思路。传统的文字记录效率低下尤其是在通勤、散步等不方便打字的场景下。而市面上许多笔记应用要么功能繁杂要么在数据隐私上令人担忧。作为一个开发者我深切体会到这种痛点我需要一个能随时随地、通过最自然的语音方式快速记录并且能保证数据绝对私密、还能帮我智能整理的工具。这就是我为自己构建Echologue的初衷。Echologue是一个私密的 AI 语音日记应用。它的核心工作流程非常直观语音输入用户通过说话来记录想法就像对着一位永远不会评判你的朋友倾诉。AI 转录与理解应用在本地或受信任的云端将语音转换为文字并利用大语言模型理解内容提取关键信息、情感或生成摘要。私密存储与检索所有数据原始音频、转录文本、AI分析结果都以加密形式存储只有用户本人可以访问。用户可以通过自然语言如“上周关于微服务架构的思考”快速检索过去的记录。智能洞察AI 可以定期回顾你的日记发现你未察觉的模式比如情绪变化趋势、反复出现的项目难点甚至为你生成周报或学习总结。与普通的录音笔或云笔记相比Echologue 的差异在于“AI赋能”与“隐私优先”。它不是一个简单的存储工具而是一个能与你对话、帮你思考的智能伴侣。同时“私密”是其不可妥协的基石这意味着在技术架构上我们需要在客户端完成尽可能多的处理或使用可自托管的开源模型避免用户敏感数据无保留地上传至第三方。对于开发者而言构建这样一个项目极具学习价值。你将综合运用到全栈开发、语音处理、AI 模型集成、数据加密、本地存储等多个领域的技术。接下来我将从零开始拆解如何构建一个简化版的 Echologue涵盖从环境搭建到核心功能实现的完整流程。2. 环境准备与版本说明在开始编码前我们需要明确技术栈并搭建开发环境。本项目将采用前后端分离的架构以实现最大的灵活性。为了突出核心逻辑我们选择 Python 作为后端主要语言使用轻量级的 Web 框架并集成开源 AI 模型。核心技术栈后端 (API 服务)Python FastAPI (高性能异步框架)语音转文本 (STT)OpenAI Whisper (开源模型可本地运行)文本理解与生成 (LLM)Llama.cpp 量化模型 (如 Llama 3.2 3B Instruct完全本地运行)前端 (Web 界面)HTML/CSS/JavaScript (Vanilla JS 或 Vue.js 简化版)数据库SQLite (本地文件简单易用) 或 PostgreSQL (如需更强功能)音频处理PyAudio / librosa数据加密cryptography 库环境与版本说明本文示例基于以下常见环境重点演示配置思路和核心代码。你的具体版本可能需要根据实际情况调整。操作系统Ubuntu 22.04 LTS / macOS Monterey 或更高 / Windows 11 (WSL2 推荐)Python3.10 或 3.11Node.js18.x (仅用于前端构建工具非必须)CUDA11.8 (可选用于 GPU 加速 Whisper 和 Llama.cpp)第一步创建项目目录并初始化 Python 环境# 创建项目根目录 mkdir echologue-project cd echologue-project # 创建虚拟环境 (推荐) python -m venv venv # 激活虚拟环境 # Linux/macOS source venv/bin/activate # Windows venv\Scripts\activate # 创建基础目录结构 mkdir -p backend/{core,api,models,utils} frontend/{static,js,css} data/{audio,db}第二步安装后端核心依赖创建一个backend/requirements.txt文件并填入以下内容# Web 框架 fastapi0.104.1 uvicorn[standard]0.24.0 # 语音处理与AI模型 openai-whisper20231117 llama-cpp-python0.2.23 numpy1.24.3 librosa0.10.1 sounddevice0.4.6 # 数据库与加密 sqlalchemy2.0.23 cryptography41.0.7 pydantic2.5.0 pydantic-settings2.1.0 # 其他工具 python-multipart0.0.6使用 pip 安装pip install -r backend/requirements.txt注意llama-cpp-python和whisper的安装可能需要系统依赖如ffmpeg、cmake。在 Ubuntu 上你可以先运行sudo apt-get install ffmpeg cmake build-essential。3. 核心原理与技术栈拆解在动手编码前理解每个组件的职责和交互方式至关重要。这能帮助你在调试和扩展时有的放矢。3.1 系统架构概览一个简化的 Echologue 系统包含以下模块前端界面提供录音按钮、日记列表、播放控件。通过 JavaScript 调用浏览器 MediaRecorder API 录制音频并通过 Fetch API 发送到后端。API 网关 (FastAPI)接收音频文件协调后续处理流程并返回结果给前端。语音转文本服务 (Whisper)将上传的音频文件如 WebM, MP3转录为文字。语言模型服务 (Llama.cpp)对转录文本进行分析执行如情感分析、关键词提取、摘要生成、问答等任务。数据存储层将用户元数据、日记条目包含加密后的文本和AI分析结果存储到数据库中。原始音频文件可存储在本地文件系统或对象存储中。加密模块在数据入库前对敏感的日记文本和AI分析结果进行加密。密钥由用户密码派生并安全存储。3.2 关键技术点详解1. 语音转文本 (Whisper)Whisper 是 OpenAI 开源的语音识别模型支持多语言准确度高且可以在 CPU 上运行速度较慢。在 Echologue 中我们使用其“基础”或“小型”模型以平衡速度与精度。工作流程接收音频 - 预处理重采样、归一化- 调用whisper.transcribe()- 获取文本。隐私考量Whisper 可以完全在本地运行音频数据无需离开你的服务器或电脑。2. 大语言模型集成 (Llama.cpp)Llama.cpp 是一个高效的 C 库用于在消费级硬件上运行 Meta 的 LLaMA 系列模型。llama-cpp-python是其 Python 绑定。模型选择选择参数量较小的指令微调模型如 3B 或 7B 参数以确保在有限硬件上可运行。模型文件.gguf格式需提前下载。提示词工程这是让 AI 理解日记任务的核心。你需要设计清晰的系统提示词System Prompt来定义 AI 的角色和任务例如“你是一个私人的日记分析助手。请分析用户的一段日记并完成以下任务1. 用一句话总结主旨。2. 提取 3-5 个关键词。3. 判断整体情绪倾向积极/中性/消极。请以 JSON 格式回复。”3. 数据加密日记内容是最高机密。我们采用对称加密。流程用户设置主密码 - 使用 PBKDF2 等算法从密码派生加密密钥 - 使用 AES-GCM 等算法加密日记文本 - 将加密后的密文和初始化向量(IV)存储到数据库。重要原则密钥绝不存储于数据库。每次需要解密时都需用户提供密码来重新派生密钥。这意味着如果用户忘记密码数据将无法恢复。4. 完整实战案例构建核心后端服务现在让我们开始构建 Echologue 的后端核心。我们将创建数据库模型、API 端点并集成 Whisper 和 Llama.cpp。4.1 创建数据库模型与加密工具首先在backend/core/database.py中定义 SQLAlchemy 模型和加密工具。# backend/core/database.py from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from datetime import datetime import os # 使用 SQLite 数据库文件位于项目 data/db 目录 SQLALCHEMY_DATABASE_URL sqlite:///./data/db/echologue.db engine create_engine(SQLALCHEMY_DATABASE_URL, connect_args{check_same_thread: False}) SessionLocal sessionmaker(autocommitFalse, autoflushFalse, bindengine) Base declarative_base() class JournalEntry(Base): 日记条目数据模型 __tablename__ journal_entries id Column(Integer, primary_keyTrue, indexTrue) # 前端生成的唯一标识可用于加密关联 entry_uuid Column(String(36), uniqueTrue, indexTrue, nullableFalse) # 录音文件名存储在文件系统 audio_filename Column(String(255), nullableFalse) # 加密后的转录文本 encrypted_transcript Column(Text, nullableFalse) # 加密后的AI分析结果 (JSON字符串) encrypted_ai_analysis Column(Text) # 加密时使用的 IV (Initialization Vector) iv Column(String(24), nullableFalse) # 用于 AES-GCM # 记录创建时间 created_at Column(DateTime, defaultdatetime.utcnow) # 创建所有表 Base.metadata.create_all(bindengine) # 数据库会话依赖 def get_db(): db SessionLocal() try: yield db finally: db.close()接下来在backend/core/encryption.py中实现加密解密功能# backend/core/encryption.py from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2 from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.ciphers.aead import AESGCM import os import base64 class DiaryEncryptor: def __init__(self, user_password: str, salt: bytes None): 初始化加密器。 :param user_password: 用户的主密码 :param salt: 盐值。如果为None则生成新的。必须保存以便后续解密。 self.salt salt if salt else os.urandom(16) # 使用 PBKDF2 从密码和盐派生密钥 kdf PBKDF2( algorithmhashes.SHA256(), length32, # AES-256 需要32字节密钥 saltself.salt, iterations100000, ) self.key kdf.derive(user_password.encode()) def encrypt(self, plaintext: str) - (str, str): 加密文本返回 (base64编码的密文, base64编码的IV) iv os.urandom(12) # GCM推荐12字节IV aesgcm AESGCM(self.key) # 加密关联数据为空 ciphertext aesgcm.encrypt(iv, plaintext.encode(), None) # 将二进制数据转换为可存储的字符串 return base64.b64encode(ciphertext).decode(), base64.b64encode(iv).decode() def decrypt(self, ciphertext_b64: str, iv_b64: str) - str: 解密文本 ciphertext base64.b64decode(ciphertext_b64) iv base64.b64decode(iv_b64) aesgcm AESGCM(self.key) plaintext aesgcm.decrypt(iv, ciphertext, None) return plaintext.decode() # 注意在实际应用中user_password 应由前端在安全的环境下获取并传输例如通过HTTPS # 或者更佳方案是加解密过程完全在前端进行后端只存储密文。本例为演示后端逻辑。4.2 集成 Whisper 进行语音转录在backend/services/transcribe_service.py中创建转录服务。# backend/services/transcribe_service.py import whisper import tempfile import os class Transcriber: _model None classmethod def get_model(cls, model_sizebase): 懒加载 Whisper 模型单例模式避免重复加载 if cls._model is None: print(fLoading Whisper {model_size} model...) cls._model whisper.load_model(model_size) print(Model loaded.) return cls._model classmethod def transcribe_audio(cls, audio_file_path: str, model_size: str base) - str: 转录音频文件为文字。 :param audio_file_path: 音频文件路径 :param model_size: Whisper模型大小可选 tiny, base, small, medium, large :return: 转录文本 model cls.get_model(model_size) # 执行转录 result model.transcribe(audio_file_path, fp16False) # CPU上运行设为False return result[text]4.3 集成 Llama.cpp 进行日记分析在backend/services/analysis_service.py中创建 AI 分析服务。首先你需要从 Hugging Face 等平台下载一个合适的 GGUF 格式模型文件例如Meta-Llama-3.2-3B-Instruct-Q4_K_M.gguf并放在backend/models/目录下。# backend/services/analysis_service.py from llama_cpp import Llama import json import os class DiaryAnalyzer: _llm None classmethod def get_llm(cls, model_path: str ./backend/models/Meta-Llama-3.2-3B-Instruct-Q4_K_M.gguf): 懒加载 Llama 模型 if cls._llm is None: print(fLoading LLM from {model_path}...) # n_ctx 是上下文长度根据模型和内存调整 cls._llm Llama(model_pathmodel_path, n_ctx2048, n_threads4, verboseFalse) print(LLM loaded.) return cls._llm classmethod def analyze_entry(cls, transcript_text: str) - dict: 分析日记转录文本。 :param transcript_text: 纯净的日记文本 :return: 包含分析结果的字典 llm cls.get_llm() system_prompt 你是一个私人的、安全的日记分析助手。你的唯一任务是帮助用户从日记中提取结构化信息。请严格遵循以下步骤分析用户提供的日记文本 1. **摘要**用一句简洁的话概括日记的核心内容。 2. **关键词**提取3到5个最能代表本次日记内容的关键词或短语。 3. **情绪**判断作者的整体情绪倾向选项为积极、中性、消极。 4. **潜在主题**识别日记可能涉及的主题如工作、学习、健康、人际关系、创意等。 请将分析结果以纯JSON格式输出键名必须为summary, keywords, sentiment, themes。 user_prompt f日记内容{transcript_text} # 构建消息 messages [ {role: system, content: system_prompt}, {role: user, content: user_prompt} ] # 生成回复 response llm.create_chat_completion( messagesmessages, max_tokens512, temperature0.1, # 低温度保证输出稳定 stop[/s, ] # 停止词 ) ai_output response[choices][0][message][content].strip() # 尝试从输出中解析JSON。模型有时会在JSON外加 Markdown 代码块或说明文字。 try: # 尝试找到 JSON 部分 start_idx ai_output.find({) end_idx ai_output.rfind(}) 1 if start_idx ! -1 and end_idx ! 0: json_str ai_output[start_idx:end_idx] analysis_result json.loads(json_str) else: # 如果找不到返回原始文本 analysis_result {raw_analysis: ai_output} except json.JSONDecodeError as e: print(fJSON解析失败: {e}, 原始输出: {ai_output}) analysis_result {error: AI分析结果格式异常, raw_output: ai_output} return analysis_result4.4 构建 FastAPI 核心接口现在在backend/api/main.py中创建主要的 API 端点。# backend/api/main.py from fastapi import FastAPI, File, UploadFile, HTTPException, Depends from fastapi.middleware.cors import CORSMiddleware from sqlalchemy.orm import Session import uuid import os from datetime import datetime # 导入我们编写的模块 from backend.core.database import get_db, JournalEntry from backend.core.encryption import DiaryEncryptor from backend.services.transcribe_service import Transcriber from backend.services.analysis_service import DiaryAnalyzer from backend.core.config import settings # 假设有一个配置文件 app FastAPI(titleEchologue API, version0.1.0) # 配置CORS允许前端访问 app.add_middleware( CORSMiddleware, allow_origins[http://localhost:3000], # 前端开发服务器地址 allow_credentialsTrue, allow_methods[*], allow_headers[*], ) # 假设从安全配置或请求头中获取密码。**生产环境需使用更安全的方案** # 例如通过登录认证后的 token 关联用户密钥或在前端加密。 USER_PASSWORD my_secure_password_placeholder # 这应该从安全的地方获取 app.post(/api/journal/entry) async def create_journal_entry( audio_file: UploadFile File(...), db: Session Depends(get_db) ): 接收音频文件进行转录、AI分析、加密存储。 if not audio_file.content_type.startswith(audio/): raise HTTPException(status_code400, detailFile must be an audio file.) # 1. 保存上传的音频文件到临时位置 file_ext os.path.splitext(audio_file.filename)[1] temp_audio_path f/tmp/{uuid.uuid4()}{file_ext} with open(temp_audio_path, wb) as buffer: content await audio_file.read() buffer.write(content) try: # 2. 语音转文本 print(Transcribing audio...) transcript_text Transcriber.transcribe_audio(temp_audio_path) print(fTranscript: {transcript_text[:100]}...) # 3. AI 分析 print(Analyzing with AI...) ai_analysis DiaryAnalyzer.analyze_entry(transcript_text) print(fAI Analysis: {ai_analysis}) # 4. 数据加密 encryptor DiaryEncryptor(user_passwordUSER_PASSWORD) encrypted_text, iv encryptor.encrypt(transcript_text) encrypted_analysis, _ encryptor.encrypt(json.dumps(ai_analysis)) # 5. 持久化到数据库和文件系统 entry_uuid str(uuid.uuid4()) # 移动音频文件到持久化存储位置 persistent_audio_path f./data/audio/{entry_uuid}{file_ext} os.rename(temp_audio_path, persistent_audio_path) # 创建数据库记录 db_entry JournalEntry( entry_uuidentry_uuid, audio_filenamepersistent_audio_path, encrypted_transcriptencrypted_text, encrypted_ai_analysisencrypted_analysis, iviv, created_atdatetime.utcnow() ) db.add(db_entry) db.commit() db.refresh(db_entry) # 6. 返回成功响应不返回敏感数据 return { id: db_entry.id, entry_uuid: entry_uuid, created_at: db_entry.created_at.isoformat(), ai_analysis_preview: ai_analysis # 注意这里返回了未加密的分析预览仅用于演示。生产环境应谨慎。 } except Exception as e: # 清理临时文件 if os.path.exists(temp_audio_path): os.remove(temp_audio_path) raise HTTPException(status_code500, detailfInternal server error: {str(e)}) app.get(/api/journal/entries) def get_journal_entries(db: Session Depends(get_db)): 获取日记条目列表仅元数据不包含加密内容 entries db.query(JournalEntry).order_by(JournalEntry.created_at.desc()).all() return [ { id: e.id, entry_uuid: e.entry_uuid, created_at: e.created_at.isoformat(), audio_filename: e.audio_filename } for e in entries ] app.get(/api/journal/entry/{entry_uuid}) def get_journal_entry( entry_uuid: str, password: str, # **警告这只是演示真实场景应通过更安全的方式传输密钥** db: Session Depends(get_db) ): 根据UUID和密码获取并解密单条日记的完整内容 entry db.query(JournalEntry).filter(JournalEntry.entry_uuid entry_uuid).first() if not entry: raise HTTPException(status_code404, detailEntry not found) # 使用用户提供的密码尝试解密 try: encryptor DiaryEncryptor(user_passwordpassword, saltbase64.b64decode(entry.iv)) # 需要从数据库获取salt这里简化了 # 注意上面的构造函数需要salt我们的简易实现需要调整。更健壮的做法是单独存储salt。 # 此处为逻辑演示略过细节。 transcript encryptor.decrypt(entry.encrypted_transcript, entry.iv) analysis json.loads(encryptor.decrypt(entry.encrypted_ai_analysis, entry.iv)) except Exception as e: raise HTTPException(status_code403, detailDecryption failed. Incorrect password or corrupted data.) return { id: entry.id, created_at: entry.created_at.isoformat(), transcript: transcript, analysis: analysis }4.5 运行与验证启动后端服务 在项目根目录下运行uvicorn backend.api.main:app --reload --host 0.0.0.0 --port 8000访问http://localhost:8000/docs查看自动生成的 Swagger API 文档。使用前端或工具测试 你可以使用curl或 Postman 测试/api/journal/entry接口。curl -X POST http://localhost:8000/api/journal/entry \ -H accept: application/json \ -H Content-Type: multipart/form-data \ -F audio_file/path/to/your/recording.wav如果一切正常你将收到一个包含entry_uuid和ai_analysis_preview的 JSON 响应。5. 常见问题与排查思路在开发和部署 Echologue 的过程中你可能会遇到以下典型问题问题现象可能原因排查步骤与解决方案启动服务时ModuleNotFoundError: No module named whisperWhisper 依赖未正确安装或虚拟环境未激活。1. 确认虚拟环境已激活 (which python)。2. 在虚拟环境中重新安装openai-whisper。3. 检查是否安装了ffmpeg(ffmpeg -version)。转录音频时速度极慢默认使用 CPU 运行 Whisper 模型且模型尺寸较大。1. 尝试使用更小的模型如model_sizetiny或base。2. 如果有 NVIDIA GPU安装支持 CUDA 的 PyTorch 并确保 Whisper 能使用 GPU。3. 考虑对长音频进行分段处理。Llama.cpp 加载模型时内存不足模型参数过大超出可用 RAM。1. 使用量化程度更高的模型如 Q4_K_M, Q3_K_S。2. 减小n_ctx上下文长度参数。3. 升级硬件或使用云 API 替代牺牲隐私。AI 分析返回的结果不是 JSON 格式大语言模型的输出具有随机性可能不严格遵守指令。1. 优化系统提示词强调“必须以纯 JSON 格式输出”。2. 在代码中添加更鲁棒的 JSON 解析逻辑如使用正则表达式提取{...}之间的内容。3. 在后处理中如果解析失败可以尝试让模型重试通过 API或返回一个包含错误信息的默认结构。上传大音频文件失败FastAPI 默认有文件大小限制。在启动uvicorn时增加限制--limit-max-request-body 104857600(100MB)或在代码中使用app.post(..., max_upload_size100_000_000)取决于框架版本。前端调用 API 时出现 CORS 错误后端未正确配置 CORS 或前端地址不在允许列表中。1. 检查backend/api/main.py中的allow_origins确保包含前端运行的地址如http://localhost:3000。2. 在生产环境中应设置为确切的域名。解密日记时始终失败加密/解密使用的密码或盐不一致。1.确保每次为同一用户创建DiaryEncryptor时使用相同的密码和盐。盐必须随加密数据一起安全存储。2. 检查数据库中的iv字段是否在解密时被正确使用。3. 验证加密解密流程在单次会话中是否能自闭环。6. 最佳实践与工程建议将 Echologue 从一个原型发展为健壮、可用的应用需要考虑以下工程化实践1. 安全与隐私深化前端加密最理想的隐私模型是“零知识”。所有加密解密操作应在用户浏览器中通过 JavaScript 完成使用 Web Crypto API 或libsodium-wrappers。后端仅存储无法解密的密文。这样即使服务器被攻破日记内容也不会泄露。密钥管理如果必须在后端处理密钥绝不能硬编码。应使用安全的密钥管理服务KMS或让用户通过强密码派生且密码不在服务端持久化。传输安全必须使用 HTTPS。音频和文本数据在传输过程中也应处于加密状态。2. 性能与可扩展性异步处理语音转录和 AI 分析是耗时操作。不应在 HTTP 请求线程中同步执行。应该将任务推入消息队列如 Redis RQ 或 Celery立即返回一个任务 ID让前端通过轮询或 WebSocket 获取结果。模型缓存与池化Whisper 和 Llama 模型加载耗时。应使用单例或连接池管理模型实例避免每次请求都重复加载。音频预处理在上传前前端可以对音频进行压缩、降噪、格式转换如统一为 mono, 16kHz以减少传输量和提升转录速度。3. 数据存储优化对象存储当音频文件很多时应考虑使用 S3 或 MinIO 等对象存储服务而非本地文件系统便于扩展和备份。数据库索引在JournalEntry表的entry_uuid和created_at上建立索引以加速查询。数据清理实现定期清理过期或用户删除的日记条目及其关联文件的功能。4. 前端用户体验实时反馈录音时提供可视化波形图。上传和处理时显示明确的进度条。离线支持考虑使用 PWA 技术允许用户在无网络时录制音频并暂存本地待网络恢复后同步。自然语言搜索利用嵌入模型如all-MiniLM-L6-v2将日记文本转换为向量存储在向量数据库如 SQLite withsqlite-vss或 Qdrant中实现语义搜索而不仅仅是关键词匹配。5. 部署与监控容器化使用 Docker 和 Docker Compose 封装应用、模型和数据库确保环境一致性。健康检查为 API 添加/health端点监控服务及模型加载状态。日志与错误追踪集成像 Sentry 这样的错误监控服务记录转录失败、模型异常等关键错误。构建 Echologue 这样的个人项目不仅是学习一系列热门技术栈的绝佳机会更是对“以用户为中心”和“隐私至上”理念的一次深度实践。从环境搭建、服务集成到安全加固每一步都挑战着开发者的综合能力。希望这篇教程能为你提供一个坚实的起点。你可以从 GitHub 上克隆类似的开源项目如输入中提到的my_ai_town获取灵感但更重要的是根据你自己的需求去迭代和打磨打造出真正属于你的、独一无二的数字思维伴侣。
返回列表