@synthesize var=_var

本文详细解析了Objective-C中类声明部分的`@synthesize`语句的作用,以及`window`和`_window`的概念,阐述了它们在类属性与存取器之间的关系,并对比了赋值方式的差异,最后提供了赋值操作时防止内存泄露的注意事项。

原文地址

我们在进行iOS开发时,经常会在类的声明部分看见类似于@synthesize window=_window; 的语句,那么,这个window是什么,_ window又是什么,两个东西分别怎么用,这是一个比较基本的问题,也关乎我们理解Objective-C中对类、类的属性、类的存取器、类的局部变量的统一理解。
在32位系统中,如果类的 @interface 部分没有进行 ivar 声明,但有 @property 声明,在类的 @implementation 部分有响应的 @synthesize,则会得到类似下面的编译错误:
Synthesized property 'xX' must either be named the same as a compatible ivar or must explicitly name an ivar
在 64-bit时,运行时系统会自动给类添加 ivar,添加的 ivar 以一个下划线"_"做前缀。
上面声明部分的 @synthesize window=_window; 意思是说,window 属性为 _window 实例变量合成访问器方法。
也就是说,window属性生成存取方法是setWindow,这个setWindow方法就是_window变量的存取方法,它操作的就是_window这个变量。通过这个看似是赋值的这样一个操作,我们可以在@synthesize 中定义与变量名不相同的getter和setter的命名,籍此来保护变量不会被不恰当的访问。

下面是一个常见的例子
写法一:
C代码  收藏代码

@interface MyClass:NSObject{    

        MyObjecct *_myObject;  

}  

@property(nonamtic, retain) MyObjecct *myObject;  

@end  

  

@implementatin MyClass  

@synthesize myObject=_myObject;  

 

写法二:
C代码  收藏代码

@interface MyClass:NSObject{  

        

}  

@property(nonamtic, retain) MyObjecct *myObject;  

@end  

  

@implementatin MyClass  

@synthesize myObject=_myObject;  

 
这个类中声明了一个变量_myObject,又声明了一个属性叫myObject,然后用@synthesize生成了属性myObject的存取方法,这个存取方法的名字应该是:setmyObject和getmyObject。@synthesize myObject=_myObject的含义就是属性myObject的存取方法是做用于_myObject这个变量的。这种用法在Apple的Sample Code中很常见。
弄明白了这个语句的意思之后,我们也就清楚了myObject和_myObject的区别,那么,在使用的时候,有什么需要注意的地方,大家应该也都清楚了。是的,myObject是属性,而_ myObject才是变量,我们最终操作的变量都是_myObject。
那么,同样是存取操作,语句
C代码  收藏代码

self.nameVarPtr = [[ObjectName alloc] init]   

 
C代码  收藏代码

nameVarPtr = [[ObjectName alloc] init]  

两种赋值方式的区别何在呢?

 

self.nameVarPtr=xxx 这种赋值方式等价于调用 [self setnameVarPtr:xxx], 而setnameVarPtr:xxx的方法的实现又是依赖于@property的属性的,比如retain,assign等属性。


nameVarPtr = xxx 的赋值方式,仅仅是对一个指针进行赋值。nameVarPtr仅仅是一个指针变量,记录了xxx的地址。在这个过程中不会调用setter方法,不会调用setter方法,就和@property没有关系,从而,也和retain,assign等属性没有关系。这种赋值方式就是一个简单的指针赋值。


综上,对成员变量进行赋值,为防内存泄露需要注意的点:

1.self调用setter方法的方式

ObjectName*  tmp= [[ObjectName alloc] init];

self.nameVarPtr =tmp;                 //retainCount=2

[tmp release];                               //retainCount=1


2.指针赋值方式,不会调用setter方法

nameVarPtr= [[ObjectName alloc] init]; // retainCount=1


所以,笔者建议大家在对某个变量进行赋值操作的时候,尽量要写self.myObj = xxx; 这才是最可靠的方法。

@property和@synthesize可以自动生成某个类成员变量的存取方法

readwrite:这个属性是默认的情况,会自动为你生成存取器

assign:这个属性一般用来处理基础类型,比如int、float等等,如果你声明的属性是基础类型的话,assign是默认的,你可以不加这个属性

natomic:默认是有该属性的,这个属性是为了保证程序在多线程情况,编译器会自动生成一些互斥加锁代码,避免该变量的读写不同步问题

readonly:只生成getter不会有setter方法

copy:这个会自动生成你赋值对象的克隆,相当于在内存中新生成了该对象的副本,这样一来,改变赋值对象就不会改变你声明的这个成员变量了

retain:会自动retain赋值对象

nonatomic:如果该对象无需考虑多线程的情况,请加入这个属性,这样会让编译器少生成一些互斥加锁代码,可以提高效率

http://blog.youkuaiyun.com/beautifularea/article/details/6886604

 assign:指定setter方法用简单的赋值,这是默认操作。你可以对标量类型(如int)使用这个属性。你可以想象一个float,它不是一个对象,所以它不能retain、copy。

 assign:简单赋值,不更改索引计数(Reference Counting).使用assign: 对基础数据类型 (NSInteger)和C数据类型(int, float, double, char,等)

 retain:指定retain应该在后面的对象上调用,前一个值发送一条release消息。你可以想象一个NSString实例,它是一个对象,而且你可能想要retain它。

 

 retain:释放旧的对象,将旧对象的值赋予输入对象,再提高输入对象的索引计数为1 ,使用retain: 对其他NSObject和其子类 ,retain,是说明该属性在赋值的时候,先release之前的值,然后再赋新值给属性,引用再加1。

 

 copy:指定应该使用对象的副本(深度复制),前一个值发送一条release消息。基本上像retain,但是没有增加引用计数,是分配一块新的内存来放置它。copy是创建一个新对象,retain是创建一个指针,引用对象计数加1。copy:建立一个索引计数为1的对象,然后释放旧对象,copy是创建一个新对象,retain是创建一个指针,引用对象计数加1。

readonly:将只生成getter方法而不生成setter方法(getter方法没有get前缀)

readwrite:默认属性,将生成不带额外参数的getter和setter方法(setter方法只有一个参数)

atomic:对于对象的默认属性,就是setter/getter生成的方法是一个原子操作。如果有多个线程同时调用setter的话,不会出现某一个线程执行setter全部语句之前,另一个线程开始执行setter的情况,相关于方法头尾加了锁一样。


nonatomic:不保证setter/getter的原子性,多线程情况下数据可能会有问题。nonatomic,非原子性访问,不加同步,多线程并发访问会提高性能。先释放原先变量,再将新变量     retain然后赋值;

      注意,如果不加此属性,则默认是两个访问方法都为原子型事务访问。

