凯叔讲故事故事机,音频替换教程

1、故事机内容介绍

科学真好玩故事机,适合4-12岁。

包含8大科普主题,160个科普冒险故事及核心知识。

主要以动物、恐龙、生活科学、太空、交通工具、身体、生活安全、食物安全等故事,用精准的科学原理讲解问题,知识精准不出错。

让孩子一面了解世界,

内容包含:

文件名称 分类篇目 起始编号 结束编号 内含集数 (不含起始)
A001.mp3 动物篇 A001 A021 20
A022.mp3 恐龙篇 A022 A042 20
A043.mp3 生活科学篇 A043 A063 20
A064.mp3 太空篇 A064 A084 20
A085.mp3 交通工具篇 A085 A105 20
A106.mp3 身体篇 A106 A126 20
A127.mp3 生活安全篇 A127 A147 20
A148.mp3 食物安全篇 A148 A168 20

总计文件:168个 smp 如何不包含

1、原始数据内容目录声音8个就是168个文件。

2、首先把smp转成mp3格式

说明:他的16进制密钥为:1a 42 86 07

python 复制代码
import os
import re
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext

SOURCE_EXT = "smp"
SAVE_EXT = "mp3"


def get_file_extension(file_name):
    fname = file_name.lower()
    last_dot = fname.rfind(".")
    if last_dot > 0 and last_dot != len(fname) - 1:
        return fname[last_dot + 1:]
    return ""


def deep_scan_files_by_ext(root_path, ext):
    result = []
    if not os.path.exists(root_path):
        return result
    if os.path.isfile(root_path):
        file_ext = get_file_extension(root_path)
        if file_ext.lower() == ext.lower():
            result.append(root_path)
        return result
    for root, _, files in os.walk(root_path):
        for file in files:
            file_ext = get_file_extension(file)
            if file_ext.lower() == ext.lower():
                result.append(os.path.join(root, file))
    return result


def read_file_to_bytes(file_path):
    with open(file_path, "rb") as f:
        return f.read()


def xor_crypt(data, key):
    key_len = len(key)
    return bytes([b ^ key[i % key_len] for i, b in enumerate(data)])


def save_file_to_hex(file_path):
    save_path = file_path + ".hex.txt"
    data = read_file_to_bytes(file_path)
    with open(save_path, "w", encoding="utf-8") as f:
        line = []
        for idx, b in enumerate(data):
            line.append(f"{b:02X} ")
            if (idx + 1) % 16 == 0:
                f.write("".join(line) + "\r\n")
                line = []
        if line:
            f.write("".join(line))
    return save_path


def save_bytes_to_file(data, file_path):
    with open(file_path, "wb") as f:
        f.write(data)
    return True


