找回密码
 立即注册
首页 业界区 业界 使用ai的方法给epub文件中的汉字加拼音

使用ai的方法给epub文件中的汉字加拼音

叶芷雁 4 小时前
前言

方便小孩阅读,对epub文件加拼音。
使用ai的方法解决多音字问题。
方法

使用python中调用ai的方法。
(1)Python环境的安装,这里直接安装python3.0以上:
https://www.python.org/downloads/windows/
1.png

安装时需要打勾添加进系统环境变量。
在cmd中确认是否安装完毕:出现下述界面表示python环境部署完毕。
2.png

(2)安装相关的库:
  1. > pip install openai ebooklib bs4 pypinyin
复制代码
(3)在文件夹下增加此代码文件add_pinyin_doubao.py,内容如下:
  1. # -*- coding: utf-8 -*-
  2. # add_pinyin_doubao.py —— 2025-12-07 终极无敌永不崩溃版
  3. # 豆包一键加注音神器,10万字小说 25~35 秒出书,兼容一切 EPUB!
  4. import os
  5. import re
  6. import time
  7. import threading
  8. import uuid
  9. from concurrent.futures import ThreadPoolExecutor, as_completed
  10. from openai import OpenAI
  11. from ebooklib import epub
  12. from bs4 import BeautifulSoup
  13. from pypinyin import pinyin, Style
  14. # ====================== 配置区 ======================
  15. os.environ["ARK_API_KEY"] = ""  # ← 填你的豆包 Key!
  16. # 一键切换模型(改这一行就行)
  17. MODEL_NAME = "doubao-1-5-pro-32k-250115"           # Pro:最准
  18. # MODEL_NAME = "doubao-seed-1-6-flash-250828"      # Seed:最快最省(推荐)
  19. # MODEL_NAME = "doubao-seed-1-6-lite-250828"       # 最便宜
  20. MAX_WORKERS = 128
  21. API_CONCURRENCY = 256
  22. API_RETRY = 3
  23. SLEEP_AFTER_API = 0.06
  24. # ====================================================
  25. client = OpenAI(
  26.     base_url="https://ark.cn-beijing.volces.com/api/v3",
  27.     api_key=os.environ.get("ARK_API_KEY"),
  28.     timeout=60
  29. )
  30. PROMPT = """请把下面这段话转换成纯拼音(头顶带声调符号),一个汉字严格对应一个拼音,用空格隔开。
  31. 不要出现任何标点符号,轻声写原音节(如吗→ma),ü 一定要带点:ǖǘǚǜ
  32. 例如:
  33. 我叫李华,住在“长寿桥”。
  34. → wǒ jiào lǐ huá zhù zài cháng shòu qiáo
  35. 现在转换:
  36. {text}"""
  37. api_sema = threading.Semaphore(API_CONCURRENCY)
  38. PINYIN_RE = re.compile(r'[āáǎàōóǒòēéěèīíǐìūúǔùǖǘǚǜüa-zA-Z]+')
  39. PUNCT_CHARS = r'''!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~,。!?;:、“”‘’()【】《》'''
  40. punctuation_pattern = f'[{re.escape(PUNCT_CHARS)}]'
  41. def num2tone(s):
  42.     tone_map = {'a': 'āáǎà', 'o': 'ōóǒò', 'e': 'ēéěè', 'i': 'īíǐì', 'u': 'ūúǔù', 'ü': 'ǖǘǚǜ'}
  43.     s = s.replace('v', 'ü')
  44.     m = re.match(r'^([a-zü]+)(\d)$', s, re.I)
  45.     if not m:
  46.         return s
  47.     base, tone = m.group(1).lower(), int(m.group(2))
  48.     for ch in 'aoeiuü':
  49.         if ch in base:
  50.             return base.replace(ch, tone_map[ch][tone - 1] if tone <= 4 else ch)
  51.     return base
  52. def local_pinyin_fallback(text: str):
  53.     raw = pinyin(text, style=Style.TONE3, heteronym=False, errors='ignore')
  54.     return [num2tone(item[0]) if item else '' for item in raw]
  55. def api_get_pinyin_list(text):
  56.     for attempt in range(1, API_RETRY + 1):
  57.         if not api_sema.acquire(timeout=60):
  58.             return local_pinyin_fallback(text)
  59.         try:
  60.             resp = client.chat.completions.create(
  61.                 model=MODEL_NAME,
  62.                 messages=[{"role": "user", "content": PROMPT.format(text=text)}],
  63.                 temperature=0.0,
  64.                 max_tokens=4096
  65.             )
  66.             raw = resp.choices[0].message.content.strip()
  67.             lst = PINYIN_RE.findall(raw.lower())
  68.             lst = [p.replace('v', 'ü') for p in lst]
  69.             time.sleep(SLEEP_AFTER_API)
  70.             return lst
  71.         except Exception as e:
  72.             print(f"\n[第{attempt}次] API调用失败: {e}")
  73.             if attempt == API_RETRY:
  74.                 print("→ 改用本地pypinyin兜底")
  75.                 return local_pinyin_fallback(text)
  76.             time.sleep(2 ** (attempt - 1))
  77.         finally:
  78.             api_sema.release()
  79.     return []
  80. def text_to_ruby_local(text: str) -> str:
  81.     chinese_chars = [c for c in text if '\u4e00' <= c <= '\u9fff']
  82.     if not chinese_chars:
  83.         return text
  84.     marks = []
  85.     clean_text = re.sub(punctuation_pattern, lambda m: (marks.append(m.group()), "__MARK__")[1], text)
  86.     py_list = api_get_pinyin_list(clean_text)
  87.     if len(py_list) != len(chinese_chars):
  88.         py_list = local_pinyin_fallback(clean_text)
  89.     result = []
  90.     py_idx = 0
  91.     i = 0
  92.     while i < len(text):
  93.         ch = text[i]
  94.         if re.match(punctuation_pattern, ch):
  95.             result.append(ch)
  96.         elif '\u4e00' <= ch <= '\u9fff':
  97.             py = py_list[py_idx] if py_idx < len(py_list) else ''
  98.             result.append(f"<ruby><rb>{ch}</rb><rt>{py}</rt></ruby>")
  99.             py_idx += 1
  100.         else:
  101.             result.append(ch)
  102.         i += 1
  103.     return ''.join(result)
  104. def is_html_item(item):
  105.     return (hasattr(epub, 'ITEM_DOCUMENT') and item.get_type() == epub.ITEM_DOCUMENT) or \
  106.            getattr(item, 'media_type', '').startswith('application/xhtml+xml')
  107. # ====================== 永久解决所有 write_epub 崩溃的核心函数 ======================
  108. def fix_epub_before_write(book):
  109.     """彻底解决 uid/None/ncx/Chapter 等所有历史遗留崩溃问题"""
  110.     # 1. 修复所有 item 的 uid 和 file_name
  111.     for item in book.get_items():
  112.         if not getattr(item, 'uid', None):
  113.             item.uid = str(uuid.uuid4())
  114.         if not getattr(item, 'file_name', None):
  115.             item.file_name = f"{item.uid}.xhtml"
  116.     # 2. 递归修复 TOC(只认 Section 和 Link!)
  117.     def walk_toc(items):
  118.         if not items:
  119.             return
  120.         for item in items:
  121.             if isinstance(item, epub.Section):
  122.                 if not getattr(item, 'uid', None):
  123.                     item.uid = str(uuid.uuid4())
  124.             elif isinstance(item, epub.Link):
  125.                 if not getattr(item, 'uid', None):
  126.                     item.uid = str(uuid.uuid4())
  127.             elif isinstance(item, tuple) and len(item) >= 1:
  128.                 walk_toc([item[0]])
  129.                 if len(item) > 1:
  130.                     walk_toc(item[1:])
  131.             elif hasattr(item, '__iter__') and not isinstance(item, str):
  132.                 walk_toc(item)
  133.     walk_toc(book.toc)
  134.     # 3. 终极保险:禁用老旧 ncx(现代阅读器全靠 nav.xhtml)
  135.     try:
  136.         book.set_option('no_ncx', True)
  137.     except:
  138.         pass  # 老版本 ebooklib 没有这个选项,直接忽略
  139. def main():
  140.     print("豆包拼音添加器 2025.12 终极无敌永不崩溃版 启动!")
  141.     epubs = [f for f in os.listdir('.') if f.lower().endswith('.epub') and '_终极拼音' not in f]
  142.     if not epubs:
  143.         print("没找到epub文件,请把小说epub拖到这个文件夹")
  144.         input("回车退出...")
  145.         return
  146.     src = epubs[0]
  147.     dst = src.replace('.epub', '_终极拼音.epub')
  148.     print(f"使用模型:{MODEL_NAME}")
  149.     print(f"处理:{src} → {dst}")
  150.     book = epub.read_epub(src)
  151.     entries = []
  152.     item_map = {}
  153.     for item in book.get_items():
  154.         if not is_html_item(item):
  155.             continue
  156.         href = getattr(item, 'file_name', f"item_{id(item)}.xhtml")
  157.         item_map[href] = item
  158.         soup = BeautifulSoup(item.get_content(), 'html.parser')
  159.         for idx, p_tag in enumerate(soup.find_all('p')):
  160.             text = p_tag.get_text()
  161.             if text.strip():
  162.                 entries.append((href, idx, text))
  163.     total = len(entries)
  164.     print(f"发现 {total} 段,开始并行加拼音...")
  165.     results = []
  166.     with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
  167.         future_to_info = {executor.submit(text_to_ruby_local, text): (href, idx)
  168.                           for href, idx, text in entries}
  169.         for future in as_completed(future_to_info):
  170.             href, idx = future_to_info[future]
  171.             try:
  172.                 ruby_text = future.result()
  173.                 results.append((href, idx, ruby_text))
  174.             except Exception as e:
  175.                 print(f"\n段落处理异常: {e},保留原文")
  176.                 results.append((href, idx, text))
  177.             print(f"\r进度:{len(results)}/{total} ({len(results)/total*100:.1f}%)", end="")
  178.     print("\n\n写回EPUB...")
  179.     # 关键修复:永别 uid/ncx 崩溃!
  180.     fix_epub_before_write(book)
  181.     from collections import defaultdict
  182.     grouped = defaultdict(list)
  183.     for href, idx, ruby in results:
  184.         grouped[href].append((idx, ruby))
  185.     for href, items in grouped.items():
  186.         item = item_map.get(href)
  187.         if not item:
  188.             continue
  189.         soup = BeautifulSoup(item.get_content(), 'html.parser')
  190.         p_tags = soup.find_all('p')
  191.         for idx, ruby in sorted(items):
  192.             if idx < len(p_tags):
  193.                 p_tags[idx].clear()
  194.                 p_tags[idx].append(BeautifulSoup(ruby, 'html.parser'))
  195.         item.set_content(str(soup).encode('utf-8'))
  196.     epub.write_epub(dst, book)
  197.     print(f"\n成功!生成:{dst}")
  198.     try:
  199.         os.startfile(dst)
  200.         print("已自动打开文件,快欣赏你的带拼音神书吧!")
  201.     except:
  202.         print("请手动打开输出文件查看")
  203.     input("\n按回车退出...")
  204. if __name__ == "__main__":
  205.     main()
复制代码
(4)把需要增加拼音的书籍放置在py代码的同级目录:
3.png

(5)在命令行中执行:
  1. >  python .\add_pinyin_doubao.py
复制代码
4.png

等待完成。
结果

5.png

6.png

Note

注意,拼音的多音字准确性取决于大模型,测试下来,豆包pro,seed系列都是准的,推荐使用doubao-1-5-pro-32k。
deepseek不准,对有些多音字识别有问题。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册