add rich output support for slide notes
Docker server image / build-and-push (push) Successful in 3m2s

This commit is contained in:
2026-05-07 10:35:37 -07:00
parent 500b767d58
commit 06d4122e4e
11 changed files with 831 additions and 140 deletions
@@ -7,6 +7,7 @@ from typing import Protocol
from connectrpc.client import ConnectClient, ConnectClientSync
from connectrpc.code import Code
from connectrpc.codec import Codec
from connectrpc.compression import Compression
from connectrpc.errors import ConnectError
from connectrpc.interceptor import Interceptor, InterceptorSync
@@ -34,7 +35,7 @@ class ConversionService(Protocol):
class ConversionServiceASGIApplication(ConnectASGIApplication[ConversionService]):
def __init__(self, service: ConversionService | AsyncGenerator[ConversionService], *, interceptors: Iterable[Interceptor]=(), read_max_bytes: int | None = None, compressions: Iterable[Compression] | None = None) -> None:
def __init__(self, service: ConversionService | AsyncGenerator[ConversionService], *, interceptors: Iterable[Interceptor]=(), read_max_bytes: int | None = None, compressions: Iterable[Compression] | None = None, codecs: Iterable[Codec] | None = None) -> None:
super().__init__(
service=service,
endpoints=lambda svc: {
@@ -92,6 +93,7 @@ class ConversionServiceASGIApplication(ConnectASGIApplication[ConversionService]
interceptors=interceptors,
read_max_bytes=read_max_bytes,
compressions=compressions,
codecs=codecs,
)
@property
@@ -202,6 +204,9 @@ class ConversionServiceClient(ConnectClient):
)
class ConversionServiceSync(Protocol):
def create_conversion(self, request: officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionRequest, ctx: RequestContext) -> officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionResponse:
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
@@ -216,7 +221,7 @@ class ConversionServiceSync(Protocol):
class ConversionServiceWSGIApplication(ConnectWSGIApplication):
def __init__(self, service: ConversionServiceSync, interceptors: Iterable[InterceptorSync]=(), read_max_bytes: int | None = None, compressions: Iterable[Compression] | None = None) -> None:
def __init__(self, service: ConversionServiceSync, interceptors: Iterable[InterceptorSync]=(), read_max_bytes: int | None = None, compressions: Iterable[Compression] | None = None, codecs: Iterable[Codec] | None = None) -> None:
super().__init__(
endpoints={
"/officeconvertapi.v1.ConversionService/CreateConversion": EndpointSync.unary(
@@ -273,6 +278,7 @@ class ConversionServiceWSGIApplication(ConnectWSGIApplication):
interceptors=interceptors,
read_max_bytes=read_max_bytes,
compressions=compressions,
codecs=codecs,
)
@property
@@ -381,3 +387,5 @@ class ConversionServiceClientSync(ConnectClientSync):
headers=headers,
timeout_ms=timeout_ms,
)
File diff suppressed because one or more lines are too long
@@ -35,6 +35,12 @@ class ConversionResolution(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
CONVERSION_RESOLUTION_FHD: _ClassVar[ConversionResolution]
CONVERSION_RESOLUTION_QHD: _ClassVar[ConversionResolution]
CONVERSION_RESOLUTION_UHD: _ClassVar[ConversionResolution]
class NotesFormat(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
NOTES_FORMAT_UNSPECIFIED: _ClassVar[NotesFormat]
NOTES_FORMAT_PLAIN: _ClassVar[NotesFormat]
NOTES_FORMAT_HTML: _ClassVar[NotesFormat]
CONVERSION_STATUS_UNSPECIFIED: ConversionStatus
CONVERSION_STATUS_PENDING: ConversionStatus
CONVERSION_STATUS_RUNNING: ConversionStatus
@@ -52,6 +58,9 @@ CONVERSION_RESOLUTION_HD: ConversionResolution
CONVERSION_RESOLUTION_FHD: ConversionResolution
CONVERSION_RESOLUTION_QHD: ConversionResolution
CONVERSION_RESOLUTION_UHD: ConversionResolution
NOTES_FORMAT_UNSPECIFIED: NotesFormat
NOTES_FORMAT_PLAIN: NotesFormat
NOTES_FORMAT_HTML: NotesFormat
class JpegOutputOptions(_message.Message):
__slots__ = ("quality",)
@@ -67,17 +76,45 @@ class SlideRasterOptions(_message.Message):
jpeg: JpegOutputOptions
def __init__(self, resolution: _Optional[_Union[ConversionResolution, str]] = ..., jpeg: _Optional[_Union[JpegOutputOptions, _Mapping]] = ...) -> None: ...
class HtmlFormattingPolicy(_message.Message):
__slots__ = ("ignore_bold", "ignore_italic", "ignore_underline", "ignore_strikethrough", "ignore_font_size", "ignore_color")
IGNORE_BOLD_FIELD_NUMBER: _ClassVar[int]
IGNORE_ITALIC_FIELD_NUMBER: _ClassVar[int]
IGNORE_UNDERLINE_FIELD_NUMBER: _ClassVar[int]
IGNORE_STRIKETHROUGH_FIELD_NUMBER: _ClassVar[int]
IGNORE_FONT_SIZE_FIELD_NUMBER: _ClassVar[int]
IGNORE_COLOR_FIELD_NUMBER: _ClassVar[int]
ignore_bold: bool
ignore_italic: bool
ignore_underline: bool
ignore_strikethrough: bool
ignore_font_size: bool
ignore_color: bool
def __init__(self, ignore_bold: _Optional[bool] = ..., ignore_italic: _Optional[bool] = ..., ignore_underline: _Optional[bool] = ..., ignore_strikethrough: _Optional[bool] = ..., ignore_font_size: _Optional[bool] = ..., ignore_color: _Optional[bool] = ...) -> None: ...
class NotesOptions(_message.Message):
__slots__ = ("format", "html_use_paragraph_tags", "html_policy")
FORMAT_FIELD_NUMBER: _ClassVar[int]
HTML_USE_PARAGRAPH_TAGS_FIELD_NUMBER: _ClassVar[int]
HTML_POLICY_FIELD_NUMBER: _ClassVar[int]
format: NotesFormat
html_use_paragraph_tags: bool
html_policy: HtmlFormattingPolicy
def __init__(self, format: _Optional[_Union[NotesFormat, str]] = ..., html_use_paragraph_tags: _Optional[bool] = ..., html_policy: _Optional[_Union[HtmlFormattingPolicy, _Mapping]] = ...) -> None: ...
class Slide(_message.Message):
__slots__ = ("index", "notes_plain", "image_url", "thumbnail_image_url")
__slots__ = ("index", "notes_plain", "image_url", "thumbnail_image_url", "notes_html")
INDEX_FIELD_NUMBER: _ClassVar[int]
NOTES_PLAIN_FIELD_NUMBER: _ClassVar[int]
IMAGE_URL_FIELD_NUMBER: _ClassVar[int]
THUMBNAIL_IMAGE_URL_FIELD_NUMBER: _ClassVar[int]
NOTES_HTML_FIELD_NUMBER: _ClassVar[int]
index: int
notes_plain: str
image_url: str
thumbnail_image_url: str
def __init__(self, index: _Optional[int] = ..., notes_plain: _Optional[str] = ..., image_url: _Optional[str] = ..., thumbnail_image_url: _Optional[str] = ...) -> None: ...
notes_html: str
def __init__(self, index: _Optional[int] = ..., notes_plain: _Optional[str] = ..., image_url: _Optional[str] = ..., thumbnail_image_url: _Optional[str] = ..., notes_html: _Optional[str] = ...) -> None: ...
class SlideDeck(_message.Message):
__slots__ = ("conversion_id", "source_filename", "slides", "created_at", "width", "height", "thumbnail_width", "thumbnail_height")
@@ -100,14 +137,16 @@ class SlideDeck(_message.Message):
def __init__(self, conversion_id: _Optional[str] = ..., source_filename: _Optional[str] = ..., slides: _Optional[_Iterable[_Union[Slide, _Mapping]]] = ..., created_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., width: _Optional[int] = ..., height: _Optional[int] = ..., thumbnail_width: _Optional[int] = ..., thumbnail_height: _Optional[int] = ...) -> None: ...
class CreateConversionRequest(_message.Message):
__slots__ = ("source_filename", "full", "thumbnail")
__slots__ = ("source_filename", "full", "thumbnail", "notes")
SOURCE_FILENAME_FIELD_NUMBER: _ClassVar[int]
FULL_FIELD_NUMBER: _ClassVar[int]
THUMBNAIL_FIELD_NUMBER: _ClassVar[int]
NOTES_FIELD_NUMBER: _ClassVar[int]
source_filename: str
full: SlideRasterOptions
thumbnail: SlideRasterOptions
def __init__(self, source_filename: _Optional[str] = ..., full: _Optional[_Union[SlideRasterOptions, _Mapping]] = ..., thumbnail: _Optional[_Union[SlideRasterOptions, _Mapping]] = ...) -> None: ...
notes: NotesOptions
def __init__(self, source_filename: _Optional[str] = ..., full: _Optional[_Union[SlideRasterOptions, _Mapping]] = ..., thumbnail: _Optional[_Union[SlideRasterOptions, _Mapping]] = ..., notes: _Optional[_Union[NotesOptions, _Mapping]] = ...) -> None: ...
class CreateConversionResponse(_message.Message):
__slots__ = ("conversion_id", "upload_bucket", "upload_object_key", "upload_url", "expires_at")