Refactor per-user channel registration and improve PDF/PPTX workflows

- Generalize per-user channel registration (telegram/discord/whatsapp)
  via vault instead of config.json userMap
- Add API endpoints for self-service channel binding
- Migrate Telegram channel to use vault-based username resolution
- Improve PDF figure extraction filters (OpenCV)
- Fix PPTX image embedding and font sizing
- Reorder search providers: searxng → ddg priority

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
kim
2026-05-17 22:37:56 +09:00
co-authored by Claude Opus 4.7
parent e7fd3de974
commit 34932ac50a
7 changed files with 441 additions and 39 deletions
+35 -13
View File
@@ -38,6 +38,7 @@ import sys
import json
import os
import re
import math
# Force UTF-8 output on Windows
if sys.platform == 'win32':
@@ -48,7 +49,7 @@ if sys.platform == 'win32':
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR, MSO_AUTO_SIZE
# ─── Paths ──────────────────────────────────────────────────────────────────────
@@ -83,13 +84,13 @@ COLORS_DARK = {
# Default font sizes (overridable via spec.font_sizes)
DEFAULT_FONT_SIZES = {
"title": 36,
"subtitle": 18,
"slide_title": 24,
"body": 16,
"bullets": 16,
"section": 32,
"image_title": 22,
"title": 40,
"subtitle": 20,
"slide_title": 28,
"body": 18,
"bullets": 18,
"section": 36,
"image_title": 24,
}
# Slide dimensions (LAYOUT_WIDE = 13.333 x 7.5 inches)
@@ -97,7 +98,7 @@ SLIDE_W = Inches(13.333)
SLIDE_H = Inches(7.5)
# Layout constants for two-column slides (text + image)
L_MARGIN = Inches(0.6) # left margin (aligns with title)
L_MARGIN = Inches(0.7) # left margin (aligns with title)
R_MARGIN = Inches(0.3) # right margin
COL_GAP = Inches(0.3) # gap between text and image columns
CONTENT_W = SLIDE_W - L_MARGIN - R_MARGIN # ~12.43"
@@ -254,6 +255,7 @@ def add_text_box(slide, left, top, width, height, text, font_size=Pt(16),
txBox = slide.shapes.add_textbox(left, top, width, height)
tf = txBox.text_frame
tf.word_wrap = True
tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
p = tf.paragraphs[0]
p.alignment = alignment
run = p.add_run()
@@ -265,12 +267,32 @@ def add_text_box(slide, left, top, width, height, text, font_size=Pt(16),
return txBox
def _fit_bullet_font_size(items, width_emu, height_emu, max_pt):
"""Return the largest font size (pt) where all bullets fit in the box."""
col_w_pt = width_emu / 12700 # EMU → pt (1pt = 12700 EMU)
col_h_pt = height_emu / 12700
for fs in range(int(max_pt), 9, -1):
space_after = min(8, max(4, fs // 3))
line_h = fs * 1.35 + space_after
chars_per_line = max(1, int(col_w_pt / (fs * 0.52)))
total = sum(max(1, math.ceil((len(str(it)) + 2) / chars_per_line)) * line_h
for it in items)
if total <= col_h_pt:
return fs
return 9
def add_bullet_list(slide, left, top, width, height, items, font_size=Pt(16),
color_hex="2D3748", bullet_color_hex="1668E3", font_name=None):
"""Add a text box with bullet points."""
txBox = slide.shapes.add_textbox(left, top, width, height)
tf = txBox.text_frame
tf.word_wrap = True
tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
fitted_pt = _fit_bullet_font_size(items, width, height, font_size.pt)
fitted_fs = Pt(fitted_pt)
space_after_pt = min(8, max(4, fitted_pt // 3))
for i, item in enumerate(items):
if i == 0:
@@ -278,19 +300,19 @@ def add_bullet_list(slide, left, top, width, height, items, font_size=Pt(16),
else:
p = tf.add_paragraph()
p.space_after = Pt(4)
p.space_after = Pt(space_after_pt)
p.level = 0
# Add bullet character manually for reliable rendering
bullet_run = p.add_run()
bullet_run.text = "● "
bullet_run.font.size = font_size
bullet_run.font.size = fitted_fs
bullet_run.font.color.rgb = hex_to_rgb(bullet_color_hex)
bullet_run.font.name = font_name or get_font()
text_run = p.add_run()
text_run.text = str(item)
text_run.font.size = font_size
text_run.font.size = fitted_fs
text_run.font.color.rgb = hex_to_rgb(color_hex)
text_run.font.name = font_name or get_font()
@@ -596,7 +618,7 @@ def make_content_slide(prs, slide_spec, project_dir, workspace_path, colors, is_
return slide
content_y = Inches(1.5) if has_title else Inches(0.5)
content_h = SLIDE_H - content_y - Inches(0.3)
content_h = SLIDE_H - content_y - Inches(0.4)
if has_title:
add_text_box(slide, L_MARGIN, Inches(0.4), SLIDE_W - L_MARGIN - R_MARGIN, Inches(0.8),