from docx import Document
|
from docx.shared import Inches
|
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
from docx.oxml.ns import qn
|
import random
|
import threading
|
import re
|
import time
|
import json
|
from tenacity import retry, stop_after_attempt, wait_fixed
|
from io import BytesIO
|
from PIL import Image
|
import google.generativeai as genai
|
from auto_media_publisher.config.conf_base import GOOGLE_API_KEY,GOOGLE_MODEL,GOOGLE_CHAT_MIN_INTERVAL
|
from auto_media_publisher.config.conf_base import GOOGLE_GEMINI_TEMPERATURE_MIN,GOOGLE_GEMINI_TEMPERATURE_MAX,GOOGLE_REWRITE_STYLE_MAP,GEMINI_PROMPT_INIT_DEFAULT
|
from auto_media_publisher.config.conf_base import GEMINI_PROMPT_TITLE,GEMINI_PROMPT_DESCRIPTION,GEMINI_PROMPT_TAG
|
from auto_media_publisher.utils.logger import get_logger
|
from auto_media_publisher.utils.utils_text2image_pollinations import Text2ImageGenerator
|
|
class GeminiDocxRewriter:
|
def __init__(self,rewrite_style="default" ):
|
genai.configure(api_key=GOOGLE_API_KEY)
|
self.temperature = round(random.uniform(GOOGLE_GEMINI_TEMPERATURE_MIN, GOOGLE_GEMINI_TEMPERATURE_MAX), 2)
|
self.model = genai.GenerativeModel(GOOGLE_MODEL,
|
generation_config=genai.types.GenerationConfig(
|
temperature=self.temperature,
|
top_p=1.0
|
)
|
)
|
if rewrite_style =="random":
|
self.rewrite_style = random.choice(list(GOOGLE_REWRITE_STYLE_MAP.keys()))
|
else:
|
self.rewrite_style = rewrite_style
|
|
self.last_call_time = 0
|
self.min_interval = GOOGLE_CHAT_MIN_INTERVAL # 限制最短请求间隔(秒)
|
self.lock = threading.Lock() # 多线程并发时也安全
|
self.logger = get_logger(self.__class__.__name__,"admin")
|
self.text2imager = Text2ImageGenerator()
|
|
#@retry(stop=stop_after_attempt(3), wait=wait_fixed(2)) # 自动重试机制
|
def safe_send_message(self, content: str):
|
with self.lock:
|
now = time.time()
|
wait_time = self.min_interval - (now - self.last_call_time)
|
if wait_time > 0:
|
time.sleep(wait_time)
|
self.last_call_time = time.time()
|
|
return self.chat.send_message(content)
|
|
|
def rewrite(self, input_path: str, output_path: str):
|
blocks = self.extract_docx_blocks_by_order(input_path)
|
self.logger.info(f"start to rewriting docx using gemini,using temperature:{self.temperature}. with style {self.rewrite_style}. {input_path}")
|
if len(blocks) >0:
|
self.chat = self.model.start_chat(history=[])
|
self.safe_send_message(GOOGLE_REWRITE_STYLE_MAP.get(self.rewrite_style, GEMINI_PROMPT_INIT_DEFAULT))
|
rewritten = self._rewrite_blocks(blocks)
|
self._save_to_new_docx(rewritten, output_path)
|
|
summary_info = self.summarize_title_intro_keywords()
|
print(summary_info)
|
self.text2imager.generate_image(summary_info["summary"],output_path.replace(".docx",".jpg"))
|
summary_info["cover_image"] = output_path.replace(".docx",".jpg")
|
|
# 3. 写入磁盘
|
with open(output_path.replace(".docx",".json"), "w", encoding="utf-8") as f:
|
json.dump(summary_info, f, ensure_ascii=False, indent=4)
|
|
return summary_info
|
|
def extract_docx_blocks_by_order(self,docx_path):
|
doc = Document(docx_path)
|
blocks = []
|
current_texts = []
|
|
def _get_blip_rids(run_element):
|
blips = []
|
for blip in run_element.findall('.//{http://schemas.openxmlformats.org/drawingml/2006/main}blip'):
|
rId = blip.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed')
|
if rId:
|
blips.append(rId)
|
return blips
|
|
# 遍历文档的 body 中的元素
|
for child in doc.element.body:
|
# 处理段落
|
if child.tag == qn('w:p'):
|
para = next((p for p in doc.paragraphs if p._element == child), None)
|
if para is not None:
|
# 判断此段是否包含图片
|
has_image = False
|
images = []
|
|
for run in para.runs:
|
rIds = _get_blip_rids(run.element)
|
for rId in rIds:
|
try:
|
image_part = run.part.related_parts[rId]
|
images.append(image_part.blob)
|
has_image = True
|
except KeyError:
|
continue
|
|
if has_image:
|
if current_texts:
|
blocks.append({'type': 'text', 'content': '\n'.join(current_texts)})
|
current_texts = []
|
for img in images:
|
blocks.append({'type': 'image', 'content': img})
|
else:
|
text = para.text.strip()
|
if text:
|
current_texts.append(text)
|
|
# 结尾残留文本也写入 block
|
if current_texts:
|
blocks.append({'type': 'text', 'content': '\n'.join(current_texts)})
|
|
return blocks
|
|
def _extract_image_bytes(self, para):
|
# 获取图片字节数据
|
for run in para.runs:
|
drawing = run._element.xpath('.//a:blip')
|
if drawing:
|
rId = drawing[0].get("r:embed")
|
part = run.part.related_parts[rId]
|
return part.blob
|
return None
|
|
def _rewrite_blocks(self, blocks):
|
rewritten_blocks = []
|
print("start to send oringal message to gemini ......")
|
for i, block in enumerate(blocks):
|
#print(block)
|
if block["type"]=="text":
|
prompt = f"{chr(12288).join(block['content'])}"
|
response = self.safe_send_message(prompt)
|
rewritten_text = response.text.strip()
|
rewritten_blocks.append({"type": "text", "content": rewritten_text})
|
else:
|
rewritten_blocks.append(block)
|
return rewritten_blocks
|
|
def _save_to_new_docx(self, blocks, output_path):
|
doc = Document()
|
for block in blocks:
|
if block["type"]=="text":
|
for line in block["content"].split("\n"):
|
doc.add_paragraph(line.strip())
|
elif block["type"] == "image":
|
image_data = block["content"]
|
image_stream = BytesIO(image_data)
|
paragraph = doc.add_paragraph()
|
run = paragraph.add_run()
|
run.add_picture(image_stream, width=Inches(5.5)) # 适当调整宽度
|
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
doc.save(output_path)
|
|
def summarize_title_intro_keywords(self) -> dict:
|
title = self.safe_send_message(GEMINI_PROMPT_TITLE).text.strip()
|
summary = self.safe_send_message(GEMINI_PROMPT_DESCRIPTION).text.strip()
|
keywords = self.safe_send_message(GEMINI_PROMPT_TAG).text.strip()
|
|
return {
|
"title": title,
|
"summary": summary,
|
"keywords": re.split(r'[、,\s]+', keywords.strip("。"))
|
}
|