feat(fastapi): adds logic to generate speaker notes and slide note support in export

This commit is contained in:
sauravniraula 2025-08-12 16:18:57 +05:45
parent 01d39d71be
commit 29841bdd06
No known key found for this signature in database
GPG key ID: 60FCC1B5A5E83326
8 changed files with 82 additions and 4 deletions

View file

@ -221,6 +221,7 @@ async def stream_presentation(
layout_group=layout.name,
layout=slide_layout.id,
index=i,
speaker_note=slide_content.get("__speaker_note__", ""),
content=slide_content,
)
slides.append(slide)

View file

@ -14,7 +14,6 @@ from utils.llm_calls.edit_slide_html import get_edited_slide_html
from utils.llm_calls.select_slide_type_on_edit import get_slide_layout_from_prompt
from utils.process_slides import process_old_and_new_slides_and_fetch_assets
from utils.randomizers import get_random_uuid
from utils.schema_utils import remove_fields_from_schema
SLIDE_ROUTER = APIRouter(prefix="/slide", tags=["Slide"])
@ -59,6 +58,7 @@ async def edit_slide(
sql_session.add(slide)
slide.content = edited_slide_content
slide.layout = slide_layout.id
slide.speaker_note = edited_slide_content.get("__speaker_note__", "")
sql_session.add_all(new_assets)
await sql_session.commit()

View file

@ -156,6 +156,7 @@ class PptxConnectorModel(PptxShapeModel):
class PptxSlideModel(BaseModel):
background: Optional[PptxFillModel] = None
note: Optional[str] = None
shapes: List[
PptxTextBoxModel
| PptxAutoShapeBoxModel

View file

@ -12,6 +12,7 @@ class SlideModel(SQLModel, table=True):
index: int
content: dict = Field(sa_column=Column(JSON))
html_content: Optional[str]
speaker_note: str
properties: Optional[dict] = Field(sa_column=Column(JSON))
def get_new_slide(self, presentation_id: str, content: Optional[dict] = None):
@ -21,6 +22,7 @@ class SlideModel(SQLModel, table=True):
layout_group=self.layout_group,
layout=self.layout,
index=self.index,
speaker_note=self.speaker_note,
content=content or self.content,
properties=self.properties,
)

View file

@ -147,6 +147,9 @@ class PptxPresentationCreator:
if slide_model.background:
self.apply_fill_to_shape(slide.background, slide_model.background)
if slide_model.note:
slide.notes_slide.notes_text_frame.text = slide_model.note
for shape_model in slide_model.shapes:
model_type = type(shape_model)

View file

@ -3,10 +3,11 @@ from models.presentation_layout import SlideLayoutModel
from models.sql.slide import SlideModel
from services.llm_client import LLMClient
from utils.llm_provider import get_model
from utils.schema_utils import remove_fields_from_schema
from utils.schema_utils import add_field_in_schema, remove_fields_from_schema
system_prompt = """
Edit Slide data based on provided prompt, follow mentioned steps and notes and provide structured output.
Edit Slide data and speaker note based on provided prompt, follow mentioned steps and notes and provide structured output.
# Notes
- Provide output in language mentioned in **Input**.
@ -14,6 +15,8 @@ system_prompt = """
- Do not change **Image prompts** and **Icon queries** if not asked for in prompt.
- Generate **Image prompts** and **Icon queries** if asked to generate or change in prompt.
- Make sure to follow language guidelines.
- Speaker note should be normal text, not markdown.
- Speaker note should be simple, clear, concise and to the point.
**Go through all notes and steps and make sure they are followed, including mentioned constraints**
"""
@ -61,6 +64,18 @@ async def get_edited_slide_content(
response_schema = remove_fields_from_schema(
slide_layout.json_schema, ["__image_url__", "__icon_url__"]
)
response_schema = add_field_in_schema(
response_schema,
{
"__speaker_note__": {
"type": "string",
"minLength": 100,
"maxLength": 250,
"description": "Speaker note for the slide",
}
},
True,
)
client = LLMClient()
response = await client.generate_structured(

View file

@ -3,7 +3,7 @@ from models.presentation_layout import SlideLayoutModel
from models.presentation_outline_model import SlideOutlineModel
from services.llm_client import LLMClient
from utils.llm_provider import get_model
from utils.schema_utils import remove_fields_from_schema
from utils.schema_utils import add_field_in_schema, remove_fields_from_schema
system_prompt = """
Generate structured slide based on provided outline, follow mentioned steps and notes and provide structured output.
@ -11,6 +11,7 @@ system_prompt = """
# Steps
1. Analyze the outline.
2. Generate structured slide based on the outline.
3. Generate speaker note that is simple, clear, concise and to the point.
# Notes
- Slide body should not use words like "This slide", "This presentation".
@ -19,6 +20,7 @@ system_prompt = """
- Provide query to search icon on "__icon_query__" property.
- Only use markdown to highlight important points.
- Make sure to follow language guidelines.
- Speaker note should be normal text, not markdown.
**Strictly follow the max and min character limit for every property in the slide.**
"""
@ -57,6 +59,18 @@ async def get_slide_content_from_type_and_outline(
response_schema = remove_fields_from_schema(
slide_layout.json_schema, ["__image_url__", "__icon_url__"]
)
response_schema = add_field_in_schema(
response_schema,
{
"__speaker_note__": {
"type": "string",
"minLength": 100,
"maxLength": 250,
"description": "Speaker note for the slide",
}
},
True,
)
response = await client.generate_structured(
model=model,

View file

@ -45,6 +45,48 @@ def remove_fields_from_schema(schema: dict, fields_to_remove: List[str]):
return schema
def add_field_in_schema(schema: dict, field: dict, required: bool = False) -> dict:
if not isinstance(field, dict) or len(field) != 1:
raise ValueError(
"`field` must be a dict with exactly one entry: {name: schema_dict}"
)
field_name, field_schema = next(iter(field.items()))
if not isinstance(field_name, str):
raise TypeError("Field name must be a string")
if not isinstance(field_schema, dict):
raise TypeError("Field schema must be a dictionary")
updated_schema: dict = deepcopy(schema)
root_properties = updated_schema.get("properties")
if not isinstance(root_properties, dict):
updated_schema["properties"] = {}
root_properties = updated_schema["properties"]
root_properties[field_name] = field_schema
# Update root-level required based on the flag
existing_required = updated_schema.get("required")
if not isinstance(existing_required, list):
existing_required = []
if required:
if field_name not in existing_required:
existing_required.append(field_name)
else:
if field_name in existing_required:
existing_required = [name for name in existing_required if name != field_name]
if existing_required:
updated_schema["required"] = existing_required
else:
updated_schema.pop("required", None)
return updated_schema
# From OpenAI
def ensure_strict_json_schema(
json_schema: object,