class SmpMp3Gui:
    def __init__(self, root):
        self.root = root
        self.root.title("SMP ↔ MP3互转工具")
        self.root.geometry("750x550")
        self.file_list = []
        self.select_mode = tk.IntVar(value=1)

        # 模式选择
        frame_mode = tk.Frame(root)
        frame_mode.pack(fill=tk.X, padx=10, pady=5)
        tk.Radiobutton(frame_mode, text="SMP解密→MP3", variable=self.select_mode, value=1).pack(side=tk.LEFT, padx=10)
        tk.Radiobutton(frame_mode, text="MP3加密→SMP", variable=self.select_mode, value=2).pack(side=tk.LEFT, padx=10)

        # 路径选择
        frame_path = tk.Frame(root)
        frame_path.pack(fill=tk.X, padx=10, pady=5)
        tk.Label(frame_path, text="文件目录:").pack(side=tk.LEFT)
        self.path_var = tk.StringVar()
        tk.Entry(frame_path, textvariable=self.path_var, width=50).pack(side=tk.LEFT, padx=5)
        tk.Button(frame_path, text="选择目录", command=self.choose_dir).pack(side=tk.LEFT)
        tk.Button(frame_path, text="扫描文件", command=self.scan_file).pack(side=tk.LEFT, padx=5)

        # 密钥输入
        frame_key = tk.Frame(root)
        frame_key.pack(fill=tk.X, padx=10, pady=5)
        tk.Label(frame_key, text="16进制密钥(空格分隔):").pack(side=tk.LEFT)
        self.key_var = tk.StringVar()
        tk.Entry(frame_key, textvariable=self.key_var, width=45).pack(side=tk.LEFT, padx=5)
        tk.Label(frame_key, text="示例:1A 2B 3C").pack(side=tk.LEFT)

        # 功能按钮
        frame_btn = tk.Frame(root)
        frame_btn.pack(fill=tk.X, padx=10, pady=5)
        tk.Button(frame_btn, text="查看选中文件Hex", command=self.show_hex).pack(side=tk.LEFT, padx=5)
        tk.Button(frame_btn, text="开始批量转换", command=self.start_convert, bg="#28a745", fg="white").pack(side=tk.LEFT, padx=10)
        tk.Button(frame_btn, text="清空日志", command=self.clear_log).pack(side=tk.LEFT)

        # 文件列表
        tk.Label(root, text="扫描文件列表(点击选中)").pack()
        self.list_box = tk.Listbox(root, width=100, height=8)
        self.list_box.pack(padx=10)

        # 日志输出
        tk.Label(root, text="运行日志").pack()
        self.log_text = scrolledtext.ScrolledText(root, width=90, height=12)
        self.log_text.pack(padx=10, pady=5)

    def log(self, msg):
        self.log_text.insert(tk.END, f"{msg}\n")
        self.log_text.see(tk.END)
        self.root.update()

    def clear_log(self):
        self.log_text.delete(1.0, tk.END)

    def choose_dir(self):
        path = filedialog.askdirectory(title="选择文件根目录")
        if path:
            self.path_var.set(path)

    def scan_file(self):
        self.list_box.delete(0, tk.END)
        root_path = self.path_var.get().strip()
        if not os.path.isdir(root_path):
            messagebox.showerror("错误", "请选择有效文件夹!")
            return
        mode = self.select_mode.get()
        if mode == 1:
            self.file_list = deep_scan_files_by_ext(root_path, SOURCE_EXT)
        else:
            self.file_list = deep_scan_files_by_ext(root_path, SAVE_EXT)
        for idx, path in enumerate(self.file_list):
            self.list_box.insert(tk.END, f"[{idx+1}] {path}")
        self.log(f"扫描完成,共找到 {len(self.file_list)} 个文件")

    def show_hex(self):
        sel = self.list_box.curselection()
        if not sel:
            messagebox.showinfo("提示", "请先在列表中选中一个文件")
            return
        idx = sel[0]
        file_path = self.file_list[idx]
        try:
            hex_path = save_file_to_hex(file_path)
            self.log(f"Hex文本已保存至: {hex_path}")
            messagebox.showinfo("完成", f"Hex文件保存成功!\n{hex_path}")
        except Exception as e:
            self.log(f"生成Hex失败: {str(e)}")

    def get_key_bytes(self):
        key_str = self.key_var.get().strip().upper()
        if not key_str:
            messagebox.showerror("错误", "请输入16进制密钥!")
            return None
        if not re.fullmatch(r"^[A-F0-9 ]+$", key_str):
            messagebox.showerror("错误", "密钥格式错误,仅支持大写16进制+空格分隔")
            return None
        try:
            parts = key_str.split()
            return bytes([int(p, 16) for p in parts])
        except:
            messagebox.showerror("错误", "密钥解析失败,请检查格式")
            return None

    def start_convert(self):
        if not self.file_list:
            messagebox.showwarning("提示", "请先扫描文件!")
            return
        key = self.get_key_bytes()
        if not key:
            return
        mode = self.select_mode.get()
        self.log("===== 开始批量转换 =====")
        for file_path in self.file_list:
            try:
                data = read_file_to_bytes(file_path)
                data = xor_crypt(data, key)
                if mode == 1:
                    out_path = re.sub(r"\.smp$", ".mp3", file_path, flags=re.IGNORECASE)
                else:
                    out_path = re.sub(r"\.mp3$", ".smp", file_path, flags=re.IGNORECASE)
                save_bytes_to_file(data, out_path)
                self.log(f"成功: {os.path.basename(file_path)} → {os.path.basename(out_path)}")
            except Exception as e:
                self.log(f"失败: {os.path.basename(file_path)} 错误:{str(e)}")
        self.log("===== 全部转换任务结束 =====")
        messagebox.showinfo("完成", "批量转换执行完毕,请查看日志!")


