PerrinT94 commited on
Commit
3373b3e
·
0 Parent(s):

初始化:办公工具平台

Browse files
.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