PerrinT94 commited on
Commit ·
3373b3e
0
Parent(s):
初始化:办公工具平台
Browse files- .devcontainer/devcontainer.json +33 -0
- .gitignore +27 -0
- README.md +64 -0
- app.py +65 -0
- pages/1_excel_tools.py +140 -0
- pages/2_pdf_tools.py +108 -0
- pages/3_ai_doc.py +120 -0
- pages/4_ppt_generator.py +119 -0
- pages/4_text_tools.py +101 -0
- pages/5_image_tools.py +65 -0
- pages/6_excel_formula.py +81 -0
- pages/7_tts_script.py +73 -0
- pages/8_tts_audio.py +101 -0
- requirements.txt +12 -0
- utils/__init__.py +1 -0
- utils/ai_utils.py +145 -0
- utils/doc_utils.py +143 -0
- utils/excel_utils.py +50 -0
- utils/image_utils.py +63 -0
- utils/pdf_utils.py +96 -0
- utils/ppt_utils.py +138 -0
- utils/text_utils.py +54 -0
- utils/tts_mimo.py +81 -0
- utils/tts_utils.py +52 -0
.devcontainer/devcontainer.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Python 3",
|
| 3 |
+
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
| 4 |
+
"image": "mcr.microsoft.com/devcontainers/python:1-3.11-bookworm",
|
| 5 |
+
"customizations": {
|
| 6 |
+
"codespaces": {
|
| 7 |
+
"openFiles": [
|
| 8 |
+
"README.md",
|
| 9 |
+
"app.py"
|
| 10 |
+
]
|
| 11 |
+
},
|
| 12 |
+
"vscode": {
|
| 13 |
+
"settings": {},
|
| 14 |
+
"extensions": [
|
| 15 |
+
"ms-python.python",
|
| 16 |
+
"ms-python.vscode-pylance"
|
| 17 |
+
]
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"updateContentCommand": "[ -f packages.txt ] && sudo apt update && sudo apt upgrade -y && sudo xargs apt install -y <packages.txt; [ -f requirements.txt ] && pip3 install --user -r requirements.txt; pip3 install --user streamlit; echo '✅ Packages installed and Requirements met'",
|
| 21 |
+
"postAttachCommand": {
|
| 22 |
+
"server": "streamlit run app.py --server.enableCORS false --server.enableXsrfProtection false"
|
| 23 |
+
},
|
| 24 |
+
"portsAttributes": {
|
| 25 |
+
"8501": {
|
| 26 |
+
"label": "Application",
|
| 27 |
+
"onAutoForward": "openPreview"
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
"forwardPorts": [
|
| 31 |
+
8501
|
| 32 |
+
]
|
| 33 |
+
}
|
.gitignore
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
venv/
|
| 8 |
+
env/
|
| 9 |
+
*.egg-info/
|
| 10 |
+
dist/
|
| 11 |
+
build/
|
| 12 |
+
|
| 13 |
+
# IDE
|
| 14 |
+
.vscode/
|
| 15 |
+
.idea/
|
| 16 |
+
*.swp
|
| 17 |
+
*.swo
|
| 18 |
+
|
| 19 |
+
# OS
|
| 20 |
+
.DS_Store
|
| 21 |
+
Thumbs.db
|
| 22 |
+
|
| 23 |
+
# Streamlit secrets (contains sensitive data)
|
| 24 |
+
.streamlit/secrets.toml
|
| 25 |
+
|
| 26 |
+
# Uploads
|
| 27 |
+
uploads/
|
README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Office Tools
|
| 3 |
+
emoji: 🛠️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: streamlit
|
| 7 |
+
sdk_version: 1.28.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# 🛠️ 办公工具平台
|
| 13 |
+
|
| 14 |
+
专为公司内部打造的自助办公工具集合,帮助员工高效处理日常文档和数据工作。
|
| 15 |
+
|
| 16 |
+
## 功能模块
|
| 17 |
+
|
| 18 |
+
### 📊 Excel处理
|
| 19 |
+
- 多Excel文件合并
|
| 20 |
+
- 数据去重与清洗
|
| 21 |
+
- 数据格式转换(CSV/Excel/JSON)
|
| 22 |
+
- 透视表生成
|
| 23 |
+
|
| 24 |
+
### 📄 PDF处理
|
| 25 |
+
- PDF合并/拆分
|
| 26 |
+
- PDF加水印/密码
|
| 27 |
+
|
| 28 |
+
### 📝 文档生成
|
| 29 |
+
- 合同模板生成
|
| 30 |
+
- 批量邮件合并
|
| 31 |
+
- 金额大写转换
|
| 32 |
+
- 条码/二维码生成
|
| 33 |
+
|
| 34 |
+
### 🔧 文本处理
|
| 35 |
+
- 文本对比(Diff)
|
| 36 |
+
- 批量文本替换
|
| 37 |
+
- JSON格式化
|
| 38 |
+
- 字数统计
|
| 39 |
+
|
| 40 |
+
## 本地运行
|
| 41 |
+
|
| 42 |
+
```bash
|
| 43 |
+
# 安装依赖
|
| 44 |
+
pip install -r requirements.txt
|
| 45 |
+
|
| 46 |
+
# 运行应用
|
| 47 |
+
streamlit run app.py
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
## 部署到 Streamlit Cloud
|
| 51 |
+
|
| 52 |
+
1. 将代码推送到 GitHub
|
| 53 |
+
2. 访问 [share.streamlit.io](https://share.streamlit.io)
|
| 54 |
+
3. 连接 GitHub 仓库
|
| 55 |
+
4. 选择 `app.py` 作为主文件
|
| 56 |
+
5. 点击 Deploy
|
| 57 |
+
|
| 58 |
+
## 技术栈
|
| 59 |
+
|
| 60 |
+
- Streamlit
|
| 61 |
+
- Pandas
|
| 62 |
+
- PyPDF2
|
| 63 |
+
- python-docx
|
| 64 |
+
- qrcode
|
app.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
|
| 3 |
+
st.set_page_config(
|
| 4 |
+
page_title="办公工具平台",
|
| 5 |
+
page_icon="🛠️",
|
| 6 |
+
layout="wide",
|
| 7 |
+
initial_sidebar_state="expanded"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
st.title("🛠️ 办公工具平台")
|
| 11 |
+
st.markdown("---")
|
| 12 |
+
|
| 13 |
+
st.markdown("""
|
| 14 |
+
### 欢迎使用办公工具平台
|
| 15 |
+
|
| 16 |
+
这是一个专为公司内部打造的自助办公工具集合,帮助您高效处理日常文档和数据工作。
|
| 17 |
+
|
| 18 |
+
#### 📊 Excel处理
|
| 19 |
+
- 多Excel文件合并
|
| 20 |
+
- 数据去重与清洗
|
| 21 |
+
- 数据格式转换(CSV/Excel/JSON)
|
| 22 |
+
- 透视表生成
|
| 23 |
+
|
| 24 |
+
#### 📄 PDF处理
|
| 25 |
+
- PDF合并/拆分
|
| 26 |
+
- PDF加水印/密码
|
| 27 |
+
- PDF转Word / Word转PDF
|
| 28 |
+
|
| 29 |
+
#### 🤖 AI文档处理
|
| 30 |
+
- 长文档摘要 - 上传文档,AI提取核心要点
|
| 31 |
+
- 文档翻译 - 支持多语言互译,保持格式
|
| 32 |
+
- 会议纪要整理 - 杂乱笔记变结构化纪要
|
| 33 |
+
- 合同条款解读 - 用大白话解释专业术语
|
| 34 |
+
|
| 35 |
+
#### 📑 PPT生成
|
| 36 |
+
- 智能PPT生成 - 输入主题,AI自动生成演示文稿
|
| 37 |
+
- 多种模板可选 - 商务、科技、教育等风格
|
| 38 |
+
- 自定义内容 - 支持编辑和调整
|
| 39 |
+
|
| 40 |
+
#### 🔧 文本处理
|
| 41 |
+
- 文本对比(Diff)
|
| 42 |
+
- 批量文本替换
|
| 43 |
+
- JSON格式化
|
| 44 |
+
- 字数统计
|
| 45 |
+
|
| 46 |
+
#### 🖼️ 图片处理
|
| 47 |
+
- 多图拼接(横向/纵向/宫格)
|
| 48 |
+
- 自定义间距和背景颜色
|
| 49 |
+
|
| 50 |
+
#### 📐 Excel公式助手
|
| 51 |
+
- 自然语言描述需求,AI生成Excel公式
|
| 52 |
+
- 支持中英文函数名、新旧版本兼容
|
| 53 |
+
|
| 54 |
+
#### 🎙️ 语音脚本
|
| 55 |
+
- 文本自动生成带停顿/强调标记的朗读脚本
|
| 56 |
+
- 支持上传文件,估算朗读时长
|
| 57 |
+
|
| 58 |
+
#### 🔊 语音合成
|
| 59 |
+
- 输入文本,AI直接生成可播放的语音
|
| 60 |
+
- 多种音色可选,支持调节语速
|
| 61 |
+
- 生成MP3文件可直接下载
|
| 62 |
+
""")
|
| 63 |
+
|
| 64 |
+
st.markdown("---")
|
| 65 |
+
st.markdown("请从左侧菜单选择需要使用的工具。")
|
pages/1_excel_tools.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import sys
|
| 4 |
+
sys.path.append('..')
|
| 5 |
+
from utils.excel_utils import *
|
| 6 |
+
|
| 7 |
+
st.set_page_config(page_title="Excel处理", page_icon="📊", layout="wide")
|
| 8 |
+
|
| 9 |
+
st.title("📊 Excel处理工具")
|
| 10 |
+
st.markdown("---")
|
| 11 |
+
|
| 12 |
+
# 功能选择
|
| 13 |
+
tab1, tab2, tab3, tab4 = st.tabs(["合并Excel", "数据清洗", "格式转换", "透视表"])
|
| 14 |
+
|
| 15 |
+
with tab1:
|
| 16 |
+
st.header("合并多个Excel文件")
|
| 17 |
+
uploaded_files = st.file_uploader("上传Excel/CSV文件", type=['xlsx', 'xls', 'csv'], accept_multiple_files=True, key="merge")
|
| 18 |
+
|
| 19 |
+
if uploaded_files:
|
| 20 |
+
merge_type = st.radio("合并方式", ["按行合并(上下拼接)", "按列合并(左右拼接)"])
|
| 21 |
+
|
| 22 |
+
if st.button("开始合并", key="merge_btn"):
|
| 23 |
+
try:
|
| 24 |
+
result_df = merge_excel_files(uploaded_files, "rows" if "按行" in merge_type else "columns")
|
| 25 |
+
st.success(f"合并完成!共 {len(result_df)} 行数据")
|
| 26 |
+
st.dataframe(result_df.head(100))
|
| 27 |
+
|
| 28 |
+
# 下载按钮
|
| 29 |
+
csv = result_df.to_csv(index=False, encoding='utf-8-sig')
|
| 30 |
+
st.download_button("下载CSV", csv, "merged_data.csv", "text/csv")
|
| 31 |
+
except Exception as e:
|
| 32 |
+
st.error(f"合并失败: {str(e)}")
|
| 33 |
+
|
| 34 |
+
with tab2:
|
| 35 |
+
st.header("数据清洗")
|
| 36 |
+
uploaded_file = st.file_uploader("上传文件", type=['xlsx', 'xls', 'csv'], key="clean")
|
| 37 |
+
|
| 38 |
+
if uploaded_file:
|
| 39 |
+
if uploaded_file.name.endswith('.csv'):
|
| 40 |
+
df = pd.read_csv(uploaded_file)
|
| 41 |
+
else:
|
| 42 |
+
df = pd.read_excel(uploaded_file)
|
| 43 |
+
|
| 44 |
+
st.write(f"原始数据: {len(df)} 行 × {len(df.columns)} 列")
|
| 45 |
+
st.dataframe(df.head())
|
| 46 |
+
|
| 47 |
+
col1, col2 = st.columns(2)
|
| 48 |
+
with col1:
|
| 49 |
+
remove_dup = st.checkbox("去除重复行", value=True)
|
| 50 |
+
with col2:
|
| 51 |
+
remove_empty = st.checkbox("去除空白行", value=True)
|
| 52 |
+
|
| 53 |
+
if st.button("开始清洗", key="clean_btn"):
|
| 54 |
+
result_df = df.copy()
|
| 55 |
+
if remove_dup:
|
| 56 |
+
result_df = remove_duplicates(result_df)
|
| 57 |
+
if remove_empty:
|
| 58 |
+
result_df = clean_data(result_df, remove_empty_rows=True, remove_empty_cols=False)
|
| 59 |
+
|
| 60 |
+
st.success(f"清洗完成!剩余 {len(result_df)} 行数据")
|
| 61 |
+
st.dataframe(result_df.head(100))
|
| 62 |
+
|
| 63 |
+
csv = result_df.to_csv(index=False, encoding='utf-8-sig')
|
| 64 |
+
st.download_button("下载清洗后数据", csv, "cleaned_data.csv", "text/csv")
|
| 65 |
+
|
| 66 |
+
with tab3:
|
| 67 |
+
st.header("数据格式转换")
|
| 68 |
+
uploaded_file = st.file_uploader("上传文件", type=['xlsx', 'xls', 'csv', 'json'], key="convert")
|
| 69 |
+
|
| 70 |
+
if uploaded_file:
|
| 71 |
+
target_format = st.selectbox("目标格式", ["Excel (.xlsx)", "CSV (.csv)", "JSON (.json)"])
|
| 72 |
+
|
| 73 |
+
if st.button("开始转换", key="convert_btn"):
|
| 74 |
+
try:
|
| 75 |
+
# 读取文件
|
| 76 |
+
if uploaded_file.name.endswith('.csv'):
|
| 77 |
+
df = pd.read_csv(uploaded_file)
|
| 78 |
+
elif uploaded_file.name.endswith('.json'):
|
| 79 |
+
df = pd.read_json(uploaded_file)
|
| 80 |
+
else:
|
| 81 |
+
df = pd.read_excel(uploaded_file)
|
| 82 |
+
|
| 83 |
+
# 转换格式
|
| 84 |
+
format_map = {
|
| 85 |
+
"Excel (.xlsx)": "excel",
|
| 86 |
+
"CSV (.csv)": "csv",
|
| 87 |
+
"JSON (.json)": "json"
|
| 88 |
+
}
|
| 89 |
+
result = convert_format(df, format_map[target_format])
|
| 90 |
+
|
| 91 |
+
# 下载
|
| 92 |
+
ext_map = {"Excel (.xlsx)": "xlsx", "CSV (.csv)": "csv", "JSON (.json)": "json"}
|
| 93 |
+
mime_map = {
|
| 94 |
+
"Excel (.xlsx)": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 95 |
+
"CSV (.csv)": "text/csv",
|
| 96 |
+
"JSON (.json)": "application/json"
|
| 97 |
+
}
|
| 98 |
+
st.download_button(
|
| 99 |
+
"下载转换结果",
|
| 100 |
+
result,
|
| 101 |
+
f"converted.{ext_map[target_format]}",
|
| 102 |
+
mime_map[target_format]
|
| 103 |
+
)
|
| 104 |
+
st.success("转换完成!")
|
| 105 |
+
except Exception as e:
|
| 106 |
+
st.error(f"转换失败: {str(e)}")
|
| 107 |
+
|
| 108 |
+
with tab4:
|
| 109 |
+
st.header("生成透视表")
|
| 110 |
+
uploaded_file = st.file_uploader("上传文件", type=['xlsx', 'xls', 'csv'], key="pivot")
|
| 111 |
+
|
| 112 |
+
if uploaded_file:
|
| 113 |
+
if uploaded_file.name.endswith('.csv'):
|
| 114 |
+
df = pd.read_csv(uploaded_file)
|
| 115 |
+
else:
|
| 116 |
+
df = pd.read_excel(uploaded_file)
|
| 117 |
+
|
| 118 |
+
st.write("原始数据预览:")
|
| 119 |
+
st.dataframe(df.head())
|
| 120 |
+
|
| 121 |
+
columns = df.columns.tolist()
|
| 122 |
+
col1, col2, col3 = st.columns(3)
|
| 123 |
+
with col1:
|
| 124 |
+
index_col = st.selectbox("行字段", columns, key="pivot_index")
|
| 125 |
+
with col2:
|
| 126 |
+
columns_col = st.selectbox("列字段", columns, key="pivot_columns")
|
| 127 |
+
with col3:
|
| 128 |
+
values_col = st.selectbox("值字段", columns, key="pivot_values")
|
| 129 |
+
|
| 130 |
+
aggfunc = st.selectbox("聚合函数", ["sum", "mean", "count", "max", "min"])
|
| 131 |
+
|
| 132 |
+
if st.button("生成透视表", key="pivot_btn"):
|
| 133 |
+
try:
|
| 134 |
+
result = create_pivot_table(df, index_col, columns_col, values_col, aggfunc)
|
| 135 |
+
st.dataframe(result)
|
| 136 |
+
|
| 137 |
+
csv = result.to_csv(encoding='utf-8-sig')
|
| 138 |
+
st.download_button("下载透视表", csv, "pivot_table.csv", "text/csv")
|
| 139 |
+
except Exception as e:
|
| 140 |
+
st.error(f"生成失败: {str(e)}")
|
pages/2_pdf_tools.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import sys
|
| 3 |
+
sys.path.append('..')
|
| 4 |
+
from utils.pdf_utils import *
|
| 5 |
+
|
| 6 |
+
st.set_page_config(page_title="PDF处理", page_icon="📄", layout="wide")
|
| 7 |
+
|
| 8 |
+
st.title("📄 PDF处理工具")
|
| 9 |
+
st.markdown("---")
|
| 10 |
+
|
| 11 |
+
tab1, tab2, tab3, tab4, tab5 = st.tabs(["PDF合并", "PDF拆分", "添加水印", "添加密码", "格式转换"])
|
| 12 |
+
|
| 13 |
+
with tab1:
|
| 14 |
+
st.header("合并多个PDF文件")
|
| 15 |
+
uploaded_files = st.file_uploader("上传PDF文件", type=['pdf'], accept_multiple_files=True, key="merge_pdf")
|
| 16 |
+
|
| 17 |
+
if uploaded_files and len(uploaded_files) >= 2:
|
| 18 |
+
st.write(f"已上传 {len(uploaded_files)} 个文件")
|
| 19 |
+
|
| 20 |
+
if st.button("开始合并", key="merge_pdf_btn"):
|
| 21 |
+
try:
|
| 22 |
+
result = merge_pdfs(uploaded_files)
|
| 23 |
+
st.success("合并完成!")
|
| 24 |
+
st.download_button("下载合并后的PDF", result, "merged.pdf", "application/pdf")
|
| 25 |
+
except Exception as e:
|
| 26 |
+
st.error(f"合并失败: {str(e)}")
|
| 27 |
+
elif uploaded_files:
|
| 28 |
+
st.warning("请至少上传2个PDF文件")
|
| 29 |
+
|
| 30 |
+
with tab2:
|
| 31 |
+
st.header("拆分PDF文件")
|
| 32 |
+
uploaded_file = st.file_uploader("上传PDF文件", type=['pdf'], key="split_pdf")
|
| 33 |
+
|
| 34 |
+
if uploaded_file:
|
| 35 |
+
reader = PyPDF2.PdfReader(uploaded_file)
|
| 36 |
+
total_pages = len(reader.pages)
|
| 37 |
+
st.write(f"文件共 {total_pages} 页")
|
| 38 |
+
|
| 39 |
+
split_mode = st.radio("拆分方式", ["按页码范围", "每页一个文件"])
|
| 40 |
+
|
| 41 |
+
if split_mode == "按页码范围":
|
| 42 |
+
pages_input = st.text_input("输入页码范围(如: 1-3,5,7-9)", "1-3")
|
| 43 |
+
if st.button("拆分", key="split_range_btn"):
|
| 44 |
+
try:
|
| 45 |
+
results = split_pdf(uploaded_file, pages_input)
|
| 46 |
+
for i, result in enumerate(results):
|
| 47 |
+
st.download_button(f"下载第{i+1}部分", result, f"split_{i+1}.pdf", "application/pdf")
|
| 48 |
+
except Exception as e:
|
| 49 |
+
st.error(f"拆分失败: {str(e)}")
|
| 50 |
+
else:
|
| 51 |
+
if st.button("拆分为单页", key="split_each_btn"):
|
| 52 |
+
try:
|
| 53 |
+
results = split_pdf(uploaded_file)
|
| 54 |
+
for i, result in enumerate(results):
|
| 55 |
+
st.download_button(f"下载第{i+1}页", result, f"page_{i+1}.pdf", "application/pdf")
|
| 56 |
+
except Exception as e:
|
| 57 |
+
st.error(f"拆分失败: {str(e)}")
|
| 58 |
+
|
| 59 |
+
with tab3:
|
| 60 |
+
st.header("添加文字水印")
|
| 61 |
+
uploaded_file = st.file_uploader("上传PDF文件", type=['pdf'], key="watermark_pdf")
|
| 62 |
+
|
| 63 |
+
if uploaded_file:
|
| 64 |
+
watermark_text = st.text_input("水印文字", "内部文件")
|
| 65 |
+
|
| 66 |
+
if st.button("添加水印", key="watermark_btn"):
|
| 67 |
+
try:
|
| 68 |
+
result = add_watermark(uploaded_file, watermark_text)
|
| 69 |
+
st.success("水印添加完成!")
|
| 70 |
+
st.download_button("下载带水印的PDF", result, "watermarked.pdf", "application/pdf")
|
| 71 |
+
except Exception as e:
|
| 72 |
+
st.error(f"添加水印失败: {str(e)}")
|
| 73 |
+
|
| 74 |
+
with tab4:
|
| 75 |
+
st.header("添加密码保护")
|
| 76 |
+
uploaded_file = st.file_uploader("上传PDF文件", type=['pdf'], key="password_pdf")
|
| 77 |
+
|
| 78 |
+
if uploaded_file:
|
| 79 |
+
user_password = st.text_input("设置打开密码", type="password")
|
| 80 |
+
owner_password = st.text_input("设置权限密码(可选)", type="password")
|
| 81 |
+
|
| 82 |
+
if st.button("添加密码", key="password_btn"):
|
| 83 |
+
if user_password:
|
| 84 |
+
try:
|
| 85 |
+
result = add_password(uploaded_file, user_password, owner_password)
|
| 86 |
+
st.success("密码添加完成!")
|
| 87 |
+
st.download_button("下载加密后的PDF", result, "encrypted.pdf", "application/pdf")
|
| 88 |
+
except Exception as e:
|
| 89 |
+
st.error(f"添加密码失败: {str(e)}")
|
| 90 |
+
else:
|
| 91 |
+
st.warning("请输入密码")
|
| 92 |
+
|
| 93 |
+
with tab5:
|
| 94 |
+
st.header("PDF格式转换")
|
| 95 |
+
|
| 96 |
+
conversion_type = st.radio("转换类型", ["PDF转Word", "Word转PDF"])
|
| 97 |
+
|
| 98 |
+
if conversion_type == "PDF转Word":
|
| 99 |
+
st.info("注意:PDF转Word功能需要安装额外依赖,复杂排版可能无法完美转换")
|
| 100 |
+
uploaded_file = st.file_uploader("上传PDF文件", type=['pdf'], key="pdf_to_word")
|
| 101 |
+
if uploaded_file:
|
| 102 |
+
if st.button("转换为Word", key="pdf2word_btn"):
|
| 103 |
+
st.warning("该功能需要安装 pdf2docx 库,当前版本暂不支持")
|
| 104 |
+
else:
|
| 105 |
+
uploaded_file = st.file_uploader("上传Word文件", type=['docx', 'doc'], key="word_to_pdf")
|
| 106 |
+
if uploaded_file:
|
| 107 |
+
if st.button("转换为PDF", key="word2pdf_btn"):
|
| 108 |
+
st.warning("该功能需要安装 LibreOffice 或 docx2pdf 库,当前版本暂不支持")
|
pages/3_ai_doc.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import sys
|
| 3 |
+
sys.path.append('..')
|
| 4 |
+
from utils.ai_utils import *
|
| 5 |
+
|
| 6 |
+
st.set_page_config(page_title="AI文档处理", page_icon="🤖", layout="wide")
|
| 7 |
+
|
| 8 |
+
st.title("🤖 AI文档处理")
|
| 9 |
+
st.markdown("---")
|
| 10 |
+
|
| 11 |
+
tab1, tab2, tab3, tab4 = st.tabs(["文档摘要", "文档翻译", "会议纪要", "合同解读"])
|
| 12 |
+
|
| 13 |
+
with tab1:
|
| 14 |
+
st.header("长文档摘要")
|
| 15 |
+
st.markdown("上传文档或粘贴文本,AI自动提取核心要点")
|
| 16 |
+
|
| 17 |
+
input_method = st.radio("输入方式", ["粘贴文本", "上传文件"], key="summary_input")
|
| 18 |
+
|
| 19 |
+
text = ""
|
| 20 |
+
if input_method == "粘贴文本":
|
| 21 |
+
text = st.text_area("粘贴文档内容", height=300, key="summary_text")
|
| 22 |
+
else:
|
| 23 |
+
uploaded_file = st.file_uploader("上传文档", type=['txt', 'md'], key="summary_file")
|
| 24 |
+
if uploaded_file:
|
| 25 |
+
text = uploaded_file.read().decode('utf-8')
|
| 26 |
+
st.text_area("文档内容预览", text[:500] + "..." if len(text) > 500 else text, height=100)
|
| 27 |
+
|
| 28 |
+
max_length = st.slider("摘要字数", 100, 1000, 500, step=50)
|
| 29 |
+
|
| 30 |
+
if st.button("生成摘要", key="summary_btn"):
|
| 31 |
+
if text:
|
| 32 |
+
with st.spinner("AI正在分析文档..."):
|
| 33 |
+
result = summarize_document(text, max_length)
|
| 34 |
+
st.markdown("### 摘要结果")
|
| 35 |
+
st.markdown(result)
|
| 36 |
+
else:
|
| 37 |
+
st.warning("请输入或上传文档内容")
|
| 38 |
+
|
| 39 |
+
with tab2:
|
| 40 |
+
st.header("文档翻译")
|
| 41 |
+
st.markdown("支持多语言互译,保持原文格式")
|
| 42 |
+
|
| 43 |
+
input_method = st.radio("输入方式", ["粘贴文本", "上传文件"], key="translate_input")
|
| 44 |
+
|
| 45 |
+
text = ""
|
| 46 |
+
if input_method == "粘贴文本":
|
| 47 |
+
text = st.text_area("粘贴文档内容", height=300, key="translate_text")
|
| 48 |
+
else:
|
| 49 |
+
uploaded_file = st.file_uploader("上传文档", type=['txt', 'md'], key="translate_file")
|
| 50 |
+
if uploaded_file:
|
| 51 |
+
text = uploaded_file.read().decode('utf-8')
|
| 52 |
+
|
| 53 |
+
col1, col2 = st.columns(2)
|
| 54 |
+
with col1:
|
| 55 |
+
source_lang = st.selectbox("源语言", ["自动检测", "中文", "英文", "日文", "韩文", "法文", "德文"])
|
| 56 |
+
with col2:
|
| 57 |
+
target_lang = st.selectbox("目标语言", ["英文", "中文", "日文", "韩文", "法文", "德文"])
|
| 58 |
+
|
| 59 |
+
if st.button("开始翻译", key="translate_btn"):
|
| 60 |
+
if text:
|
| 61 |
+
with st.spinner("AI正在翻译..."):
|
| 62 |
+
result = translate_document(text, target_lang)
|
| 63 |
+
st.markdown("### 翻译结果")
|
| 64 |
+
st.markdown(result)
|
| 65 |
+
else:
|
| 66 |
+
st.warning("请输入或上传文档内容")
|
| 67 |
+
|
| 68 |
+
with tab3:
|
| 69 |
+
st.header("会议纪要整理")
|
| 70 |
+
st.markdown("将杂乱的会议笔记整理成结构化的会议纪要")
|
| 71 |
+
|
| 72 |
+
input_method = st.radio("输入方式", ["粘贴文本", "上传文件"], key="meeting_input")
|
| 73 |
+
|
| 74 |
+
text = ""
|
| 75 |
+
if input_method == "粘贴文本":
|
| 76 |
+
text = st.text_area("粘贴会议笔记", height=300, key="meeting_text", placeholder="请输入会议记录,可以是语音转文字的原始内容...")
|
| 77 |
+
else:
|
| 78 |
+
uploaded_file = st.file_uploader("上传会议记录", type=['txt', 'md'], key="meeting_file")
|
| 79 |
+
if uploaded_file:
|
| 80 |
+
text = uploaded_file.read().decode('utf-8')
|
| 81 |
+
|
| 82 |
+
if st.button("整理纪要", key="meeting_btn"):
|
| 83 |
+
if text:
|
| 84 |
+
with st.spinner("AI正在整理会议纪要..."):
|
| 85 |
+
result = organize_meeting_notes(text)
|
| 86 |
+
st.markdown("### 会议纪要")
|
| 87 |
+
st.markdown(result)
|
| 88 |
+
|
| 89 |
+
# 下载按钮
|
| 90 |
+
st.download_button("下载纪要", result, "会议纪要.md", "text/markdown")
|
| 91 |
+
else:
|
| 92 |
+
st.warning("请输入或上传会议记录")
|
| 93 |
+
|
| 94 |
+
with tab4:
|
| 95 |
+
st.header("合同条款解读")
|
| 96 |
+
st.markdown("用通俗易懂的语言解释专业合同条款")
|
| 97 |
+
|
| 98 |
+
input_method = st.radio("输入方式", ["粘贴文本", "上传文件"], key="contract_input")
|
| 99 |
+
|
| 100 |
+
text = ""
|
| 101 |
+
if input_method == "粘贴文本":
|
| 102 |
+
text = st.text_area("粘贴合同条款", height=300, key="contract_text", placeholder="请粘贴需要解读的合同条款...")
|
| 103 |
+
else:
|
| 104 |
+
uploaded_file = st.file_uploader("上传合同文档", type=['txt', 'md', 'pdf'], key="contract_file")
|
| 105 |
+
if uploaded_file:
|
| 106 |
+
if uploaded_file.name.endswith('.pdf'):
|
| 107 |
+
import PyPDF2
|
| 108 |
+
reader = PyPDF2.PdfReader(uploaded_file)
|
| 109 |
+
text = "\n".join([page.extract_text() for page in reader.pages])
|
| 110 |
+
else:
|
| 111 |
+
text = uploaded_file.read().decode('utf-8')
|
| 112 |
+
|
| 113 |
+
if st.button("解读条款", key="contract_btn"):
|
| 114 |
+
if text:
|
| 115 |
+
with st.spinner("AI正在分析合同条款..."):
|
| 116 |
+
result = explain_contract_terms(text)
|
| 117 |
+
st.markdown("### 条款解读")
|
| 118 |
+
st.markdown(result)
|
| 119 |
+
else:
|
| 120 |
+
st.warning("请输入或上传合同内容")
|
pages/4_ppt_generator.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import sys
|
| 3 |
+
sys.path.append('..')
|
| 4 |
+
from utils.ppt_utils import create_ppt, create_ppt_from_outline
|
| 5 |
+
from utils.ai_utils import generate_ppt_content
|
| 6 |
+
|
| 7 |
+
st.set_page_config(page_title="PPT生成", page_icon="📑", layout="wide")
|
| 8 |
+
|
| 9 |
+
st.title("📑 PPT生成器")
|
| 10 |
+
st.markdown("---")
|
| 11 |
+
|
| 12 |
+
tab1, tab2 = st.tabs(["AI智能生成", "手动编辑"])
|
| 13 |
+
|
| 14 |
+
with tab1:
|
| 15 |
+
st.header("AI智能生成PPT")
|
| 16 |
+
st.markdown("输入主题,AI自动生成演示文稿内容")
|
| 17 |
+
|
| 18 |
+
topic = st.text_input("输入PPT主题", placeholder="例如:2024年度工作总结")
|
| 19 |
+
|
| 20 |
+
col1, col2 = st.columns(2)
|
| 21 |
+
with col1:
|
| 22 |
+
slides_count = st.slider("幻灯片数量", 5, 20, 10)
|
| 23 |
+
with col2:
|
| 24 |
+
template_style = st.selectbox("模板风格", ["商务", "科技", "教育", "简约"])
|
| 25 |
+
|
| 26 |
+
if st.button("生成PPT", key="ai_ppt_btn"):
|
| 27 |
+
if topic:
|
| 28 |
+
with st.spinner("AI正在生成PPT内容..."):
|
| 29 |
+
# 生成内容
|
| 30 |
+
content = generate_ppt_content(topic, slides_count, template_style)
|
| 31 |
+
|
| 32 |
+
if content:
|
| 33 |
+
# 生成PPT
|
| 34 |
+
ppt_bytes = create_ppt(
|
| 35 |
+
content.get("title", topic),
|
| 36 |
+
content.get("slides", []),
|
| 37 |
+
template_style
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# 预览
|
| 41 |
+
st.success("PPT生成完成!")
|
| 42 |
+
st.markdown("### 内容预览")
|
| 43 |
+
st.markdown(f"**标题:** {content.get('title', topic)}")
|
| 44 |
+
for i, slide in enumerate(content.get("slides", []), 1):
|
| 45 |
+
with st.expander(f"第{i}页: {slide.get('title', '')}"):
|
| 46 |
+
for point in slide.get("content", []):
|
| 47 |
+
st.markdown(f"- {point}")
|
| 48 |
+
|
| 49 |
+
# 下载
|
| 50 |
+
st.download_button(
|
| 51 |
+
"下载PPT",
|
| 52 |
+
ppt_bytes,
|
| 53 |
+
f"{topic}.pptx",
|
| 54 |
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
| 55 |
+
)
|
| 56 |
+
else:
|
| 57 |
+
st.error("AI生成内容失败,请检查配置")
|
| 58 |
+
else:
|
| 59 |
+
st.warning("请输入PPT主题")
|
| 60 |
+
|
| 61 |
+
with tab2:
|
| 62 |
+
st.header("手动编辑PPT")
|
| 63 |
+
st.markdown("手动输入PPT大纲,系统自动生成演示文稿")
|
| 64 |
+
|
| 65 |
+
template_style = st.selectbox("模板风格", ["商务", "科技", "教育", "简约"], key="manual_style")
|
| 66 |
+
|
| 67 |
+
outline = st.text_area(
|
| 68 |
+
"输入PPT大纲",
|
| 69 |
+
height=400,
|
| 70 |
+
placeholder="""公司年度总结
|
| 71 |
+
|
| 72 |
+
# 第一部分:业绩回顾
|
| 73 |
+
- 营收增长30%
|
| 74 |
+
- 新增客户100家
|
| 75 |
+
- 市场份额提升5%
|
| 76 |
+
|
| 77 |
+
# 第二部分:团队建设
|
| 78 |
+
- 新增员工50人
|
| 79 |
+
- 培训覆盖率100%
|
| 80 |
+
- 员工满意度95%
|
| 81 |
+
|
| 82 |
+
# 第三部分:未来规划
|
| 83 |
+
- 2025年营收目标
|
| 84 |
+
- 新产品发布计划
|
| 85 |
+
- 市场拓展策略""",
|
| 86 |
+
help="使用 # 或数字开头表示页面标题,- 表示要点"
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
if st.button("生成PPT", key="manual_ppt_btn"):
|
| 90 |
+
if outline:
|
| 91 |
+
with st.spinner("正在生成PPT..."):
|
| 92 |
+
ppt_bytes = create_ppt_from_outline(outline, template_style)
|
| 93 |
+
|
| 94 |
+
st.success("PPT生成完成!")
|
| 95 |
+
st.download_button(
|
| 96 |
+
"下载PPT",
|
| 97 |
+
ppt_bytes,
|
| 98 |
+
"演示文稿.pptx",
|
| 99 |
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
| 100 |
+
)
|
| 101 |
+
else:
|
| 102 |
+
st.warning("请输入PPT大纲")
|
| 103 |
+
|
| 104 |
+
# 使用说明
|
| 105 |
+
st.markdown("---")
|
| 106 |
+
with st.expander("使用说明"):
|
| 107 |
+
st.markdown("""
|
| 108 |
+
### AI智能生成
|
| 109 |
+
1. 输入PPT主题
|
| 110 |
+
2. 选择幻灯片数量和模板风格
|
| 111 |
+
3. 点击"生成PPT"
|
| 112 |
+
4. AI会自动生成内容并创建PPT
|
| 113 |
+
|
| 114 |
+
### 手动编辑
|
| 115 |
+
1. 使用 `#` 或数字开头表示页面标题
|
| 116 |
+
2. 使用 `-` 表示要点内容
|
| 117 |
+
3. 第一行作为PPT标题
|
| 118 |
+
4. 点击"生成PPT"即可下载
|
| 119 |
+
""")
|
pages/4_text_tools.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import sys
|
| 3 |
+
sys.path.append('..')
|
| 4 |
+
from utils.text_utils import *
|
| 5 |
+
|
| 6 |
+
st.set_page_config(page_title="文本处理", page_icon="🔧", layout="wide")
|
| 7 |
+
|
| 8 |
+
st.title("🔧 文本处理工具")
|
| 9 |
+
st.markdown("---")
|
| 10 |
+
|
| 11 |
+
tab1, tab2, tab3, tab4 = st.tabs(["文本对比", "批量替换", "JSON格式化", "字数统计"])
|
| 12 |
+
|
| 13 |
+
with tab1:
|
| 14 |
+
st.header("文本对比(Diff)")
|
| 15 |
+
|
| 16 |
+
col1, col2 = st.columns(2)
|
| 17 |
+
with col1:
|
| 18 |
+
text1 = st.text_area("原始文本", height=300, key="text1")
|
| 19 |
+
with col2:
|
| 20 |
+
text2 = st.text_area("修改后文本", height=300, key="text2")
|
| 21 |
+
|
| 22 |
+
if st.button("开始对比", key="diff_btn"):
|
| 23 |
+
if text1 and text2:
|
| 24 |
+
diff_result = compare_texts(text1, text2)
|
| 25 |
+
if diff_result:
|
| 26 |
+
st.code(diff_result, language="diff")
|
| 27 |
+
else:
|
| 28 |
+
st.success("两段文本完全相同!")
|
| 29 |
+
else:
|
| 30 |
+
st.warning("请输入两段文本")
|
| 31 |
+
|
| 32 |
+
with tab2:
|
| 33 |
+
st.header("批量文本替换")
|
| 34 |
+
|
| 35 |
+
text_input = st.text_area("输入文本", height=200, key="replace_input")
|
| 36 |
+
|
| 37 |
+
st.markdown("**替换规则**(每行一个,格式:原文 → 替换为)")
|
| 38 |
+
rules_input = st.text_area("替换规则", "旧文本 → 新文本\n错误 → 正确", height=100)
|
| 39 |
+
|
| 40 |
+
if st.button("开始替换", key="replace_btn"):
|
| 41 |
+
if text_input and rules_input:
|
| 42 |
+
# 解析替换规则
|
| 43 |
+
replacements = {}
|
| 44 |
+
for line in rules_input.strip().split('\n'):
|
| 45 |
+
if '→' in line:
|
| 46 |
+
old, new = line.split('→', 1)
|
| 47 |
+
replacements[old.strip()] = new.strip()
|
| 48 |
+
|
| 49 |
+
if replacements:
|
| 50 |
+
result = batch_replace(text_input, replacements)
|
| 51 |
+
st.text_area("替换结果", result, height=200)
|
| 52 |
+
else:
|
| 53 |
+
st.warning("未找到有效的替换规则")
|
| 54 |
+
else:
|
| 55 |
+
st.warning("请输入文本和替换规则")
|
| 56 |
+
|
| 57 |
+
with tab3:
|
| 58 |
+
st.header("JSON格式化")
|
| 59 |
+
|
| 60 |
+
json_input = st.text_area("输入JSON", height=200, key="json_input")
|
| 61 |
+
|
| 62 |
+
col1, col2 = st.columns(2)
|
| 63 |
+
with col1:
|
| 64 |
+
if st.button("格式化(美化)", key="format_btn"):
|
| 65 |
+
if json_input:
|
| 66 |
+
result = format_json(json_input)
|
| 67 |
+
st.code(result, language="json")
|
| 68 |
+
else:
|
| 69 |
+
st.warning("请输入JSON")
|
| 70 |
+
with col2:
|
| 71 |
+
if st.button("压缩", key="minify_btn"):
|
| 72 |
+
if json_input:
|
| 73 |
+
result = minify_json(json_input)
|
| 74 |
+
st.code(result, language="json")
|
| 75 |
+
else:
|
| 76 |
+
st.warning("请输入JSON")
|
| 77 |
+
|
| 78 |
+
with tab4:
|
| 79 |
+
st.header("字数统计")
|
| 80 |
+
|
| 81 |
+
text_input = st.text_area("输入文本", height=200, key="count_input")
|
| 82 |
+
|
| 83 |
+
if st.button("统计", key="count_btn"):
|
| 84 |
+
if text_input:
|
| 85 |
+
result = count_chars(text_input)
|
| 86 |
+
|
| 87 |
+
col1, col2, col3 = st.columns(3)
|
| 88 |
+
with col1:
|
| 89 |
+
st.metric("总字符数", result['总字符数'])
|
| 90 |
+
st.metric("中文字数", result['中文字数'])
|
| 91 |
+
with col2:
|
| 92 |
+
st.metric("英文字母", result['英文字母'])
|
| 93 |
+
st.metric("数字", result['数字'])
|
| 94 |
+
with col3:
|
| 95 |
+
st.metric("空格", result['空格'])
|
| 96 |
+
st.metric("总行数", result['总行数'])
|
| 97 |
+
|
| 98 |
+
st.markdown("---")
|
| 99 |
+
st.json(result)
|
| 100 |
+
else:
|
| 101 |
+
st.warning("请输入文本")
|
pages/5_image_tools.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import sys
|
| 3 |
+
sys.path.append('..')
|
| 4 |
+
from utils.image_utils import merge_images
|
| 5 |
+
|
| 6 |
+
st.set_page_config(page_title="图片处理", page_icon="🖼️", layout="wide")
|
| 7 |
+
|
| 8 |
+
st.title("🖼️ 图片处理工具")
|
| 9 |
+
st.markdown("---")
|
| 10 |
+
|
| 11 |
+
tab1, = st.tabs(["图片拼接"])
|
| 12 |
+
|
| 13 |
+
with tab1:
|
| 14 |
+
st.header("多图拼接")
|
| 15 |
+
st.markdown("上传多张图片,选择拼接方式,一键生成拼接图")
|
| 16 |
+
|
| 17 |
+
uploaded_files = st.file_uploader(
|
| 18 |
+
"上传图片", type=['png', 'jpg', 'jpeg', 'webp', 'bmp'],
|
| 19 |
+
accept_multiple_files=True, key="merge_img"
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
if uploaded_files:
|
| 23 |
+
st.info(f"已上传 {len(uploaded_files)} 张图片")
|
| 24 |
+
|
| 25 |
+
col1, col2, col3 = st.columns(3)
|
| 26 |
+
with col1:
|
| 27 |
+
direction = st.radio("拼接方向", ["横向拼接", "纵向拼接", "宫格排列"], key="direction")
|
| 28 |
+
with col2:
|
| 29 |
+
gap = st.slider("间距(像素)", 0, 50, 5, key="gap")
|
| 30 |
+
with col3:
|
| 31 |
+
bg_color = st.color_picker("背景颜色", "#FFFFFF", key="bg_color")
|
| 32 |
+
|
| 33 |
+
# 预览上传的图片
|
| 34 |
+
with st.expander("预览上传的图片"):
|
| 35 |
+
preview_cols = st.columns(min(len(uploaded_files), 4))
|
| 36 |
+
for i, f in enumerate(uploaded_files):
|
| 37 |
+
with preview_cols[i % 4]:
|
| 38 |
+
st.image(f, caption=f.name, use_container_width=True)
|
| 39 |
+
|
| 40 |
+
if st.button("开始拼接", key="merge_img_btn"):
|
| 41 |
+
with st.spinner("拼接中..."):
|
| 42 |
+
try:
|
| 43 |
+
direction_map = {
|
| 44 |
+
"横向拼接": "horizontal",
|
| 45 |
+
"纵向拼接": "vertical",
|
| 46 |
+
"宫格排列": "grid"
|
| 47 |
+
}
|
| 48 |
+
# 解析颜色
|
| 49 |
+
r, g, b = int(bg_color[1:3], 16), int(bg_color[3:5], 16), int(bg_color[5:7], 16)
|
| 50 |
+
|
| 51 |
+
result = merge_images(
|
| 52 |
+
uploaded_files,
|
| 53 |
+
direction=direction_map[direction],
|
| 54 |
+
gap=gap,
|
| 55 |
+
bg_color=(r, g, b)
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
st.success("拼接完成!")
|
| 59 |
+
st.image(result, caption="拼接结果", use_container_width=True)
|
| 60 |
+
|
| 61 |
+
st.download_button(
|
| 62 |
+
"下载拼接图", result, "merged_image.png", "image/png"
|
| 63 |
+
)
|
| 64 |
+
except Exception as e:
|
| 65 |
+
st.error(f"拼接失败: {str(e)}")
|
pages/6_excel_formula.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import sys
|
| 3 |
+
sys.path.append('..')
|
| 4 |
+
from utils.ai_utils import get_mimo_client
|
| 5 |
+
|
| 6 |
+
st.set_page_config(page_title="Excel公式助手", page_icon="📐", layout="wide")
|
| 7 |
+
|
| 8 |
+
st.title("📐 Excel公式助手")
|
| 9 |
+
st.markdown("---")
|
| 10 |
+
|
| 11 |
+
st.markdown("用自然语言描述你的需求,AI帮你生成Excel公式")
|
| 12 |
+
|
| 13 |
+
# 常用场景快捷按钮
|
| 14 |
+
st.subheader("常用场景")
|
| 15 |
+
quick_scenarios = [
|
| 16 |
+
"查找A列中重复的值",
|
| 17 |
+
"统计B列中大于100的个数",
|
| 18 |
+
"根据姓名查找对应的工资",
|
| 19 |
+
"将日期格式从YYYYMMDD转为YYYY-MM-DD",
|
| 20 |
+
"提取单元格中的数字部分",
|
| 21 |
+
"合并A1到A10的内容,用逗号分隔",
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
cols = st.columns(3)
|
| 25 |
+
for i, scenario in enumerate(quick_scenarios):
|
| 26 |
+
with cols[i % 3]:
|
| 27 |
+
if st.button(scenario, key=f"quick_{i}", use_container_width=True):
|
| 28 |
+
st.session_state.formula_input = scenario
|
| 29 |
+
|
| 30 |
+
# 输入区域
|
| 31 |
+
user_input = st.text_area(
|
| 32 |
+
"描述你的需求",
|
| 33 |
+
value=st.session_state.get("formula_input", ""),
|
| 34 |
+
height=100,
|
| 35 |
+
placeholder="例如:我想在Sheet2中根据工号查找Sheet1中的姓名和部门",
|
| 36 |
+
key="formula_text"
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
col1, col2 = st.columns([1, 1])
|
| 40 |
+
with col1:
|
| 41 |
+
excel_version = st.selectbox("Excel版本", ["Excel 365/2021", "Excel 2019及更早"], key="version")
|
| 42 |
+
with col2:
|
| 43 |
+
language = st.selectbox("函数语言", ["中文函数名", "英文函数名"], key="lang")
|
| 44 |
+
|
| 45 |
+
if st.button("生成公式", key="gen_formula"):
|
| 46 |
+
if not user_input.strip():
|
| 47 |
+
st.warning("请描述你的需求")
|
| 48 |
+
else:
|
| 49 |
+
client = get_mimo_client()
|
| 50 |
+
if not client:
|
| 51 |
+
st.error("AI服务未配置,请设置 MIMO_API_KEY 环境变量")
|
| 52 |
+
else:
|
| 53 |
+
with st.spinner("AI正在思考..."):
|
| 54 |
+
lang_hint = "使用中文函数名(如 求和、如果、垂直查找)" if "中文" in language else "use English function names (SUM, IF, VLOOKUP)"
|
| 55 |
+
ver_hint = "可以使用 XLOOKUP、FILTER、UNIQUE 等新函数" if "365" in excel_version else "请使用兼容旧版的函数,避免 XLOOKUP 等新函数"
|
| 56 |
+
|
| 57 |
+
prompt = f"""你是一个Excel公式专家。用户会描述一个需求,请生成对应的Excel公式。
|
| 58 |
+
|
| 59 |
+
要求:
|
| 60 |
+
1. 直接给出公式,不要多余的解释
|
| 61 |
+
2. 如果需要辅助列或特殊操作,简要说明
|
| 62 |
+
3. {lang_hint}
|
| 63 |
+
4. {ver_hint}
|
| 64 |
+
5. 如果一个公式不够,给出完整的解决方案
|
| 65 |
+
|
| 66 |
+
用户需求:{user_input}"""
|
| 67 |
+
|
| 68 |
+
try:
|
| 69 |
+
response = client.chat.completions.create(
|
| 70 |
+
model="mimo-v2.5-pro",
|
| 71 |
+
messages=[{"role": "user", "content": prompt}],
|
| 72 |
+
max_tokens=1000
|
| 73 |
+
)
|
| 74 |
+
result = response.choices[0].message.content
|
| 75 |
+
|
| 76 |
+
st.subheader("生成结果")
|
| 77 |
+
st.code(result, language="text")
|
| 78 |
+
|
| 79 |
+
st.button("复制公式", on_click=lambda: st.write("已复制"))
|
| 80 |
+
except Exception as e:
|
| 81 |
+
st.error(f"生成失败: {str(e)}")
|
pages/7_tts_script.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import sys
|
| 3 |
+
sys.path.append('..')
|
| 4 |
+
from utils.tts_utils import generate_tts_script
|
| 5 |
+
|
| 6 |
+
st.set_page_config(page_title="语音脚本", page_icon="🎙️", layout="wide")
|
| 7 |
+
|
| 8 |
+
st.title("🎙️ 文本转语音脚本")
|
| 9 |
+
st.markdown("---")
|
| 10 |
+
|
| 11 |
+
st.markdown("输入文本,自动生成带停顿、语速、强调标记的朗读脚本,可直接用于TTS工具")
|
| 12 |
+
|
| 13 |
+
# 输入区域
|
| 14 |
+
tab1, tab2 = st.tabs(["手动输入", "上传文件"])
|
| 15 |
+
|
| 16 |
+
text = ""
|
| 17 |
+
with tab1:
|
| 18 |
+
text = st.text_area(
|
| 19 |
+
"粘贴文本内容",
|
| 20 |
+
height=300,
|
| 21 |
+
placeholder="在这里粘贴需要转换为语音脚本的文本...",
|
| 22 |
+
key="tts_text"
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
with tab2:
|
| 26 |
+
uploaded = st.file_uploader("上传文本文件", type=['txt', 'md'], key="tts_file")
|
| 27 |
+
if uploaded:
|
| 28 |
+
text = uploaded.read().decode('utf-8')
|
| 29 |
+
st.text_area("文件内容预览", text[:500] + "..." if len(text) > 500 else text, height=100, key="tts_preview")
|
| 30 |
+
|
| 31 |
+
# 设置选项
|
| 32 |
+
st.subheader("脚本设置")
|
| 33 |
+
col1, col2 = st.columns(2)
|
| 34 |
+
|
| 35 |
+
with col1:
|
| 36 |
+
speed = st.selectbox("默认语速", ["慢速", "正常", "快速"], index=1, key="speed")
|
| 37 |
+
add_pauses = st.checkbox("自动添加停顿标记", value=True, key="pauses")
|
| 38 |
+
|
| 39 |
+
with col2:
|
| 40 |
+
add_emphasis = st.checkbox("自动标记数字和强调", value=True, key="emphasis")
|
| 41 |
+
output_format = st.selectbox("输出格式", ["纯文本", "带标记文本", "SSML"], key="format")
|
| 42 |
+
|
| 43 |
+
# 生成
|
| 44 |
+
if st.button("生成语音脚本", key="gen_tts"):
|
| 45 |
+
if not text.strip():
|
| 46 |
+
st.warning("请输入或上传文本内容")
|
| 47 |
+
else:
|
| 48 |
+
speed_map = {"慢速": "slow", "正常": "normal", "快速": "fast"}
|
| 49 |
+
|
| 50 |
+
result = generate_tts_script(
|
| 51 |
+
text,
|
| 52 |
+
speed=speed_map[speed],
|
| 53 |
+
add_pauses=add_pauses,
|
| 54 |
+
add_emphasis=add_emphasis
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
st.subheader("生成结果")
|
| 58 |
+
st.code(result, language="text")
|
| 59 |
+
|
| 60 |
+
# 统计信息
|
| 61 |
+
line_count = len(result.strip().split("\n"))
|
| 62 |
+
char_count = len(result)
|
| 63 |
+
st.caption(f"共 {line_count} 行,{char_count} 字符")
|
| 64 |
+
|
| 65 |
+
col1, col2 = st.columns(2)
|
| 66 |
+
with col1:
|
| 67 |
+
st.download_button(
|
| 68 |
+
"下载脚本", result, "tts_script.txt", "text/plain"
|
| 69 |
+
)
|
| 70 |
+
with col2:
|
| 71 |
+
# 估算朗读时间(中文约每分钟200字)
|
| 72 |
+
est_minutes = char_count / 200
|
| 73 |
+
st.info(f"预计朗读时长:约 {est_minutes:.1f} 分钟")
|
pages/8_tts_audio.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import sys
|
| 3 |
+
sys.path.append('..')
|
| 4 |
+
|
| 5 |
+
st.set_page_config(page_title="语音合成", page_icon="🔊", layout="wide")
|
| 6 |
+
|
| 7 |
+
try:
|
| 8 |
+
from utils.tts_mimo import text_to_speech, PRESET_VOICES
|
| 9 |
+
except Exception as e:
|
| 10 |
+
st.error(f"模块加载失败: {e}")
|
| 11 |
+
st.stop()
|
| 12 |
+
|
| 13 |
+
st.title("🔊 文本转语音")
|
| 14 |
+
st.markdown("---")
|
| 15 |
+
|
| 16 |
+
st.markdown("输入文本,使用 mimo-v2.5-tts 模型直接生成可播放、可下载的语音文件")
|
| 17 |
+
|
| 18 |
+
# 输入
|
| 19 |
+
tab1, tab2 = st.tabs(["手动输入", "上传文件"])
|
| 20 |
+
|
| 21 |
+
text = ""
|
| 22 |
+
with tab1:
|
| 23 |
+
text = st.text_area(
|
| 24 |
+
"输入要转换的文本",
|
| 25 |
+
height=200,
|
| 26 |
+
placeholder="在这里输入或粘贴文本...",
|
| 27 |
+
key="tts_input"
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
with tab2:
|
| 31 |
+
uploaded = st.file_uploader("上传文本文件", type=['txt', 'md'], key="tts_file_upload")
|
| 32 |
+
if uploaded:
|
| 33 |
+
text = uploaded.read().decode('utf-8')
|
| 34 |
+
st.text_area("文件预览", text[:300] + "..." if len(text) > 300 else text, height=80, key="tts_file_preview")
|
| 35 |
+
|
| 36 |
+
# 参数设置
|
| 37 |
+
st.subheader("语音设置")
|
| 38 |
+
col1, col2 = st.columns(2)
|
| 39 |
+
|
| 40 |
+
with col1:
|
| 41 |
+
voice_options = list(PRESET_VOICES.keys())
|
| 42 |
+
voice_labels = [f"{name} ({info['lang']}·{info['gender']})" for name, info in PRESET_VOICES.items()]
|
| 43 |
+
voice_idx = st.selectbox(
|
| 44 |
+
"音色",
|
| 45 |
+
range(len(voice_options)),
|
| 46 |
+
format_func=lambda i: voice_labels[i],
|
| 47 |
+
index=0,
|
| 48 |
+
help="选择预置音色",
|
| 49 |
+
key="voice"
|
| 50 |
+
)
|
| 51 |
+
voice = voice_options[voice_idx]
|
| 52 |
+
|
| 53 |
+
with col2:
|
| 54 |
+
style = st.text_input(
|
| 55 |
+
"风格指令(可选)",
|
| 56 |
+
placeholder="例如:温柔、活泼、东北话、悲伤、磁性...",
|
| 57 |
+
help="自然语言描述发音风格,留空使用默认风格",
|
| 58 |
+
key="style"
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# 高级选项
|
| 62 |
+
with st.expander("高级选项"):
|
| 63 |
+
st.markdown("""
|
| 64 |
+
**风格控制提示:**
|
| 65 |
+
- 基础情绪:开心 / 悲伤 / 愤怒 / 惊讶 / 兴奋 / 平静
|
| 66 |
+
- 复合情绪:怅然 / 欣慰 / 无奈 / 愧疚 / 释然
|
| 67 |
+
- 整体语调:温柔 / 高冷 / 活泼 / 严肃 / 慵懒 / 俏皮
|
| 68 |
+
- 音色定位:磁性 / 醇厚 / 清亮 / 空灵 / 甜美 / 沙哑
|
| 69 |
+
- 方言:东北话 / 四川话 / 河南话 / 粤语
|
| 70 |
+
- 也可以在文本中插入标签,如 `(温柔)` `(东北话)` `(唱歌)`
|
| 71 |
+
""")
|
| 72 |
+
|
| 73 |
+
# 字符统计
|
| 74 |
+
if text:
|
| 75 |
+
st.caption(f"文本长度:{len(text)} 字符")
|
| 76 |
+
|
| 77 |
+
# 生成
|
| 78 |
+
if st.button("生成语音", key="gen_audio"):
|
| 79 |
+
if not text.strip():
|
| 80 |
+
st.warning("请输入文本内容")
|
| 81 |
+
elif len(text) > 4096:
|
| 82 |
+
st.warning(f"文本过长({len(text)}字),TTS 单次最多支持 4096 字符,请分段生成")
|
| 83 |
+
else:
|
| 84 |
+
with st.spinner("正在生成语音,请稍候..."):
|
| 85 |
+
try:
|
| 86 |
+
audio_bytes = text_to_speech(text, voice=voice, style=style)
|
| 87 |
+
|
| 88 |
+
st.success("生成完成!")
|
| 89 |
+
|
| 90 |
+
# 播放
|
| 91 |
+
st.audio(audio_bytes, format="audio/wav")
|
| 92 |
+
|
| 93 |
+
# 下载
|
| 94 |
+
st.download_button(
|
| 95 |
+
"下载音频",
|
| 96 |
+
audio_bytes,
|
| 97 |
+
"speech.wav",
|
| 98 |
+
"audio/wav"
|
| 99 |
+
)
|
| 100 |
+
except Exception as e:
|
| 101 |
+
st.error(f"生成失败: {str(e)}")
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit>=1.28.0
|
| 2 |
+
pandas>=2.0.0
|
| 3 |
+
openpyxl>=3.1.0
|
| 4 |
+
xlrd>=2.0.0
|
| 5 |
+
PyPDF2>=3.0.0
|
| 6 |
+
python-docx>=1.0.0
|
| 7 |
+
reportlab>=4.0.0
|
| 8 |
+
qrcode[pil]>=7.4.0
|
| 9 |
+
Pillow>=10.0.0
|
| 10 |
+
xlsxwriter>=3.1.0
|
| 11 |
+
python-pptx>=0.6.21
|
| 12 |
+
openai>=1.0.0
|
utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Utils package
|
utils/ai_utils.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import os
|
| 3 |
+
from openai import OpenAI
|
| 4 |
+
|
| 5 |
+
def get_mimo_client():
|
| 6 |
+
"""获取mimo API客户端"""
|
| 7 |
+
# 优先从环境变量读取
|
| 8 |
+
api_key = os.getenv("MIMO_API_KEY")
|
| 9 |
+
base_url = os.getenv("MIMO_BASE_URL")
|
| 10 |
+
|
| 11 |
+
# 如果环境变量没有,尝试从st.secrets读取
|
| 12 |
+
if not api_key:
|
| 13 |
+
try:
|
| 14 |
+
api_key = st.secrets["MIMO_API_KEY"]
|
| 15 |
+
except:
|
| 16 |
+
pass
|
| 17 |
+
|
| 18 |
+
if not base_url:
|
| 19 |
+
try:
|
| 20 |
+
base_url = st.secrets["MIMO_BASE_URL"]
|
| 21 |
+
except:
|
| 22 |
+
base_url = "https://api.xiaomimimo.com/v1"
|
| 23 |
+
|
| 24 |
+
if not api_key:
|
| 25 |
+
return None
|
| 26 |
+
|
| 27 |
+
return OpenAI(
|
| 28 |
+
api_key=api_key,
|
| 29 |
+
base_url=base_url
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
def summarize_document(text: str, max_length: int = 500) -> str:
|
| 33 |
+
"""文档摘要"""
|
| 34 |
+
client = get_mimo_client()
|
| 35 |
+
if not client:
|
| 36 |
+
return "错误:未配置 MIMO API Key,请在 Settings → Secrets 中配置"
|
| 37 |
+
|
| 38 |
+
try:
|
| 39 |
+
response = client.chat.completions.create(
|
| 40 |
+
model="mimo-v2.5-pro",
|
| 41 |
+
messages=[
|
| 42 |
+
{"role": "system", "content": "你是一个专业的文档摘要助手,请用简洁的语言总结文档核心内容。"},
|
| 43 |
+
{"role": "user", "content": f"请为以下文档生成摘要,控制在{max_length}字以内:\n\n{text}"}
|
| 44 |
+
],
|
| 45 |
+
max_tokens=1000
|
| 46 |
+
)
|
| 47 |
+
return response.choices[0].message.content
|
| 48 |
+
except Exception as e:
|
| 49 |
+
return f"API调用失败: {str(e)}"
|
| 50 |
+
|
| 51 |
+
def translate_document(text: str, target_lang: str = "英文") -> str:
|
| 52 |
+
"""文档翻译"""
|
| 53 |
+
client = get_mimo_client()
|
| 54 |
+
if not client:
|
| 55 |
+
return "错误:未配置 MIMO API Key,请在 Settings → Secrets 中配置"
|
| 56 |
+
|
| 57 |
+
try:
|
| 58 |
+
response = client.chat.completions.create(
|
| 59 |
+
model="mimo-v2.5-pro",
|
| 60 |
+
messages=[
|
| 61 |
+
{"role": "system", "content": f"你是一个专业的翻译助手,请将文档翻译成{target_lang},保持原文格式和专业术语。"},
|
| 62 |
+
{"role": "user", "content": f"请翻译以下文档:\n\n{text}"}
|
| 63 |
+
],
|
| 64 |
+
max_tokens=2000
|
| 65 |
+
)
|
| 66 |
+
return response.choices[0].message.content
|
| 67 |
+
except Exception as e:
|
| 68 |
+
return f"API调用失败: {str(e)}"
|
| 69 |
+
|
| 70 |
+
def organize_meeting_notes(notes: str) -> str:
|
| 71 |
+
"""会议纪要整理"""
|
| 72 |
+
client = get_mimo_client()
|
| 73 |
+
if not client:
|
| 74 |
+
return "错误:未配置 MIMO API Key,请在 Settings → Secrets 中配置"
|
| 75 |
+
|
| 76 |
+
try:
|
| 77 |
+
response = client.chat.completions.create(
|
| 78 |
+
model="mimo-v2.5-pro",
|
| 79 |
+
messages=[
|
| 80 |
+
{"role": "system", "content": "你是一个专业的会议纪要整理助手,请将杂乱的会议笔记整理成结构化的会议纪要格式,包含:会议主题、参会人员、主要议题、讨论要点、决议事项、待办事项。"},
|
| 81 |
+
{"role": "user", "content": f"请整理以下会议笔记:\n\n{notes}"}
|
| 82 |
+
],
|
| 83 |
+
max_tokens=1500
|
| 84 |
+
)
|
| 85 |
+
return response.choices[0].message.content
|
| 86 |
+
except Exception as e:
|
| 87 |
+
return f"API调用失败: {str(e)}"
|
| 88 |
+
|
| 89 |
+
def explain_contract_terms(text: str) -> str:
|
| 90 |
+
"""合同条款解读"""
|
| 91 |
+
client = get_mimo_client()
|
| 92 |
+
if not client:
|
| 93 |
+
return "错误:未配置 MIMO API Key,请在 Settings → Secrets 中配置"
|
| 94 |
+
|
| 95 |
+
try:
|
| 96 |
+
response = client.chat.completions.create(
|
| 97 |
+
model="mimo-v2.5-pro",
|
| 98 |
+
messages=[
|
| 99 |
+
{"role": "system", "content": "你是一个法律顾问助手,请用通俗易懂的语言解释合同条款,指出可能的风险点和注意事项。"},
|
| 100 |
+
{"role": "user", "content": f"请解读以下合同条款:\n\n{text}"}
|
| 101 |
+
],
|
| 102 |
+
max_tokens=1500
|
| 103 |
+
)
|
| 104 |
+
return response.choices[0].message.content
|
| 105 |
+
except Exception as e:
|
| 106 |
+
return f"API调用失败: {str(e)}"
|
| 107 |
+
|
| 108 |
+
def generate_ppt_content(topic: str, slides_count: int = 10, style: str = "商务") -> dict:
|
| 109 |
+
"""生成PPT内容"""
|
| 110 |
+
client = get_mimo_client()
|
| 111 |
+
if not client:
|
| 112 |
+
return None
|
| 113 |
+
|
| 114 |
+
prompt = f"""请为以下主题生成PPT内容,要求:
|
| 115 |
+
- 主题:{topic}
|
| 116 |
+
- 页数:{slides_count}页
|
| 117 |
+
- 风格:{style}
|
| 118 |
+
|
| 119 |
+
请以JSON格式返回,结构如下:
|
| 120 |
+
{{
|
| 121 |
+
"title": "PPT标题",
|
| 122 |
+
"slides": [
|
| 123 |
+
{{
|
| 124 |
+
"title": "页面标题",
|
| 125 |
+
"content": ["要点1", "要点2", "要点3"],
|
| 126 |
+
"notes": "演讲者备注"
|
| 127 |
+
}}
|
| 128 |
+
]
|
| 129 |
+
}}"""
|
| 130 |
+
|
| 131 |
+
try:
|
| 132 |
+
response = client.chat.completions.create(
|
| 133 |
+
model="mimo-v2.5-pro",
|
| 134 |
+
messages=[
|
| 135 |
+
{"role": "system", "content": "你是一个专业的PPT内容创作助手,请生成结构清晰、内容专业的PPT大纲。"},
|
| 136 |
+
{"role": "user", "content": prompt}
|
| 137 |
+
],
|
| 138 |
+
max_tokens=3000
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
import json
|
| 142 |
+
return json.loads(response.choices[0].message.content)
|
| 143 |
+
except Exception as e:
|
| 144 |
+
st.error(f"API调用失败: {str(e)}")
|
| 145 |
+
return None
|
utils/doc_utils.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from docx import Document
|
| 2 |
+
from docx.shared import Inches
|
| 3 |
+
import io
|
| 4 |
+
import qrcode
|
| 5 |
+
from typing import Dict
|
| 6 |
+
|
| 7 |
+
def generate_contract(template_type: str, data: Dict) -> bytes:
|
| 8 |
+
"""生成合同文档"""
|
| 9 |
+
doc = Document()
|
| 10 |
+
|
| 11 |
+
if template_type == "采购合同":
|
| 12 |
+
doc.add_heading('采购合同', 0)
|
| 13 |
+
doc.add_paragraph(f'合同编号:{data.get("contract_no", "")}')
|
| 14 |
+
doc.add_paragraph(f'甲方(采购方):{data.get("party_a", "")}')
|
| 15 |
+
doc.add_paragraph(f'乙方(供应方):{data.get("party_b", "")}')
|
| 16 |
+
doc.add_paragraph(f'签订日期:{data.get("date", "")}')
|
| 17 |
+
doc.add_paragraph('')
|
| 18 |
+
doc.add_paragraph(f'一、采购物品:{data.get("item", "")}')
|
| 19 |
+
doc.add_paragraph(f'二、数量:{data.get("quantity", "")}')
|
| 20 |
+
doc.add_paragraph(f'三、单价:{data.get("unit_price", "")}')
|
| 21 |
+
doc.add_paragraph(f'四、总金额:{data.get("total", "")}')
|
| 22 |
+
doc.add_paragraph(f'五、交货日期:{data.get("delivery_date", "")}')
|
| 23 |
+
doc.add_paragraph('')
|
| 24 |
+
doc.add_paragraph('甲方签字:________________ 乙方签字:________________')
|
| 25 |
+
|
| 26 |
+
elif template_type == "服务合同":
|
| 27 |
+
doc.add_heading('服务合同', 0)
|
| 28 |
+
doc.add_paragraph(f'合同编号:{data.get("contract_no", "")}')
|
| 29 |
+
doc.add_paragraph(f'甲方(委托方):{data.get("party_a", "")}')
|
| 30 |
+
doc.add_paragraph(f'乙方(服务方):{data.get("party_b", "")}')
|
| 31 |
+
doc.add_paragraph(f'签订日期:{data.get("date", "")}')
|
| 32 |
+
doc.add_paragraph('')
|
| 33 |
+
doc.add_paragraph(f'一、服务内容:{data.get("service_content", "")}')
|
| 34 |
+
doc.add_paragraph(f'二、服务期限:{data.get("service_period", "")}')
|
| 35 |
+
doc.add_paragraph(f'三、服务费用:{data.get("fee", "")}')
|
| 36 |
+
doc.add_paragraph(f'四、付款方式:{data.get("payment_method", "")}')
|
| 37 |
+
doc.add_paragraph('')
|
| 38 |
+
doc.add_paragraph('甲方签字:________________ 乙方签字:________________')
|
| 39 |
+
|
| 40 |
+
elif template_type == "劳动合同":
|
| 41 |
+
doc.add_heading('劳动合同', 0)
|
| 42 |
+
doc.add_paragraph(f'甲方(用人单位):{data.get("company", "")}')
|
| 43 |
+
doc.add_paragraph(f'乙方(劳动者):{data.get("employee", "")}')
|
| 44 |
+
doc.add_paragraph(f'身份证号:{data.get("id_card", "")}')
|
| 45 |
+
doc.add_paragraph(f'签订日期:{data.get("date", "")}')
|
| 46 |
+
doc.add_paragraph('')
|
| 47 |
+
doc.add_paragraph(f'一、工作岗位:{data.get("position", "")}')
|
| 48 |
+
doc.add_paragraph(f'二、工作地点:{data.get("location", "")}')
|
| 49 |
+
doc.add_paragraph(f'三、合同期限:{data.get("contract_period", "")}')
|
| 50 |
+
doc.add_paragraph(f'四、薪资待遇:{data.get("salary", "")}')
|
| 51 |
+
doc.add_paragraph('')
|
| 52 |
+
doc.add_paragraph('甲方盖章:________________ 乙方签字:________________')
|
| 53 |
+
|
| 54 |
+
buffer = io.BytesIO()
|
| 55 |
+
doc.save(buffer)
|
| 56 |
+
return buffer.getvalue()
|
| 57 |
+
|
| 58 |
+
def mail_merge(template_text: str, data_list: list) -> list:
|
| 59 |
+
"""批量邮件合并"""
|
| 60 |
+
results = []
|
| 61 |
+
for data in data_list:
|
| 62 |
+
result = template_text
|
| 63 |
+
for key, value in data.items():
|
| 64 |
+
result = result.replace(f'{{{{{key}}}}}', str(value))
|
| 65 |
+
results.append(result)
|
| 66 |
+
return results
|
| 67 |
+
|
| 68 |
+
def amount_to_chinese(amount: float) -> str:
|
| 69 |
+
"""金额转换为中文大写"""
|
| 70 |
+
digits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']
|
| 71 |
+
units = ['', '拾', '佰', '仟']
|
| 72 |
+
big_units = ['', '万', '亿']
|
| 73 |
+
|
| 74 |
+
if amount == 0:
|
| 75 |
+
return '零元整'
|
| 76 |
+
|
| 77 |
+
# 分离整数和小数部分
|
| 78 |
+
amount = round(amount, 2)
|
| 79 |
+
integer_part = int(amount)
|
| 80 |
+
decimal_part = round((amount - integer_part) * 100)
|
| 81 |
+
|
| 82 |
+
result = ''
|
| 83 |
+
|
| 84 |
+
# 处理整数部分
|
| 85 |
+
if integer_part > 0:
|
| 86 |
+
str_int = str(integer_part)
|
| 87 |
+
length = len(str_int)
|
| 88 |
+
for i, char in enumerate(str_int):
|
| 89 |
+
digit = int(char)
|
| 90 |
+
pos = length - i - 1
|
| 91 |
+
big_pos = pos // 4
|
| 92 |
+
unit_pos = pos % 4
|
| 93 |
+
|
| 94 |
+
if digit != 0:
|
| 95 |
+
result += digits[digit] + units[unit_pos]
|
| 96 |
+
else:
|
| 97 |
+
if unit_pos == 0 and big_pos > 0:
|
| 98 |
+
result += big_units[big_pos]
|
| 99 |
+
elif result and not result.endswith('零'):
|
| 100 |
+
result += '零'
|
| 101 |
+
|
| 102 |
+
if unit_pos == 0 and big_pos > 0:
|
| 103 |
+
result += big_units[big_pos]
|
| 104 |
+
|
| 105 |
+
result = result.rstrip('零') + '元'
|
| 106 |
+
|
| 107 |
+
# 处理小数部分
|
| 108 |
+
if decimal_part > 0:
|
| 109 |
+
jiao = decimal_part // 10
|
| 110 |
+
fen = decimal_part % 10
|
| 111 |
+
if jiao > 0:
|
| 112 |
+
result += digits[jiao] + '角'
|
| 113 |
+
if fen > 0:
|
| 114 |
+
result += digits[fen] + '分'
|
| 115 |
+
else:
|
| 116 |
+
result += '整'
|
| 117 |
+
|
| 118 |
+
return result
|
| 119 |
+
|
| 120 |
+
def generate_qrcode(data: str, size: int = 10) -> bytes:
|
| 121 |
+
"""生成二维码"""
|
| 122 |
+
qr = qrcode.QRCode(version=1, box_size=size, border=2)
|
| 123 |
+
qr.add_data(data)
|
| 124 |
+
qr.make(fit=True)
|
| 125 |
+
img = qr.make_image(fill_color="black", back_color="white")
|
| 126 |
+
buffer = io.BytesIO()
|
| 127 |
+
img.save(buffer, format='PNG')
|
| 128 |
+
return buffer.getvalue()
|
| 129 |
+
|
| 130 |
+
def generate_barcode(data: str, barcode_type: str = "code128") -> bytes:
|
| 131 |
+
"""生成条形码"""
|
| 132 |
+
try:
|
| 133 |
+
import barcode
|
| 134 |
+
from barcode.writer import ImageWriter
|
| 135 |
+
|
| 136 |
+
code_class = barcode.get_barcode_class(barcode_type)
|
| 137 |
+
code = code_class(data, writer=ImageWriter())
|
| 138 |
+
buffer = io.BytesIO()
|
| 139 |
+
code.write(buffer)
|
| 140 |
+
return buffer.getvalue()
|
| 141 |
+
except ImportError:
|
| 142 |
+
# 如果没有python-barcode库,返回None
|
| 143 |
+
return None
|
utils/excel_utils.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import io
|
| 3 |
+
from typing import List, Optional
|
| 4 |
+
|
| 5 |
+
def merge_excel_files(files: List, merge_type: str = "rows") -> pd.DataFrame:
|
| 6 |
+
"""合并多个Excel文件"""
|
| 7 |
+
all_dfs = []
|
| 8 |
+
for file in files:
|
| 9 |
+
if file.name.endswith('.csv'):
|
| 10 |
+
df = pd.read_csv(file)
|
| 11 |
+
else:
|
| 12 |
+
df = pd.read_excel(file)
|
| 13 |
+
all_dfs.append(df)
|
| 14 |
+
|
| 15 |
+
if merge_type == "rows":
|
| 16 |
+
return pd.concat(all_dfs, ignore_index=True)
|
| 17 |
+
else:
|
| 18 |
+
return pd.concat(all_dfs, axis=1)
|
| 19 |
+
|
| 20 |
+
def remove_duplicates(df: pd.DataFrame, columns: Optional[List] = None) -> pd.DataFrame:
|
| 21 |
+
"""去除重复行"""
|
| 22 |
+
return df.drop_duplicates(subset=columns, keep='first')
|
| 23 |
+
|
| 24 |
+
def clean_data(df: pd.DataFrame, remove_empty_rows: bool = True, remove_empty_cols: bool = True) -> pd.DataFrame:
|
| 25 |
+
"""清洗数据"""
|
| 26 |
+
if remove_empty_rows:
|
| 27 |
+
df = df.dropna(how='all')
|
| 28 |
+
if remove_empty_cols:
|
| 29 |
+
df = df.dropna(axis=1, how='all')
|
| 30 |
+
return df.reset_index(drop=True)
|
| 31 |
+
|
| 32 |
+
def convert_format(df: pd.DataFrame, target_format: str) -> bytes:
|
| 33 |
+
"""转换数据格式"""
|
| 34 |
+
buffer = io.BytesIO()
|
| 35 |
+
if target_format == 'csv':
|
| 36 |
+
df.to_csv(buffer, index=False, encoding='utf-8-sig')
|
| 37 |
+
elif target_format == 'excel':
|
| 38 |
+
df.to_excel(buffer, index=False, engine='openpyxl')
|
| 39 |
+
elif target_format == 'json':
|
| 40 |
+
df.to_json(buffer, orient='records', force_ascii=False, indent=2)
|
| 41 |
+
return buffer.getvalue()
|
| 42 |
+
|
| 43 |
+
def create_pivot_table(df: pd.DataFrame, index_col: str, columns_col: str, values_col: str, aggfunc: str = 'sum') -> pd.DataFrame:
|
| 44 |
+
"""创建透视表"""
|
| 45 |
+
return pd.pivot_table(df, index=index_col, columns=columns_col, values=values_col, aggfunc=aggfunc)
|
| 46 |
+
|
| 47 |
+
def vlookup_merge(left_df: pd.DataFrame, right_df: pd.DataFrame, key_col: str, value_col: str) -> pd.DataFrame:
|
| 48 |
+
"""类似VLOOKUP的合并"""
|
| 49 |
+
right_subset = right_df[[key_col, value_col]]
|
| 50 |
+
return left_df.merge(right_subset, on=key_col, how='left')
|
utils/image_utils.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
from PIL import Image
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def merge_images(images, direction="horizontal", gap=0, bg_color=(255, 255, 255)):
|
| 6 |
+
"""拼接多张图片"""
|
| 7 |
+
if not images:
|
| 8 |
+
raise ValueError("没有图片")
|
| 9 |
+
|
| 10 |
+
imgs = [Image.open(f) for f in images]
|
| 11 |
+
|
| 12 |
+
if direction == "horizontal":
|
| 13 |
+
total_w = sum(im.width for im in imgs) + gap * (len(imgs) - 1)
|
| 14 |
+
max_h = max(im.height for im in imgs)
|
| 15 |
+
result = Image.new("RGB", (total_w, max_h), bg_color)
|
| 16 |
+
x = 0
|
| 17 |
+
for im in imgs:
|
| 18 |
+
y = (max_h - im.height) // 2
|
| 19 |
+
result.paste(im, (x, y))
|
| 20 |
+
x += im.width + gap
|
| 21 |
+
|
| 22 |
+
elif direction == "vertical":
|
| 23 |
+
max_w = max(im.width for im in imgs)
|
| 24 |
+
total_h = sum(im.height for im in imgs) + gap * (len(imgs) - 1)
|
| 25 |
+
result = Image.new("RGB", (max_w, total_h), bg_color)
|
| 26 |
+
y = 0
|
| 27 |
+
for im in imgs:
|
| 28 |
+
x = (max_w - im.width) // 2
|
| 29 |
+
result.paste(im, (x, y))
|
| 30 |
+
y += im.height + gap
|
| 31 |
+
|
| 32 |
+
elif direction == "grid":
|
| 33 |
+
cols = _calc_cols(len(imgs))
|
| 34 |
+
rows = (len(imgs) + cols - 1) // cols
|
| 35 |
+
|
| 36 |
+
max_w = max(im.width for im in imgs)
|
| 37 |
+
max_h = max(im.height for im in imgs)
|
| 38 |
+
total_w = cols * max_w + gap * (cols - 1)
|
| 39 |
+
total_h = rows * max_h + gap * (rows - 1)
|
| 40 |
+
result = Image.new("RGB", (total_w, total_h), bg_color)
|
| 41 |
+
|
| 42 |
+
for idx, im in enumerate(imgs):
|
| 43 |
+
r, c = divmod(idx, cols)
|
| 44 |
+
x = c * (max_w + gap) + (max_w - im.width) // 2
|
| 45 |
+
y = r * (max_h + gap) + (max_h - im.height) // 2
|
| 46 |
+
result.paste(im, (x, y))
|
| 47 |
+
else:
|
| 48 |
+
raise ValueError(f"不支持的方向: {direction}")
|
| 49 |
+
|
| 50 |
+
buf = io.BytesIO()
|
| 51 |
+
result.save(buf, format="PNG")
|
| 52 |
+
return buf.getvalue()
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _calc_cols(n):
|
| 56 |
+
"""计算宫格列数"""
|
| 57 |
+
if n <= 1:
|
| 58 |
+
return 1
|
| 59 |
+
if n <= 4:
|
| 60 |
+
return 2
|
| 61 |
+
if n <= 9:
|
| 62 |
+
return 3
|
| 63 |
+
return 4
|
utils/pdf_utils.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import PyPDF2
|
| 2 |
+
import io
|
| 3 |
+
from typing import List
|
| 4 |
+
|
| 5 |
+
def merge_pdfs(files: List) -> bytes:
|
| 6 |
+
"""合并多个PDF文件"""
|
| 7 |
+
merger = PyPDF2.PdfMerger()
|
| 8 |
+
for file in files:
|
| 9 |
+
merger.append(file)
|
| 10 |
+
buffer = io.BytesIO()
|
| 11 |
+
merger.write(buffer)
|
| 12 |
+
merger.close()
|
| 13 |
+
return buffer.getvalue()
|
| 14 |
+
|
| 15 |
+
def split_pdf(file, pages: str = None) -> List[bytes]:
|
| 16 |
+
"""拆分PDF文件"""
|
| 17 |
+
reader = PyPDF2.PdfReader(file)
|
| 18 |
+
total_pages = len(reader.pages)
|
| 19 |
+
|
| 20 |
+
if pages:
|
| 21 |
+
page_list = parse_page_range(pages, total_pages)
|
| 22 |
+
else:
|
| 23 |
+
page_list = list(range(total_pages))
|
| 24 |
+
|
| 25 |
+
results = []
|
| 26 |
+
for page_num in page_list:
|
| 27 |
+
writer = PyPDF2.PdfWriter()
|
| 28 |
+
writer.add_page(reader.pages[page_num])
|
| 29 |
+
buffer = io.BytesIO()
|
| 30 |
+
writer.write(buffer)
|
| 31 |
+
results.append(buffer.getvalue())
|
| 32 |
+
|
| 33 |
+
return results
|
| 34 |
+
|
| 35 |
+
def parse_page_range(pages_str: str, total_pages: int) -> List[int]:
|
| 36 |
+
"""解析页码范围字符串"""
|
| 37 |
+
result = []
|
| 38 |
+
parts = pages_str.split(',')
|
| 39 |
+
for part in parts:
|
| 40 |
+
part = part.strip()
|
| 41 |
+
if '-' in part:
|
| 42 |
+
start, end = part.split('-')
|
| 43 |
+
start = int(start) - 1
|
| 44 |
+
end = int(end) if end else total_pages
|
| 45 |
+
result.extend(range(start, end))
|
| 46 |
+
else:
|
| 47 |
+
result.append(int(part) - 1)
|
| 48 |
+
return result
|
| 49 |
+
|
| 50 |
+
def add_watermark(pdf_file, watermark_text: str) -> bytes:
|
| 51 |
+
"""添加文字水印"""
|
| 52 |
+
from reportlab.lib.pagesizes import letter
|
| 53 |
+
from reportlab.pdfgen import canvas
|
| 54 |
+
|
| 55 |
+
reader = PyPDF2.PdfReader(pdf_file)
|
| 56 |
+
writer = PyPDF2.PdfWriter()
|
| 57 |
+
|
| 58 |
+
for page in reader.pages:
|
| 59 |
+
# 创建水印
|
| 60 |
+
watermark_buffer = io.BytesIO()
|
| 61 |
+
c = canvas.Canvas(watermark_buffer, pagesize=letter)
|
| 62 |
+
c.setFont("Helvetica", 50)
|
| 63 |
+
c.setFillAlpha(0.3)
|
| 64 |
+
c.saveState()
|
| 65 |
+
c.translate(300, 400)
|
| 66 |
+
c.rotate(45)
|
| 67 |
+
c.drawCentredString(0, 0, watermark_text)
|
| 68 |
+
c.restoreState()
|
| 69 |
+
c.save()
|
| 70 |
+
|
| 71 |
+
# 添加水印到页面
|
| 72 |
+
watermark_buffer.seek(0)
|
| 73 |
+
watermark_reader = PyPDF2.PdfReader(watermark_buffer)
|
| 74 |
+
watermark_page = watermark_reader.pages[0]
|
| 75 |
+
page.merge_page(watermark_page)
|
| 76 |
+
writer.add_page(page)
|
| 77 |
+
|
| 78 |
+
buffer = io.BytesIO()
|
| 79 |
+
writer.write(buffer)
|
| 80 |
+
return buffer.getvalue()
|
| 81 |
+
|
| 82 |
+
def add_password(pdf_file, user_password: str, owner_password: str = None) -> bytes:
|
| 83 |
+
"""添加密码保护"""
|
| 84 |
+
reader = PyPDF2.PdfReader(pdf_file)
|
| 85 |
+
writer = PyPDF2.PdfWriter()
|
| 86 |
+
|
| 87 |
+
for page in reader.pages:
|
| 88 |
+
writer.add_page(page)
|
| 89 |
+
|
| 90 |
+
if not owner_password:
|
| 91 |
+
owner_password = user_password
|
| 92 |
+
|
| 93 |
+
writer.encrypt(user_password, owner_password)
|
| 94 |
+
buffer = io.BytesIO()
|
| 95 |
+
writer.write(buffer)
|
| 96 |
+
return buffer.getvalue()
|
utils/ppt_utils.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pptx import Presentation
|
| 2 |
+
from pptx.util import Inches, Pt
|
| 3 |
+
from pptx.enum.text import PP_ALIGN
|
| 4 |
+
from pptx.dml.color import RGBColor
|
| 5 |
+
import io
|
| 6 |
+
|
| 7 |
+
def create_ppt(title: str, slides_data: list, template_style: str = "商务") -> bytes:
|
| 8 |
+
"""创建PPT"""
|
| 9 |
+
prs = Presentation()
|
| 10 |
+
|
| 11 |
+
# 设置模板颜色
|
| 12 |
+
colors = get_template_colors(template_style)
|
| 13 |
+
|
| 14 |
+
# 添加标题页
|
| 15 |
+
add_title_slide(prs, title, colors)
|
| 16 |
+
|
| 17 |
+
# 添加内容页
|
| 18 |
+
for slide_data in slides_data:
|
| 19 |
+
add_content_slide(prs, slide_data, colors)
|
| 20 |
+
|
| 21 |
+
# 保存到内存
|
| 22 |
+
buffer = io.BytesIO()
|
| 23 |
+
prs.save(buffer)
|
| 24 |
+
return buffer.getvalue()
|
| 25 |
+
|
| 26 |
+
def get_template_colors(style: str) -> dict:
|
| 27 |
+
"""获取模板颜色"""
|
| 28 |
+
templates = {
|
| 29 |
+
"商务": {
|
| 30 |
+
"primary": RGBColor(0, 82, 136), # 深蓝
|
| 31 |
+
"secondary": RGBColor(0, 120, 215), # 蓝色
|
| 32 |
+
"accent": RGBColor(255, 185, 0), # 金色
|
| 33 |
+
"text": RGBColor(51, 51, 51), # 深灰
|
| 34 |
+
"background": RGBColor(255, 255, 255) # 白色
|
| 35 |
+
},
|
| 36 |
+
"科技": {
|
| 37 |
+
"primary": RGBColor(88, 86, 214), # 紫色
|
| 38 |
+
"secondary": RGBColor(0, 200, 150), # 青色
|
| 39 |
+
"accent": RGBColor(255, 100, 100), # 红色
|
| 40 |
+
"text": RGBColor(30, 30, 30), # 近黑
|
| 41 |
+
"background": RGBColor(245, 245, 250) # 浅灰蓝
|
| 42 |
+
},
|
| 43 |
+
"教育": {
|
| 44 |
+
"primary": RGBColor(46, 139, 87), # 绿色
|
| 45 |
+
"secondary": RGBColor(70, 130, 180), # 钢蓝
|
| 46 |
+
"accent": RGBColor(255, 165, 0), # 橙色
|
| 47 |
+
"text": RGBColor(51, 51, 51), # 深灰
|
| 48 |
+
"background": RGBColor(255, 255, 255) # 白色
|
| 49 |
+
},
|
| 50 |
+
"简约": {
|
| 51 |
+
"primary": RGBColor(60, 60, 60), # 深灰
|
| 52 |
+
"secondary": RGBColor(120, 120, 120), # 中灰
|
| 53 |
+
"accent": RGBColor(200, 50, 50), # 红色
|
| 54 |
+
"text": RGBColor(51, 51, 51), # 深灰
|
| 55 |
+
"background": RGBColor(255, 255, 255) # 白色
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
return templates.get(style, templates["商务"])
|
| 59 |
+
|
| 60 |
+
def add_title_slide(prs, title: str, colors: dict):
|
| 61 |
+
"""添加标题页"""
|
| 62 |
+
slide_layout = prs.slide_layouts[0] # 标题布局
|
| 63 |
+
slide = prs.slides.add_slide(slide_layout)
|
| 64 |
+
|
| 65 |
+
# 设置标题
|
| 66 |
+
title_shape = slide.shapes.title
|
| 67 |
+
title_shape.text = title
|
| 68 |
+
title_shape.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
|
| 69 |
+
title_shape.text_frame.paragraphs[0].font.size = Pt(44)
|
| 70 |
+
title_shape.text_frame.paragraphs[0].font.color.rgb = colors["primary"]
|
| 71 |
+
|
| 72 |
+
# 设置副标题
|
| 73 |
+
if len(slide.placeholders) > 1:
|
| 74 |
+
subtitle = slide.placeholders[1]
|
| 75 |
+
subtitle.text = "自动生成的演示文稿"
|
| 76 |
+
subtitle.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
|
| 77 |
+
subtitle.text_frame.paragraphs[0].font.size = Pt(20)
|
| 78 |
+
subtitle.text_frame.paragraphs[0].font.color.rgb = colors["secondary"]
|
| 79 |
+
|
| 80 |
+
def add_content_slide(prs, slide_data: dict, colors: dict):
|
| 81 |
+
"""添加内容页"""
|
| 82 |
+
slide_layout = prs.slide_layouts[1] # 标题和内容布局
|
| 83 |
+
slide = prs.slides.add_slide(slide_layout)
|
| 84 |
+
|
| 85 |
+
# 设置标题
|
| 86 |
+
title_shape = slide.shapes.title
|
| 87 |
+
title_shape.text = slide_data.get("title", "")
|
| 88 |
+
title_shape.text_frame.paragraphs[0].font.size = Pt(32)
|
| 89 |
+
title_shape.text_frame.paragraphs[0].font.color.rgb = colors["primary"]
|
| 90 |
+
|
| 91 |
+
# 设置内容
|
| 92 |
+
if len(slide.placeholders) > 1:
|
| 93 |
+
content = slide.placeholders[1]
|
| 94 |
+
content.text = ""
|
| 95 |
+
|
| 96 |
+
for point in slide_data.get("content", []):
|
| 97 |
+
p = content.text_frame.add_paragraph()
|
| 98 |
+
p.text = point
|
| 99 |
+
p.font.size = Pt(18)
|
| 100 |
+
p.font.color.rgb = colors["text"]
|
| 101 |
+
p.space_after = Pt(12)
|
| 102 |
+
p.level = 0
|
| 103 |
+
|
| 104 |
+
# 添加备注
|
| 105 |
+
if slide_data.get("notes"):
|
| 106 |
+
notes_slide = slide.notes_slide
|
| 107 |
+
notes_slide.notes_text_frame.text = slide_data["notes"]
|
| 108 |
+
|
| 109 |
+
def create_ppt_from_outline(outline: str, template_style: str = "商务") -> bytes:
|
| 110 |
+
"""从大纲文本创建PPT"""
|
| 111 |
+
lines = outline.strip().split('\n')
|
| 112 |
+
|
| 113 |
+
title = lines[0] if lines else "演示文稿"
|
| 114 |
+
slides_data = []
|
| 115 |
+
|
| 116 |
+
current_slide = None
|
| 117 |
+
for line in lines[1:]:
|
| 118 |
+
line = line.strip()
|
| 119 |
+
if not line:
|
| 120 |
+
continue
|
| 121 |
+
|
| 122 |
+
# 判断是否是标题行(以数字开头或以#开头)
|
| 123 |
+
if line[0].isdigit() or line.startswith('#') or line.startswith('**'):
|
| 124 |
+
if current_slide:
|
| 125 |
+
slides_data.append(current_slide)
|
| 126 |
+
# 提取标题文本
|
| 127 |
+
slide_title = line.lstrip('0123456789.#* ').strip()
|
| 128 |
+
current_slide = {"title": slide_title, "content": []}
|
| 129 |
+
elif current_slide:
|
| 130 |
+
# 作为要点添加
|
| 131 |
+
point = line.lstrip('- •* ').strip()
|
| 132 |
+
if point:
|
| 133 |
+
current_slide["content"].append(point)
|
| 134 |
+
|
| 135 |
+
if current_slide:
|
| 136 |
+
slides_data.append(current_slide)
|
| 137 |
+
|
| 138 |
+
return create_ppt(title, slides_data, template_style)
|
utils/text_utils.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import difflib
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
from typing import List
|
| 5 |
+
|
| 6 |
+
def compare_texts(text1: str, text2: str) -> str:
|
| 7 |
+
"""对比两段文本,返回差异"""
|
| 8 |
+
lines1 = text1.splitlines(keepends=True)
|
| 9 |
+
lines2 = text2.splitlines(keepends=True)
|
| 10 |
+
|
| 11 |
+
diff = difflib.unified_diff(lines1, lines2, lineterm='')
|
| 12 |
+
return ''.join(diff)
|
| 13 |
+
|
| 14 |
+
def batch_replace(text: str, replacements: dict) -> str:
|
| 15 |
+
"""批量文本替换"""
|
| 16 |
+
result = text
|
| 17 |
+
for old, new in replacements.items():
|
| 18 |
+
result = result.replace(old, new)
|
| 19 |
+
return result
|
| 20 |
+
|
| 21 |
+
def format_json(json_str: str, indent: int = 2) -> str:
|
| 22 |
+
"""格式化JSON"""
|
| 23 |
+
try:
|
| 24 |
+
data = json.loads(json_str)
|
| 25 |
+
return json.dumps(data, indent=indent, ensure_ascii=False)
|
| 26 |
+
except json.JSONDecodeError as e:
|
| 27 |
+
return f"JSON解析错误: {str(e)}"
|
| 28 |
+
|
| 29 |
+
def minify_json(json_str: str) -> str:
|
| 30 |
+
"""压缩JSON"""
|
| 31 |
+
try:
|
| 32 |
+
data = json.loads(json_str)
|
| 33 |
+
return json.dumps(data, separators=(',', ':'), ensure_ascii=False)
|
| 34 |
+
except json.JSONDecodeError as e:
|
| 35 |
+
return f"JSON解析错误: {str(e)}"
|
| 36 |
+
|
| 37 |
+
def count_chars(text: str) -> dict:
|
| 38 |
+
"""统计字数"""
|
| 39 |
+
total_chars = len(text)
|
| 40 |
+
chinese_chars = len(re.findall(r'[一-鿿]', text))
|
| 41 |
+
english_chars = len(re.findall(r'[a-zA-Z]', text))
|
| 42 |
+
digits = len(re.findall(r'\d', text))
|
| 43 |
+
spaces = len(re.findall(r'\s', text))
|
| 44 |
+
punctuation = total_chars - chinese_chars - english_chars - digits - spaces
|
| 45 |
+
|
| 46 |
+
return {
|
| 47 |
+
'总字符数': total_chars,
|
| 48 |
+
'中文字数': chinese_chars,
|
| 49 |
+
'英文字母': english_chars,
|
| 50 |
+
'数字': digits,
|
| 51 |
+
'空格': spaces,
|
| 52 |
+
'标点符号': punctuation,
|
| 53 |
+
'总行数': len(text.splitlines())
|
| 54 |
+
}
|
utils/tts_mimo.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import os
|
| 3 |
+
import streamlit as st
|
| 4 |
+
from openai import OpenAI
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def get_mimo_tts_client():
|
| 8 |
+
"""获取 mimo TTS 客户端(复用同一套密钥)"""
|
| 9 |
+
api_key = os.getenv("MIMO_API_KEY")
|
| 10 |
+
base_url = os.getenv("MIMO_BASE_URL")
|
| 11 |
+
|
| 12 |
+
if not api_key:
|
| 13 |
+
try:
|
| 14 |
+
api_key = st.secrets["MIMO_API_KEY"]
|
| 15 |
+
except Exception:
|
| 16 |
+
pass
|
| 17 |
+
|
| 18 |
+
if not base_url:
|
| 19 |
+
try:
|
| 20 |
+
base_url = st.secrets["MIMO_BASE_URL"]
|
| 21 |
+
except Exception:
|
| 22 |
+
base_url = "https://api.xiaomimimo.com/v1"
|
| 23 |
+
|
| 24 |
+
if not api_key:
|
| 25 |
+
return None
|
| 26 |
+
|
| 27 |
+
return OpenAI(api_key=api_key, base_url=base_url)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# mimo-v2.5-tts 预置音色列表
|
| 31 |
+
PRESET_VOICES = {
|
| 32 |
+
"冰糖": {"id": "冰糖", "lang": "中文", "gender": "女性"},
|
| 33 |
+
"茉莉": {"id": "茉莉", "lang": "中文", "gender": "女性"},
|
| 34 |
+
"苏打": {"id": "苏打", "lang": "中文", "gender": "男性"},
|
| 35 |
+
"白桦": {"id": "白桦", "lang": "中文", "gender": "男性"},
|
| 36 |
+
"Mia": {"id": "Mia", "lang": "英文", "gender": "女性"},
|
| 37 |
+
"Chloe": {"id": "Chloe", "lang": "英文", "gender": "女性"},
|
| 38 |
+
"Milo": {"id": "Milo", "lang": "英文", "gender": "男性"},
|
| 39 |
+
"Dean": {"id": "Dean", "lang": "英文", "gender": "男性"},
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def text_to_speech(
|
| 44 |
+
text: str,
|
| 45 |
+
voice: str = "冰糖",
|
| 46 |
+
style: str = "",
|
| 47 |
+
) -> bytes:
|
| 48 |
+
"""调用 mimo-v2.5-tts 模型生成语音
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
text: 要合成的文本(放在 assistant 消息中)
|
| 52 |
+
voice: 预置音色名称
|
| 53 |
+
style: 风格指令(放在 user 消息中,可选)
|
| 54 |
+
|
| 55 |
+
Returns:
|
| 56 |
+
WAV 格式的音频字节数据
|
| 57 |
+
"""
|
| 58 |
+
client = get_mimo_tts_client()
|
| 59 |
+
if not client:
|
| 60 |
+
raise RuntimeError("未配置 MIMO API Key")
|
| 61 |
+
|
| 62 |
+
messages = []
|
| 63 |
+
|
| 64 |
+
# user 消息:风格指令(可选)
|
| 65 |
+
if style:
|
| 66 |
+
messages.append({"role": "user", "content": style})
|
| 67 |
+
else:
|
| 68 |
+
messages.append({"role": "user", "content": ""})
|
| 69 |
+
|
| 70 |
+
# assistant 消息:要合成的目标文本
|
| 71 |
+
messages.append({"role": "assistant", "content": text})
|
| 72 |
+
|
| 73 |
+
completion = client.chat.completions.create(
|
| 74 |
+
model="mimo-v2.5-tts",
|
| 75 |
+
messages=messages,
|
| 76 |
+
audio={"format": "wav", "voice": voice},
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
message = completion.choices[0].message
|
| 80 |
+
audio_bytes = base64.b64decode(message.audio.data)
|
| 81 |
+
return audio_bytes
|
utils/tts_utils.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def generate_tts_script(text, speed="normal", add_pauses=True, add_emphasis=True):
|
| 5 |
+
"""生成带停顿和标记的朗读脚本"""
|
| 6 |
+
lines = text.strip().split("\n")
|
| 7 |
+
result_lines = []
|
| 8 |
+
|
| 9 |
+
for line in lines:
|
| 10 |
+
line = line.strip()
|
| 11 |
+
if not line:
|
| 12 |
+
if add_pauses:
|
| 13 |
+
result_lines.append("[停顿 0.5秒]")
|
| 14 |
+
continue
|
| 15 |
+
|
| 16 |
+
# 检测标题行(数字开头或全大写或短行)
|
| 17 |
+
is_title = (
|
| 18 |
+
re.match(r'^[\d一二三四五六七八九十]+[、..)\s]', line)
|
| 19 |
+
or (len(line) <= 15 and not line.endswith(("。", ",", ";", "!", "?")))
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
if is_title and add_emphasis:
|
| 23 |
+
result_lines.append(f"[强调] {line} [/强调]")
|
| 24 |
+
if add_pauses:
|
| 25 |
+
result_lines.append("[停顿 1秒]")
|
| 26 |
+
continue
|
| 27 |
+
|
| 28 |
+
# 处理标点停顿
|
| 29 |
+
processed = line
|
| 30 |
+
if add_pauses:
|
| 31 |
+
processed = processed.replace("。", "。[停顿 0.3秒]")
|
| 32 |
+
processed = processed.replace("!", "![停顿 0.3秒]")
|
| 33 |
+
processed = processed.replace("?", "?[停顿 0.3秒]")
|
| 34 |
+
processed = processed.replace(";", ";[停顿 0.2秒]")
|
| 35 |
+
|
| 36 |
+
# 检测数字和英文,标记需放慢
|
| 37 |
+
if add_emphasis:
|
| 38 |
+
processed = re.sub(
|
| 39 |
+
r'(\d+(?:\.\d+)?(?:%|万|亿|元|个|条|项|次|天|年|月|日)?)',
|
| 40 |
+
r'[\1]',
|
| 41 |
+
processed
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
result_lines.append(processed)
|
| 45 |
+
|
| 46 |
+
script = "\n".join(result_lines)
|
| 47 |
+
|
| 48 |
+
# 添加头部说明
|
| 49 |
+
speed_map = {"slow": "慢速", "normal": "正常", "fast": "快速"}
|
| 50 |
+
header = f"[语速: {speed_map.get(speed, speed)}]\n\n"
|
| 51 |
+
|
| 52 |
+
return header + script
|