if __name__ == "__main__":
    win = tk.Tk()
    app = SmpMp3Gui(win)
    win.mainloop()

3、喜马拉雅下载音频下载

用到的工具:喜马拉雅xm文件解密工具

把喜马拉雅的.xm文件转成.m4a

转换前

转换后

4、把转换的m4a格式转成mp3

提供代码工具

python 复制代码
import os
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import subprocess

class M4AtoMP3Converter:
    def __init__(self, root):
        self.root = root
        self.root.title("M4A 批量转 MP3 工具 - 带FFmpeg选择")
        self.root.geometry("600x420")

        # 变量
        self.ffmpeg_path = tk.StringVar()
        self.input_dir = tk.StringVar()
        self.output_dir = tk.StringVar()

        # ========== UI 界面 ==========
        # FFmpeg 选择
        ttk.Label(root, text="FFmpeg.exe 路径:").place(x=20, y=20)
        ttk.Entry(root, textvariable=self.ffmpeg_path, width=50).place(x=150, y=20)
        ttk.Button(root, text="选择", command=self.select_ffmpeg).place(x=470, y=18)

        # 输入文件夹
        ttk.Label(root, text="M4A 输入文件夹:").place(x=20, y=60)
        ttk.Entry(root, textvariable=self.input_dir, width=50).place(x=150, y=60)
        ttk.Button(root, text="浏览", command=self.select_input).place(x=470, y=58)

        # 输出文件夹
        ttk.Label(root, text="MP3 输出文件夹:").place(x=20, y=100)
        ttk.Entry(root, textvariable=self.output_dir, width=50).place(x=150, y=100)
        ttk.Button(root, text="浏览", command=self.select_output).place(x=470, y=98)

        # 转换按钮
        self.start_btn = ttk.Button(root, text="开始批量转换", command=self.start_convert)
        self.start_btn.place(x=230, y=140)

        # 日志框
        ttk.Label(root, text="转换日志:").place(x=20, y=180)
        self.log_text = tk.Text(root, width=70, height=10)
        self.log_text.place(x=20, y=210)

    def log(self, msg):
        self.log_text.insert(tk.END, msg + "\n")
        self.log_text.see(tk.END)
        self.root.update()

    def select_ffmpeg(self):
        path = filedialog.askopenfilename(title="选择 ffmpeg.exe", filetypes=[("FFmpeg", "ffmpeg.exe")])
        if path:
            self.ffmpeg_path.set(path)

    def select_input(self):
        path = filedialog.askdirectory(title="选择包含 M4A 的文件夹")
        if path:
            self.input_dir.set(path)

    def select_output(self):
        path = filedialog.askdirectory(title="选择 MP3 输出文件夹")
        if path:
            self.output_dir.set(path)

    def start_convert(self):
        ff = self.ffmpeg_path.get().strip()
        in_dir = self.input_dir.get().strip()
        out_dir = self.output_dir.get().strip()

        if not ff or not os.path.exists(ff):
            messagebox.showerror("错误", "请选择有效的 ffmpeg.exe")
            return
        if not in_dir or not os.path.isdir(in_dir):
            messagebox.showerror("错误", "请选择输入文件夹")
            return
        if not out_dir or not os.path.isdir(out_dir):
            messagebox.showerror("错误", "请选择输出文件夹")
            return

        self.start_btn.config(state=tk.DISABLED)
        self.log("开始转换...")

        files = [f for f in os.listdir(in_dir) if f.lower().endswith(".m4a")]
        if not files:
            self.log("未找到任何 M4A 文件")
            self.start_btn.config(state=tk.NORMAL)
            return

        for idx, file in enumerate(files, 1):
            name = os.path.splitext(file)[0]
            m4a = os.path.join(in_dir, file)
            mp3 = os.path.join(out_dir, name + ".mp3")

            self.log(f"[{idx}/{len(files)}] 转换:{file}")

            cmd = [
                ff,
                "-i", m4a,
                "-vn",
                "-c:a", "libmp3lame",
                "-q:a", "0",
                mp3,
                "-y",
                "-hide_banner",
                "-loglevel", "error"
            ]

            try:
                subprocess.run(cmd, check=True)
            except Exception as e:
                self.log(f"失败:{str(e)}")

        self.log("✅ 全部转换完成!")
        self.start_btn.config(state=tk.NORMAL)