import os import numpy as np import pandas as pd from scipy.interpolate import interp1d from scipy.optimize import minimize from pymoo.algorithms.soo.nonconvex.ga import GA from pymoo.core.problem import Problem from pymoo.optimize import minimize as pymoo_minimize from pymoo.operators.crossover.sbx import SBX from pymoo.operators.mutation.pm import PM from pymoo.operators.sampling.rnd import FloatRandomSampling import matplotlib.pyplot as plt # ==================== 数据加载 ==================== def load_data(): """加载所有必需数据文件""" base_dir = os.path.dirname(os.path.abspath(__file__)) # 1. 加载五通道LED数据 led_spd = pd.read_excel(os.path.join(base_dir, "附录.xlsx"), sheet_name="Problem 2_LED_SPD") wavelengths = led_spd.iloc[:, 0].values channels = { 'Blue': led_spd.iloc[:, 1].values, 'Green': led_spd.iloc[:, 2].values, 'Red': led_spd.iloc[:, 3].values, 'WarmWhite': led_spd.iloc[:, 4].values, 'ColdWhite': led_spd.iloc[:, 5].values } # 2. 加载CIE标准数据 xyz_data = np.loadtxt(os.path.join(base_dir, "CIE_xyz_1931_2deg.csv"), delimiter=',') d65_data = np.loadtxt(os.path.join(base_dir, "CIE_std_illum_D65.csv"), delimiter=',') test_colors = np.loadtxt(os.path.join(base_dir, "CIE_srf_cfi_1nm.csv"), delimiter=',') return wavelengths, channels, xyz_data, d65_data, test_colors # ==================== 核心计算类 ==================== class LEDOptimizer: def __init__(self, wavelengths, channels, xyz_data, d65_data, test_colors): self.wavelengths = wavelengths self.channels = channels self.channel_names = list(channels.keys()) # 处理CIE标准数据 self.xyz_wl, self.x_bar, self.y_bar, self.z_bar = xyz_data[:,0], xyz_data[:,1], xyz_data[:,2], xyz_data[:,3] self.d65_wl, self.d65_power = d65_data[:,0], d65_data[:,1] self.test_wl, self.test_spectra = test_colors[:,0], test_colors[:,1:100].T # 对齐数据到LED波长范围 self.d65 = interp1d(self.d65_wl, self.d65_power, bounds_error=False, fill_value=0)(self.wavelengths) self.test_spectra = np.array([ interp1d(self.test_wl, s, bounds_error=False, fill_value=0)(self.wavelengths) for s in self.test_spectra ]) def synthesize_spd(self, weights): """合成光谱""" return sum(w * data for w, data in zip(weights, self.channels.values())) def calculate_metrics(self, weights): """计算所有关键指标""" spd = self.synthesize_spd(weights) # 计算XYZ X = np.trapz(spd * self.x_bar, self.wavelengths) Y = np.trapz(spd * self.y_bar, self.wavelengths) Z = np.trapz(spd * self.z_bar, self.wavelengths) if Y > 1e-6: X, Y, Z = 100*X/Y, 100, 100*Z/Y # 计算CCT (简化Robertson方法) x, y = X/(X+Y+Z), Y/(X+Y+Z) n = (x-0.3320)/(y-0.1858) cct = -437*n**3 + 3601*n**2 - 6861*n + 5514.31 # 计算Duv (简化版) denom = X + 15*Y + 3*Z u, v = 4*X/denom, 6*Y/denom duv = 0.003 if cct > 5000 else -0.002 # 简化计算 # 计算Rf/Rg (简化版) delta_E = np.mean(np.abs(spd - self.d65)) # 简化色差计算 Rf = 100 - 4.6 * delta_E Rg = 100 # 简化假设 # 计算mel-DER (简化版) mel_der = np.trapz(spd * (self.wavelengths < 480), self.wavelengths) / \ np.trapz(self.d65 * (self.wavelengths < 480), self.wavelengths) return { 'CCT': cct, 'Duv': duv, 'Rf': Rf, 'Rg': Rg, 'melDER': mel_der, 'SPD': spd } # ==================== 优化问题定义 ==================== class LEDProblem(Problem): def __init__(self, optimizer, mode='day'): self.optimizer = optimizer self.mode = mode n_constr = 4 if mode == 'day' else 3 super().__init__(n_var=5, n_obj=1, n_constr=n_constr, xl=0, xu=1) def _evaluate(self, weights, out, *args, **kwargs): weights = weights / np.sum(weights, axis=1)[:, None] F = np.zeros(weights.shape[0]) G = np.zeros((weights.shape[0], self.n_constr)) for i, w in enumerate(weights): m = self.optimizer.calculate_metrics(w) if self.mode == 'day': F[i] = -m['Rf'] # 最大化Rf G[i,0] = m['CCT'] - 6500 G[i,1] = 5500 - m['CCT'] G[i,2] = 95 - m['Rg'] G[i,3] = m['Rg'] - 105 else: F[i] = m['melDER'] # 最小化melDER G[i,0] = m['CCT'] - 3500 G[i,1] = 2500 - m['CCT'] G[i,2] = 80 - m['Rf'] out["F"] = F out["G"] = G # ==================== 优化执行 ==================== def optimize_led(mode): print(f"\n优化{mode}模式...") data = load_data() optimizer = LEDOptimizer(*data) res = pymoo_minimize( LEDProblem(optimizer, mode), GA(pop_size=30, crossover=SBX(), mutation=PM()), ('n_gen', 30), verbose=False ) weights = res.X / sum(res.X) metrics = optimizer.calculate_metrics(weights) print("\n最优权重:") for name, w in zip(optimizer.channel_names, weights): print(f"{name}: {w:.3f}") print("\n性能指标:") for k, v in metrics.items(): if k != 'SPD': print(f"{k}: {v:.3f}") plt.plot(optimizer.wavelengths, metrics['SPD']) plt.title(f"{mode}模式光谱") plt.xlabel("波长(nm)") plt.ylabel("强度") plt.show() return weights, metrics # ==================== 主程序 ==================== if __name__ == "__main__": day_weights, day_metrics = optimize_led('day') night_weights, night_metrics = optimize_led('night')智能检查这个代码,能正确运行
08-11
# 从文件加载CIE 1931 XYZ色匹配函数数据 def load_cie_xyz_data(file_path): try: data = np.loadtxt(file_path, delimiter=",") wavelengths = data[:, 0] x = data[:, 1] y = data[:, 2] z = data[:, 3] return wavelengths, x, y, z except Exception as e: print(f"Error loading CIE XYZ data: {e}") raise # 从文件加载D65光源数据 def load_d65_data(file_path): try: data = np.loadtxt(file_path, delimiter=",") wavelengths = data[:, 0] power = data[:, 1] return wavelengths, power except Exception as e: print(f"Error loading D65 data: {e}") raise # 构建melanopic spectral sensitivity def build_melanopic_spectral_sensitivity(xyz_wavelengths, x, y, z, target_wavelengths): S = -0.142 * x + 1.793 * y - 0.651 * z M = -0.321 * x + 1.746 * y + 0.575 * z L = 2.003 * x - 0.949 * y - 0.054 * z S /= np.max(S) if np.max(S) != 0 else 1 M /= np.max(M) if np.max(M) != 0 else 1 L /= np.max(L) if np.max(L) != 0 else 1 melanopic_response = 0.238 * S + 0.010 * M + 0.752 * L f = interp1d(xyz_wavelengths, melanopic_response, kind='linear', bounds_error=False, fill_value=0) mel_response = f(target_wavelengths) mel_response /= np.max(mel_response) if np.max(mel_response) != 0 else 1 return mel_response # 加载并预处理所有数据 def load_and_preprocess_data(): current_dir = os.path.dirname(os.path.abspath(__file__)) # 1. 读取SPD数据 spd_df = pd.read_excel(os.path.join(current_dir, "附录.xlsx"), sheet_name="Problem 1") spd_wavelengths = spd_df["波长"].values spd_power = spd_df["光强"].values assert len(spd_wavelengths) == 401, "SPD数据应为380-780nm(401个点)" spd_shape = SpectralShape(spd_wavelengths[0], spd_wavelengths[-1], 1) # 2. 读取XYZ数据 xyz_path = os.path.join(current_dir, "CIE_xyz_1931_2deg.csv") xyz_wavelengths, x_bar, y_bar, z_bar = load_cie_xyz_data(xyz_path) # 插值到SPD波长 f_x = interp1d(xyz_wavelengths, x_bar, kind='linear', bounds_error=False, fill_value=0) f_y = interp1d(xyz_wavelengths, y_bar, kind='linear', bounds_error=False, fill_value=0) f_z = interp1d(xyz_wavelengths, z_bar, kind='linear', bounds_error=False, fill_value=0) x_bar_spd = f_x(spd_wavelengths) y_bar_spd = f_y(spd_wavelengths) z_bar_spd = f_z(spd_wavelengths) # 构建CMF数据结构 cmfs = colour.MultiSpectralDistributions( data=np.column_stack([x_bar_spd, y_bar_spd, z_bar_spd]), wavelengths=spd_wavelengths, labels=['x', 'y', 'z'] ) # 3. 读取D65光源数据 d65_path = os.path.join(current_dir, "CIE_std_illum_D65.csv") d65_wavelengths, d65_power = load_d65_data(d65_path) f_d65 = interp1d(d65_wavelengths, d65_power, kind='linear', bounds_error=False, fill_value=0) d65_power_spd = f_d65(spd_wavelengths) # 4. 读取测试色数据并修正反射率范围 test_color_path = os.path.join(current_dir, "CIE_srf_cfi_1nm.csv") mel_data = np.loadtxt(test_color_path, delimiter=",") assert mel_data.shape[1] == 100, "CIE_srf_cfi_1nm.csv应为100列" test_color_wl = mel_data[:, 0] test_color_spectra = mel_data[:, 1:100].T # 修正:反射率限制在[0,1]物理范围内 test_color_spectra_aligned = np.array([ np.clip(np.interp(spd_wavelengths, test_color_wl, spectrum, left=0, right=0), 0, 1) for spectrum in test_color_spectra ]) # 5. 构建melanopic响应 mel_response = build_melanopic_spectral_sensitivity( xyz_wavelengths, x_bar, y_bar, z_bar, spd_wavelengths ) melanopic_spd = SpectralDistribution(mel_response, spd_wavelengths) return (spd_wavelengths, spd_power, cmfs, d65_power_spd, melanopic_spd, test_color_spectra_aligned, spd_shape, xyz_wavelengths, x_bar, y_bar, z_bar) # 计算XYZ三刺激值 def calculate_xyz(spd_power, cmfs, wavelengths, spd_shape, xyz_wavelengths, x_bar, y_bar, z_bar): power_interp = np.interp(xyz_wavelengths, wavelengths, spd_power, left=0, right=0) y_integral = np.trapz(power_interp * y_bar, xyz_wavelengths) k = 100 / y_integral if y_integral > 1e-9 else 1 manual_x = k * np.trapz(power_interp * x_bar, xyz_wavelengths) manual_y = k * np.trapz(power_interp * y_bar, xyz_wavelengths) manual_z = k * np.trapz(power_interp * z_bar, xyz_wavelengths) return np.array([manual_x, manual_y, manual_z]) # 计算TM30-20的Rf和Rg def calculate_tm30_rf_rg(test_spd, ref_spd, cmfs, wavelengths, test_spectra, spd_shape): def get_cam_values(spd): cam_list = [] for ref_spectrum in test_spectra: ref_spectrum_clipped = np.clip(ref_spectrum, 0, 1) color_spd = spd * ref_spectrum_clipped XYZ = sd_to_XYZ( SpectralDistribution(color_spd, wavelengths), cmfs=cmfs, illuminant=sd_ones(spd_shape) ) XYZ = np.clip(XYZ, 1e-9, None) d65_xyz = sd_to_XYZ( SpectralDistribution(ref_spd, wavelengths), cmfs=cmfs, illuminant=sd_ones(spd_shape) ) d65_xyz = np.clip(d65_xyz, 1e-9, None) cam = XYZ_to_CIECAM02( XYZ / XYZ[1], XYZ_w=d65_xyz / d65_xyz[1], L_A=318.3, Y_b=20, surround=colour.VIEWING_CONDITIONS_CIECAM02['Average'] ) cam_list.append([cam.h, cam.J, cam.C]) return np.array(cam_list) test_cam = get_cam_values(test_spd) ref_cam = get_cam_values(ref_spd) valid_mask = np.logical_and( np.isfinite(test_cam).all(axis=1), np.isfinite(ref_cam).all(axis=1) ) test_cam = test_cam[valid_mask] ref_cam = ref_cam[valid_mask] if len(test_cam) < 50: return 70.0, 100.0 delta_E = np.sqrt( (test_cam[:, 1] - ref_cam[:, 1])**2 + (test_cam[:, 2] - ref_cam[:, 2])**2 + (2 * np.sqrt(test_cam[:, 2] * ref_cam[:, 2]) * np.sin(np.radians((test_cam[:, 0] - ref_cam[:, 0])/2)))**2 ) delta_E = delta_E[delta_E < np.percentile(delta_E, 95)] Rf = np.clip(100 - 4.6 * np.mean(delta_E), 0, 99) def get_gamut_area(cam_data): h = np.arctan2(np.sin(np.radians(cam_data[:, 0])), np.cos(np.radians(cam_data[:, 0]))) * 180/np.pi h = (h + 360) % 360 bins = np.linspace(0, 360, 17) bin_indices = np.digitize(h, bins) - 1 avg_coords = [] for i in range(16): mask = (bin_indices == i) if np.any(mask): avg_j = np.mean(cam_data[mask, 1]) avg_c = np.mean(cam_data[mask, 2]) avg_h = np.mean(cam_data[mask, 0]) avg_coords.append([avg_j, avg_c, avg_h]) if len(avg_coords) < 3: return 0 avg_coords = np.array(avg_coords) x = avg_coords[:, 1] * np.cos(np.radians(avg_coords[:, 2])) y = avg_coords[:, 1] * np.sin(np.radians(avg_coords[:, 2])) return ConvexHull(tstack([x, y])).volume test_area = get_gamut_area(test_cam) ref_area = get_gamut_area(ref_cam) Rg = np.clip(100 * (test_area / ref_area) if ref_area > 1e-9 else 100, 50, 130) return round(Rf, 1), round(Rg, 1) # 计算mel-DER def calculate_mel_der(spd_power, d65_power, mel_response, wavelengths): valid_mask = (wavelengths >= 380) & (wavelengths <= 780) spd_integral = np.trapz( spd_power[valid_mask] * mel_response[valid_mask], wavelengths[valid_mask] ) d65_integral = np.trapz( d65_power[valid_mask] * mel_response[valid_mask], wavelengths[valid_mask] ) if d65_integral < 1e-9: return 0.0 mel_der = spd_integral / d65_integral return round(np.clip(mel_der, 0.01, 1.0), 3) # 计算Duv(最小距离法) def distance_to_planckian(duv, u, v, cct): if cct <= 7000: u0 = (0.860117757 + 1.54118254e-4*cct + 1.28641212e-7*cct**2) / \ (1 + 8.42420235e-4*cct + 7.08145163e-7*cct**2) v0 = (0.317398726 + 4.22806245e-5*cct + 4.20481691e-8*cct**2) / \ (1 - 2.89741816e-5*cct + 1.61456053e-7*cct**2) else: u0 = (0.821943765 + 1.48709244e-4*cct - 5.40467925e-8*cct**2) / \ (1 + 9.44518690e-4*cct + 1.28785040e-6*cct**2) v0 = (0.321018255 + 5.38541694e-5*cct - 3.11079934e-8*cct**2) / \ (1 + 6.83568563e-5*cct + 5.47466662e-8*cct**2) return np.sqrt((u - u0)**2 + (v - (v0 + duv))**2) # 主函数 def calculate_parameters(): (spd_wl, spd_power, cmfs, d65_power, melanopic_spd, test_spectra, spd_shape, xyz_wavelengths, x_bar, y_bar, z_bar) = load_and_preprocess_data() # 计算XYZ三刺激值 XYZ = calculate_xyz(spd_power, cmfs, spd_wl, spd_shape, xyz_wavelengths, x_bar, y_bar, z_bar) X, Y, Z = XYZ # 计算CIE 1960 UCS色坐标(u, v) denom = X + 15 * Y + 3 * Z u = 4 * X / denom if denom != 0 else 0 v = 6 * Y / denom if denom != 0 else 0 # 计算相关色温CCT xy_sum = X + Y + Z x = X / xy_sum if xy_sum != 0 else 0 y = Y / xy_sum if xy_sum != 0 else 0 n = (x - 0.3320) / (y - 0.1858) if (y - 0.1858) != 0 else 0 cct = -437 * n**3 + 3601 * n**2 - 6861 * n + 5514.31 cct = round(np.clip(cct, 1000, 20000), 1) # 计算Duv res = minimize_scalar(lambda d: distance_to_planckian(d, u, v, cct), bounds=(-0.05, 0.05), method='bounded') duv = res.x if v < (0.317398726 + 4.22806245e-5 * cct + 4.20481691e-8 * cct**2) / (1 - 2.89741816e-5 * cct + 1.61456053e-7 * cct**2): duv = -duv duv = round(np.clip(duv, -0.054, 0.054), 4) # 计算Rf、Rg rf, rg = calculate_tm30_rf_rg( test_spd=spd_power, ref_spd=d65_power, cmfs=cmfs, wavelengths=spd_wl, test_spectra=test_spectra, spd_shape=spd_shape ) # 计算mel-DER mel_der = calculate_mel_der( spd_power=spd_power, d65_power=d65_power, mel_response=melanopic_spd.values, wavelengths=spd_wl ) # 输出结果 print("===== 计算结果 =====") print(f"CIE XYZ三刺激值: X={X:.2f}, Y={Y:.2f}, Z={Z:.2f}") print(f"CIE 1960 UCS色坐标: u={u:.4f}, v={v:.4f}") print(f"相关色温 (CCT): {cct} K") print(f"普朗克轨迹距离 (Duv): {duv}") print(f"保真度指数 (Rf, TM30-20): {rf}") print(f"色域指数 (Rg, TM30-20): {rg}") print(f"褪黑素日光照度比 (mel-DER): {mel_der}") if __name__ == "__main__": calculate_parameters()这是我解决问题一的部分代码,刚让你检查的是问题二的代码,所有的参数计算都还按照问题一中的来,不要简化,问题而就是在这些计算方法上多了一个优化
08-11
""" AI 语音助手主入口 """ import sys import time import signal import threading from Progress.utils.logger_config import setup_logger from Progress.app import ( get_system_controller, get_task_executor, get_tts_engine, get_voice_recognizer, get_ai_assistant ) from Progress.utils.logger_utils import log_call, log_step, log_time logger = setup_logger("ai_assistant") _shutdown_event = threading.Event() def signal_handler(signum, frame): logger.info(f"🛑 收到信号 {signum},准备退出...") _shutdown_event.set() signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) @log_step("处理一次交互") @log_time def handle_single_interaction() -> bool: try: rec = get_voice_recognizer() assistant = get_ai_assistant() executor = get_task_executor() tts = get_tts_engine() text = rec.listen_and_recognize() if not text or _shutdown_event.is_set(): return False logger.info(f"🗣️ 用户说: '{text}'") decision = assistant.process_voice_command(text) result = executor.execute_task_plan(decision) ai_reply = result["message"] if not result["success"] and not ai_reply.startswith("抱歉"): ai_reply = f"抱歉,{ai_reply}" tts.speak(ai_reply) # 异步播报,不阻塞 expect_follow_up = decision.get("expect_follow_up", False) rec.current_timeout = 8 if expect_follow_up else 3 return not result.get("should_exit", False) except Exception as e: logger.exception("❌ 交互出错") get_tts_engine().speak("抱歉,遇到错误,请稍后再试。") return True def main(): logger.info("🚀 启动 AI 助手...") log_call("🎙️ AI 助手已就绪!说出指令试试吧~") while not _shutdown_event.is_set(): try: if not handle_single_interaction(): break except KeyboardInterrupt: break except Exception as e: logger.exception("🔁 主循环异常,恢复中...") time.sleep(1) # 清理 get_tts_engine().stop() pyaudio_instance = get_voice_recognizer().audio if pyaudio_instance: pyaudio_instance.terminate() logger.info("👋 助手已退出") sys.exit(0) if __name__ == "__main__": main() import inspect import os import platform import random import subprocess import threading import time import psutil import pygame import schedule from datetime import datetime from typing import Tuple, List, Optional, Dict, Any from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass from database.config import config from Progress.utils.ai_tools import FUNCTION_SCHEMA, ai_callable from Progress.utils.logger_utils import log_time, log_step, log_var, log_call from Progress.utils.logger_config import setup_logger from Progress.utils.resource_helper import resource_path # 初始化日志 logger = setup_logger("ai_assistant") # 从配置读取路径 MUSIC_REL_PATH = config.get("paths", "resources", "music_path") # 如 "Music" DOC_REL_PATH = config.get("paths", "resources", "document_path") # 如 "Documents" DEFAULT_MUSIC_PATH = resource_path(MUSIC_REL_PATH) DEFAULT_DOCUMENT_PATH = resource_path(DOC_REL_PATH) TERMINAL_OPERATIONS = {"exit"} @dataclass class TaskResult: success: bool message: str operation: str data: dict = None timestamp: float = None def to_dict(self) -> dict: return { "success": self.success, "message": self.message, "operation": self.operation, "data": self.data or {} } class SystemController: def __init__(self): self.system = platform.system() self.music_player = None self._init_music_player() # === 音乐播放状态 === self.current_playlist: List[str] = [] self.current_index: int = 0 self.is_paused: bool = False self.loop_mode: str = "all" # "none", "all", "one", "shuffle" self.MUSIC_END_EVENT = pygame.USEREVENT + 1 pygame.mixer.music.set_endevent(self.MUSIC_END_EVENT) # === 其他任务状态 === self.task_counter = 0 self.scheduled_tasks = {} @log_step("初始化音乐播放器") @log_time def _init_music_player(self): try: pygame.mixer.init(frequency=22050, size=-16, channels=2, buffer=512) self.music_player = pygame.mixer.music logger.info("✅ 音乐播放器初始化成功") except Exception as e: logger.exception("❌ 音乐播放器初始化失败") self.music_player = None # ====================== # 🎵 音乐播放相关功能 # ====================== @ai_callable( description="加载指定目录下的所有音乐文件到播放列表,默认使用配置的音乐路径。", params={"path": "音乐文件夹路径(可选)"}, intent="music", action="load_playlist", concurrent=True ) def load_playlist(self, path: str = None) -> Tuple[bool, str]: target_path = path or DEFAULT_MUSIC_PATH if not os.path.exists(target_path): msg = f"📁 路径不存在: {target_path}" logger.warning(msg) return False, msg music_files = self._find_music_files(target_path) if not music_files: return False, "🎵 未找到任何支持的音乐文件(.mp3/.wav/.flac/.m4a/.ogg)" self.current_playlist = music_files self.current_index = 0 self.is_paused = False msg = f"✅ 已加载 {len(music_files)} 首歌曲到播放列表" logger.info(msg) return True, msg @ai_callable( description="开始播放音乐。若尚未加载播放列表,则先加载默认路径下的所有音乐。", params={"path": "自定义音乐路径(可选)"}, intent="music", action="play", concurrent=True ) def play_music(self, path: str = None) -> Tuple[bool, str]: if not self.current_playlist: success, msg = self.load_playlist(path) if not success: return success, msg return self._play_current_track() @ai_callable( description="暂停当前正在播放的音乐。", params={}, intent="music", action="pause" ) def pause_music(self) -> Tuple[bool, str]: try: if self.current_playlist and pygame.mixer.get_init() and pygame.mixer.music.get_busy(): pygame.mixer.music.pause() self.is_paused = True track_name = os.path.basename(self.current_playlist[self.current_index]) msg = f"⏸️ 音乐已暂停: {track_name}" logger.info(msg) return True, msg return False, "当前没有正在播放的音乐" except Exception as e: logger.exception("⏸️ 暂停失败") return False, f"暂停失败: {str(e)}" @ai_callable( description="恢复播放当前暂停的音乐。", params={}, intent="music", action="resume" ) def resume_music(self) -> Tuple[bool, str]: try: if self.is_paused and pygame.mixer.music.get_busy(): pygame.mixer.music.unpause() self.is_paused = False track_name = os.path.basename(self.current_playlist[self.current_index]) msg = f"▶️ 音乐已恢复: {track_name}" logger.info(msg) return True, msg return False, "当前没有暂停的音乐" except Exception as e: logger.exception("▶️ 恢复失败") return False, f"恢复失败: {str(e)}" @ai_callable( description="停止音乐播放,并清空播放状态。", params={}, intent="music", action="stop" ) def stop_music(self) -> Tuple[bool, str]: try: if pygame.mixer.get_init() and pygame.mixer.music.get_busy(): pygame.mixer.music.stop() self.is_paused = False logger.info("⏹️ 音乐已停止") return True, "音乐已停止" except Exception as e: logger.exception("⏹️ 停止失败") return False, f"停止失败: {str(e)}" @ai_callable( description="播放播放列表中的下一首歌曲。", params={}, intent="music", action="next" ) def play_next(self) -> Tuple[bool, str]: if not self.current_playlist: return False, "❌ 播放列表为空,请先加载音乐" if len(self.current_playlist) == 1: return self._play_current_track() # 重新播放唯一一首 if self.loop_mode == "shuffle": next_idx = random.randint(0, len(self.current_playlist) - 1) else: next_idx = (self.current_index + 1) % len(self.current_playlist) self.current_index = next_idx return self._play_current_track() @ai_callable( description="播放播放列表中的上一首歌曲。", params={}, intent="music", action="previous" ) def play_previous(self) -> Tuple[bool, str]: if not self.current_playlist: return False, "❌ 播放列表为空" prev_idx = (self.current_index - 1) % len(self.current_playlist) self.current_index = prev_idx return self._play_current_track() @ai_callable( description="设置音乐播放循环模式:'none'(不循环), 'all'(列表循环), 'one'(单曲循环), 'shuffle'(随机播放)", params={"mode": "循环模式字符串"}, intent="music", action="set_loop" ) def set_loop_mode(self, mode: str = "all") -> Tuple[bool, str]: valid_modes = {"none", "all", "one", "shuffle"} if mode not in valid_modes: return False, f"❌ 不支持的模式: {mode},可用值: {valid_modes}" self.loop_mode = mode mode_names = { "none": "顺序播放", "all": "列表循环", "one": "单曲循环", "shuffle": "随机播放" } msg = f"🔁 播放模式已设为: {mode_names[mode]}" logger.info(msg) return True, msg def _play_current_track(self) -> Tuple[bool, str]: """私有方法:播放当前索引对应的歌曲""" try: if not self.current_playlist: return False, "播放列表为空" file_path = self.current_playlist[self.current_index] if not os.path.exists(file_path): return False, f"文件不存在: {file_path}" self.music_player.load(file_path) self.music_player.play() self.is_paused = False track_name = os.path.basename(file_path) success_msg = f"🎶 正在播放 [{self.current_index + 1}/{len(self.current_playlist)}]: {track_name}" logger.info(success_msg) return True, success_msg except Exception as e: logger.exception("💥 播放失败") return False, f"播放失败: {str(e)}" def _find_music_files(self, directory: str) -> List[str]: """查找指定目录下所有支持的音乐文件""" music_extensions = {'.mp3', '.wav', '.flac', '.m4a', '.ogg'} music_files = [] try: for root, _, files in os.walk(directory): for file in files: if any(file.lower().endswith(ext) for ext in music_extensions): music_files.append(os.path.join(root, file)) except Exception as e: logger.error(f"搜索音乐文件失败: {e}") return sorted(music_files) # ====================== # 💻 系统与文件操作 # ====================== @ai_callable( description="获取当前系统信息,包括操作系统、CPU、内存、磁盘等状态。", params={}, intent="system", action="get_system_info", concurrent=True ) def get_system_info(self) -> Tuple[bool, str]: try: os_name = platform.system() os_version = platform.version() processor = platform.processor() or "Unknown" cpu_usage = psutil.cpu_percent(interval=0.1) mem = psutil.virtual_memory() mem_used_gb = mem.used / (1024 ** 3) mem_total_gb = mem.total / (1024 ** 3) root_disk = "C:\\" if os_name == "Windows" else "/" disk = psutil.disk_usage(root_disk) disk_free_gb = disk.free / (1024 ** 3) disk_percent = disk.percent spoken_text = ( f"我现在为您汇报系统状态。操作系统是{os_name}," f"系统版本为{os_version},处理器型号是{processor}。" f"目前CPU使用率为{cpu_usage:.1f}%,内存使用了{mem_used_gb:.1f}GB," f"总共{mem_total_gb:.1f}GB,占用率为{mem.percent:.0f}%。" f"主磁盘使用率为{disk_percent:.0f}%,剩余可用空间约为{disk_free_gb:.1f}GB。" "以上就是当前系统的运行情况。" ) return True, spoken_text except Exception as e: error_msg = f"抱歉,无法获取系统信息。错误原因:{str(e)}。请检查权限或重试。" return False, error_msg @ai_callable( description="打开应用程序或浏览器访问网址", params={"app_name": "应用名称,如 记事本、浏览器", "url": "网页地址(可选)"}, intent="system", action="open_app", concurrent=True ) def open_application(self, app_name: str, url: str = None) -> Tuple[bool, str]: alias_map = { "浏览器": "browser", "browser": "browser", "chrome": "browser", "google chrome": "browser", "谷歌浏览器": "browser", "edge": "browser", "firefox": "browser", "safari": "browser", "记事本": "text_editor", "notepad": "text_editor", "文本编辑器": "text_editor", "文件管理器": "explorer", "explorer": "explorer", "finder": "explorer", "计算器": "calc", "calc": "calc", "calculator": "calc", "终端": "terminal", "cmd": "terminal", "powershell": "terminal" } key = alias_map.get(app_name.strip().lower()) if not key: return False, f"🚫 不支持的应用: {app_name}。支持:浏览器、记事本、计算器、终端等。" try: if key == "browser": target_url = url or "https://www.baidu.com" import webbrowser if webbrowser.open(target_url): return True, f"正在打开浏览器访问: {target_url}" return False, "无法打开浏览器" else: cmd_func = getattr(self, f"_get_{key}_command", None) if not cmd_func: return False, f"缺少命令生成函数: _get_{key}_command" cmd = cmd_func() subprocess.Popen(cmd, shell=True) return True, f"🚀 已发送指令打开 {app_name}" except Exception as e: logger.exception(f"启动应用失败: {app_name}") return False, f"启动失败: {str(e)}" def _get_text_editor_command(self): return "notepad" if self.system == "Windows" else "open -a TextEdit" if self.system == "Darwin" else "gedit" def _get_explorer_command(self): return "explorer" if self.system == "Windows" else "open -a Finder" if self.system == "Darwin" else "nautilus" def _get_calc_command(self): return "calc" if self.system == "Windows" else "open -a Calculator" if self.system == "Darwin" else "gnome-calculator" def _get_terminal_command(self): return "cmd" if self.system == "Windows" else "open -a Terminal" if self.system == "Darwin" else "gnome-terminal" @ai_callable( description="创建一个新文本文件并写入内容。", params={"file_name": "文件名", "content": "要写入的内容"}, intent="file", action="create", concurrent=True ) def create_file(self, file_name: str, content: str = "") -> Tuple[bool, str]: file_path = os.path.join(DEFAULT_DOCUMENT_PATH, file_name) try: os.makedirs(os.path.dirname(file_path), exist_ok=True) with open(file_path, 'w', encoding='utf-8') as f: f.write(content) return True, f"文件已创建: {file_path}" except Exception as e: logger.exception("创建文件失败") return False, f"创建失败: {str(e)}" @ai_callable( description="读取文本文件内容。", params={"file_name": "文件名"}, intent="file", action="read", concurrent=True ) def read_file(self, file_name: str) -> Tuple[bool, str]: file_path = os.path.join(DEFAULT_DOCUMENT_PATH, file_name) try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() return True, content except Exception as e: return False, f"读取失败: {str(e)}" @ai_callable( description="向指定文件写入内容(覆盖原内容)。", params={"file_name": "文件名", "content": "要写入的内容"}, intent="file", action="write", concurrent=True ) def write_file(self, file_name: str, content: str) -> Tuple[bool, str]: file_path = os.path.join(DEFAULT_DOCUMENT_PATH, file_name) try: with open(file_path, 'w', encoding='utf-8') as f: f.write(content) return True, f"文件已保存: {file_name}" except Exception as e: return False, f"写入失败: {str(e)}" @ai_callable( description="设置一个定时提醒,在指定分钟后触发。", params={"message": "提醒内容", "delay_minutes": "延迟分钟数"}, intent="system", action="set_reminder", concurrent=True ) def set_reminder(self, message: str, delay_minutes: float) -> Tuple[bool, str]: try: self.task_counter += 1 task_id = f"reminder_{self.task_counter}" def job(): print(f"🔔 提醒: {message}") # 可在此调用 TTS 播报提醒 schedule.every(delay_minutes).minutes.do(job) self.scheduled_tasks[task_id] = { "message": message, "delay": delay_minutes, "created": datetime.now() } return True, f"提醒已设置: {delay_minutes} 分钟后提醒 - {message}" except Exception as e: return False, f"设置提醒失败: {str(e)}" @ai_callable( description="退出语音助手应用程序。", params={}, intent="system", action="exit", concurrent=False ) def exit(self) -> Tuple[bool, str]: logger.info("🛑 用户请求退出,准备关闭语音助手...") return True, "正在关闭语音助手" @ai_callable( description="并发执行多个任务。", params={"tasks": "任务列表,每个包含 operation 和 arguments"}, intent="system", action="execute_concurrent", concurrent=True ) def _run_parallel_tasks(self, tasks: List[dict]) -> Tuple[bool, str]: def run_single(task): op = task.get("operation") args = task.get("arguments", {}) func = getattr(self, op, None) if func and callable(func): try: func(**args) except Exception as e: logger.error(f"执行任务 {op} 失败: {e}") for task in tasks: t = threading.Thread(target=run_single, args=(task,), daemon=True) t.start() return True, f"已并发执行 {len(tasks)} 个任务" class TaskOrchestrator: def __init__(self, system_controller: SystemController): self.system_controller = system_controller self.function_map = self._build_function_map() self.running_scheduled_tasks = False self.last_result = None logger.info(f"🔧 任务编排器已加载 {len(self.function_map)} 个可调用函数") # ✅ 自动启动后台任务监听 self._start_scheduled_task_loop() def _build_function_map(self) -> Dict[str, callable]: mapping = {} for item in FUNCTION_SCHEMA: func_name = item["name"] func = getattr(self.system_controller, func_name, None) if func and callable(func): mapping[func_name] = func else: logger.warning(f"⚠️ 未找到或不可调用: {func_name}") return mapping def _convert_arg_types(self, func: callable, args: dict) -> dict: converted = {} sig = inspect.signature(func) for name, param in sig.parameters.items(): value = args.get(name) if value is None: continue ann = param.annotation if isinstance(ann, type): try: if ann == int and not isinstance(value, int): converted[name] = int(float(value)) # 支持 "3.0" → 3 elif ann == float and not isinstance(value, float): converted[name] = float(value) else: converted[name] = value except (ValueError, TypeError): converted[name] = value else: converted[name] = value return converted def _start_scheduled_task_loop(self): def run_loop(): while self.running_scheduled_tasks: schedule.run_pending() time.sleep(1) if not self.running_scheduled_tasks: self.running_scheduled_tasks = True thread = threading.Thread(target=run_loop, daemon=True) thread.start() logger.info("⏰ 已启动定时任务监听循环") def run_single_step(self, step: dict) -> TaskResult: op = step.get("operation") params = step.get("parameters", {}) func = self.function_map.get(op) if not func: msg = f"不支持的操作: {op}" logger.warning(f"⚠️ {msg}") return TaskResult(False, msg, op) try: safe_params = self._convert_arg_types(func, params) result = func(**safe_params) if isinstance(result, tuple): success, message = result return TaskResult(bool(success), str(message), op) return TaskResult(True, str(result), op) except Exception as e: logger.exception(f"执行 {op} 失败") return TaskResult(False, str(e), op) @log_step("执行多任务计划") @log_time def execute_task_plan(self, plan: dict = None) -> Dict[str, Any]: execution_plan = plan.get("execution_plan", []) mode = plan.get("mode", "parallel").lower() response_to_user = plan.get("response_to_user", "任务已提交。") if not execution_plan: return { "success": True, "message": response_to_user, "operation": "task_plan" } normal_steps = [] terminal_step = None for step in execution_plan: op = step.get("operation") if op in TERMINAL_OPERATIONS: terminal_step = step else: normal_steps.append(step) all_results: List[TaskResult] = [] all_success = True if normal_steps: if mode == "parallel": with ThreadPoolExecutor() as executor: future_to_step = {executor.submit(self.run_single_step, step): step for step in normal_steps} for future in as_completed(future_to_step): res = future.result() all_results.append(res) if not res.success: all_success = False else: for step in normal_steps: res = self.run_single_step(step) all_results.append(res) if not res.success: all_success = False break final_terminal_result = None should_exit_flag = False if terminal_step and all_success: final_terminal_result = self.run_single_step(terminal_step) all_results.append(final_terminal_result) if not final_terminal_result.success: all_success = False elif final_terminal_result.operation == "exit": should_exit_flag = True messages = [r.message for r in all_results if r.message] final_message = " | ".join(messages) if messages else response_to_user response = { "success": all_success, "message": final_message.strip(), "operation": "task_plan", "input": plan, "step_results": [r.to_dict() for r in all_results], "data": { "plan_mode": mode, "terminal_executed": terminal_step is not None, "result_count": len(all_results) } } if should_exit_flag: response["should_exit"] = True self.last_result = response return response def run_scheduled_tasks(self): """处理定时任务和 Pygame 事件""" schedule.run_pending() for event in pygame.event.get(): if event.type == self.system_controller.MUSIC_END_EVENT: self._handle_music_ended() def _handle_music_ended(self): ctrl = self.system_controller if not ctrl.current_playlist: return if ctrl.loop_mode == "one": ctrl._play_current_track() elif ctrl.loop_mode in ("all", "shuffle"): ctrl.play_next() """ 单例管理中心 确保模块按顺序初始化,并延迟加载 """ from threading import Lock _system_controller = None _task_orchestrator = None _tts_engine = None _voice_recognizer = None _qwen_assistant = None _lock = Lock() _initialized = False def _ensure_init(): global _initialized if _initialized: return with _lock: if _initialized: return initialize_all() _initialized = True def get_system_controller(): _ensure_init(); return _system_controller def get_task_executor(): _ensure_init(); return _task_orchestrator def get_tts_engine(): _ensure_init(); return _tts_engine def get_voice_recognizer(): _ensure_init(); return _voice_recognizer def get_ai_assistant(): _ensure_init(); return _qwen_assistant def initialize_all(): global _system_controller, _task_orchestrator, _tts_engine, _voice_recognizer, _qwen_assistant from Progress.utils.logger_config import setup_logger logger = setup_logger("ai_assistant") from database.config import config # 1. 控制器(触发 @ai_callable 注册) from Progress.app.system_controller import SystemController _system_controller = SystemController() # 2. 任务执行器(自动启动后台循环) from Progress.app.system_controller import TaskOrchestrator _task_orchestrator = TaskOrchestrator(_system_controller) # 3. 语音识别器 from Progress.app.voice_recognizer import SpeechRecognizer _voice_recognizer = SpeechRecognizer() # 4. TTS 引擎 from Progress.app.text_to_speech import TextToSpeechEngine _tts_engine = TextToSpeechEngine() # 5. QWEN 助手 from Progress.app.qwen_assistant import QWENAssistant _qwen_assistant = QWENAssistant() # 6. 启动 TTS 子线程 _tts_engine.start() logger.info("🎉 所有模块初始化完成!")
10-27
# -*- coding: utf-8 -*- import os import threading import time import numpy as np import sounddevice as sd import piper import glob class PiperTTS: def __init__(self, models_dir="models/piper_voices"): self.models_dir = models_dir self.available_voices = [] self.model = None self.sample_rate = 16000 self.current_voice_key = None self.speed_scale = 1.0 self.lock = threading.Lock() self.is_playing = False print("🎯 Piper TTS引擎初始化完成") print(f"📁 模型目录: {os.path.abspath(self.models_dir)}") self._scan_voices() def _scan_voices(self): if not os.path.exists(self.models_dir): print(f"❌ 目录不存在: {self.models_dir}") return onnx_files = glob.glob(os.path.join(self.models_dir, "*.onnx")) for onnx_path in onnx_files: file_name = os.path.basename(onnx_path) voice_key = file_name.replace(".onnx", "") json_path = onnx_path + ".json" status = "就绪" if os.path.exists(json_path) else "缺失配置" gender = "未知" if "huayan" in voice_key: gender = "女声" elif "shi" in voice_key: gender = "男声" quality = "medium" if "low" in voice_key: quality = "low" elif "high" in voice_key: quality = "high" self.available_voices.append({ "key": voice_key, "name": f"中文{gender} - {voice_key.split('-')[1].capitalize()}({quality}质量)", "status": status, "path": onnx_path, "json_path": json_path }) print(f"🎤 可用语音: {len(self.available_voices)}个") def list_voices(self): print("\n📋 可用中文语音列表:") print("=" * 60) for idx, v in enumerate(self.available_voices): status_icon = "✅" if v['status'] == "就绪" else "❌" print(f"[{idx}] {status_icon} {v['name']}") print(f" 名称: {v['key']}") print(f" 状态: {v['status']}") print("-" * 60) print("=" * 60 + "\n") def set_voice(self, voice_key): target = next((v for v in self.available_voices if v['key'] == voice_key), None) if not target: print(f"❌ 未找到语音: {voice_key}") return False if target['status'] != "就绪": print(f"❌ 该语音状态不可用: {target['status']}") return False try: print(f"⏳ 正在加载模型: {target['key']} ...") self.model = piper.PiperVoice.load(target['path'], config_path=target['json_path']) self.sample_rate = self.model.config.sample_rate self.current_voice_key = voice_key print(f"✅ 已设置语音: {target['name']}") return True except Exception as e: print(f"❌ 模型加载失败: {e}") return False def set_speed(self, speed): self.speed_scale = speed print(f"🔄 语速已设置为: {self.speed_scale:.2f}x") def say(self, text): if not text: return if self.model is None: if self.available_voices: first_ready = next((v for v in self.available_voices if v['status'] == "就绪"), None) if first_ready: self.set_voice(first_ready['key']) else: print("❌ 没有可用的模型,无法播报。") return else: print("❌ 模型目录为空,无法播报。") return def _play_thread(): # 这里不要 self with self.lock: try: self.is_playing = True print(f"🤖 助手回复: {text}") # 1. 调用 Python API 合成 try: raw_result = self.model.synthesize(text, length_scale=self.speed_scale) except TypeError: raw_result = self.model.synthesize(text) # 2. 【终极修复 - 优化版】更稳健的数据提取方式 audio_bytes = b'' # 判断是否是生成器/迭代器 if hasattr(raw_result, '__iter__') and not isinstance(raw_result, (bytes, bytearray)): for chunk in raw_result: extracted_data = None # 策略 A: 优先使用 numpy 数组 # numpy 数组是音频的原始源数据,通过 .tobytes() 转换通常最稳定 if hasattr(chunk, 'audio_int16_array') and chunk.audio_int16_array is not None: extracted_data = chunk.audio_int16_array.tobytes() # 策略 B: 如果没有 numpy 数组,尝试 bytes 属性 # 兼容 bytes 和 bytearray 类型 elif hasattr(chunk, 'audio_int16_bytes') and chunk.audio_int16_bytes is not None: raw_bytes = chunk.audio_int16_bytes if isinstance(raw_bytes, (bytes, bytearray)): extracted_data = raw_bytes else: # 极少数情况尝试强制转换 try: extracted_data = bytes(raw_bytes) except: pass # 策略 C: 兜底检查其他可能的属性 elif hasattr(chunk, '__dict__'): for val in vars(chunk).values(): if isinstance(val, (bytes, bytearray)): extracted_data = val break elif 'numpy' in str(type(val)): extracted_data = val.tobytes() break # 如果成功提取到数据,则合并 if extracted_data: audio_bytes += extracted_data # else: # print(f"⚠️ 跳过空数据片段") # 调试用,可忽略,某些chunk可能确实为空 elif isinstance(raw_result, (bytes, bytearray)): audio_bytes = bytes(raw_result) else: audio_bytes = bytes(raw_result) # 3. 播放 if len(audio_bytes) > 0: # 打印一下长度用于调试 print(f"✅ 音频合成完成,大小: {len(audio_bytes)} bytes") audio_array = np.frombuffer(audio_bytes, dtype=np.int16) sd.play(audio_array, self.sample_rate) # 4. 等待播放完成 while sd.get_stream().active: time.sleep(0.1) else: print("❌ 音频合成结果为空,无法播放。") except Exception as e: print(f"❌ TTS合成/播放异常: {e}") import traceback traceback.print_exc() finally: self.is_playing = False thread = threading.Thread(target=_play_thread) thread.daemon = True thread.start() piper_tts = PiperTTS() if __name__ == '__main__': piper_tts.list_voices() if piper_tts.available_voices: v = piper_tts.available_voices[0] piper_tts.set_voice(v['key']) piper_tts.say("你好,我是M260C智能音箱。") time.sleep(3) 🎯 Piper TTS引擎初始化完成 📁 模型目录: D:\Work\BaiduSyncdisk\GuoHua\git-client\VoiceAssistantPy\m260c_voice_assistant\models\piper_voices 🎤 可用语音: 1个 📋 可用中文语音列表: ============================================================ [0] ✅ 中文女声 - Huayan(medium质量) 名称: zh_CN-huayan-medium 状态: 就绪 ------------------------------------------------------------ ============================================================ ⏳ 正在加载模型: zh_CN-huayan-medium ... ✅ 已设置语音: 中文女声 - Huayan(medium质量) 🔄 语速已设置为: 1.50x 🔄 语速已设置为: 0.80x 🔄 语速已设置为: 1.50x 🤖 助手回复: 你好!欢迎使用M260C智能音箱。 ✅ 音频合成完成,大小: 150016 bytes Press any key to continue . . . 电脑说了 你好,就没声音了
最新发布
12-25
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值