JiangAI/main.py
2026-05-22 18:52:43 +08:00

397 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
文章处理工具 · 去AI排版 + 直角引号版
Core logic ported from HTML/JS → Python
GUI: CustomTkinter
"""
import re
import random
import sys
import customtkinter as ctk
# ──────────────────────────────────────────────────────────────
# 示例文本
# ──────────────────────────────────────────────────────────────
EXAMPLE_TEXT = (
'他说:"时间会证明一切"。*这是一个星号标记*还有下划线_test_。'
'实际上,这段话里包含引号、星号、下划线等复杂符号。'
'我们的工具应该能自动删除这些符号,只保留常规标点,比如逗号,句号。'
'感叹号!问号?冒号:以及书名号《西游记》。'
'长句子超过十五个字符的时候,我们会在逗号处强制拆分成短句。'
'另外段落会随机分成2-4句一段并且相邻段落的句子数量不会相同。'
'每个段落内部,除了最后一个句子末尾是句号,其他句子末尾都会变成逗号。'
'这是一个特别长的句子,它里面有很多内容,比如我们故意写很多字,'
'让它超过十五个字符,并且中间有逗号,那么它就会被拆开。'
)
# ──────────────────────────────────────────────────────────────
# 核心处理逻辑(移植自 JS
# ──────────────────────────────────────────────────────────────
def convert_quotes_to_corner(text: str) -> str:
"""双引号 → 直角引号「」DeepSeek版专用"""
# 全角左右双引号
text = text.replace('\u201c', '').replace('\u201d', '')
# 全角双引号变体 (U+FF02) → 半角预备配对
text = text.replace('\uff02', '"')
# 半角双引号顺序配对
out, left_flag = [], True
for ch in text:
if ch == '"':
out.append('' if left_flag else '')
left_flag = not left_flag
else:
out.append(ch)
return ''.join(out)
def clean_symbols(text: str, keep_corner: bool = False) -> str:
"""过滤复杂符号,只保留汉字 / 数字 / 字母 / 常规标点"""
if keep_corner:
pattern = r'[^\u4e00-\u9fff0-9a-zA-Z\s《》,.!?:「」]'
else:
pattern = r'[^\u4e00-\u9fff0-9a-zA-Z\s《》,.!?:]'
return re.sub(pattern, '', text)
def split_long_sentence(sentence: str) -> list[str]:
"""长句强制拆分(递归):长度 > 15 且含全角逗号 → 在第一个逗号处断开"""
s = sentence.strip()
if len(s) <= 15:
return [s]
comma_pos = -1
for i, ch in enumerate(s):
if ch == '' and 2 < i < len(s) - 2:
comma_pos = i
break
if comma_pos == -1:
return [s]
chars = list(s)
chars[comma_pos] = ''
new_text = ''.join(chars)
parts = new_text.split('')
if len(parts) < 2:
return [s]
first = parts[0] + ''
rest = ''.join(parts[1:])
second = rest if new_text.endswith('') else rest + ''
return split_long_sentence(first) + split_long_sentence(second)
def apply_split(text: str) -> str:
"""对整段文本执行长句拆分"""
raws = re.findall(r'[^。!?]+[。!?]', text)
if not raws:
return text
result = []
for sent in raws:
result.extend(split_long_sentence(sent))
return ''.join(result)
def get_sentences(text: str) -> list[str]:
"""提取句子列表"""
matches = re.findall(r'[^。!?]+[。!?]', text)
return [s.strip() for s in matches if s.strip()]
def group_paragraphs(sentences: list[str]) -> list[str]:
"""段落分组:每段 2~4 句,相邻段句数不同"""
if not sentences:
return []
paras, i, total = [], 0, len(sentences)
while i < total:
size = random.randint(2, 4)
if total - i == 1 and paras:
paras[-1] += sentences[i]
break
end = min(i + size, total)
if total - end == 1 and end < total:
end -= 1
if end <= i:
end = i + 1
paras.append(''.join(sentences[i:end]))
i = end
# 相邻段句数不同
idx = 1
while idx < len(paras):
pc = len(get_sentences(paras[idx - 1]))
cc = len(get_sentences(paras[idx]))
if pc == cc:
cs = get_sentences(paras[idx])
if len(cs) > 2:
paras[idx - 1] += cs.pop(0)
paras[idx] = ''.join(cs)
elif len(get_sentences(paras[idx - 1])) > 2:
ps = get_sentences(paras[idx - 1])
last = ps.pop()
paras[idx - 1] = ''.join(ps)
paras[idx] = last + paras[idx]
else:
paras[idx - 1] += paras.pop(idx)
idx -= 1
idx += 1
# 每段至少 2 句
idx2 = 0
while idx2 < len(paras):
if len(get_sentences(paras[idx2])) == 1 and len(paras) > 1:
if idx2 > 0:
paras[idx2 - 1] += paras.pop(idx2)
idx2 -= 1
elif idx2 + 1 < len(paras):
paras[idx2 + 1] = paras.pop(idx2) + paras[idx2]
idx2 -= 1
idx2 += 1
return paras
def unify_punctuation(paragraph: str) -> str:
"""末句句号,其余句子改逗号"""
sents = re.findall(r'[^。!?]+[。!?]', paragraph)
if not sents:
return paragraph
sents = [s.strip() for s in sents]
result = []
for i, sent in enumerate(sents):
body = sent
for j in range(len(sent) - 1, -1, -1):
if sent[j] in '。!?':
body = sent[:j]
break
result.append(body + ('' if i == len(sents) - 1 else ''))
return ''.join(result)
def transform(text: str, mode: str = 'remove') -> str:
"""
主处理入口
mode='remove' → 去AI排版删除引号
mode='corner' → 转直角引号版
"""
if not text or not text.strip():
return ''
try:
if mode == 'corner':
text = convert_quotes_to_corner(text)
cleaned = clean_symbols(text, keep_corner=True)
else:
cleaned = clean_symbols(text, keep_corner=False)
split_text = apply_split(cleaned)
sentences = get_sentences(split_text)
if not sentences:
return split_text
paragraphs = group_paragraphs(sentences)
return '\n\n'.join(unify_punctuation(p) for p in paragraphs)
except Exception as e:
return f'处理出错:{e}'
# ──────────────────────────────────────────────────────────────
# GUI
# ──────────────────────────────────────────────────────────────
# 配色常量
C_GREEN = "#1f6e43"
C_GREEN_HOVER = "#155a38"
C_BLUE = "#2c6e8f"
C_BLUE_HOVER = "#1f5c7a"
C_SLATE = "#4a6f88"
C_SLATE_HOVER = "#375a70"
ctk.set_appearance_mode("System")
ctk.set_default_color_theme("blue")
class Toast(ctk.CTkToplevel):
"""轻量 toast 弹窗1.5 s 自动关闭)"""
def __init__(self, parent, msg: str):
super().__init__(parent)
self.title("")
self.geometry("300x70")
self.resizable(False, False)
self.grab_set()
# 居中到父窗口
px = parent.winfo_rootx() + parent.winfo_width() // 2 - 150
py = parent.winfo_rooty() + parent.winfo_height() // 2 - 35
self.geometry(f"+{px}+{py}")
ctk.CTkLabel(self, text=msg, font=ctk.CTkFont(size=13)).pack(expand=True)
self.after(1500, self.destroy)
class App(ctk.CTk):
def __init__(self):
super().__init__()
self.title("✍️ 文章处理工具")
self.geometry("1280x800")
self.minsize(900, 620)
self._build_ui()
# ── 构建界面 ──────────────────────────────────────────────
def _build_ui(self):
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
# ── 顶部 Hero ──────────────────────────────────────────
hero = ctk.CTkFrame(self, corner_radius=18)
hero.grid(row=0, column=0, sticky="ew", padx=20, pady=(16, 8))
hero.grid_columnconfigure(1, weight=1)
ctk.CTkLabel(
hero,
text="✍️ 文章处理工具",
font=ctk.CTkFont(size=22, weight="bold"),
).grid(row=0, column=0, padx=20, pady=(14, 4), sticky="w")
sub = ctk.CTkLabel(
hero,
text="过滤符号 · 长句拆分 · 段落分组2~4句· 末句句号,其余逗号",
font=ctk.CTkFont(size=12),
text_color=("gray40", "gray70"),
)
sub.grid(row=1, column=0, columnspan=2, padx=24, pady=(0, 14), sticky="w")
# 模式切换
self._mode_btn = ctk.CTkSegmentedButton(
hero,
values=["🧹 去AI排版", "「」 直角引号版"],
font=ctk.CTkFont(size=13),
width=280,
)
self._mode_btn.set("🧹 去AI排版")
self._mode_btn.grid(row=0, column=2, padx=20, pady=14, sticky="e")
# ── 两列文本区 ─────────────────────────────────────────
columns = ctk.CTkFrame(self, fg_color="transparent")
columns.grid(row=1, column=0, sticky="nsew", padx=20, pady=0)
columns.grid_columnconfigure(0, weight=1)
columns.grid_columnconfigure(1, weight=1)
columns.grid_rowconfigure(0, weight=1)
# 左列
left = ctk.CTkFrame(columns, corner_radius=18)
left.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
left.grid_rowconfigure(1, weight=1)
left.grid_columnconfigure(0, weight=1)
lh = ctk.CTkFrame(left, fg_color="transparent")
lh.grid(row=0, column=0, sticky="ew", padx=14, pady=(12, 0))
ctk.CTkLabel(lh, text="📄 原始文章",
font=ctk.CTkFont(size=14, weight="bold")).pack(side="left")
for text, cmd, color, hov in [
("加载示例", self._load_example, C_BLUE, C_BLUE_HOVER),
("清 空", self._clear_left, C_SLATE, C_SLATE_HOVER),
("复制原文", self._copy_source, C_SLATE, C_SLATE_HOVER),
]:
ctk.CTkButton(lh, text=text, width=72, height=30, corner_radius=20,
fg_color=color, hover_color=hov,
font=ctk.CTkFont(size=12),
command=cmd).pack(side="right", padx=3)
self._src = ctk.CTkTextbox(left, font=ctk.CTkFont(size=14),
wrap="word", corner_radius=12)
self._src.grid(row=1, column=0, sticky="nsew", padx=12, pady=10)
# 右列
right = ctk.CTkFrame(columns, corner_radius=18)
right.grid(row=0, column=1, sticky="nsew", padx=(10, 0))
right.grid_rowconfigure(1, weight=1)
right.grid_columnconfigure(0, weight=1)
rh = ctk.CTkFrame(right, fg_color="transparent")
rh.grid(row=0, column=0, sticky="ew", padx=14, pady=(12, 0))
ctk.CTkLabel(rh, text="✨ 调整后",
font=ctk.CTkFont(size=14, weight="bold")).pack(side="left")
ctk.CTkButton(rh, text="复制结果", width=72, height=30, corner_radius=20,
fg_color=C_SLATE, hover_color=C_SLATE_HOVER,
font=ctk.CTkFont(size=12),
command=self._copy_result).pack(side="right", padx=3)
self._res = ctk.CTkTextbox(right, font=ctk.CTkFont(size=14),
wrap="word", corner_radius=12)
self._res.grid(row=1, column=0, sticky="nsew", padx=12, pady=10)
self._set_result("等待调整… 请粘贴文章或点击「加载示例」后点击「一键调整」")
# ── 底部操作栏 ─────────────────────────────────────────
bar = ctk.CTkFrame(self, fg_color="transparent")
bar.grid(row=2, column=0, pady=(6, 18))
ctk.CTkButton(
bar, text="✨ 一键调整", width=180, height=48,
corner_radius=30, font=ctk.CTkFont(size=15, weight="bold"),
fg_color=C_GREEN, hover_color=C_GREEN_HOVER,
command=self._do_transform,
).pack(side="left", padx=12)
ctk.CTkButton(
bar, text="🔄 同步并调整", width=180, height=48,
corner_radius=30, font=ctk.CTkFont(size=15),
fg_color=C_SLATE, hover_color=C_SLATE_HOVER,
command=self._do_transform,
).pack(side="left", padx=12)
# ── 辅助方法 ───────────────────────────────────────────────
def _get_mode(self) -> str:
return 'corner' if '直角' in self._mode_btn.get() else 'remove'
def _get_source(self) -> str:
return self._src.get("1.0", "end-1c")
def _set_result(self, text: str):
self._res.configure(state="normal")
self._res.delete("1.0", "end")
self._res.insert("1.0", text)
def _toast(self, msg: str):
Toast(self, msg)
# ── 按钮回调 ───────────────────────────────────────────────
def _load_example(self):
self._src.delete("1.0", "end")
self._src.insert("1.0", EXAMPLE_TEXT)
self._set_result("示例已加载,点击「一键调整」查看效果。")
def _clear_left(self):
self._src.delete("1.0", "end")
self._set_result("左侧已清空,请粘贴新文章或加载示例。")
def _copy_source(self):
content = self._get_source()
if not content.strip():
self._toast("原文为空,无可复制内容。")
return
self.clipboard_clear()
self.clipboard_append(content)
self._toast("✅ 原文已复制")
def _copy_result(self):
content = self._res.get("1.0", "end-1c")
skip = ("等待调整", "左侧无内容", "左侧已清空", "示例已加载", "⚠️")
if not content.strip() or any(k in content for k in skip):
self._toast("没有可复制的结果,请先生成。")
return
self.clipboard_clear()
self.clipboard_append(content)
self._toast("✅ 结果已复制")
def _do_transform(self):
src = self._get_source()
if not src.strip():
self._set_result("⚠️ 左侧无内容,请粘贴文章或点击「加载示例」。")
return
result = transform(src, self._get_mode())
self._set_result(result)
# ──────────────────────────────────────────────────────────────
if __name__ == "__main__":
app = App()
app.mainloop()