feat: homelab deployment - NPM + IONOS DNS + monitoring + NAS backup
- Restructured docker-compose for Nginx Proxy Manager (no custom nginx) - Added domain wordly.art configuration - Added Prometheus + Grafana monitoring stack with pre-configured dashboards - Added PostgreSQL backup script to NAS (daily/weekly/monthly rotation) - Added alert rules for backend, system, and Docker metrics - Updated deployment guide for NPM + IONOS DNS homelab setup - Added marketing plan document - PDF translator and watermark support - Enhanced middleware, routes, and translator modules Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,8 @@ Updated to use new TranslationProvider interface with structured error handling.
|
||||
"""
|
||||
|
||||
import time
|
||||
import zipfile
|
||||
import io
|
||||
import concurrent.futures
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple, Optional, Callable, Any
|
||||
@@ -313,6 +315,9 @@ class PowerPointTranslator:
|
||||
details={"file_name": output_path.name, "error": str(e)},
|
||||
)
|
||||
|
||||
# Re-inject chart translations into chart XML parts
|
||||
self._apply_chart_translations(output_path)
|
||||
|
||||
processing_time_ms = round((time.time() - start_time) * 1000, 2)
|
||||
|
||||
_log_info(
|
||||
@@ -439,6 +444,10 @@ class PowerPointTranslator:
|
||||
for sub_shape in shape.shapes:
|
||||
self._collect_from_shape(sub_shape, text_elements)
|
||||
|
||||
# Chart shapes — text is stored in separate chart XML parts
|
||||
if shape.shape_type == MSO_SHAPE_TYPE.CHART:
|
||||
self._collect_from_chart_shape(shape, text_elements)
|
||||
|
||||
if hasattr(shape, "shapes"):
|
||||
try:
|
||||
for sub_shape in shape.shapes:
|
||||
@@ -446,6 +455,82 @@ class PowerPointTranslator:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _collect_from_chart_shape(
|
||||
self, shape: BaseShape, text_elements: List[Tuple[str, Callable[[str], None]]]
|
||||
) -> None:
|
||||
"""Collect translatable text from a chart shape.
|
||||
|
||||
Chart text (title, axis titles, series names, data labels) is stored
|
||||
in a separate chart XML part, not in shape.text_frame.
|
||||
"""
|
||||
_NS_A = "http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
_NS_C = "http://schemas.openxmlformats.org/drawingml/2006/chart"
|
||||
|
||||
try:
|
||||
chart_data = shape.chart
|
||||
# Access the chart XML part through the chart's part
|
||||
chart_part = chart_data.part
|
||||
chart_xml = etree.fromstring(chart_part.blob)
|
||||
|
||||
# Collect text from <a:t> elements in chart XML
|
||||
# These include: chart title, axis titles, legend entries, data labels
|
||||
seen_texts: set = set()
|
||||
chart_text_entries: List[Dict[str, Any]] = []
|
||||
|
||||
for t_elem in chart_xml.iter(f'{{{_NS_A}}}t'):
|
||||
text = t_elem.text
|
||||
if text and text.strip() and text.strip() not in seen_texts:
|
||||
seen_texts.add(text.strip())
|
||||
entry = {
|
||||
'element': t_elem,
|
||||
'original': text.strip(),
|
||||
'translated': None,
|
||||
}
|
||||
chart_text_entries.append(entry)
|
||||
|
||||
def make_chart_setter(entries, idx):
|
||||
def setter(translated_text):
|
||||
entries[idx]['translated'] = translated_text.strip()
|
||||
return setter
|
||||
|
||||
text_elements.append(
|
||||
(text.strip(), make_chart_setter(chart_text_entries, len(chart_text_entries) - 1))
|
||||
)
|
||||
|
||||
# Also collect from <c:v> (cell values used as category names)
|
||||
for v_elem in chart_xml.iter(f'{{{_NS_C}}}v'):
|
||||
text = v_elem.text
|
||||
if text and text.strip() and not text.strip().replace('.', '').replace('-', '').replace(',', '').isdigit():
|
||||
if text.strip() not in seen_texts:
|
||||
seen_texts.add(text.strip())
|
||||
entry = {
|
||||
'element': v_elem,
|
||||
'original': text.strip(),
|
||||
'translated': None,
|
||||
}
|
||||
chart_text_entries.append(entry)
|
||||
|
||||
def make_chart_v_setter(entries, idx):
|
||||
def setter(translated_text):
|
||||
entries[idx]['translated'] = translated_text.strip()
|
||||
return setter
|
||||
|
||||
text_elements.append(
|
||||
(text.strip(), make_chart_v_setter(chart_text_entries, len(chart_text_entries) - 1))
|
||||
)
|
||||
|
||||
# Store chart_part reference and entries for later re-injection
|
||||
if chart_text_entries:
|
||||
if not hasattr(self, '_chart_entries'):
|
||||
self._chart_entries = []
|
||||
self._chart_entries.append({
|
||||
'chart_part': chart_part,
|
||||
'entries': chart_text_entries,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
_log_error("pptx_chart_collect_error", error=str(e))
|
||||
|
||||
def _collect_from_text_frame(
|
||||
self, text_frame, text_elements: List[Tuple[str, Callable[[str], None]]]
|
||||
) -> None:
|
||||
@@ -472,5 +557,53 @@ class PowerPointTranslator:
|
||||
|
||||
text_elements.append((stripped, make_setter(run, leading, trailing)))
|
||||
|
||||
def _apply_chart_translations(self, output_path: Path) -> None:
|
||||
"""Re-inject chart text translations by modifying chart XML parts in the .pptx ZIP."""
|
||||
if not hasattr(self, '_chart_entries') or not self._chart_entries:
|
||||
return
|
||||
|
||||
_NS_A = "http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
_NS_C = "http://schemas.openxmlformats.org/drawingml/2006/chart"
|
||||
|
||||
total_translated = 0
|
||||
|
||||
for chart_data in self._chart_entries:
|
||||
entries = chart_data['entries']
|
||||
chart_part = chart_data['chart_part']
|
||||
|
||||
translated_entries = [e for e in entries if e.get('translated')]
|
||||
if not translated_entries:
|
||||
continue
|
||||
|
||||
try:
|
||||
chart_xml = etree.fromstring(chart_part.blob)
|
||||
|
||||
for entry in translated_entries:
|
||||
# Try to find and update <a:t> elements
|
||||
for t_elem in chart_xml.iter(f'{{{_NS_A}}}t'):
|
||||
if t_elem.text and t_elem.text.strip() == entry['original']:
|
||||
t_elem.text = entry['translated']
|
||||
total_translated += 1
|
||||
break
|
||||
else:
|
||||
# Try <c:v> elements
|
||||
for v_elem in chart_xml.iter(f'{{{_NS_C}}}v'):
|
||||
if v_elem.text and v_elem.text.strip() == entry['original']:
|
||||
v_elem.text = entry['translated']
|
||||
total_translated += 1
|
||||
break
|
||||
|
||||
# Update the chart part blob
|
||||
chart_part._blob = etree.tostring(chart_xml, xml_declaration=True, encoding='UTF-8', standalone=True)
|
||||
|
||||
except Exception as e:
|
||||
_log_error("pptx_chart_update_error", error=str(e))
|
||||
|
||||
# Clean up
|
||||
self._chart_entries = []
|
||||
|
||||
if total_translated > 0:
|
||||
_log_info("pptx_charts_translated", total=total_translated)
|
||||
|
||||
|
||||
pptx_translator = PowerPointTranslator()
|
||||
|
||||
Reference in New Issue
Block a user