JiangAI/main.py

397 lines
16 KiB
Python
Raw Permalink Normal View History

2026-05-22 18:52:43 +08:00
"""
文章处理工具 · 去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()