最新文章
-
线性回归实现
线性回归 线性回归模型根据给定的数据集和对应的标签,通过一个函数模型来拟合数据集以及对应标签的映射关系。而这个模型可以设置为y=wx+b的一个函数,其中x和w是一个向量。目标就是找出权重w和偏执b的值,使得模型更逼近数据集合的规律,也就是能够预测的更准确。 线性回归示例实现 pytorch本身有线性回归的函数,只是这里通过实现pytoch来加深理解 读取数据集 def data_iter(batch_size, features, labels): num_examples = len(features) #获取数据的长度,假1000行,输出1000 indices = list(range(num_examples)) #生成一个下标,结果[0,...,999] random.shuffle(indices)#打散indices,使数据随机,结果[77,99,0,13,....] for i in range(0, num_examples, batch_size): #表示从0到num_examples,步长为 batch_size batch_indices = torch.tensor( indices[i: min(i + batch_size, num_examples)]) print(batch_indices) #i 到 i + batch_size 的索引转换为一 PyTorch张量 yield features[batch_indices], labels[batch_indices] #每次循环时,yield 会返回一个元组 (features_batch, labels_batch), #其中 features_batch 是一个包含该批次特征数据的 Tensor,labels_batch #是该批次对应的标签数据。 定义一个函数data_iter,将数据集(x)、以及数据集对应的特征(y)作为函数输入,分割成大小为batch_size的小批量数据集(x)和特征集(y)。之所以要进行分割每次抽取小批量样本,是利用了GPU并行运算的优势。每个样本都可以并行地进行模型计算,同时在后续计算梯度时,每个样本损失函数的梯度可以被并行计算。 batch_size = 10 for X, y in data_iter(batch_size, features, labels): print(X, '\n', y) break 运行结果 tensor([940, 41, 385, 262, 655, 402, 317, 256, 984, 644]) --print(batch_indices) tensor([[-0.9666, 0.8299], [-1.8890, 0.1645], [ 0.0274, -0.6944], [ 2.0289, 0.7227], [ 1.0077, 0.6674], [ 1.8692, 0.5002], [-0.9469, 1.7404], [ 0.8589, -0.5467], [ 1.1260, 0.1262], [-0.6988, -0.0683]]) tensor([[-0.5347], [-0.1296], [ 6.6105], [ 5.7961], [ 3.9675], [ 6.2448], [-3.5983], [ 7.7625], [ 6.0183], [ 3.0294]]) 那么训练的数据集和特征怎么来了,一般是通过需要训练的目标处理得来,为了方便本章用一个函数来模拟生成数据集。 def synthetic_data(w, b, num_examples): X = torch.normal(0, 1, (num_examples, len(w))) y = torch.matmul(X, w) + b y += torch.normal(0, 0.01, y.shape) return X, y.reshape((-1, 1)) 上面这个函数,实际上就是给先指了w和b生成y=wx+b模型中的x和y, 而我们就是要训练找出w和b。 生成输入数据 X:torch.normal(mean, std, size) 用于从正态分布中生成数据。这里,mean=0 表示均值为0,std=1 表示标准差为1。(num_examples, len(w)) 是生成张量的形状,这里 num_examples 是生成的样本数,len(w) 是每个样本的特征数(即权重向量 w 的长度)。所以 X 是一个形状为 (num_examples, len(w)) 的矩阵,其中包含了从标准正态分布中采样的特征数据。 生成标签 y: torch.matmul(X, w) 计算输入特征 X 和权重 w 的矩阵乘法。结果是一个形状为 (num_examples,) 的张量,表示每个样本的预测值(不包括偏置)。+ b 将偏置 b 加到每个样本的预测值中,这样就得到最终的标签 y。这就是线性回归模型中的公式 y = Xw + b。 添加噪声: torch.normal(0, 0.01, y.shape) 生成一个与 y 形状相同的噪声项,噪声来自均值为 0,标准差为 0.01 的正态分布。这一步是为了给数据添加一些随机噪声,使得生成的数据更符合实际情况。现实中,数据通常会有一些误差或噪声,因此我们在标签 y 上添加小的随机波动。 返回数据: X 是生成的输入特征数据。y.reshape((-1, 1)) 将标签 y 转换为一个形状为 (num_examples, 1) 的列向量,以确保标签的形状是列向量。 生成合成的线性数据集,数据集的特征 X 是从标准正态分布中采样的,而标签 y 是通过线性方程 y = Xw + b 生成的,并且在 y 上添加了一些小的噪声。 true_w = torch.tensor([2, -3.4]) true_b = 4.2 features, labels = synthetic_data(true_w, true_b, 10) print('features:', features, '\nfeatures len', len(features), '\nlabel:', labels) 运行结果 features: tensor([[ 4.3255e-01, -1.4288e+00], [ 2.2412e-01, -1.8749e-01], [-5.6843e-01, 1.0930e+00], [ 1.3660e+00, -1.8141e-03], [ 3.9331e-01, -2.4553e-02], [-6.3184e-01, -8.4748e-01], [-1.7891e-02, -1.4018e+00], [-4.8070e-01, 8.5689e-01], [ 2.0670e+00, 3.8301e-02], [ 1.7682e+00, 1.9595e-01]]) features len 10 label: tensor([[ 9.9307], [ 5.2856], [-0.6669], [ 6.9439], [ 5.0759], [ 5.8344], [ 8.9642], [ 0.3175], [ 8.2140], [ 7.0458]]) 定义模型 我们的模型函数是y=wX+b,也就是计算输入特征X和权重W,这里的Xw是一个向量,而b是一个标量,但是用一个向量加上一个标量是,标量会被加到每个分量上,这是广播机制。 def linreg(X, w, b): return torch.matmul(X, w) + b 在开始计算随机梯度下降优化模型参数之前,需要先预设一些参数。下面是使用正态分布随机初始化w和b。 w = torch.normal(0, 0.01, size=(2,1), requires_grad=True) b = torch.zeros(1, requires_grad=True) w, b 定义损失函数 损失函数就是根据我们采样处理的数据X输入到我们的模型中计算处理的值y’跟真实值y的差距,这里使用平方损失函数,即loss=(y’-y)^2,详细的公式为:$ L(w, b) = \frac{1}{n} \sum_{i=1}^{n} \ell^{(i)}(w, b) = \frac{1}{n} \sum_{i=1}^{n} \left( \frac{1}{2} \left( w^\top x^{(i)} + b - y^{(i)} \right)^2 \right) $ def squared_loss(y_hat, y): return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2 优化参数 详细的公式为:$ (w, b) \leftarrow (w, b) - \frac{\eta}{|B|} \sum_{i \in B} \nabla_{(w, b)} \ell^{(i)}(w, b) $ 损失函数是对对应参数求的偏导,即如果是$w$就是对$w$的偏导,如果是$b$就是$b$的偏导,$x$是当前采样的具体值(不是变量),公式中的$B$是抽样的小批量,是固定数量的训练样本。 具体算法的步骤如下,对于$W$更新参数的公式为: $ w \leftarrow w - \frac{\eta}{|B|} \sum_{i \in B} \frac{\partial \ell^{(i)}}{\partial w}(w, b) = w - \frac{\eta}{|B|} \sum_{i \in B} x^{(i)} \left( w^\top x^{(i)} + b - y^{(i)} \right) $ 对于$b$更新参数的公式为: $ b \leftarrow b - \frac{\eta}{|B|} \sum_{i \in B} \frac{\partial \ell^{(i)}}{\partial b}(w, b) = b - \frac{\eta}{|B|} \sum_{i \in B} \left( w^\top x^{(i)} + b - y^{(i)} \right) $ 从上面的公式可以看出,梯度是批量误差的和,没处理一个批量数据,更新一次参数,而不是每处理一个数据更新一次参数。 def sgd(params, lr, batch_size): with torch.no_grad(): for param in params: param -= lr * param.grad / batch_size #梯度值为param.grad param.grad.zero_() param.grad是哪里来的? 系统自动计算而来,下一章节会介绍。 训练 在训练之前,需要先初始化参数, w = torch.normal(0, 0.01, size=(2,1), requires_grad=True) b = torch.zeros(1, requires_grad=True) w, b 接下来开始训练 lr = 0.03 num_epochs = 5000 net = linreg loss = squared_loss for epoch in range(num_epochs): for X, y in data_iter(batch_size, features, labels): l = loss(net(X, w, b), y) # X和y的小批量损失 # 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起, # 并以此计算关于[w,b]的梯度 l.sum().backward() sgd([w, b], lr, batch_size) # 使用参数的梯度更新参数 print('true_w',true_w, 'w', w, '\ntrue_b', true_b, 'b',b) with torch.no_grad(): train_l = loss(net(features, w, b), labels) print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}') print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}') print(f'b的估计误差: {true_b - b}') 为什么l.sum().backward能够自动计算存储梯度值? 在初始化w和b的参数时,设定了requires_grad=True。在计算损失时,net(X, w, b)会生成预测值y_hat,并通过loss函数与真实值y构建计算图。调用l.sum().backward()时,PyTorch的autograd系统会从标量损失l.sum()开反向传播,自动计算w和b的梯度,并存储在w.grad和b.grad中。 tensor([4, 3, 9, 6, 8, 2, 5, 7, 1, 0]) ---batch_size = 10 true_w tensor([ 2.0000, -3.4000]) w tensor([[ 2.0056],[-3.4014]], requires_grad=True) true_b 4.2 b tensor([4.1983], requires_grad=True) epoch 1, loss 0.000056 tensor([5, 6, 2, 4, 0, 7, 8, 1, 9, 3]) true_w tensor([ 2.0000, -3.4000]) w tensor([[ 2.0056],[-3.4014]], requires_grad=True) true_b 4.2 b tensor([4.1983], requires_grad=True) epoch 2, loss 0.000056 tensor([8, 5, 6, 9, 7, 4, 2, 1, 0, 3]) true_w tensor([ 2.0000, -3.4000]) w tensor([[ 2.0056],[-3.4014]], requires_grad=True) true_b 4.2 b tensor([4.1983], requires_grad=True) epoch 3, loss 0.000056 tensor([9, 3, 5, 2, 8, 0, 7, 4, 6, 1]) true_w tensor([ 2.0000, -3.4000]) w tensor([[ 2.0056],[-3.4014]], requires_grad=True) true_b 4.2 b tensor([4.1983], requires_grad=True) epoch 4, loss 0.000056 tensor([8, 1, 5, 3, 0, 6, 2, 4, 9, 7]) true_w tensor([ 2.0000, -3.4000]) w tensor([[ 2.0056],[-3.4014]], requires_grad=True) true_b 4.2 b tensor([4.1983], requires_grad=True) epoch 5, loss 0.000056 tensor([3, 7, 4, 0, 6, 9, 2, 1, 5, 8]) true_w tensor([ 2.0000, -3.4000]) w tensor([[ 2.0056],[-3.4014]], requires_grad=True) true_b 4.2 b tensor([4.1983], requires_grad=True) epoch 6, loss 0.000056 tensor([7, 4, 6, 1, 0, 5, 3, 8, 9, 2]) true_w tensor([ 2.0000, -3.4000]) w tensor([[ 2.0056],[-3.4014]], requires_grad=True) true_b 4.2 b tensor([4.1983], requires_grad=True) 误差结果: w的估计误差: tensor([-0.0056, 0.0014], grad_fn=<SubBackward0>) b的估计误差: tensor([0.0017], grad_fn=<RsubBackward1>) 本文来自: <动手学深度学习 V2> 的学习笔记 -
perf工具使用
perf介绍 perf 是一个强大的 Linux 性能分析工具,广泛用于分析程序的性能瓶颈,帮助开发者进行调优。perf 工具能够收集并分析多种硬件和软件事件,包括 CPU 的指令执行、缓存命中与失误、上下文切换等。 硬件事件驱:通过访问 CPU 的 PMU(性能监控单元)捕获硬件级事件,如 CPU 周期数、缓存命中/未命中、分支预测失败等。 采样与统计机制:采样模式,周期性记录程序执行状态,生成热点函数分布(默认基于 CPU 时钟周期);统计模式,精确记录特定事件的发生次数(如指令数、缓存访问次数); 内核集成优势:直接调用内核的 tracepoint 和 kprobe 机制,支持用户态与内核态的全栈追 基本语法: perf <command> [options] command:perf 工具的子命令,例如 record、stat、report 等。 options:提供给 command 的选项和参数。 perf record perf record 命令用于收集性能数据,通常用来分析程序的性能瓶颈。 perf record [options] <command> -p \<pid>:指定要分析的进程的 PID。 -F \<frequency>:指定采样的频率(每秒钟采样次数)。例如,-F 99 每秒采样 99 次。 -g:收集调用图信息(调用栈信息),可以用来分析函数调用的上下文。 -e \<event>:指定要计数的事件。例如:-e cycles 计数 CPU 周期,-e cache-misses 计数缓存未命中 -- sleep \<time>:执行指定命令,并在给定的时间内采样性能数据。例如,-- sleep 30 表示记录 30 秒的数据。 示例: perf record -F 99 -p 12345 -g -- sleep 30 # 这会对进程 PID 为 12345 的程序进行 30 秒的性能采样,采样频率为99Hz(默认是1000HZ),并收集调用图信息。 # record生成的是原始数据bin,无法直接查看,默认是生成perf.data,可以使用-o指定输出文件。 # 需要使用perf script转化才可解析。 perf report perf report 命令用于分析和展示 perf record 记录的性能数据。 perf report [options] -g:显示调用图(调用堆栈)信息,帮助分析函数的调用关系,如果要绘制图像,需要加这个参数。 -i \<file>:指定输入文件,默认情况下会使用 perf.data 文件。 示例: perf report -g 这会显示 perf record 记录的性能数据的调用图。 perf script perf script 是一个 perf 工具的子命令,主要用于将 perf record 采集到的性能数据转换为可读的格式,并允许用户对其进行进一步处理。它的主要功能是解析性能数据文件并输出到标准输出或指定文件,方便进一步分析。 perf script [options] 将 perf record 生成的性能数据(默认文件名为 perf.data)转化为易于阅读的文本格式。可以与其他工具结合,进一步分析和处理数据。 perf script -i perf.data > output.txt 指定输入文件转化为输出文件。 实践应用 perf sched perf sched 是 perf 工具中的一个子命令,用于分析与调度相关的性能数据,主要用于分析 Linux 系统中的调度器行为(即进程和线程的调度)。这个命令可以帮助开发人员深入了解进程或线程如何在 CPU 上执行,以及在多核系统上如何分配 CPU 时间。 perf sched 命令通过分析内核的调度事件(如进程切换、上下文切换、进程调度延迟等),帮助开发人员识别系统中可能的调度瓶颈或性能问题。 抓取数据 perf sched record -a -g -o sched_raw.data & 解析数据: killall perf #结束进程,注意是不要使用-9强行退出,需要等待退出,保证写入的文件完整。 perf sched timehist -i sched_raw.data > sched_timehist.log perf sched latency -i sched_raw.data > sched_latency.log # 显示进程或线程的调度延迟,帮助你理解调度延迟如何影响系统性能。 perf sched script -i sched_raw.data > sched.log 解析数据 killall perf #结束进程,注意是不要使用-9强行退出,需要等待退出,保证写入的文件完整。 perf sched timehist -i sched_raw.data > sched_timehist.log perf sched latency -i sched_raw.data > sched_latency.log # 显示进程或线程的调度延迟,帮助你理解调度延迟如何影响系统性能。 perf sched script -i sched_raw.data > sched.log perf irq perf irq是perf 工具中的一个子命令,用于分析与中断(IRQ, Interrupt Request)相关的性能数据。中断是操作系统用来响应硬件或软件事件的机制。perf irq 可以帮助开发者分析中断的发生频率、持续时间及其对系统性能的影响。 抓取数据 抓取中断的进入和退出 perf record -e irq:irq_handler_entry,irq:irq_handler_exit -a -g -o irq_raw.data & 解析数据 killall perf perf script -i irq_raw.data > irq.log perf report -i irq_raw.data > irq_report.log 火焰图 通过perf script将原始数据转换的数据,可以使用工具转换为火焰图。需要注意的时,在使用perf script转换之前,perf record需要加-g参数,记录调用栈。 火焰图工具下载链接:https://github.com/brendangregg/FlameGraph 下面是转换命令: ../FlameGraph-master/stackcollapse-perf.pl < sched.log | ../FlameGraph-master/flamegraph.pl > sched.svg 其中sched.log是perf script转换的处理的数据,先使用stackcollapse-perf.pl处理数据,然后再使用flamegraph.pl绘制图像,即可使用网页打开。 y轴(竖)表示调用栈,每一层都是一个函数,调用栈越深,火焰就越高,顶部就是正在执行的函数,下方都是它的父函数。 x轴(横)表示抽样数,若一个函数在x轴占据的宽度越宽,就表示它被抽到的次数多,即执行的时间长。注意,x 轴不代表时间,而是所有的调用栈合并后,按字母顺序排列的。 火焰图就是看顶层的哪个函数占据的宽度最大。只要有"平顶"(plateaus),就表示该函数可能存在性能问题。 点击一层会水平放大,左上角会同时显示"Reset Zoom",点击该链接,图片就会恢复原样。 -
非暴力沟通
非暴力沟通要素 观察: 不带评论的观察 感受:体会(对方)和表达(自己)感受(区分想法和感受) 需要:体会(对方)和表达(自己)需求 请求:体会(对方)和表达(自己)请求(一定是要具体的请求,不要含糊) 沟通的过程 观察实际发生了什么事实? 表达出我们看到的这些行为的感受 表达出我们的感受与什么需求相关联 说出我们一个具体的请求 疏离生命的语言 道德评判: 别人的行为与我们价值观不符,便认为这个人是错的。如指责、辱骂说别人蠢、混蛋、冷漠等等色彩的词。 做比较:与比人做比较,谁谁怎么样?你怎么样?或者自己怎么样? 不得不: 我们不是主人,是被强迫的要求的 推卸责任:将行动归结于外部因素,如我必须得怎么做?谁安排的?不得不怎么做? 其他:“要求”的方式 不带评论的观察(事实) 不夹杂任何评论,而是将看到的事实。区分出观察与评论,不夹杂任何。 评论:他冲我发脾气,他是个好人,他太懒了。 观察:他用拳头砸了一下桌子,他将他收入1/10捐给了慈善机构,他一天到晚都躺在床上玩手机。 当别人说你不好时?有以下做法 法1:讨好他,承认自己的不足。 法2:反击他,这是他的不对。 法3:不辩解也不指责,试着沟通找出对方的观察 作者给出的结论见P36。 体会和表达感受(是什么) 先来区分一下感受和想法? 感受:是指我们在某个情境中体验到的情绪或身体感受,比如快乐、愤怒、悲伤、焦虑、兴奋等。这些情绪是我们内心的真实反应,与外部事件的关系直接,但它们是我们对外界刺激的个人体验。 想法: 是我们对某种情境的理解、分析、判断或信念,它们通常是我们大脑的认知活动,如“我觉得他不尊重我”或“我认为这不公平”。想法往往是我们对自己感受的解释或推理。 区分方法: 感受通常是情绪或身体上的反应,是一种“感觉”或“状态”,比如“我感到害怕”或“我感到开心”。想法是对事件的解释或评判,通常以“我认为”或“我觉得”开始,比如“我认为他没有听我说话”或“我觉得这件事不对”。 举例: 感受:我感到伤心和失望。 想法:我觉得他没有关注我的感受。 总结,简单来说,感受是情绪和身体状态,想法是思维和判断。 为什么要尽可能表达感受而不是想法 表达感受反应的是我们内心的真实体验,没有带评判,而是呈现我们的情绪状态。而表达想法往往包含对对方的评价或指责,容易激发对方的防御反应。表达感受专注我们内心的体验,容易获得理解或共鸣。 怎么表达自己的感受? 表达感受时,尽可能要清晰,核心点事尽量的先不要说评价、评论、判断,容易导致对方防御。 表达感受的词:开心、平静、放松、担心、难过、尴尬、不舒服。 表达想法的词:这样做不对,你让我失望,你对不起我,得不到支持,拒绝。 体会和表达需要(为什么) 感受的根因(为什么) 需求则是指我们内心深处的基本需求,能够驱动我们行为的动机。它是抽象的、长期性的,并且是感受的根本原因。例如,当我们感到沮丧时,可能是因为我们的需求(如尊重、理解、归属感等)没有得到满足。 对他人的指责、批评、评论以及分析反应了我们的需要和价值观,如果我们通过批评来提出主张,人们的反应常常是申辩或反击。反之如果我们直接输出我们的需要,其他人就较有可能做出积极回应。 大多数人倾向考虑别人有什么错,而不是习惯从别人需要什么来考虑。因此在于别人沟通时,关注别人的感受,然后从感受中体会需求。可以经过沟通进一步确认。 请求帮助(怎么做) 请求(How),请求需要具体 -
站点centos系统迁移ubuntu系统
概述 本站点最早使用的是centos系统,但是该系统官方不再更新维护了,随最近将服务器的系统更换为ubuntu系统,先记录站点的迁移过程。 迁移要点 由于本站点主要是基于php+nginx+wordpress框架设计。备份的原理是php、nginx、wordpress软件属于工具是可以在新的系统上新安装,但是wordpress相关的配置如主题、数据库、插件等属于数据是需要进行复制迁移的,因此备份的关键核心就是打包wordpress+mysql数据库。 wordpress:wordpress组件包括插件、主题等。 数据库:wordpress所需要的数据库。 上面两个主要就是站点的关键数据了,需要原封迁移到新的服务器上去。其他次要点依情况选择: nginx配置:一般只需要把ssl的证书保留就行,不过也可以在云平台上重新下载。保守就是把/etc/nginx都打包一份。 版本:注意记录一下php、nginx的版本,以免出现版本不匹配。 迁移方式 迁移的方式有两种方式,一种是使用wordpress插件的方式,另外就是完全手动复制的方式。下面先总结一下这两种方式关键的路径: 使用wordpress插件方式 该方式主要使用wordpress插件WPvivid Backup插件,该插件可以直接备份wordpress+数据库。核心思路是: 备份wordpress+数据库:在WPvivid Backup后台做一次最新的备份。 备份nginx(可选):/etc/nginx压缩打包,可以只保留证书也行。 存储:将备份文件存储到本地或云端。 迁移:在服务器重置安装新的系统,然后将备份文件复制到新的服务器上。 安装:搭建一个新的php、nginx、wordpress环境。包括配置nginx和创建wordpress新的数据库链接等。 恢复:在新的wordpress站点上安装WPviviBackup插件,扫描备份文件进行还原。 配置:重新配置nginx支持https等。 使用手动方式 该方式不使用插件,手动进行会稍微多一些步骤。核心思路如下: 备份wordpress:将wordpress进行压缩打包。 备份数据库:使用mysqldump备份数据库。 备份nginx(可选):/etc/nginx压缩打包,可以只保留证书也行。 存储:将备份的文件存储到本地或云端。 迁移:服务器重置系统后,将备份文件复制到新服务器上。 按装:按照php、nginx。 恢复:将wordpress解压到nginx的主页目录下,恢复mysql数据库。 配置:重新配置nginx支持https等。 插件方式 备份 步骤1:备份wordpress与数据库 在插件WPvivid Backup后台备份wordpress+数据库到本地,然后通过ftp或scp拷贝到本地电脑。 步骤2:备份nginx的证书 在服务器/etc/nginx目录下,把cert证书压缩同理通过ftp或scp拷贝到本地电脑。这个证书路径可能不一样,根据自己实际情况。 重装 用插件做好数据迁移到本地备份后,即可销毁服务器上的系统重新安装了,怎么安装选择系统这里就不阐述了,本文重装的是ubuntu系统,系统启动后进行如下步骤按照软件。 步骤1:按照基本的软件 首先,确保软件包列表是最新的,这样可以避免安装过时的软件版本。 sudo apt update 如果你想升级已安装的软件到最新版本,使用:sudo apt upgrade 安装 Nginx:sudo apt install nginx 检查 Nginx 服务状态:sudo systemctl status nginx 启动Nginx:sudo systemctl start nginx 安装 MySQL:sudo apt install mysql-server 启动MySQL: sudo systemctl start mysql 安装 PHP 和相关的扩展:sudo apt install php-fpm php-mysql php-cli php-curl php-mbstring php-xml php-zip php-gd 安装其他如curl 或 git:sudo apt install curl git 按照wordpress: wget https://wordpress.org/latest.tar.gz tar -xzvf latest.tar.gz mv wordpress /var/www/html/ 步骤2:配置wordpress的数据库 先创建一个数据库。 mysql -u root -p CREATE DATABASE wordpress_db; CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'your_password'; GRANT ALL PRIVILEGES ON wordpress_db.* TO 'wpuser'@'localhost'; FLUSH PRIVILEGES; EXIT; 创建一个mysql的数据库,用于给wordpress创建一个数据库。 wordpress_db:数据库的名称。 wpuser: 访问数据库的用户名。 your_password:访问数据需要的密码。 接着修改wordpress的配置可以访问数据库。 cd /var/www/html/wordpress/ cp wp-config-sample.php wp-config.php vim wp-config.php 更新刚设置的数据库名 define('DB_NAME', 'wordpress_db'); define('DB_USER', 'wpuser'); define('DB_PASSWORD', 'your_password'); define( 'DB_HOST', 'localhost' ); 步骤3:配置nginx 先做个备份 cp /etc/nginx/sites-available/default /etc/nginx/sites-available/default_backup vim /etc/nginx/sites-available/default server { listen 80; server_name laumy.tech www.laumy.tech; root /var/www/html/wordpress; index index.php index.html index.htm index.nginx-debian.html; location / { try_files $uri $uri/ /index.php?$args; } location ~ .php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~ /.ht { deny all; } } server { listen 443 ssl; server_name laumy.tech www.laumy.tech; ssl_certificate /etc/nginx/cert/www.laumy.tech.pem; ssl_certificate_key /etc/nginx/cert/www.laumy.tech.key; root /var/www/html/wordpress; index index.php index.html index.htm index.nginx-debian.html; location / { try_files $uri $uri/ /index.php?$args; } location ~ .php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~ /.ht { deny all; } } 一个是配置http的监听端口,另外一个是配置https的监听端口。 server_name: 表示监听的网站,填自己的域名。 root /var/www/html/wordpress: 指定了网站寻址的路径,这里是wordpress存放的路径。 ssl_certificate**: 这个指定的是https的证书路径,只有443这个端口才需要填。 配置好后执行: sudo nginx -t && sudo systemctl reload nginx 修改wordpress配置强制支持https,vim /var/www/html/wordpress/wp-config.php,加上下面两行。 define('FORCE_SSL_ADMIN', true); define('FORCE_SSL_LOGIN', true); 步骤4:登录配置wordpress 没什么问题的话,登录自己的域名就可以进入到wordpress界面设置用户名和密码进入到后台了。 注意记得把后台的站点地址用https的方式,不然http和https混合访问的问题。 恢复 按照前面的步骤,顺利进入到wordpress后台之后,到这里就简单了。只需要按照插件WPvivid Backup进行还原即可。 步骤1:将备份文件拷贝到插件指定目录 默认一般是在/var/www/html/wordpress/wp-content/wpvividbackups 步骤:还原 在插件界面扫描到备份文件,进行还原。等还原完成,到此就大功告成了。 手动方式 手动跟插件的方式这里只列出差异点,就不再过多赘述了。 备份 对wordpress整个文件夹进行压缩,其次对mysql数据库进行备份。 mysqldump -u root -p your_database_name > wordpress-db-backup.sql your_database_name 是你的数据库名称,要是忘记了就去wordpress/wp-config.php看看DB_NAME,密码亦是如此如下。 define('DB_NAME', 'wordpress_db'); define('DB_USER', 'wpuser'); define('DB_PASSWORD', 'your_password'); 接着把wordpress压缩包、sql、nginx的证书拷贝到本地或云端。 恢复 将wordpress解压到/var/www/html/下。然后对数据库进行恢复,这里重点补充一下。 首先是要在mysql里面创建一个新的数据库。 mysql -u root -p CREATE DATABASE wordpress_db; CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'your_password'; GRANT ALL PRIVILEGES ON wordpress_db.* TO 'laumy'@'localhost'; FLUSH PRIVILEGES; exit 注意这里的wordpress_db、wpuser、your_password分别为数据库名称、用户名、密码。可以跟你wordpress里面wp-config.php的一样,这样到时候就不用改了。 接入导入新的数据库。 mysql -uwpuser -pyour_password wordpress_db < wordpress-db-backup.sql 这样数据库就导入完成了。 总结一下,无论是那种方式的备份,最关键的就是将wordpress和数据库能够迁移复制过去就行。而php、nginx正常按照新的就行。而nginx网站的配置,主要就是注意将wordpress要解压位置设置好,以及读写权限要配置好。其次就是配置/etc/nginx/sites-available/default指定好网站的root路径(即wordpress路径)和证书的路径即可完成。 最后要是要是过程中遇到问题可以询问ai,当下ai已经非常强大了,只要把环境信息告知好,基本上所有的问题都能能解决。 -
站点开源
Laumy Fresh Theme 本站点是基于wordpress设计,现开源其主题代码,让感兴趣的爱好者直接可复用。源码地址:https://github.com/laumy0929/wd-laumy-fresh-theme。 🚀 快速开始 方法一:WordPress后台安装(推荐) 下载主题压缩包 laumy-fresh-theme.zip 在WordPress后台进入 外观 > 主题 点击 添加新主题 > 上传主题 选择下载的压缩包文件 点击 现在安装 并激活主题 方法二:FTP上传 解压主题文件到本地 通过FTP上传到 /wp-content/themes/laumy-fresh-theme/ 目录 在WordPress后台激活主题 方法三:Git克隆 cd wp-content/themes/ git clone https://github.com/laumy0929/wd-laumy-fresh-theme.git 🌟 主题特色 设计理念 简洁美观:采用现代简约设计风格,专注于内容阅读体验 响应式布局:完美适配桌面端、平板和移动设备 深色模式:支持一键切换深色/浅色主题 高效性能:优化的CSS和JavaScript,确保快速加载 核心功能 📱 三栏布局:左侧分类导航 + 中间内容区域 + 右侧个人信息 🔍 智能搜索:顶部居中搜索框,快速查找内容 📂 分类管理:支持多级分类展开/收起,显示文章数量 📖 文章目录:自动生成多级目录,支持滚动高亮 💬 评论系统:完整的WordPress评论功能 📊 数据统计:显示文章数量和阅读量统计 🎨 主题切换:一键切换深色/浅色模式 ⚙️ 主题配置详解 个人资料设置 进入自定义设置 在WordPress后台点击 外观 > 自定义 或者点击 外观 > 主题 中的 自定义 按钮 配置个人信息 在 作者信息 部分设置: 头像: 点击 选择图片 上传个人头像 推荐尺寸:320×180 像素 支持格式:JPG、PNG、GIF 如果不上传,会显示默认头像 昵称: 输入你想显示的名称 会显示在右侧个人信息卡片的顶部 支持中文和英文 职业: 输入你的职业或身份标签 会显示为昵称右上角的绿色徽章 例如:前端工程师、全栈开发、技术博主 个性签名: 输入个人简介或座右铭 会显示在昵称下方 支持多行文本 保存设置 点击 发布 按钮保存所有设置 分类菜单配置 创建主菜单 进入 外观 > 菜单 点击 创建新菜单 输入菜单名称(如:主菜单) 选择菜单位置为 Primary Menu 添加分类到菜单 在左侧 分类 面板中选择需要的分类 点击 添加到菜单 拖拽调整分类顺序 设置父子分类关系: 将子分类拖拽到父分类下方 向右缩进表示层级关系 菜单结构示例 📁 技术笔记 📁 前端开发 📁 HTML/CSS 📁 JavaScript 📁 Vue.js 📁 后端开发 📁 PHP 📁 Python 📁 Node.js 📁 运维部署 📁 Linux 📁 Docker 📁 Nginx 文章缩略图设置 方法一:设置特色图片(推荐) 在编辑文章时,找到右侧 特色图片 面板 点击 设置特色图片 上传或选择图片 推荐尺寸:160×100 像素 点击 设置特色图片 确认 方法二:在文章内容中插入图片 在文章开头位置插入图片 主题会自动提取第一张图片作为缩略图 建议图片尺寸:160×100 像素或更大 缩略图优先级 特色图片(最高优先级) 文章内容第一张图片 默认占位图(如果以上都没有) 默认缩略图位置 文件路径:assets/images/default-thumbnail.svg 可以替换为自定义图片 建议保持 160×100 像素尺寸 文章目录功能 自动生成规则 基于文章中的 H2-H5 标题自动生成 支持无限级嵌套目录 自动编号:1、1.1、1.1.1... 使用方法 在文章中使用标准的标题标签: <h2>主要章节</h2> <h3>子章节</h3> <h4>详细内容</h4> 目录会自动显示在文章左侧 支持点击跳转和滚动高亮 目录样式 左侧:📋 剪贴板图标 右侧:☰ 汉堡菜单图标 支持展开/收起功能 深色模式配置 启用深色模式 在页面顶部找到主题切换按钮(太阳/月亮图标) 点击即可切换深色/浅色模式 设置会自动保存到浏览器本地存储 深色模式特性 自动调整所有颜色 背景色:深灰色 (#181a1b) 文字色:浅色 (#eceff1) 卡片背景:深色 (#202225) 📱 响应式设计说明 桌面端 (>1200px) 完整三栏布局 左侧分类导航:260px 右侧个人信息:300px 中间内容区域:自适应宽度 平板端 (992px-1200px) 保持三栏布局 侧边栏宽度自动调整 内容区域适当缩小 移动端 (<768px) 单列布局 隐藏网站标题和部分导航 搜索框居中显示 保留主题切换功能 🎨 自定义样式 CSS变量 主题使用CSS变量,便于自定义: :root { --header-height: 56px; --sidebar-left-width: 260px; --sidebar-right-width: 300px; --container-max-width: 1800px; --gap: 14px; --color-bg: #ffffff; --color-text: #222; --color-muted: #666; --color-primary: #3498db; --color-border: #e6e6e6; } 自定义颜色 在 外观 > 自定义 > 颜色 中调整: - 主色调 - 背景色 - 文字颜色 - 边框颜色 📁 文件结构 laumy-fresh-theme/ ├── style.css # 主题样式文件 ├── index.php # 首页模板 ├── single.php # 文章页面模板 ├── category.php # 分类页面模板 ├── search.php # 搜索结果模板 ├── page.php # 页面模板 ├── header.php # 头部模板 ├── footer.php # 底部模板 ├── functions.php # 主题功能文件 ├── comments.php # 评论模板 ├── searchform.php # 搜索表单 ├── screenshot.png # 主题截图 ├── assets/ │ ├── js/ │ │ └── theme.js # 主题JavaScript │ └── images/ │ ├── default-thumbnail.svg # 默认缩略图 │ └── default-profile.svg # 默认头像 └── README.md # 说明文档 🔧 高级功能配置 评论系统设置 进入 设置 > 讨论 启用 嵌套评论 功能 设置评论审核规则 配置评论通知 搜索功能优化 进入 设置 > 固定链接 选择 文章名 或 自定义结构 保存设置以优化搜索 性能优化 启用WordPress缓存插件 压缩图片文件 定期清理数据库 使用CDN加速 🎯 使用技巧 分类管理最佳实践 使用清晰的分类名称 合理设置分类层级(建议不超过3级) 定期整理和合并相似分类 为每个分类添加描述 文章写作建议 使用标准的H2-H5标题结构 在文章开头插入相关图片 设置合适的特色图片 添加适当的标签 用户体验优化 定期更新个人资料 保持分类结构清晰 使用高质量的缩略图 及时回复评论 🐛 常见问题解答 Q: 分类不显示或显示异常? A: 检查以下设置: 1. 在 外观 > 菜单 中是否设置了主菜单 2. 菜单位置是否选择为 Primary Menu 3. 分类是否已添加到菜单中 4. 分类是否有文章内容 Q: 个人资料头像不显示? A: 检查以下设置: 1. 在 外观 > 自定义 > 作者信息 中是否上传了头像 2. 图片格式是否支持(JPG、PNG、GIF) 3. 图片大小是否合适(建议320×180像素) Q: 文章缩略图显示异常? A: 尝试以下方法: 1. 设置文章特色图片 2. 在文章开头插入图片 3. 检查图片格式和大小 4. 替换默认缩略图文件 Q: 深色模式切换不生效? A: 检查以下设置: 1. 确保浏览器支持localStorage 2. 清除浏览器缓存 3. 检查是否有JavaScript错误 4. 尝试刷新页面 Q: 移动端显示异常? A: 检查以下设置: 1. 主题已针对移动端优化 2. 检查是否有其他插件冲突 3. 清除浏览器缓存 4. 检查CSS是否被其他插件覆盖 Q: 文章目录不显示? A: 检查以下设置: 1. 文章内容是否包含H2-H5标题 2. 标题标签是否正确使用 3. 检查JavaScript是否正常加载 4. 查看浏览器控制台是否有错误 📞 交流方式 作者:Laumy 邮箱:laumy0929@gmail.com 网站:http://www.laumy.tech/ GitHub:https://github.com/laumy0929/wd-laumy-fresh-theme 📄 许可证 本主题基于 GPL v2 或更高版本开源协议发布。 -
小智Ai语音交互简要分析
app start 主要是初始化板级、显示、WiFi连接、音频codec、编解码、协议、音效、唤醒几个环节。 auto& board = Board::GetInstance(); //获取板级实例 SetDeviceState(kDeviceStateStarting);//设置出事状态为kDeviceStateStarting /* Setup the display */ auto display = board.GetDisplay(); //获取显示实例 /* Setup the audio codec */ auto codec = board.GetAudioCodec();//获取codec实例 opus_decode_sample_rate_ = codec->output_sample_rate();//获取当前codec的采样率 opus_decoder_ = std::make_unique<OpusDecoderWrapper>(opus_decode_sample_rate_, 1);//初始化opus解码,设置解码采样率 opus_encoder_ = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);//初始化opus编码,设置采样率16Khz // For ML307 boards, we use complexity 5 to save bandwidth // For other boards, we use complexity 3 to save CPU //根据板级来设置opus编码的复杂度 if (board.GetBoardType() == "ml307") { ESP_LOGI(TAG, "ML307 board detected, setting opus encoder complexity to 5"); opus_encoder_->SetComplexity(5); } else { ESP_LOGI(TAG, "WiFi board detected, setting opus encoder complexity to 3"); opus_encoder_->SetComplexity(3); } //如果codec的采样率不是16Khz,需要进行重采样,下面是重采样初始化。 if (codec->input_sample_rate() != 16000) { input_resampler_.Configure(codec->input_sample_rate(), 16000); reference_resampler_.Configure(codec->input_sample_rate(), 16000); } //注册codec输入音频的回调,表示有录音的pcm,触发mainloop处理。 codec->OnInputReady([this, codec]() { BaseType_t higher_priority_task_woken = pdFALSE; xEventGroupSetBitsFromISR(event_group_, AUDIO_INPUT_READY_EVENT, &higher_priority_task_woken); return higher_priority_task_woken == pdTRUE; }); //注册codec输出音频的回调,表示有录音的pcm,触发mainloop处理。 codec->OnOutputReady([this]() { BaseType_t higher_priority_task_woken = pdFALSE; xEventGroupSetBitsFromISR(event_group_, AUDIO_OUTPUT_READY_EVENT, &higher_priority_task_woken); return higher_priority_task_woken == pdTRUE; }); //启动硬件codec,使能录音和播放。 codec->Start(); //开启一个mainloop线程,处理主要逻辑 /* Start the main loop */ xTaskCreate([](void* arg) { Application* app = (Application*)arg; app->MainLoop(); vTaskDelete(NULL); }, "main_loop", 4096 * 2, this, 4, nullptr); //等待WiFi连接好 /* Wait for the network to be ready */ board.StartNetwork(); // Initialize the protocol display->SetStatus(Lang::Strings::LOADING_PROTOCOL);//显示正在加载协议 根据使用MQTT还是Websocet来选择通信协议 #ifdef CONFIG_CONNECTION_TYPE_WEBSOCKET protocol_ = std::make_unique<WebsocketProtocol>(); #else protocol_ = std::make_unique<MqttProtocol>(); #endif //注册网络接收异常回调函数 protocol_->OnNetworkError([this](const std::string& message) { SetDeviceState(kDeviceStateIdle); Alert(Lang::Strings::ERROR, message.c_str(), "sad", Lang::Sounds::P3_EXCLAMATION); }); //注册接收音频的回调函数,接收到音频后,往加入解码队列 protocol_->OnIncomingAudio([this](std::vector<uint8_t>&& data) { std::lock_guard<std::mutex> lock(mutex_); if (device_state_ == kDeviceStateSpeaking) { audio_decode_queue_.emplace_back(std::move(data)); } }); //注册接收协议打开音频的回调,主要是下发解码的的属性信息,包括采样率等。 protocol_->OnAudioChannelOpened([this, codec, &board]() { board.SetPowerSaveMode(false); if (protocol_->server_sample_rate() != codec->output_sample_rate()) { ESP_LOGW(TAG, "Server sample rate %d does not match device output sample rate %d, resampling may cause distortion", protocol_->server_sample_rate(), codec->output_sample_rate()); } SetDecodeSampleRate(protocol_->server_sample_rate()); auto& thing_manager = iot::ThingManager::GetInstance(); protocol_->SendIotDescriptors(thing_manager.GetDescriptorsJson()); std::string states; if (thing_manager.GetStatesJson(states, false)) { protocol_->SendIotStates(states); } }); //注册音频的关闭回调 protocol_->OnAudioChannelClosed([this, &board]() { board.SetPowerSaveMode(true); Schedule([this]() { auto display = Board::GetInstance().GetDisplay(); display->SetChatMessage("system", ""); SetDeviceState(kDeviceStateIdle); }); }); //注册json解析回调,通知文本,状态等信息 protocol_->OnIncomingJson([this, display](const cJSON* root) { // Parse JSON data auto type = cJSON_GetObjectItem(root, "type"); //文字转语音的状态,包括start,stop,sentence_start/stop(句子开始结束), if (strcmp(type->valuestring, "tts") == 0) { auto state = cJSON_GetObjectItem(root, "state"); if (strcmp(state->valuestring, "start") == 0) { Schedule([this]() { aborted_ = false; if (device_state_ == kDeviceStateIdle || device_state_ == kDeviceStateListening) { SetDeviceState(kDeviceStateSpeaking); } }); } else if (strcmp(state->valuestring, "stop") == 0) { Schedule([this]() { if (device_state_ == kDeviceStateSpeaking) { background_task_->WaitForCompletion(); if (keep_listening_) { protocol_->SendStartListening(kListeningModeAutoStop); SetDeviceState(kDeviceStateListening); } else { SetDeviceState(kDeviceStateIdle); } } }); //句子开始 } else if (strcmp(state->valuestring, "sentence_start") == 0) { auto text = cJSON_GetObjectItem(root, "text"); if (text != NULL) { ESP_LOGI(TAG, "<< %s", text->valuestring); Schedule([this, display, message = std::string(text->valuestring)]() { display->SetChatMessage("assistant", message.c_str()); }); } } =//stt:语音转文字信息 } else if (strcmp(type->valuestring, "stt") == 0) { auto text = cJSON_GetObjectItem(root, "text"); if (text != NULL) { ESP_LOGI(TAG, ">> %s", text->valuestring); Schedule([this, display, message = std::string(text->valuestring)]() { display->SetChatMessage("user", message.c_str()); }); } } else if (strcmp(type->valuestring, "llm") == 0) { auto emotion = cJSON_GetObjectItem(root, "emotion"); if (emotion != NULL) { Schedule([this, display, emotion_str = std::string(emotion->valuestring)]() { display->SetEmotion(emotion_str.c_str()); }); } } else if (strcmp(type->valuestring, "iot") == 0) { auto commands = cJSON_GetObjectItem(root, "commands"); if (commands != NULL) { auto& thing_manager = iot::ThingManager::GetInstance(); for (int i = 0; i < cJSON_GetArraySize(commands); ++i) { auto command = cJSON_GetArrayItem(commands, i); thing_manager.Invoke(command); } } } }); //启动协议 protocol_->Start(); //检测OTA的版本,如果版本比较低则进行升级 // Check for new firmware version or get the MQTT broker address ota_.SetCheckVersionUrl(CONFIG_OTA_VERSION_URL); ota_.SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str()); ota_.SetHeader("Client-Id", board.GetUuid()); ota_.SetHeader("Accept-Language", Lang::CODE); auto app_desc = esp_app_get_description(); ota_.SetHeader("User-Agent", std::string(BOARD_NAME "/") + app_desc->version); xTaskCreate([](void* arg) { Application* app = (Application*)arg; app->CheckNewVersion(); vTaskDelete(NULL); }, "check_new_version", 4096 * 2, this, 2, nullptr); #if CONFIG_USE_AUDIO_PROCESSOR //初始化音频处理,主要是降噪,回声消除,VAD检测等。 audio_processor_.Initialize(codec->input_channels(), codec->input_reference()); audio_processor_.OnOutput([this](std::vector<int16_t>&& data) { background_task_->Schedule([this, data = std::move(data)]() mutable { opus_encoder_->Encode(std::move(data), [this](std::vector<uint8_t>&& opus) { //如果启动了音效处理,注册ouput的输出回调。 Schedule([this, opus = std::move(opus)]() { protocol_->SendAudio(opus); }); }); }); }); //注册VAD状态变化 audio_processor_.OnVadStateChange([this](bool speaking) { if (device_state_ == kDeviceStateListening) { Schedule([this, speaking]() { if (speaking) { voice_detected_ = true; } else { voice_detected_ = false; } auto led = Board::GetInstance().GetLed(); led->OnStateChanged();//只点个灯?? }); } }); #endif #if CONFIG_USE_WAKE_WORD_DETECT //启动唤醒检测,初始化唤醒 wake_word_detect_.Initialize(codec->input_channels(), codec->input_reference()); //唤醒词处理回调函数,其中获取到的唤醒词是字符串,还包括获取处理唤醒词的音频编解码 //唤醒词音频部分是否仅仅是唤醒词部分,还包含其他内容数据?需要确认 wake_word_detect_.OnWakeWordDetected([this](const std::string& wake_word) { Schedule([this, &wake_word]() { //如果是idle状态,主要逻辑是,处理业务为连接网络,编码唤醒词,重开唤醒检测 //推送唤醒的音频数据和预料字符串到云端服务器。 if (device_state_ == kDeviceStateIdle) { SetDeviceState(kDeviceStateConnecting); //将唤醒音频内容进行编码 wake_word_detect_.EncodeWakeWordData(); if (!protocol_->OpenAudioChannel()) { //重新再次打开唤醒检测, wake_word_detect_.StartDetection(); return; } //哪些情况会停止唤醒检测:1 检测到唤醒词后会停止。2.处于listening的时候会停止。3.OTA升级过程会停止 std::vector<uint8_t> opus; //编码并将唤醒数据推送到服务器(除了唤醒词可能还包括说话数据?) // Encode and send the wake word data to the server while (wake_word_detect_.GetWakeWordOpus(opus)) { protocol_->SendAudio(opus); } //发送唤醒词的字符串 // Set the chat state to wake word detected protocol_->SendWakeWordDetected(wake_word); ESP_LOGI(TAG, "Wake word detected: %s", wake_word.c_str()); keep_listening_ = true; SetDeviceState(kDeviceStateIdle); } else if (device_state_ == kDeviceStateSpeaking) { //如果说话状态,则将说话进行停止,设置一个停止标志位,并发送停止speak给服务不要再发opus了? AbortSpeaking(kAbortReasonWakeWordDetected); } else if (device_state_ == kDeviceStateActivating) { SetDeviceState(kDeviceStateIdle); } }); }); //启动唤醒检测 wake_word_detect_.StartDetection(); #endif //设置状态为IDLE状态 SetDeviceState(kDeviceStateIdle); esp_timer_start_periodic(clock_timer_handle_, 1000000); mainloop void Application::MainLoop() { while (true) { auto bits = xEventGroupWaitBits(event_group_, SCHEDULE_EVENT | AUDIO_INPUT_READY_EVENT | AUDIO_OUTPUT_READY_EVENT, pdTRUE, pdFALSE, portMAX_DELAY); //处理录音音频处理,将收到的音频做处理送到队列 if (bits & AUDIO_INPUT_READY_EVENT) { InputAudio(); } //处理云端音频处理,将编码的音频进行解码送播放器 if (bits & AUDIO_OUTPUT_READY_EVENT) { OutputAudio(); } //处理其他任务的队列 if (bits & SCHEDULE_EVENT) { std::unique_lock<std::mutex> lock(mutex_); std::list<std::function<void()>> tasks = std::move(main_tasks_); lock.unlock(); for (auto& task : tasks) { task(); } } } } 录音通路 录音处理 // I2S收到音频,触发app应用注册的回调函数通知函数codec->OnInputReady,如下 //通知有数据了,实际读数据通过Read去读。 IRAM_ATTR bool AudioCodec::on_recv(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) { auto audio_codec = (AudioCodec*)user_ctx; if (audio_codec->input_enabled_ && audio_codec->on_input_ready_) { return audio_codec->on_input_ready_(); } return false; } //通过eventsetbit触发通知mainloop线程处理音频 codec->OnInputReady([this, codec]() { BaseType_t higher_priority_task_woken = pdFALSE; xEventGroupSetBitsFromISR(event_group_, AUDIO_INPUT_READY_EVENT, &higher_priority_task_woken); return higher_priority_task_woken == pdTRUE; }); //在mainloop中触发Application::InputAudio() void Application::InputAudio() { //获取codec的实例 auto codec = Board::GetInstance().GetAudioCodec(); std::vector<int16_t> data; //获取codec的音频pcm数据存到data中。 if (!codec->InputData(data)) { return;//如果数据为空,直接返回 } //如果采样率不是16Khz,需要进行重采样 if (codec->input_sample_rate() != 16000) { if (codec->input_channels() == 2) { auto mic_channel = std::vector<int16_t>(data.size() / 2); auto reference_channel = std::vector<int16_t>(data.size() / 2); for (size_t i = 0, j = 0; i < mic_channel.size(); ++i, j += 2) { mic_channel[i] = data[j]; reference_channel[i] = data[j + 1]; } auto resampled_mic = std::vector<int16_t>(input_resampler_.GetOutputSamples(mic_channel.size())); auto resampled_reference = std::vector<int16_t>(reference_resampler_.GetOutputSamples(reference_channel.size())); input_resampler_.Process(mic_channel.data(), mic_channel.size(), resampled_mic.data()); reference_resampler_.Process(reference_channel.data(), reference_channel.size(), resampled_reference.data()); data.resize(resampled_mic.size() + resampled_reference.size()); for (size_t i = 0, j = 0; i < resampled_mic.size(); ++i, j += 2) { data[j] = resampled_mic[i]; data[j + 1] = resampled_reference[i]; } } else { auto resampled = std::vector<int16_t>(input_resampler_.GetOutputSamples(data.size())); input_resampler_.Process(data.data(), data.size(), resampled.data()); data = std::move(resampled); } } //如果启动了唤醒检测,判断唤醒检测是否还在运行,如果还在运行将当前的数据合并到唤醒 //检测的buffer中。 #if CONFIG_USE_WAKE_WORD_DETECT if (wake_word_detect_.IsDetectionRunning()) { wake_word_detect_.Feed(data); //会将当前的数据喂给AFE接口,用于做唤醒词 //唤醒词也直接送到云端了??? } #endif //如果打开了音效处理,将音频数据push到音效处理中,直接返回 #if CONFIG_USE_AUDIO_PROCESSOR if (audio_processor_.IsRunning()) { audio_processor_.Input(data); } #else //如果没有打开音效处理,判断当前的状态是否是监听状态,如果是将音频进行编码 //然后推送到远端服务中。 if (device_state_ == kDeviceStateListening) { background_task_->Schedule([this, data = std::move(data)]() mutable { opus_encoder_->Encode(std::move(data), [this](std::vector<uint8_t>&& opus) { Schedule([this, opus = std::move(opus)]() { protocol_->SendAudio(opus); }); }); }); } #endif } 音效处理 以下是音效处理过程 //将数据喂给AFE模块,当处理完了之后会触发回调? void AudioProcessor::Input(const std::vector<int16_t>& data) { input_buffer_.insert(input_buffer_.end(), data.begin(), data.end()); auto feed_size = afe_iface_->get_feed_chunksize(afe_data_) * channels_; while (input_buffer_.size() >= feed_size) { auto chunk = input_buffer_.data(); afe_iface_->feed(afe_data_, chunk); input_buffer_.erase(input_buffer_.begin(), input_buffer_.begin() + feed_size); } } void AudioProcessor::AudioProcessorTask() { auto fetch_size = afe_iface_->get_fetch_chunksize(afe_data_); auto feed_size = afe_iface_->get_feed_chunksize(afe_data_); ESP_LOGI(TAG, "Audio communication task started, feed size: %d fetch size: %d", feed_size, fetch_size); while (true) { //获取到PROCESSOR_RUNNING后,不会清除bit(第三个参数),也就说会再次得到运行。 //也就是说AudioProcessor::Start()后,这个会循环运行,直到调用Stop清除。 xEventGroupWaitBits(event_group_, PROCESSOR_RUNNING, pdFALSE, pdTRUE, portMAX_DELAY); //等待获取处理后的数据。 auto res = afe_iface_->fetch_with_delay(afe_data_, portMAX_DELAY); if ((xEventGroupGetBits(event_group_) & PROCESSOR_RUNNING) == 0) { continue; } if (res == nullptr || res->ret_value == ESP_FAIL) { if (res != nullptr) { ESP_LOGI(TAG, "Error code: %d", res->ret_value); } continue; } // VAD state change if (vad_state_change_callback_) { if (res->vad_state == VAD_SPEECH && !is_speaking_) { is_speaking_ = true; vad_state_change_callback_(true); } else if (res->vad_state == VAD_SILENCE && is_speaking_) { is_speaking_ = false; vad_state_change_callback_(false); } } //获取到数据,将数据回调给app->audio_processor_.OnOutput if (output_callback_) { output_callback_(std::vector<int16_t>(res->data, res->data + res->data_size / sizeof(int16_t))); } } } //处理的音效数据的回调,将数据进行编码,然后推送到云端服务器。 audio_processor_.OnOutput([this](std::vector<int16_t>&& data) { background_task_->Schedule([this, data = std::move(data)]() mutable { opus_encoder_->Encode(std::move(data), [this](std::vector<uint8_t>&& opus) { Schedule([this, opus = std::move(opus)]() { protocol_->SendAudio(opus); }); }); }); }); 播放通路 //1. 通过解析输入的json来启动状态的切换。 protocol_->OnIncomingJson([this, display](const cJSON* root) { // Parse JSON data auto type = cJSON_GetObjectItem(root, "type"); if (strcmp(type->valuestring, "tts") == 0) { auto state = cJSON_GetObjectItem(root, "state"); //收到云端音频,云端会发送start,需要切换到speaking状态。 if (strcmp(state->valuestring, "start") == 0) { Schedule([this]() { aborted_ = false; if (device_state_ == kDeviceStateIdle || device_state_ == kDeviceStateListening) { SetDeviceState(kDeviceStateSpeaking); } }); //本次话题结束后,云端会发送stop,可切换到idle。 } else if (strcmp(state->valuestring, "stop") == 0) { Schedule([this]() { if (device_state_ == kDeviceStateSpeaking) { background_task_->WaitForCompletion(); if (keep_listening_) { protocol_->SendStartListening(kListeningModeAutoStop); SetDeviceState(kDeviceStateListening); } else { SetDeviceState(kDeviceStateIdle); } } }); } else if (strcmp(state->valuestring, "sentence_start") == 0) { auto text = cJSON_GetObjectItem(root, "text"); if (text != NULL) { ESP_LOGI(TAG, "<< %s", text->valuestring); Schedule([this, display, message = std::string(text->valuestring)]() { display->SetChatMessage("assistant", message.c_str()); }); } } //2.解析到云端的json后,会发生状态的迁移 void Application::SetDeviceState(DeviceState state) { if (device_state_ == state) { return; } clock_ticks_ = 0; auto previous_state = device_state_; device_state_ = state; ESP_LOGI(TAG, "STATE: %s", STATE_STRINGS[device_state_]); // The state is changed, wait for all background tasks to finish background_task_->WaitForCompletion(); //如果后台有线程还在运行,等待运行结束 auto& board = Board::GetInstance(); auto codec = board.GetAudioCodec(); auto display = board.GetDisplay(); auto led = board.GetLed(); led->OnStateChanged(); switch (state) { case kDeviceStateUnknown: case kDeviceStateIdle: //idle状态,显示"待命" display->SetStatus(Lang::Strings::STANDBY); display->SetEmotion("neutral"); #if CONFIG_USE_AUDIO_PROCESSOR //关掉音效处理 audio_processor_.Stop(); #endif #if CONFIG_USE_WAKE_WORD_DETECT //开启语音唤醒检测 wake_word_detect_.StartDetection(); #endif break; case kDeviceStateConnecting: //连接状态,表示连接服务器 display->SetStatus(Lang::Strings::CONNECTING); display->SetEmotion("neutral"); display->SetChatMessage("system", ""); break; case kDeviceStateListening: //说话状态,显示说话中 display->SetStatus(Lang::Strings::LISTENING); display->SetEmotion("neutral"); //复位解码器,清除掉原来的 ResetDecoder(); //复位编码器的状态 opus_encoder_->ResetState(); #if CONFIG_USE_AUDIO_PROCESSOR //启动音效处理(回声消除?) audio_processor_.Start(); #endif #if CONFIG_USE_WAKE_WORD_DETECT //关闭唤醒检测 wake_word_detect_.StopDetection(); #endif //更新IOT状态 UpdateIotStates(); if (previous_state == kDeviceStateSpeaking) { // FIXME: Wait for the speaker to empty the buffer vTaskDelay(pdMS_TO_TICKS(120)); } break; case kDeviceStateSpeaking: display->SetStatus(Lang::Strings::SPEAKING); //复位解码器 ResetDecoder(); //使能codec输出 codec->EnableOutput(true); #if CONFIG_USE_AUDIO_PROCESSOR //音效处理停止 audio_processor_.Stop(); #endif #if CONFIG_USE_WAKE_WORD_DETECT //开启唤醒检测 wake_word_detect_.StartDetection(); #endif break; default: // Do nothing break; } } //3. 接收云端音频数据的回调,如果是speak状态,将数据入队到队列 protocol_->OnIncomingAudio([this](std::vector<uint8_t>&& data) { std::lock_guard<std::mutex> lock(mutex_); if (device_state_ == kDeviceStateSpeaking) { audio_decode_queue_.emplace_back(std::move(data)); } }); //4.当音频输出准备好后,不会不断的调用这个回调??触发mainloop调用OutputAudio codec->OnOutputReady([this]() { BaseType_t higher_priority_task_woken = pdFALSE; xEventGroupSetBitsFromISR(event_group_, AUDIO_OUTPUT_READY_EVENT, &higher_priority_task_woken); return higher_priority_task_woken == pdTRUE; }); //5. output处理 void Application::OutputAudio() { auto now = std::chrono::steady_clock::now(); auto codec = Board::GetInstance().GetAudioCodec(); const int max_silence_seconds = 10; std::unique_lock<std::mutex> lock(mutex_); //判断解码队列是否为空,如果为空,把codec输出关了,也就是不要再触发回调 if (audio_decode_queue_.empty()) { // Disable the output if there is no audio data for a long time if (device_state_ == kDeviceStateIdle) { auto duration = std::chrono::duration_cast<std::chrono::seconds>(now - last_output_time_).count(); if (duration > max_silence_seconds) { codec->EnableOutput(false); } } return; } //如果是在监听状态,清除掉解码队列,直接返回 if (device_state_ == kDeviceStateListening) { audio_decode_queue_.clear(); return; } //获取编码的数据 last_output_time_ = now; auto opus = std::move(audio_decode_queue_.front()); audio_decode_queue_.pop_front(); lock.unlock(); //将解码数据添加到调度中进行解码播放 background_task_->Schedule([this, codec, opus = std::move(opus)]() mutable { //如果禁止标志位置起,直接退出。在打断唤醒的时候回置起 if (aborted_) { return; } std::vector<int16_t> pcm; //解码为pcm if (!opus_decoder_->Decode(std::move(opus), pcm)) { return; } //如果云端的采样率和codec采样率不一样,进行重采样。 // Resample if the sample rate is different if (opus_decode_sample_rate_ != codec->output_sample_rate()) { int target_size = output_resampler_.GetOutputSamples(pcm.size()); std::vector<int16_t> resampled(target_size); output_resampler_.Process(pcm.data(), pcm.size(), resampled.data()); pcm = std::move(resampled); } //播放音频 codec->OutputData(pcm); }); } -
C++回顾
平时用C++比较少,最近项目需要用到C++,现简单再回顾一下。 单例模式 在某些场景下,一个类只需要有一个实例就足够了,例如配置管理类、日志记录器、数据库连接池等。使用单例模式可以避免创建多个实例导致的资源浪费、数据不一致等问题。 单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。在程序运行期间,这个类的实例始终只有一个,所有对该类实例的访问都通过这个全局访问点进行。 #include <iostream> class Board { private: // 私有构造函数,防止外部实例化 Board() {} // 禁止拷贝构造函数 Board(const Board&) = delete; // 禁止赋值运算符 Board& operator=(const Board&) = delete; static Board* instance; public: // 静态方法,用于获取唯一实例 static Board& GetInstance() { if (instance == nullptr) { instance = new Board(); } return *instance; } void printMessage() { std::cout << "This is the Board instance." << std::endl; } }; // 初始化静态成员变量 Board* Board::instance = nullptr; int main() { Board& board = Board::GetInstance(); board.printMessage(); return 0; } 智能指针 在传统的 C++ 中,使用 new 运算符手动分配内存,需要使用 delete 运算符手动释放内存。如果忘记释放内存,就会导致内存泄漏。使用智能指针可以自动管理对象的生命周期,避免内存泄漏的问题。 std::make_unique 是 C++14 引入的一个函数模板,用于创建 std::unique_ptr 对象。std::unique_ptr 是一种智能指针,它对所指向的对象拥有唯一所有权,即同一时间只能有一个 std::unique_ptr 指向同一个对象。当 std::unique_ptr 被销毁时,它所指向的对象也会被自动销毁。 #include <iostream> #include <memory> class MyClass { public: MyClass() { std::cout << "MyClass constructor" << std::endl; } ~MyClass() { std::cout << "MyClass destructor" << std::endl; } void doSomething() { std::cout << "Doing something..." << std::endl; } }; int main() { // 使用 std::make_unique 创建 std::unique_ptr auto ptr = std::make_unique<MyClass>(); ptr->doSomething(); // 当 ptr 离开作用域时,MyClass 对象会自动销毁 return 0; } Lambda 表达式 Lambda 表达式的主要优点是简洁性和灵活性。在一些场景下,我们只需要一个简单的函数,而且这个函数只在某个特定的地方使用一次,使用 Lambda 表达式可以避免定义一个单独的命名函数,使代码更加简洁。 Lambda 表达式是 C++11 引入的一种匿名函数机制,它允许在代码中定义一个临时的、没有名称的函数。Lambda 表达式可以捕获外部变量,从而在函数内部使用这些变量。 [capture list] (parameter list) mutable(可选) -> return type(可选) { function body } 实例,lambda一般用于设置回调函数。 codec->OnInputReady([this, codec]() { // 捕获列表 [this, codec] 表示捕获当前对象和 codec 变量 xEventGroupSetBitsFromISR(...); return ...; }); 移动语义std::move 在某些情况下,对象的拷贝操作可能会非常昂贵,例如对象包含大量的数据或者动态分配的内存。使用移动语义可以避免这些不必要的拷贝操作,提高程序的性能。 移动语义是 C++11 引入的一个重要特性,它允许将对象的资源所有权从一个对象转移到另一个对象,避免不必要的拷贝操作。std::move 是一个标准库函数,用于将左值转换为右值引用,从而触发移动构造函数或移动赋值运算符。 #include <iostream> #include <vector> class MyVector { private: std::vector<int> data; public: MyVector(const std::vector<int>& vec) : data(vec) { std::cout << \"Copy constructor called\" << std::endl; } // 移动构造函数 MyVector(MyVector&& other) noexcept : data(std::move(other.data)) { std::cout << \"Move constructor called\" << std::endl; } void printSize() { std::cout << \"Size: \" << data.size() << std::endl; } }; int main() { std::vector<int> vec = {1, 2, 3, 4, 5}; MyVector v1(vec); // 使用 std::move 调用移动构造函数 MyVector v2(std::move(v1)); v2.printSize(); return 0; } 在上述代码中,std::move(v1) 将 v1 转换为右值引用,从而调用 MyVector 的移动构造函数。移动构造函数将 v1 的 data 资源所有权转移到 v2,避免了不必要的拷贝操作。 模版 C++ 模板(Template)是一种强大的编程特性,它允许你编写泛型代码,使得代码能够在不同数据类型上重复使用。模板支持函数模板和类模板,它们能在编译时根据具体类型生成代码,从而提高代码的复用性和灵活性。 函数模版 #include <iostream> using namespace std; template <typename T> T add(T a, T b) { return a + b; } //template <typename T>:这是函数模板的声明。T 是类型参数,表示函数可以处理任何类型。 //T add(T a, T b):定义了一个接受两个 T 类型参数并返回一个 T 类型结果的函数。 int main() { cout << add(3, 5) << endl; // 调用模板函数,类型为 int cout << add(3.5, 5.5) << endl; // 调用模板函数,类型为 double return 0; } 类模版 #include <iostream> using namespace std; template <typename T> class Box { private: T value; public: void setValue(T v) { value = v; } T getValue() { return value; } }; //template <typename T>:定义类模板,T 是类型参数。 //T value:类中的成员变量 value 是类型 T。 //setValue(T v) 和 getValue():成员函数也使用模板类型 T。 int main() { Box<int> intBox; intBox.setValue(5); cout << intBox.getValue() << endl; // 输出:5 Box<double> doubleBox; doubleBox.setValue(3.14); cout << doubleBox.getValue() << endl; // 输出:3.14 return 0; } -
裁剪脚本
查找当前目录大于指定大小 #!/bin/bash # 遍历当前目录及子目录中的所有文件 find . -type f -exec du -b {} + | while read size file; do # 如果文件大小为50KB(即50 * 1024字节) if [ "$size" -gt 51200 ]; then echo "文件: $file, 大小: $((size / 1024)) KB" fi done 查找当前的库是哪些应用依赖 #!/bin/bash # 当前目录 dst_lib=libblkid.so search_dir="." # 递归查找所有可执行文件 find "$search_dir" | while read -r file; do # 使用 ldd 检查依赖 if riscv32-linux-musl-readelf -d "$file" 2>/dev/null | grep -q "$dst_lib"; then echo "$file 依赖 $dst_lib" fi done -
2条命令本地部署deepseek
环境是centos,下面是部署步骤。 命令1: 安装ollama 安装命令:curl -fsSL https://ollama.com/install.sh | sh 安装日志: >>> Cleaning up old version at /usr/local/lib/ollama >>> Installing ollama to /usr/local >>> Downloading Linux amd64 bundle ######################################################################## 100.0% >>> Creating ollama user... >>> Adding ollama user to video group... >>> Adding current user to ollama group... >>> Creating ollama systemd service... >>> Enabling and starting ollama service... Created symlink from /etc/systemd/system/default.target.wants/ollama.service to /etc/systemd/system/ollama.service. >>> The Ollama API is now available at 127.0.0.1:11434. >>> Install complete. Run "ollama" from the command line. 命令2:下载deepseek模型 安装命令:ollama run deepseek-r1:7b 安装完成后,会直接进入交互控制台: pulling manifest pulling 96c415656d37... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 4.7 GB pulling 369ca498f347... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 387 B pulling 6e4c38e1172f... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 1.1 KB pulling f4d24e9138dd... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 148 B pulling 40fb844194b2... 100% ▕█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 487 B verifying sha256 digest writing manifest success >>> hello <think> </think> Hello! How can I assist you today? 😊 >>> 你好 <think> </think> 你好!有什么我可以帮助你的吗?😊 >>> 你是什么模型 <think> </think> 您好!我是由中国的深度求索(DeepSeek)公司开发的智能助手DeepSeek-R1。如您有任何任何问题,我会尽我所能为您提供帮助。 运行时,如果加上--verbose可以查看运行性能参数,如下: total duration: 379.511567ms load duration: 14.749448ms prompt eval count: 60 token(s) prompt eval duration: 15.863495ms prompt eval rate: 3782.27 tokens/s eval count: 64 token(s) eval duration: 322.980292ms eval rate: 198.15 tokens/s total duration:总耗时379.51ms,表示从请求开始到响应完成的整体处理时间 load duration: 模型加载耗时14.75ms,可能涉及模型初始化或数据加载阶段的时间消耗 prompt eval count:输入提示词(prompt)解析的token数量为60个 prompt eval duration:提示词解析耗时15.86ms,反映模型对输入文本的预处理效率 prompt eval rate: 提示词解析速率3782.27 tokens/s,属于高性能表现(通常千级tokens/s为优秀) eval count: 生成输出的token数量为64个 eval duration: 生成耗时322.98ms,占整体耗时的主要部分。 eval rate: 生成速率198.15 tokens/s,属于典型的大模型推理速度(百级tokens/s为常见范围) GGUF导入部署 这种方式可以通过导入GUFF格式的大模型,GUFF格式大模型可以从Hugging Face获取https://huggingface.co/。也可以在modelscope上获取https://modelscope.cn/models。 首先从Hugging Face或者modelscope下载GGUF格式的模型,然后部署主要分为两个步骤 创建模型 通过create指定模型modelfile。 ollama create qwen2.5:7b -f qwen2.5-7b.modelfile modelfile内容如下,指定了模型的路径,模型配置文件描述了模型的参数,更多信息这里不做阐述。 FROM "./qwen2.5-7b-instruct-q4_0.gguf" 运行模型 列出模型 ollama list 运行模型 verbose参数可以打印性能。 ollama run qwen2.5:7b --verbose 也可以使用ollama pull从ollama官方下载,https://ollama.com/search 支持API访问 修改ollama的本地端口 /etc/systemd/system/ollama.service [Unit] Description=Ollama Service After=network-online.target [Service] ExecStart=/usr/local/bin/ollama serve User=ollama Group=ollama Restart=always RestartSec=3 Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin" Environment="OLLAMA_HOST=0.0.0.0" [Install] WantedBy=default.target 然后重新启动 systemctl daemon-reload systemctl restart ollama 确认是否启动成功: sudo netstat -tulpn | grep 11434 # 确认监听0.0.0.0:11434:cite[3]:cite[6] 远程API调用示例 # 查询API版本(验证连通性) curl http://<服务器公网IP>:11434/api/version # 发送生成请求 curl http://localhost:11434/api/generate -d "{\\\\\\\\\\\\\\\\"model\\\\\\\\\\\\\\\\": \\\\\\\\\\\\\\\\"deepseek-r1:7b\\\\\\\\\\\\\\\\", \\\\\\\\\\\\\\\\"prompt\\\\\\\\\\\\\\\\": \\\\\\\\\\\\\\\\"为什么草是绿的\\\\\\\\\\\\\\\\"}" 参考:https://github.com/datawhalechina/handy-ollama/blob/main/docs/C4/1.%20Ollama%20API%20%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97.md 支持web聊天 安装docker 如果要按照网页版的聊天需要安装open ui,先安装docker。 (1)更新系统 sudo yum update -y (2)Docker 需要一些依赖包,你可以通过运行以下命令来安装: sudo yum install -y yum-utils device-mapper-persistent-data lvm2 (3)更新本地镜像源 sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo sed -i 's/download.docker.com/mirrors.aliyun.com\\\\\\\\\\\\\\\\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo yum makecache fast (4)安装docker sudo yum install -y docker-ce (5)设置开机自启动 sudo systemctl start docker sudo systemctl enable docker (6)验证 sudo docker --version systemctl status docker docker安装open webui 拉取并运行 Open WebUI 容器,将容器端口 8080 映射到主机 3000 端口 docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main 如果3000端口被占用了,会报错,重新启动也会提示错误如下。 报错解决: docker run -d -p 6664:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main docker: Error response from daemon: Conflict. The container name "/open-webui" is already in use by container "88f6e12e8e3814038911c30d788cb222d0792a9fc0af45f41140e07186e62a16". You have to remove (or rename) that container to be able to reuse that name. 你遇到的问题是 Docker 容器名称冲突。错误消息表明,容器名称 /open-webui 已经被另一个正在运行的容器占用,因此你无法启动新的容器。 (1)查看当前运行的容器: docker ps -a 88f6e12e8e38 ghcr.io/open-webui/open-webui:main "bash start.sh" 3 minutes ago Created open-webui (2)停止并删除已有的容器 docker stop open-webui docker rm open-webui 登录网址https://xxx:6664 配置即可访问。 -
豆包大模型接入体验
前置条件 需要先创建获得API key和创建推理接入点。 API key获取 https://www.volcengine.com/docs/82379/1361424#f79da451 创建推理接入点 https://www.volcengine.com/docs/82379/1099522 安装python环境 python版本需要安装到Python 2.7或以上版本。执行python --version可以检查当前Python的版本信息。我这里的版本已经到3.8.10 python3 --version Python 3.8.10 接着安装豆包sdk pip install volcengine-python-sdk Collecting volcengine-python-sdk Downloading volcengine-python-sdk-1.0.118.tar.gz (3.1 MB) |████████████████████████████████| 3.1 MB 9.7 kB/s Requirement already satisfied: certifi>=2017.4.17 in /usr/lib/python3/dist-packages (from volcengine-python-sdk) (2019.11.28) Requirement already satisfied: python-dateutil>=2.1 in /usr/lib/python3/dist-packages (from volcengine-python-sdk) (2.7.3) Requirement already satisfied: six>=1.10 in /usr/lib/python3/dist-packages (from volcengine-python-sdk) (1.14.0) Requirement already satisfied: urllib3>=1.23 in /usr/lib/python3/dist-packages (from volcengine-python-sdk) (1.25.8) Building wheels for collected packages: volcengine-python-sdk Building wheel for volcengine-python-sdk (setup.py) ... done Created wheel for volcengine-python-sdk: filename=volcengine_python_sdk-1.0.118-py3-none-any.whl size=10397043 sha256=c4546246eb0ef4e1c68e8047c6f2773d601821bd1acb7bc3a6162919f161423b Stored in directory: /home/apple/.cache/pip/wheels/d2/dc/23/70fa1060e1a527a290fc87a35469401b7588cdb51a2b75797d Successfully built volcengine-python-sdk Installing collected packages: volcengine-python-sdk Successfully installed volcengine-python-sdk-1.0.118 需要更新 pip install --upgrade 'volcengine-python-sdk[ark]' Requirement already up-to-date: volcengine-python-sdk[ark] in /home/apple/.local/lib/python3.8/site-packages (1.0.118) Requirement already satisfied, skipping upgrade: urllib3>=1.23 in /usr/lib/python3/dist-packages (from volcengine-python-sdk[ark]) (1.25.8) Requirement already satisfied, skipping upgrade: six>=1.10 in /usr/lib/python3/dist-packages (from volcengine-python-sdk[ark]) (1.14.0) Requirement already satisfied, skipping upgrade: python-dateutil>=2.1 in /usr/lib/python3/dist-packages (from volcengine-python-sdk[ark]) (2.7.3) Requirement already satisfied, skipping upgrade: certifi>=2017.4.17 in /usr/lib/python3/dist-packages (from volcengine-python-sdk[ark]) (2019.11.28) Collecting cryptography<43.0.4,>=43.0.3; extra == "ark" Downloading cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.0 MB) |████████████████████████████████| 4.0 MB 1.7 MB/s Collecting httpx<1,>=0.23.0; extra == "ark" Downloading httpx-0.28.1-py3-none-any.whl (73 kB) |████████████████████████████████| 73 kB 1.0 MB/s Collecting pydantic<3,>=1.9.0; extra == "ark" Downloading pydantic-2.10.4-py3-none-any.whl (431 kB) |████████████████████████████████| 431 kB 1.6 MB/s Collecting anyio<5,>=3.5.0; extra == "ark" Downloading anyio-4.5.2-py3-none-any.whl (89 kB) |████████████████████████████████| 89 kB 1.8 MB/s Collecting cffi>=1.12; platform_python_implementation != "PyPy" Downloading cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (446 kB) |████████████████████████████████| 446 kB 1.2 MB/s Collecting httpcore==1.* Downloading httpcore-1.0.7-py3-none-any.whl (78 kB) |████████████████████████████████| 78 kB 1.8 MB/s Requirement already satisfied, skipping upgrade: idna in /usr/lib/python3/dist-packages (from httpx<1,>=0.23.0; extra == "ark"->volcengine-python-sdk[ark]) (2.8) Collecting pydantic-core==2.27.2 Downloading pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.0 MB) |████████████████████████████████| 2.0 MB 1.0 MB/s Collecting typing-extensions>=4.12.2 Downloading typing_extensions-4.12.2-py3-none-any.whl (37 kB) Collecting annotated-types>=0.6.0 Downloading annotated_types-0.7.0-py3-none-any.whl (13 kB) Collecting exceptiongroup>=1.0.2; python_version < "3.11" Downloading exceptiongroup-1.2.2-py3-none-any.whl (16 kB) Collecting sniffio>=1.1 Downloading sniffio-1.3.1-py3-none-any.whl (10 kB) Collecting pycparser Downloading pycparser-2.22-py3-none-any.whl (117 kB) |████████████████████████████████| 117 kB 2.9 MB/s Collecting h11<0.15,>=0.13 Downloading h11-0.14.0-py3-none-any.whl (58 kB) |████████████████████████████████| 58 kB 3.3 MB/s Installing collected packages: pycparser, cffi, cryptography, h11, httpcore, exceptiongroup, typing-extensions, sniffio, anyio, httpx, pydantic-core, annotated-types, pydantic WARNING: The script httpx is installed in '/home/apple/.local/bin' which is not on PATH. Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location. Successfully installed annotated-types-0.7.0 anyio-4.5.2 cffi-1.17.1 cryptography-43.0.3 exceptiongroup-1.2.2 h11-0.14.0 httpcore-1.0.7 httpx-0.28.1 pycparser-2.22 pydantic-2.10.4 pydantic-core-2.27.2 sniffio-1.3.1 typing-extensions-4.12.2 测试 单张图片测试 vim test.py import os # 通过 pip install volcengine-python-sdk[ark] 安装方舟SDK from volcenginesdkarkruntime import Ark # 替换为您的模型推理接入点 model="ep-20250101121404-stw4s" # 初始化Ark客户端,从环境变量中读取您的API Key client = Ark( api_key=os.getenv('ARK_API_KEY'), ) # 创建一个对话请求 response = client.chat.completions.create( # 指定您部署了视觉理解大模型的推理接入点ID model = model, messages = [ { "role": "user", # 指定消息的角色为用户 "content": [ # 消息内容列表 {"type": "text", "text":"这张图片讲了什么?"}, # 文本消息 { "type": "image_url", # 图片消息 # 图片的URL,需要大模型进行理解的图片链接 "image_url": {"url": "http://www.laumy.tech/wp-content/uploads/2024/12/wp_editor_md_7a3e5882d13fb51eecfaaf7fc8c53b59.jpg"} }, ], } ], ) print(response.choices[0]) 执行返回结果 python3 test.py Choice( finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage( content='这张图片展示了一个WebRTC(Web实时通信)的流程示意图,涉及到PC(个人计算机)、MQTT代理(mqtt broker)和CEMARA设备。以下是流程图的主要步骤: \n\n1. **PC端操作**: \n - **连接和订阅**:PC端首先进行连接(connect),然后订阅相关主题("webrtc/id/jsonrpc"和"webrtc/id/jsonrpc-replay")。 \n - **发布消息**:PC端发布消息(pub),发送"offer"请求(offer (req))。 \n - **接收消息**:PC端接收来自MQTT代理的消息,包括"message"事件和相关的应答(res)。 \n - **创建应答**:PC端创建应答(pc.createAnswer),并设置远程描述(pc.setRemoteDescription)。 \n\n2. **STUN/TURN服务器交互**: \n - **STUN/TURN绑定请求和应答**:在STUN/TURN服务器上,PC端发起绑定请求(binding req)和应答(binding res),获取SDP(Session Description Protocol)信息。 \n - **ANSWER请求和应答**:PC端发送ANSWER请求(anser (req)),并接收ANSWER应答(anser (res))。 \n\n3. **检查和连接过程**: \n - **检查连接**:PC端按照优先级顺序检查连接的顺畅性(host、srflx、relay)。 \n - **连接完成**:经过一系列的检查和交互,PC端与CEMARA设备成功连接(CONNECTED)。\n\n4. **数据交互和完成**: \n - **数据交互**:PC端和CEMARA设备开始进行数据交互(agent_send和agent_recv)。 \n - **完成状态**:数据交互完成后,流程进入“COMPLETED”状态,表示整个WebRTC通信过程结束。 \n\n整个流程图清晰地展示了WebRTC通信过程中PC端与MQTT代理以及STUN/TURN服务器之间的交互过程,包括连接、消息发布、应答接收、绑定请求、检查连接等步骤,最终实现了PC端与CEMARA设备的数据通信。', role='assistant', function_call=None, tool_calls=None, audio=None ) )