# -*- coding: utf-8 -*-
"""
:File: EuljiroWorship/controller/utils/emergency_slide_factory.py
:Author: Benjamin Jaedon Choi - https://github.com/saintbenjamin
:Affiliated Church: The Eulji-ro Presbyterian Church [대한예수교장로회(통합) 을지로교회]
:Address: The Eulji-ro Presbyterian Church, 24-10, Eulji-ro 20-gil, Jung-gu, Seoul 04549, South Korea
:Telephone: +82-2-2266-3070
:E-mail: euljirochurch [at] G.M.A.I.L. (replace [at] with @ and G.M.A.I.L as you understood.)
:License: MIT License with Attribution Requirement (see LICENSE file for details); Copyright (c) 2025 The Eulji-ro Presbyterian Church.
Generates slide dictionaries for emergency captions.
This module defines :class:`controller.utils.emergency_slide_factory.EmergencySlideFactory`, a utility that builds slide payloads
consumable by the slide controller / overlay pipeline.
Supported inputs:
- Bible references (parsed by :func:`core.utils.bible_parser.parse_reference`)
- Manual fallback captions and messages
- Preset responsive readings (교독문) loaded from JSON files
- Preset hymns loaded from JSON files
Outputs:
- A list of slide dictionaries with keys: ``style``, ``caption``, ``headline``
Note:
- Bible verse text is wrapped into smaller chunks (currently ``width=60``) to avoid overly long single-slide lines.
- Version display aliases are loaded from :py:data:`core.config.paths.ALIASES_VERSION_FILE`.
"""
import os
import json
import textwrap
from core.config import paths, constants
from core.utils.bible_data_loader import BibleDataLoader
from core.utils.bible_parser import parse_reference
[docs]
class EmergencySlideFactory:
"""
Factory for constructing emergency slide blocks.
This class converts user-facing emergency inputs into a list of slide
dictionaries suitable for immediate export to the slide controller.
It supports multiple input modes, including:
- Bible references (single verse, range, or full chapter)
- Manual text fallback
- Responsive readings (교독문)
- Hymns (찬송가)
- Arbitrary manual slide content
Verse-based slides are retrieved via :class:`core.utils.bible_data_loader.BibleDataLoader`
and wrapped into screen-friendly chunks using `textwrap.wrap <https://docs.python.org/3/library/textwrap.html#textwrap.wrap>`_.
Slide dict schema::
{
"style": str, # e.g., "verse", "lyrics", "greet", ...
"caption": str, # title / reference line
"headline": str, # main body text shown on screen
}
Attributes:
VERSION_ALIASES (dict):
Mapping of Bible version keys to human-readable aliases,
loaded from :py:data:`core.config.paths.ALIASES_VERSION_FILE`.
Used when rendering verse captions.
loader (BibleDataLoader):
Bible data loader instance used to retrieve verse text,
book names, and chapter metadata. Either provided externally
or created internally during initialization.
"""
[docs]
def __init__(self, bible_loader=None):
"""
Initialize the factory.
Loads Bible version display aliases from :py:data:`core.config.paths.ALIASES_VERSION_FILE` and
prepares a :class:`core.utils.bible_data_loader.BibleDataLoader` instance (either the provided one or a default).
Args:
bible_loader (BibleDataLoader | None):
Optional custom Bible loader. If None, a default ``BibleDataLoader()``
is created and used.
Returns:
None
"""
with open(paths.ALIASES_VERSION_FILE, encoding="utf-8") as f:
self.VERSION_ALIASES = json.load(f)
self.loader = bible_loader or BibleDataLoader()
[docs]
def build_bible_slides(self, book_id, chapter, verses, version=None) -> list[dict]:
"""
Build verse-style slides for the given Bible location and verse range.
This method attempts to retrieve verse text using :meth:`core.utils.bible_data_loader.BibleDataLoader.get_verse()`.
If ``version`` is provided, it tries that version first; otherwise it iterates
available versions and returns the first successful slide set.
Each verse is wrapped using `textwrap.wrap(..., width=60) <https://docs.python.org/3/library/textwrap.html#textwrap.wrap>`_ to avoid overly long
single lines, producing multiple slides per verse when needed.
See also :py:data:`core.config.constants.MAX_CHARS`.
Args:
book_id (str):
Internal Bible book identifier (e.g., "John").
chapter (int):
Chapter number.
verses (list[int] | tuple[int, int]):
Verse numbers to include. The implementation currently uses
``min(verses)`` and ``max(verses)`` to define an inclusive range.
version (str | None):
Preferred Bible version name. If None, tries multiple versions.
Returns:
list[dict]:
A list of verse-style slide dictionaries. If verse retrieval fails
for all attempted versions, returns an empty list.
"""
result = []
start, end = min(verses), max(verses)
target_versions = [version] if version else self.loader.aliases_version
for ver in target_versions:
alias = self.VERSION_ALIASES.get(ver, ver)
slides = []
for verse_num in range(start, end + 1):
try:
verse_text = self.loader.get_verse(ver, book_id, chapter, verse_num)
reftext = f"{self.loader.get_standard_book(book_id, 'ko')} {chapter}장 {verse_num}절 ({alias})"
chunks = textwrap.wrap(verse_text.strip(), width=constants.MAX_CHARS)
for chunk in chunks:
slides.append({
"style": "verse",
"caption": reftext,
"headline": chunk
})
except Exception:
continue
if slides:
return slides
return result
[docs]
def create_from_respo(self, number: int) -> list[dict]:
"""
Load a responsive reading (교독문) JSON by number and generate slides.
The expected JSON format contains:
- ``title``: str (optional)
- ``slides``: list of entries, each typically containing:
- ``speaker``: str
- ``headline``: str
For each entry, one slide is created containing a single speaker-response
line formatted in an HTML-like style (e.g., ``"<b>...</b>"``).
Args:
number (int):
Responsive reading number (e.g., 123).
Returns:
list[dict]:
A list of slide dictionaries (style "verse").
If loading fails, returns a one-slide fallback with an error message.
"""
path = os.path.join("data", "respo", f"responsive_{number:03d}.json")
try:
with open(path, encoding="utf-8") as f:
data = json.load(f)
title = data.get("title", f"성시교독 {number}번")
slides_raw = data.get("slides", [])
slides = []
for entry in slides_raw:
speaker = entry.get("speaker", "").strip()
headline = entry.get("headline", "").strip()
if speaker or headline:
slides.append({
"style": "verse",
"caption": title,
"headline": f"<b>{speaker}:</b> {headline}"
})
return slides
except Exception:
return [{
"style": "verse",
"caption": f"성시교독 {number}번",
"headline": f"(교독문 {number}번을 불러올 수 없습니다)"
}]
[docs]
def format_responsive_text(self, slides_raw: list[dict]) -> str:
"""
Format responsive reading entries into a single joined string.
Each entry is converted into one line using an HTML-like emphasis for the
speaker name::
"<b>{speaker}:</b> {headline}"
Args:
slides_raw (list[dict]):
Raw entry list, where each entry may include:
- ``speaker``: str
- ``headline``: str
Returns:
str:
A newline-joined formatted text block. Empty entries are skipped.
"""
lines = []
for entry in slides_raw:
speaker = entry.get("speaker", "").strip()
headline = entry.get("headline", "").strip()
if speaker or headline:
lines.append(f"<b>{speaker}:</b> {headline}")
return "\n".join(lines)
[docs]
def create_from_hymn(self, number: int) -> list[dict]:
"""
Load a hymn JSON by number and split it into lyric slides.
The expected JSON format contains:
- "title": str (optional)
- "headline": str (lyrics text, typically multi-line)
Lyrics are split into chunks of two lines per slide.
Args:
number (int):
Hymn number (e.g., 88).
Returns:
list[dict]:
A list of slide dictionaries with style "lyrics".
If loading fails, returns a one-slide fallback with an error message.
"""
path = os.path.join("data", "hymns", f"hymn_{number:03d}.json")
try:
with open(path, encoding="utf-8") as f:
data = json.load(f)
title = data.get("title", f"새찬송가 {number}장")
body = data.get("headline", "")
lines = body.strip().split("\n")
slides = []
for i in range(0, len(lines), 2): # Group by 2 lines
chunk = "\n".join(lines[i:i+2]).strip()
if chunk:
slides.append({
"style": "lyrics",
"caption": title,
"headline": chunk
})
return slides
except Exception as e:
return [{
"style": "lyrics",
"caption": f"새찬송가 {number}장",
"headline": f"(찬송가 {number}번을 불러올 수 없습니다)"
}]
[docs]
def create_manual_slide(self, style: str, caption: str, text: str) -> list[dict]:
"""
Generate slide(s) from manually provided content.
Behavior:
- If `style` is "lyrics", the input text is split by lines and grouped into 2-line chunks per slide.
- For all other styles, a single slide is produced as-is.
Args:
style (str):
Internal slide style (e.g., "verse", "greet", "lyrics").
caption (str):
Caption/title string shown above or alongside the main text.
text (str):
Main body text for the slide(s).
Returns:
list[dict]:
List of slide dictionaries ready to be exported.
"""
if style == "lyrics":
lines = text.strip().split("\n")
slides = []
for i in range(0, len(lines), 2):
chunk = "\n".join(lines[i:i+2]).strip()
if chunk:
slides.append({
"style": "lyrics",
"caption": caption,
"headline": chunk
})
return slides
else:
return [{
"style": style,
"caption": caption,
"headline": text
}]