banana-slides / scripts /export_editable_pptx.py
asem
feat: banana-slides 最新版部署到 HF Spaces (i18n/dark-mode/v0.4.0)
c696d3f
Raw
History Blame Contribute Delete
12.7 kB
#!/usr/bin/env python3
"""
可编辑 PPTX 导出脚本
此脚本用于从指定的图片生成可编辑的 PPTX 文件。
支持单张图片或多张图片批量处理。
使用方法:
# 处理单张图片
python scripts/export_editable_pptx.py path/to/image.png
# 处理多张图片
python scripts/export_editable_pptx.py img1.png img2.png img3.png
# 处理目录中的所有图片
python scripts/export_editable_pptx.py path/to/images/
# 指定输出文件
python scripts/export_editable_pptx.py image.png -o output.pptx
# 使用不同的提取方法
python scripts/export_editable_pptx.py image.png --extractor mineru
python scripts/export_editable_pptx.py image.png --extractor hybrid
# 使用不同的背景修复方法
python scripts/export_editable_pptx.py image.png --inpaint baidu
python scripts/export_editable_pptx.py image.png --inpaint generative
python scripts/export_editable_pptx.py image.png --inpaint hybrid
环境要求:
需要配置 .env 文件,包含以下变量:
- MINERU_TOKEN: MinerU API token
- BAIDU_API_KEY, BAIDU_SECRET_KEY: 百度 API 密钥(用于 baidu/hybrid 方法)
- GEMINI_API_KEY 或 OPENAI_API_KEY: 用于 generative/hybrid 方法
成本提示:
- 'generative' 和 'hybrid' 背景修复方法会调用文生图模型 API,产生额外费用
- 'baidu' 方法使用百度图像修复 API,费用较低
- 'mineru' 和 'hybrid' 提取方法都使用 MinerU API
"""
import os
import sys
import argparse
import logging
from pathlib import Path
from typing import List, Optional
# 添加项目根目录到 Python 路径
SCRIPT_DIR = Path(__file__).resolve().parent
PROJECT_ROOT = SCRIPT_DIR.parent
BACKEND_DIR = PROJECT_ROOT / 'backend'
sys.path.insert(0, str(BACKEND_DIR))
# 设置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def setup_flask_app():
"""初始化 Flask 应用上下文(用于加载配置)"""
from dotenv import load_dotenv
# 加载 .env 文件
env_path = PROJECT_ROOT / '.env'
if env_path.exists():
load_dotenv(env_path)
logger.info(f"已加载环境变量: {env_path}")
# 创建 Flask 应用
from app import create_app
app = create_app()
return app
def collect_image_paths(paths: List[str]) -> List[str]:
"""收集所有要处理的图片路径"""
image_extensions = {'.png', '.jpg', '.jpeg', '.webp', '.bmp'}
result = []
for path_str in paths:
path = Path(path_str)
if path.is_file():
if path.suffix.lower() in image_extensions:
result.append(str(path.resolve()))
else:
logger.warning(f"跳过非图片文件: {path}")
elif path.is_dir():
for file in sorted(path.iterdir()):
if file.suffix.lower() in image_extensions:
result.append(str(file.resolve()))
else:
logger.warning(f"路径不存在: {path}")
return result
def create_service_config(
extractor_method: str = 'hybrid',
inpaint_method: str = 'hybrid'
):
"""
创建服务配置
Args:
extractor_method: 提取方法 ('mineru' 或 'hybrid')
inpaint_method: 背景修复方法 ('generative', 'baidu', 'hybrid')
"""
from services.image_editability import ServiceConfig
# 根据方法选择配置
use_hybrid_extractor = (extractor_method == 'hybrid')
use_hybrid_inpaint = (inpaint_method == 'hybrid')
logger.info(f"配置: 提取方法={extractor_method}, 背景修复={inpaint_method}")
config = ServiceConfig.from_defaults(
use_hybrid_extractor=use_hybrid_extractor,
use_hybrid_inpaint=use_hybrid_inpaint,
max_depth=1 # 递归深度
)
# 如果指定了非 hybrid 的 inpaint 方法,需要手动配置
if inpaint_method != 'hybrid':
from services.image_editability import (
InpaintProviderFactory,
InpaintProviderRegistry
)
inpaint_registry = InpaintProviderRegistry()
if inpaint_method == 'generative':
provider = InpaintProviderFactory.create_generative_edit_provider()
inpaint_registry.register_default(provider)
logger.info("使用生成式修复方法(会调用文生图模型 API)")
elif inpaint_method == 'baidu':
provider = InpaintProviderFactory.create_baidu_inpaint_provider()
if provider:
inpaint_registry.register_default(provider)
logger.info("使用百度图像修复方法")
else:
logger.warning("百度修复不可用,回退到生成式方法")
provider = InpaintProviderFactory.create_generative_edit_provider()
inpaint_registry.register_default(provider)
config.inpaint_registry = inpaint_registry
return config
def export_editable_pptx(
image_paths: List[str],
output_file: str,
extractor_method: str = 'hybrid',
inpaint_method: str = 'hybrid',
extract_text_styles: bool = True
):
"""
导出可编辑 PPTX
Args:
image_paths: 图片路径列表
output_file: 输出文件路径
extractor_method: 提取方法
inpaint_method: 背景修复方法
extract_text_styles: 是否提取文字样式(颜色、粗体等)
"""
from services.image_editability import ImageEditabilityService
from services.export_service import ExportService
from concurrent.futures import ThreadPoolExecutor, as_completed
logger.info(f"开始处理 {len(image_paths)} 张图片...")
# 创建配置和服务
config = create_service_config(extractor_method, inpaint_method)
service = ImageEditabilityService(config)
# 并行分析所有图片
logger.info("步骤 1/3: 分析图片结构...")
editable_images = []
with ThreadPoolExecutor(max_workers=4) as executor:
futures = {
executor.submit(service.make_image_editable, path): idx
for idx, path in enumerate(image_paths)
}
results = [None] * len(image_paths)
for future in as_completed(futures):
idx = futures[future]
try:
results[idx] = future.result()
logger.info(f" 完成: {image_paths[idx]}")
except Exception as e:
logger.error(f" 失败: {image_paths[idx]} - {e}")
raise
editable_images = results
# 创建文字属性提取器(可选)
text_attribute_extractor = None
if extract_text_styles:
logger.info("步骤 2/3: 提取文字样式...")
try:
from services.image_editability import TextAttributeExtractorFactory
text_attribute_extractor = TextAttributeExtractorFactory.create_caption_model_extractor()
logger.info(" 文字样式提取器已创建(会调用视觉语言模型 API)")
except Exception as e:
logger.warning(f" 无法创建文字样式提取器: {e}")
else:
logger.info("步骤 2/3: 跳过文字样式提取")
# 生成 PPTX
logger.info("步骤 3/3: 生成可编辑 PPTX...")
def progress_callback(step, message, percent):
logger.info(f" [{percent}%] {step}: {message}")
# 如果output_file已经存在,给一个后缀防止冲突
if os.path.exists(output_file):
output_file = output_file.rsplit('.', 1)[0] + '_1.pptx'
logger.warning(f"输出文件已存在,给一个后缀防止冲突: {output_file}")
# 根据实际图片尺寸动态设置幻灯片尺寸
# 统一到最小尺寸,并检查所有图片是否为16:9比例
if editable_images:
# 16:9 比例的标准值
ASPECT_RATIO_16_9 = 16 / 9 # ≈ 1.7778
ASPECT_RATIO_TOLERANCE = 0.02 # 允许2%的误差
# 检查所有图片是否为16:9比例,并找到最小尺寸
min_width = float('inf')
min_height = float('inf')
for idx, img in enumerate(editable_images):
aspect_ratio = img.width / img.height
ratio_diff = abs(aspect_ratio - ASPECT_RATIO_16_9) / ASPECT_RATIO_16_9
if ratio_diff > ASPECT_RATIO_TOLERANCE:
logger.error(f"图片 {idx + 1} ({image_paths[idx]}) 不是16:9比例: "
f"{img.width}x{img.height} (比例 {aspect_ratio:.4f}, 期望 {ASPECT_RATIO_16_9:.4f})")
raise ValueError(f"所有图片必须是16:9比例,但第 {idx + 1} 张图片 ({img.width}x{img.height}) 不符合要求")
min_width = min(min_width, img.width)
min_height = min(min_height, img.height)
logger.info(f"图片 {idx + 1}: {img.width}x{img.height} (比例 {aspect_ratio:.4f})")
slide_width_pixels = int(min_width)
slide_height_pixels = int(min_height)
logger.info(f"统一使用最小尺寸作为幻灯片尺寸: {slide_width_pixels}x{slide_height_pixels}")
# 如果图片尺寸不一致,给出警告
if any(img.width != slide_width_pixels or img.height != slide_height_pixels for img in editable_images):
logger.warning(f"图片尺寸不一致,已统一到最小尺寸 {slide_width_pixels}x{slide_height_pixels}")
else:
# 如果没有图片,使用默认尺寸
slide_width_pixels = 1920
slide_height_pixels = 1080
logger.warning("没有图片,使用默认尺寸: 1920x1080")
ExportService.create_editable_pptx_with_recursive_analysis(
editable_images=editable_images,
output_file=output_file,
slide_width_pixels=slide_width_pixels,
slide_height_pixels=slide_height_pixels,
text_attribute_extractor=text_attribute_extractor,
progress_callback=progress_callback
)
logger.info(f"✓ 导出完成: {output_file}")
def main():
parser = argparse.ArgumentParser(
description='从图片生成可编辑的 PPTX 文件',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
%(prog)s slide1.png slide2.png -o presentation.pptx
%(prog)s ./slides/ --extractor hybrid --inpaint baidu
%(prog)s image.png --no-text-styles
成本提示:
- 'generative' 和 'hybrid' 背景修复方法会调用文生图模型 API
- '--no-text-styles' 可跳过文字样式提取,减少 API 调用
"""
)
parser.add_argument(
'images',
nargs='+',
help='图片文件或目录路径'
)
parser.add_argument(
'-o', '--output',
default='output_editable.pptx',
help='输出 PPTX 文件路径(默认: output_editable.pptx)'
)
parser.add_argument(
'--extractor',
choices=['mineru', 'hybrid'],
default='hybrid',
help='组件提取方法(默认: hybrid)'
)
parser.add_argument(
'--inpaint',
choices=['generative', 'baidu', 'hybrid'],
default='hybrid',
help='背景修复方法(默认: hybrid)。generative/hybrid 会调用文生图模型'
)
parser.add_argument(
'--no-text-styles',
action='store_true',
help='跳过文字样式提取(减少 API 调用)'
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help='显示详细日志'
)
args = parser.parse_args()
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
# 收集图片路径
image_paths = collect_image_paths(args.images)
if not image_paths:
logger.error("未找到任何图片文件")
sys.exit(1)
logger.info(f"找到 {len(image_paths)} 张图片:")
for path in image_paths:
logger.info(f" - {path}")
# 初始化 Flask 应用
app = setup_flask_app()
with app.app_context():
try:
export_editable_pptx(
image_paths=image_paths,
output_file=args.output,
extractor_method=args.extractor,
inpaint_method=args.inpaint,
extract_text_styles=not args.no_text_styles
)
except Exception as e:
logger.error(f"导出失败: {e}", exc_info=True)
sys.exit(1)
if __name__ == '__main__':
main()