if __name__ == "__main__":
    root = tk.Tk()
    app = M4AtoMP3Converter(root)
    root.mainloop()

5、把转换好的mp3分割处理前面的广告

备注:因为很多音频前面废话太多。例如我要批量去掉文件夹下面的所有mp3 固定10秒

bash 复制代码
@echo off
setlocal enabledelayedexpansion
:: 切掉前N秒,这里改成你要的秒数
set TRIM_SEC=21
:: 输出到子文件夹
mkdir trimmed 2>nul

for %%f in (*.mp3) do (
    echo 处理: %%f
    ffmpeg -ss %TRIM_SEC% -i "%%f" -c:a copy -q:a 0 "trimmed\%%f" -y
)
echo 完成!
pause

处理好以后,我们就得到了干净完整的儿童故事文件

6、分类

bash 复制代码
import os
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import subprocess

def gb2byte(gb):
    return gb * 1024 ** 3
def byte2gb(b):
    return round(b / 1024 ** 3, 3)
def num2a(n):
    return f"A{n:03d}"

class Mp3SuperTool:
    def __init__(self, root):
        self.root = root
        self.root.title("MP3 筛选分类分组合并 最终精准版")
        self.root.geometry("920x880")

        self.src_dir = tk.StringVar()
        self.max_total_gb = tk.StringVar(value="4")
        self.category_num = tk.StringVar(value="8")
        self.ffmpeg_path = tk.StringVar()

        self.selected_files = []
        self.categories = []
        self.group_counts = []
        self.category_names = [
            "动物篇","恐龙篇","生活科学篇","太空篇",
            "交通工具篇","身体篇","生活安全篇","食物安全篇"
        ]

        ttk.Label(root, text="MP3 源文件夹:").place(x=20, y=20)
        ttk.Entry(root, textvariable=self.src_dir, width=62).place(x=150, y=20)
        ttk.Button(root, text="浏览", command=self.select_dir).place(x=580, y=18)

        ttk.Label(root, text="内存卡上限 GB:").place(x=20, y=55)
        ttk.Entry(root, textvariable=self.max_total_gb, width=10).place(x=140, y=55)

        ttk.Label(root, text="分类数量:").place(x=220, y=55)
        ttk.Entry(root, textvariable=self.category_num, width=10).place(x=290, y=55)

        ttk.Label(root, text="FFmpeg.exe 路径:").place(x=20, y=90)
        ttk.Entry(root, textvariable=self.ffmpeg_path, width=55).place(x=150, y=90)
        ttk.Button(root, text="选择FFmpeg", command=self.select_ffmpeg).place(x=550, y=88)

        ttk.Button(root, text="1.筛选容量", command=self.step1_select).place(x=50, y=130)
        ttk.Button(root, text="2.均分分类", command=self.step2_category).place(x=180, y=130)
        ttk.Button(root, text="3.生成分类输入框", command=self.step3_create_inputs).place(x=310, y=130)
        ttk.Button(root, text="4.生成分组表格", command=self.step4_make_table).place(x=440, y=130)
        ttk.Button(root, text="5.合并所有音频", command=self.step5_merge).place(x=570, y=130)

        self.scroll_canvas = tk.Canvas(root, width=860, height=160)
        self.scroll_canvas.place(x=20, y=170)
        self.scrollbar = ttk.Scrollbar(root, orient=tk.VERTICAL, command=self.scroll_canvas.yview)
        self.scrollbar.place(x=880, y=170, height=160)
        self.scroll_canvas.configure(yscrollcommand=self.scrollbar.set)

        self.input_frame = ttk.Frame(self.scroll_canvas)
        self.scroll_canvas.create_window((0, 0), window=self.input_frame, anchor="nw")
        self.input_frame.bind("<Configure>", lambda e: self.scroll_canvas.configure(scrollregion=self.scroll_canvas.bbox("all")))
        self.input_entries = []

        ttk.Label(root, text="日志输出:").place(x=20, y=340)
        self.log = tk.Text(root, width=115, height=35)
        self.log.place(x=15, y=360)

        self.cat_range_info = []

    def select_dir(self):
        d = filedialog.askdirectory()
        if d:
            self.src_dir.set(d)

    def select_ffmpeg(self):
        p = filedialog.askopenfilename(filetypes=[("FFmpeg", "ffmpeg.exe")])
        if p:
            self.ffmpeg_path.set(p)

    def get_mp3(self):
        d = self.src_dir.get()
        if not os.path.isdir(d):
            return []
        arr = []
        for f in sorted(os.listdir(d)):
            if f.lower().endswith(".mp3"):
                p = os.path.join(d, f)
                s = os.path.getsize(p)
                arr.append({"name": f, "path": p, "size": s})
        return arr

    def step1_select(self):
        self.log.delete(1.0, tk.END)
        try:
            max_gb = float(self.max_total_gb.get())
        except:
            messagebox.showerror("错误", "请输入数字")
            return
        max_b = gb2byte(max_gb)
        all_mp3 = self.get_mp3()
        total = 0
        selected = []
        for f in all_mp3:
            if total + f["size"] <= max_b:
                selected.append(f)
                total += f["size"]
        self.selected_files = selected
        self.log.insert(tk.END, f"✅ 筛选完成:{len(selected)} 个文件 | {byte2gb(total)} GB\n\n")

    def step2_category(self):
        if not self.selected_files:
            messagebox.showwarning("提示", "请先执行步骤1")
            return
        cat_num = int(self.category_num.get())
        files = self.selected_files.copy()
        total = len(files)
        per = total // cat_num
        rem = total % cat_num
        categories = []
        idx = 0
        for i in range(cat_num):
            cnt = per + 1 if i < rem else per
            categories.append(files[idx:idx+cnt])
            idx += cnt
        self.categories = categories
        self.log.insert(tk.END, f"✅ 分类完成:共 {cat_num} 个分类,每个分类均有文件\n\n")

    def step3_create_inputs(self):
        for widget in self.input_frame.winfo_children():
            widget.destroy()
        self.input_entries.clear()
        cat_num = int(self.category_num.get())
        for i in range(cat_num):
            name = self.category_names[i] if i < len(self.category_names) else f"分类{i+1}"
            ttk.Label(self.input_frame, text=f"{name} 分成几组:").grid(row=i, column=0, padx=10, pady=8, sticky="w")
            entry = ttk.Entry(self.input_frame, width=12, font=("微软雅黑", 10))
            entry.insert(0, "20")
            entry.grid(row=i, column=1, padx=10, pady=8)
            self.input_entries.append(entry)
        self.log.insert(tk.END, "✅ 已生成分类分组输入框,请填写每组数量\n\n")

    def step4_make_table(self):
        if len(self.categories) != len(self.input_entries):
            messagebox.showerror("错误", "请先生成分类输入框")
            return
        self.group_counts = []
        for entry in self.input_entries:
            try:
                self.group_counts.append(int(entry.get().strip()))
            except:
                messagebox.showerror("错误", "请输入正确数字")
                return

        self.log.insert(tk.END, "="*110 + "\n")
        self.log.insert(tk.END, "文件名称\t分类篇目\t起始编号\t结束编号\t内含集数(不含起始)\n")
        self.log.insert(tk.END, "="*110 + "\n")

        self.final_groups = []
        self.cat_range_info.clear()
        now_start = 1

        for idx, cat_files in enumerate(self.categories):
            group_cnt = self.group_counts[idx]
            cat_name = self.category_names[idx]
            total_f = len(cat_files)
            avg = total_f // group_cnt
            rest = total_f % group_cnt

            groups = []
            pos = 0
            for g in range(group_cnt):
                take = avg + 1 if g < rest else avg
                groups.append(cat_files[pos:pos+take])
                pos += take
            self.final_groups.append(groups)

            # 严格对齐你样板
            table_start = now_start
            table_end = now_start + group_cnt
            self.cat_range_info.append((table_start, table_end))

            self.log.insert(tk.END, f"{num2a(table_start)}.mp3\t{cat_name}\t{num2a(table_start)}\t{num2a(table_end)}\t{group_cnt}\n")
            now_start = table_end + 1

        self.log.insert(tk.END, "="*110 + "\n✅ 表格生成完毕,格式完全匹配样板\n\n")

    def step5_merge(self):
        if not self.cat_range_info:
            messagebox.showwarning("提示", "请先生成分组表格")
            return
        ff = self.ffmpeg_path.get()
        if not os.path.exists(ff):
            messagebox.showerror("错误", "请选择ffmpeg.exe")
            return

        out_root = os.path.join(self.src_dir.get(), "【最终分类合并结果】")
        os.makedirs(out_root, exist_ok=True)

        for i, groups in enumerate(self.final_groups):
            cat_name = self.category_names[i]
            start_num, end_num = self.cat_range_info[i]
            folder = os.path.join(out_root, f"{i+1}_{cat_name}")
            os.makedirs(folder, exist_ok=True)
            # 合成文件从 起始+1 开始
            curr = start_num + 1
            for file_group in groups:
                out_name = num2a(curr)
                curr += 1
                list_txt = os.path.join(folder, "tmp_list.txt")
                with open(list_txt, "w", encoding="utf-8") as f:
                    for mp in file_group:
                        f.write(f"file '{mp['path']}'\n")
                out_mp3 = os.path.join(folder, f"{out_name}.mp3")
                cmd = [ff, "-f", "concat", "-safe", "0", "-i", list_txt, "-c", "copy", out_mp3, "-y"]
                subprocess.run(cmd, capture_output=True)
                os.remove(list_txt)

        self.log.insert(tk.END, "🎉 全部合并完成,编号格式完全符合要求!\n")
        messagebox.showinfo("完成", "分类、表格、音频命名全部对齐标准格式")

if __name__ == "__main__":
    root = tk.Tk()
    app = Mp3SuperTool(root)
    root.mainloop()

把分类号的mp3 放到一个文件夹里面

7、把整合好的mp3转成smp格式

转好以后,把原始目录文件复制到转好的文件夹

下面是原始的目录音频

8、复制到内存卡里面,完成 总计168份音频。完美!!!这样就超过原始数据太多太多。以前只有160个音频,现在我能塞入总计:2000-3000个音频

内容均为本人原创.帖子留言,如果你有其他凯叔机器,需要置换内容的请联系我。