diff --git a/servers/fastapi/api/v1/ppt/endpoints/presentation.py b/servers/fastapi/api/v1/ppt/endpoints/presentation.py index 52920367..4452553b 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/presentation.py +++ b/servers/fastapi/api/v1/ppt/endpoints/presentation.py @@ -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) diff --git a/servers/fastapi/api/v1/ppt/endpoints/slide.py b/servers/fastapi/api/v1/ppt/endpoints/slide.py index a0c81107..e1ec9e6b 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/slide.py +++ b/servers/fastapi/api/v1/ppt/endpoints/slide.py @@ -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() diff --git a/servers/fastapi/models/pptx_models.py b/servers/fastapi/models/pptx_models.py index cf51d966..80da5cd9 100644 --- a/servers/fastapi/models/pptx_models.py +++ b/servers/fastapi/models/pptx_models.py @@ -156,6 +156,7 @@ class PptxConnectorModel(PptxShapeModel): class PptxSlideModel(BaseModel): background: Optional[PptxFillModel] = None + note: Optional[str] = None shapes: List[ PptxTextBoxModel | PptxAutoShapeBoxModel diff --git a/servers/fastapi/models/sql/slide.py b/servers/fastapi/models/sql/slide.py index 7c0cb7e3..5d859d82 100644 --- a/servers/fastapi/models/sql/slide.py +++ b/servers/fastapi/models/sql/slide.py @@ -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, ) diff --git a/servers/fastapi/services/pptx_presentation_creator.py b/servers/fastapi/services/pptx_presentation_creator.py index 6c778b0d..6563bd89 100644 --- a/servers/fastapi/services/pptx_presentation_creator.py +++ b/servers/fastapi/services/pptx_presentation_creator.py @@ -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) diff --git a/servers/fastapi/utils/llm_calls/edit_slide.py b/servers/fastapi/utils/llm_calls/edit_slide.py index 30599d08..5d91a607 100644 --- a/servers/fastapi/utils/llm_calls/edit_slide.py +++ b/servers/fastapi/utils/llm_calls/edit_slide.py @@ -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( diff --git a/servers/fastapi/utils/llm_calls/generate_slide_content.py b/servers/fastapi/utils/llm_calls/generate_slide_content.py index be19b168..e8f695a0 100644 --- a/servers/fastapi/utils/llm_calls/generate_slide_content.py +++ b/servers/fastapi/utils/llm_calls/generate_slide_content.py @@ -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, diff --git a/servers/fastapi/utils/schema_utils.py b/servers/fastapi/utils/schema_utils.py index 3c82ad0f..92aafd97 100644 --- a/servers/fastapi/utils/schema_utils.py +++ b/servers/fastapi/utils/schema_utils.py @@ -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, diff --git a/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx b/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx index 922c9f87..d88bd670 100644 --- a/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx @@ -115,7 +115,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => { presentationData.slides && presentationData.slides.length > 0 && presentationData.slides.map((slide: any, index: number) => ( -