diff --git a/scripts/ogg_converter/README.md b/scripts/ogg_converter/README.md new file mode 100644 index 00000000..2272d8da --- /dev/null +++ b/scripts/ogg_converter/README.md @@ -0,0 +1,29 @@ +# ogg_covertor 小智AI OGG 批量转换器 + +本脚本为OGG批量转换工具,支持将输入的音频文件转换为小智可使用的OGG格式 +基于Python第三方库`ffmpeg-python`实现 +支持OGG和音频之间的互转,响度调节等功能 + +# 创建并激活虚拟环境 + +```bash +# 创建虚拟环境 +python -m venv venv +# 激活虚拟环境 +source venv/bin/activate # Mac/Linux +venv\Scripts\activate # Windows +``` + +# 安装依赖 + +请在虚拟环境中执行 + +```bash +pip install ffmpeg-python +``` + +# 运行脚本 +```bash +python ogg_covertor.py +``` + diff --git a/scripts/ogg_converter/xiaozhi_ogg_converter.py b/scripts/ogg_converter/xiaozhi_ogg_converter.py new file mode 100644 index 00000000..5c3ddb29 --- /dev/null +++ b/scripts/ogg_converter/xiaozhi_ogg_converter.py @@ -0,0 +1,230 @@ +import tkinter as tk +from tkinter import ttk, filedialog, messagebox +import os +import threading +import sys +import ffmpeg + +class AudioConverterApp: + def __init__(self, master): + self.master = master + master.title("小智AI OGG音频批量转换工具") + master.geometry("680x600") # 调整窗口高度 + + # 初始化变量 + self.mode = tk.StringVar(value="audio_to_ogg") + self.output_dir = tk.StringVar() + self.output_dir.set(os.path.abspath("output")) + self.enable_loudnorm = tk.BooleanVar(value=True) + self.target_lufs = tk.DoubleVar(value=-16.0) + + # 创建UI组件 + self.create_widgets() + self.redirect_output() + + def create_widgets(self): + # 模式选择 + mode_frame = ttk.LabelFrame(self.master, text="转换模式") + mode_frame.grid(row=0, column=0, padx=10, pady=5, sticky="ew") + + ttk.Radiobutton(mode_frame, text="音频转到OGG", variable=self.mode, + value="audio_to_ogg", command=self.toggle_settings, + width=12).grid(row=0, column=0, padx=5) + ttk.Radiobutton(mode_frame, text="OGG转回音频", variable=self.mode, + value="ogg_to_audio", command=self.toggle_settings, + width=12).grid(row=0, column=1, padx=5) + + # 响度设置 + self.loudnorm_frame = ttk.Frame(self.master) + self.loudnorm_frame.grid(row=1, column=0, padx=10, pady=5, sticky="ew") + + ttk.Checkbutton(self.loudnorm_frame, text="启用响度调整", + variable=self.enable_loudnorm, width=15 + ).grid(row=0, column=0, padx=2) + ttk.Entry(self.loudnorm_frame, textvariable=self.target_lufs, + width=6).grid(row=0, column=1, padx=2) + ttk.Label(self.loudnorm_frame, text="LUFS").grid(row=0, column=2, padx=2) + + # 文件选择 + file_frame = ttk.LabelFrame(self.master, text="输入文件") + file_frame.grid(row=2, column=0, padx=10, pady=5, sticky="nsew") + + # 文件操作按钮 + ttk.Button(file_frame, text="选择文件", command=self.select_files, + width=12).grid(row=0, column=0, padx=5, pady=2) + ttk.Button(file_frame, text="移除选中", command=self.remove_selected, + width=12).grid(row=0, column=1, padx=5, pady=2) + ttk.Button(file_frame, text="清空列表", command=self.clear_files, + width=12).grid(row=0, column=2, padx=5, pady=2) + + # 文件列表(使用Treeview) + self.tree = ttk.Treeview(file_frame, columns=("selected", "filename"), + show="headings", height=8) + self.tree.heading("selected", text="选中", anchor=tk.W) + self.tree.heading("filename", text="文件名", anchor=tk.W) + self.tree.column("selected", width=60, anchor=tk.W) + self.tree.column("filename", width=600, anchor=tk.W) + self.tree.grid(row=1, column=0, columnspan=3, sticky="nsew", padx=5, pady=2) + self.tree.bind("", self.on_tree_click) + + # 输出目录 + output_frame = ttk.LabelFrame(self.master, text="输出目录") + output_frame.grid(row=3, column=0, padx=10, pady=5, sticky="ew") + + ttk.Entry(output_frame, textvariable=self.output_dir, width=60 + ).grid(row=0, column=0, padx=5, sticky="ew") + ttk.Button(output_frame, text="浏览", command=self.select_output_dir, + width=8).grid(row=0, column=1, padx=5) + + # 转换按钮区域 + button_frame = ttk.Frame(self.master) + button_frame.grid(row=4, column=0, padx=10, pady=10, sticky="ew") + + ttk.Button(button_frame, text="转换全部文件", command=lambda: self.start_conversion(True), + width=15).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="转换选中文件", command=lambda: self.start_conversion(False), + width=15).pack(side=tk.LEFT, padx=5) + + # 日志区域 + log_frame = ttk.LabelFrame(self.master, text="日志") + log_frame.grid(row=5, column=0, padx=10, pady=5, sticky="nsew") + + self.log_text = tk.Text(log_frame, height=14, width=80) + self.log_text.pack(fill=tk.BOTH, expand=True) + + # 配置布局权重 + self.master.columnconfigure(0, weight=1) + self.master.rowconfigure(2, weight=1) + self.master.rowconfigure(5, weight=3) + file_frame.columnconfigure(0, weight=1) + file_frame.rowconfigure(1, weight=1) + + def toggle_settings(self): + if self.mode.get() == "audio_to_ogg": + self.loudnorm_frame.grid() + else: + self.loudnorm_frame.grid_remove() + + def select_files(self): + file_types = [ + ("音频文件", "*.wav *.mogg *.ogg *.flac") if self.mode.get() == "audio_to_ogg" + else ("ogg文件", "*.ogg") + ] + + files = filedialog.askopenfilenames(filetypes=file_types) + for f in files: + self.tree.insert("", tk.END, values=("[ ]", os.path.basename(f)), tags=(f,)) + + def on_tree_click(self, event): + """处理复选框点击事件""" + region = self.tree.identify("region", event.x, event.y) + if region == "cell": + col = self.tree.identify_column(event.x) + item = self.tree.identify_row(event.y) + if col == "#1": # 点击的是选中列 + current_val = self.tree.item(item, "values")[0] + new_val = "[√]" if current_val == "[ ]" else "[ ]" + self.tree.item(item, values=(new_val, self.tree.item(item, "values")[1])) + + def remove_selected(self): + """移除选中的文件""" + to_remove = [] + for item in self.tree.get_children(): + if self.tree.item(item, "values")[0] == "[√]": + to_remove.append(item) + for item in reversed(to_remove): + self.tree.delete(item) + + def clear_files(self): + """清空所有文件""" + for item in self.tree.get_children(): + self.tree.delete(item) + + def select_output_dir(self): + path = filedialog.askdirectory() + if path: + self.output_dir.set(path) + + def redirect_output(self): + class StdoutRedirector: + def __init__(self, text_widget): + self.text_widget = text_widget + self.original_stdout = sys.stdout + + def write(self, message): + self.text_widget.insert(tk.END, message) + self.text_widget.see(tk.END) + self.original_stdout.write(message) + + def flush(self): + self.original_stdout.flush() + + sys.stdout = StdoutRedirector(self.log_text) + + def start_conversion(self, convert_all): + """开始转换""" + input_files = [] + for item in self.tree.get_children(): + if convert_all or self.tree.item(item, "values")[0] == "[√]": + input_files.append(self.tree.item(item, "tags")[0]) + + if not input_files: + msg = "没有找到可转换的文件" if convert_all else "没有选中任何文件" + messagebox.showwarning("警告", msg) + return + + os.makedirs(self.output_dir.get(), exist_ok=True) + + try: + if self.mode.get() == "audio_to_ogg": + target_lufs = self.target_lufs.get() if self.enable_loudnorm.get() else None + thread = threading.Thread(target=self.convert_audio_to_ogg, args=(target_lufs, input_files)) + else: + thread = threading.Thread(target=self.convert_ogg_to_audio, args=(input_files,)) + + thread.start() + except Exception as e: + print(f"转换初始化失败: {str(e)}") + + def convert_audio_to_ogg(self, target_lufs, input_files): + """音频转到ogg转换逻辑""" + for input_path in input_files: + try: + filename = os.path.basename(input_path) + base_name = os.path.splitext(filename)[0] + output_path = os.path.join(self.output_dir.get(), f"{base_name}.ogg") + + print(f"正在转换: {filename}") + ( + ffmpeg + .input(input_path) + .output(output_path, acodec='libopus', audio_bitrate='16k', ac=1, ar=16000, frame_duration=60) + .run(overwrite_output=True) + ) + print(f"转换成功: {filename}\n") + except Exception as e: + print(f"转换失败: {str(e)}\n") + + def convert_ogg_to_audio(self, input_files): + """ogg转回音频转换逻辑""" + for input_path in input_files: + try: + filename = os.path.basename(input_path) + base_name = os.path.splitext(filename)[0] + output_path = os.path.join(self.output_dir.get(), f"{base_name}.ogg") + + print(f"正在转换: {filename}") + ( + ffmpeg + .input(input_path) + .output(output_path, acodec='libopus', audio_bitrate='16k', ac=1, ar=16000, frame_duration=60) + .run(overwrite_output=True) + ) + print(f"转换成功: {filename}\n") + except Exception as e: + print(f"转换失败: {str(e)}\n") + +if __name__ == "__main__": + root = tk.Tk() + app = AudioConverterApp(root) + root.mainloop() diff --git a/docs/sonic_wifi_config.html b/scripts/sonic_wifi_config.html similarity index 100% rename from docs/sonic_wifi_config.html rename to scripts/sonic_wifi_config.html