@_vrsen/openswarm 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/bin/openswarm.js +38 -0
- package/config.py +34 -0
- package/data_analyst_agent/.cursor/rules/data_analyst.mdc +43 -0
- package/data_analyst_agent/__init__.py +3 -0
- package/data_analyst_agent/__pycache__/__init__.cpython-312.pyc +0 -0
- package/data_analyst_agent/__pycache__/data_analyst_agent.cpython-312.pyc +0 -0
- package/data_analyst_agent/data_analyst_agent.py +45 -0
- package/data_analyst_agent/instructions.md +173 -0
- package/data_analyst_agent/test_files/test_file.csv +21 -0
- package/data_analyst_agent/tools/__init__.py +6 -0
- package/deep_research/__init__.py +1 -0
- package/deep_research/__pycache__/__init__.cpython-312.pyc +0 -0
- package/deep_research/__pycache__/deep_research.cpython-312.pyc +0 -0
- package/deep_research/deep_research.py +27 -0
- package/deep_research/instructions.md +104 -0
- package/deep_research/tools/__init__.py +1 -0
- package/docs_agent/__init__.py +3 -0
- package/docs_agent/__pycache__/__init__.cpython-312.pyc +0 -0
- package/docs_agent/__pycache__/docs_agent.cpython-312.pyc +0 -0
- package/docs_agent/docs_agent.py +61 -0
- package/docs_agent/instructions.md +418 -0
- package/docs_agent/tools/ConvertDocument.py +323 -0
- package/docs_agent/tools/CreateDocument.py +287 -0
- package/docs_agent/tools/ListDocuments.py +134 -0
- package/docs_agent/tools/ModifyDocument.py +247 -0
- package/docs_agent/tools/RestoreDocument.py +79 -0
- package/docs_agent/tools/ViewDocument.py +153 -0
- package/docs_agent/tools/__init__.py +1 -0
- package/docs_agent/tools/__pycache__/ConvertDocument.cpython-312.pyc +0 -0
- package/docs_agent/tools/__pycache__/CreateDocument.cpython-312.pyc +0 -0
- package/docs_agent/tools/__pycache__/ListDocuments.cpython-312.pyc +0 -0
- package/docs_agent/tools/__pycache__/ModifyDocument.cpython-312.pyc +0 -0
- package/docs_agent/tools/__pycache__/RestoreDocument.cpython-312.pyc +0 -0
- package/docs_agent/tools/__pycache__/ViewDocument.cpython-312.pyc +0 -0
- package/docs_agent/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__init__.py +1 -0
- package/docs_agent/tools/utils/__pycache__/__init__.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/doc_file_utils.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_blocks.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_constants.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_core.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_css.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_images.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_page.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_paragraphs.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_playwright.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_selectors.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_shared.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_validation.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/doc_file_utils.py +29 -0
- package/docs_agent/tools/utils/html_docx_blocks.py +262 -0
- package/docs_agent/tools/utils/html_docx_constants.py +78 -0
- package/docs_agent/tools/utils/html_docx_core.py +138 -0
- package/docs_agent/tools/utils/html_docx_css.py +262 -0
- package/docs_agent/tools/utils/html_docx_images.py +293 -0
- package/docs_agent/tools/utils/html_docx_page.py +185 -0
- package/docs_agent/tools/utils/html_docx_paragraphs.py +342 -0
- package/docs_agent/tools/utils/html_docx_playwright.py +184 -0
- package/docs_agent/tools/utils/html_docx_selectors.py +196 -0
- package/docs_agent/tools/utils/html_docx_shared.py +23 -0
- package/docs_agent/tools/utils/html_docx_tables.py +942 -0
- package/docs_agent/tools/utils/html_validation.py +102 -0
- package/helpers.py +59 -0
- package/image_generation_agent/__init__.py +1 -0
- package/image_generation_agent/__pycache__/__init__.cpython-312.pyc +0 -0
- package/image_generation_agent/__pycache__/image_generation_agent.cpython-312.pyc +0 -0
- package/image_generation_agent/image_generation_agent.py +31 -0
- package/image_generation_agent/instructions.md +80 -0
- package/image_generation_agent/tools/CombineImages.py +211 -0
- package/image_generation_agent/tools/EditImages.py +200 -0
- package/image_generation_agent/tools/GenerateImages.py +184 -0
- package/image_generation_agent/tools/RemoveBackground.py +108 -0
- package/image_generation_agent/tools/__init__.py +2 -0
- package/image_generation_agent/tools/__pycache__/CombineImages.cpython-312.pyc +0 -0
- package/image_generation_agent/tools/__pycache__/EditImages.cpython-312.pyc +0 -0
- package/image_generation_agent/tools/__pycache__/GenerateImages.cpython-312.pyc +0 -0
- package/image_generation_agent/tools/__pycache__/RemoveBackground.cpython-312.pyc +0 -0
- package/image_generation_agent/tools/utils/__init__.py +2 -0
- package/image_generation_agent/tools/utils/__pycache__/__init__.cpython-312.pyc +0 -0
- package/image_generation_agent/tools/utils/__pycache__/image_io.cpython-312.pyc +0 -0
- package/image_generation_agent/tools/utils/image_io.py +308 -0
- package/onboard.py +325 -0
- package/orchestrator/__init__.py +3 -0
- package/orchestrator/__pycache__/__init__.cpython-312.pyc +0 -0
- package/orchestrator/__pycache__/orchestrator.cpython-312.pyc +0 -0
- package/orchestrator/instructions.md +90 -0
- package/orchestrator/orchestrator.py +33 -0
- package/package.json +49 -0
- package/patches/__init__.py +1 -0
- package/patches/__pycache__/__init__.cpython-312.pyc +0 -0
- package/patches/__pycache__/patch_agency_swarm_dual_comms.cpython-312.pyc +0 -0
- package/patches/__pycache__/patch_file_attachment_refs.cpython-312.pyc +0 -0
- package/patches/__pycache__/patch_ipython_interpreter_composio.cpython-312.pyc +0 -0
- package/patches/dom-to-pptx+1.1.5.patch +133440 -0
- package/patches/patch_agency_swarm_dual_comms.py +199 -0
- package/patches/patch_file_attachment_refs.py +74 -0
- package/patches/patch_ipython_interpreter_composio.py +54 -0
- package/pyproject.toml +67 -0
- package/run.py +343 -0
- package/server.py +26 -0
- package/shared_instructions.md +119 -0
- package/shared_tools/CopyFile.py +68 -0
- package/shared_tools/ExecuteTool.py +184 -0
- package/shared_tools/FindTools.py +101 -0
- package/shared_tools/ManageConnections.py +43 -0
- package/shared_tools/SearchTools.py +44 -0
- package/shared_tools/__init__.py +7 -0
- package/shared_tools/__pycache__/CopyFile.cpython-312.pyc +0 -0
- package/shared_tools/__pycache__/ExecuteTool.cpython-312.pyc +0 -0
- package/shared_tools/__pycache__/FindTools.cpython-312.pyc +0 -0
- package/shared_tools/__pycache__/ManageConnections.cpython-312.pyc +0 -0
- package/shared_tools/__pycache__/SearchTools.cpython-312.pyc +0 -0
- package/shared_tools/__pycache__/__init__.cpython-312.pyc +0 -0
- package/slides_agent/.cursor/rules/slides-agent-workflow.mdc +9 -0
- package/slides_agent/__init__.py +1 -0
- package/slides_agent/__pycache__/__init__.cpython-312.pyc +0 -0
- package/slides_agent/__pycache__/slides_agent.cpython-312.pyc +0 -0
- package/slides_agent/instructions.md +298 -0
- package/slides_agent/pptx/SKILL.md +528 -0
- package/slides_agent/pptx/html2pptx.md +625 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/slides_agent/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/slides_agent/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/slides_agent/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/slides_agent/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/slides_agent/pptx/ooxml/schemas/mce/mc.xsd +75 -0
- package/slides_agent/pptx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/slides_agent/pptx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/slides_agent/pptx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/slides_agent/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/slides_agent/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/slides_agent/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/slides_agent/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/slides_agent/pptx/ooxml/scripts/pack.py +169 -0
- package/slides_agent/pptx/ooxml/scripts/unpack.py +29 -0
- package/slides_agent/pptx/ooxml/scripts/validate.py +69 -0
- package/slides_agent/pptx/ooxml/scripts/validation/__init__.py +15 -0
- package/slides_agent/pptx/ooxml/scripts/validation/base.py +951 -0
- package/slides_agent/pptx/ooxml/scripts/validation/docx.py +274 -0
- package/slides_agent/pptx/ooxml/scripts/validation/pptx.py +315 -0
- package/slides_agent/pptx/ooxml/scripts/validation/redlining.py +279 -0
- package/slides_agent/pptx/ooxml.md +427 -0
- package/slides_agent/pptx/scripts/html2pptx.js +1092 -0
- package/slides_agent/pptx/scripts/inventory.py +1020 -0
- package/slides_agent/pptx/scripts/rearrange.py +231 -0
- package/slides_agent/pptx/scripts/replace.py +385 -0
- package/slides_agent/pptx/scripts/thumbnail.py +451 -0
- package/slides_agent/slides_agent.py +109 -0
- package/slides_agent/test_deck/_theme.css +92 -0
- package/slides_agent/test_deck/assets/placeholder.svg +11 -0
- package/slides_agent/test_deck/slide_01_title.html +10 -0
- package/slides_agent/test_deck/slide_02_image_split.html +23 -0
- package/slides_agent/test_deck/slide_03_kpi.html +21 -0
- package/slides_agent/tools/ApplyPptxTextReplacements.py +91 -0
- package/slides_agent/tools/BuildPptxFromHtmlSlides.py +221 -0
- package/slides_agent/tools/CheckSlide.py +218 -0
- package/slides_agent/tools/CheckSlideCanvasOverflow.py +221 -0
- package/slides_agent/tools/CreateImageMontage.py +261 -0
- package/slides_agent/tools/CreatePptxThumbnailGrid.py +168 -0
- package/slides_agent/tools/DeleteSlide.py +78 -0
- package/slides_agent/tools/DownloadImage.py +79 -0
- package/slides_agent/tools/EnsureRasterImage.py +157 -0
- package/slides_agent/tools/ExtractPptxTextInventory.py +104 -0
- package/slides_agent/tools/GenerateImage.py +189 -0
- package/slides_agent/tools/ImageSearch.py +127 -0
- package/slides_agent/tools/InsertNewSlides.py +393 -0
- package/slides_agent/tools/ManageTheme.py +112 -0
- package/slides_agent/tools/ModifySlide.py +563 -0
- package/slides_agent/tools/ReadSlide.py +26 -0
- package/slides_agent/tools/RearrangePptxSlidesFromTemplate.py +114 -0
- package/slides_agent/tools/RestoreSnapshot.py +89 -0
- package/slides_agent/tools/SlideScreenshot.py +66 -0
- package/slides_agent/tools/__init__.py +54 -0
- package/slides_agent/tools/__pycache__/ApplyPptxTextReplacements.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/BuildPptxFromHtmlSlides.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/CheckSlide.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/CheckSlideCanvasOverflow.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/CreateImageMontage.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/CreatePptxThumbnailGrid.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/DeleteSlide.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/DownloadImage.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/EnsureRasterImage.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/ExtractPptxTextInventory.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/GenerateImage.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/ImageSearch.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/InsertNewSlides.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/ManageTheme.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/ModifySlide.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/ReadSlide.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/RearrangePptxSlidesFromTemplate.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/RestoreSnapshot.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/SlideScreenshot.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/slide_file_utils.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/slide_html_utils.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/template_registry.cpython-312.pyc +0 -0
- package/slides_agent/tools/deck_utils.py +31 -0
- package/slides_agent/tools/html2pptx_runner.js +1183 -0
- package/slides_agent/tools/html_writer_instructions.md +149 -0
- package/slides_agent/tools/slide_file_utils.py +108 -0
- package/slides_agent/tools/slide_html_utils.py +354 -0
- package/slides_agent/tools/template_registry.py +55 -0
- package/swarm.py +82 -0
- package/video_generation_agent/__init__.py +1 -0
- package/video_generation_agent/__pycache__/__init__.cpython-312.pyc +0 -0
- package/video_generation_agent/__pycache__/video_generation_agent.cpython-312.pyc +0 -0
- package/video_generation_agent/instructions.md +178 -0
- package/video_generation_agent/tools/AddSubtitles.py +425 -0
- package/video_generation_agent/tools/CombineImages.py +166 -0
- package/video_generation_agent/tools/CombineVideos.py +113 -0
- package/video_generation_agent/tools/EditAudio.py +297 -0
- package/video_generation_agent/tools/EditImage.py +144 -0
- package/video_generation_agent/tools/EditVideoContent.py +369 -0
- package/video_generation_agent/tools/GenerateImage.py +133 -0
- package/video_generation_agent/tools/GenerateVideo.py +556 -0
- package/video_generation_agent/tools/TrimVideo.py +233 -0
- package/video_generation_agent/tools/__init__.py +1 -0
- package/video_generation_agent/tools/__pycache__/AddSubtitles.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/__pycache__/CombineImages.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/__pycache__/CombineVideos.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/__pycache__/EditAudio.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/__pycache__/EditImage.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/__pycache__/EditVideoContent.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/__pycache__/GenerateImage.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/__pycache__/GenerateVideo.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/__pycache__/TrimVideo.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/utils/__init__.py +1 -0
- package/video_generation_agent/tools/utils/__pycache__/__init__.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/utils/__pycache__/image_utils.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/utils/__pycache__/video_utils.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/utils/image_utils.py +174 -0
- package/video_generation_agent/tools/utils/video_utils.py +522 -0
- package/video_generation_agent/video_generation_agent.py +26 -0
- package/virtual_assistant/__init__.py +1 -0
- package/virtual_assistant/__pycache__/__init__.cpython-312.pyc +0 -0
- package/virtual_assistant/__pycache__/virtual_assistant.cpython-312.pyc +0 -0
- package/virtual_assistant/instructions.md +206 -0
- package/virtual_assistant/tools/AddLabelToEmail.py +154 -0
- package/virtual_assistant/tools/CheckEventsForDate.py +218 -0
- package/virtual_assistant/tools/CheckUnreadSlackMessages.py +216 -0
- package/virtual_assistant/tools/CreateCalendarEvent.py +261 -0
- package/virtual_assistant/tools/DeleteCalendarEvent.py +137 -0
- package/virtual_assistant/tools/DeleteDraft.py +95 -0
- package/virtual_assistant/tools/DraftEmail.py +239 -0
- package/virtual_assistant/tools/EditFile.py +113 -0
- package/virtual_assistant/tools/FindEmails.py +330 -0
- package/virtual_assistant/tools/GetCurrentTime.py +69 -0
- package/virtual_assistant/tools/GetSlackUserInfo.py +117 -0
- package/virtual_assistant/tools/ListDirectory.py +113 -0
- package/virtual_assistant/tools/ListSkills.py +94 -0
- package/virtual_assistant/tools/ManageLabels.py +295 -0
- package/virtual_assistant/tools/ProductSearch.py +254 -0
- package/virtual_assistant/tools/ReadEmail.py +251 -0
- package/virtual_assistant/tools/ReadFile.py +108 -0
- package/virtual_assistant/tools/ReadSlackMessages.py +191 -0
- package/virtual_assistant/tools/RemoveLabelFromEmail.py +137 -0
- package/virtual_assistant/tools/RescheduleCalendarEvent.py +227 -0
- package/virtual_assistant/tools/ScholarSearch.py +216 -0
- package/virtual_assistant/tools/SendDraft.py +101 -0
- package/virtual_assistant/tools/SendSlackMessage.py +148 -0
- package/virtual_assistant/tools/WriteFile.py +95 -0
- package/virtual_assistant/tools/__init__.py +1 -0
- package/virtual_assistant/tools/__pycache__/AddLabelToEmail.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/CheckEventsForDate.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/CheckUnreadSlackMessages.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/CreateCalendarEvent.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/DeleteCalendarEvent.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/DeleteDraft.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/DraftEmail.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/EditFile.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/FindEmails.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/GetCurrentTime.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/GetSlackUserInfo.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/ListDirectory.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/ListSkills.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/ManageLabels.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/ProductSearch.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/ReadEmail.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/ReadFile.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/ReadSlackMessages.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/RemoveLabelFromEmail.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/RescheduleCalendarEvent.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/ScholarSearch.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/SendDraft.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/SendSlackMessage.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/WriteFile.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- package/virtual_assistant/virtual_assistant.py +52 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""Tool for adding animated subtitles to videos using OpenAI Whisper API for timing."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from typing import Optional, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import Field, field_validator
|
|
8
|
+
|
|
9
|
+
from agency_swarm import BaseTool
|
|
10
|
+
from moviepy.editor import VideoFileClip, ImageClip, CompositeVideoClip
|
|
11
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
12
|
+
import numpy as np
|
|
13
|
+
from openai import OpenAI
|
|
14
|
+
|
|
15
|
+
from .utils.video_utils import get_videos_dir
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AddSubtitles(BaseTool):
|
|
21
|
+
"""
|
|
22
|
+
Add animated subtitles to a video.
|
|
23
|
+
Uses OpenAI Whisper API to automatically transcribe audio and extract word-level timestamps.
|
|
24
|
+
Subtitles appear word-by-word or phrase-by-phrase with highlighting effect.
|
|
25
|
+
|
|
26
|
+
Videos are saved to: mnt/{product_name}/generated_videos/
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
product_name: str = Field(
|
|
30
|
+
...,
|
|
31
|
+
description="Name of the product this video is for (e.g., 'Acme_Widget_Pro', 'Green_Tea_Extract'). Used to organize files into product-specific folders.",
|
|
32
|
+
)
|
|
33
|
+
video_name: str = Field(
|
|
34
|
+
...,
|
|
35
|
+
description="Name of the video file (without extension) to add subtitles to",
|
|
36
|
+
)
|
|
37
|
+
original_script: str = Field(
|
|
38
|
+
...,
|
|
39
|
+
description="Original script of the video to provide guidance for the subtitles. Should be provided in a single text block, without any formatting.",
|
|
40
|
+
)
|
|
41
|
+
output_name: Optional[str] = Field(
|
|
42
|
+
None,
|
|
43
|
+
description="Output video name (without extension). If not provided, adds '_subtitled' to original name",
|
|
44
|
+
)
|
|
45
|
+
font_size: int = Field(
|
|
46
|
+
default=60,
|
|
47
|
+
description="Font size for subtitles (default: 60, recommended range: 40-80 for vertical videos)",
|
|
48
|
+
)
|
|
49
|
+
position: Literal["center", "bottom", "top"] = Field(
|
|
50
|
+
default="bottom", description="Vertical position of subtitles on screen"
|
|
51
|
+
)
|
|
52
|
+
words_per_clip: int = Field(
|
|
53
|
+
default=6,
|
|
54
|
+
description="Number of words to show per subtitle clip (default: 6, usually 2-6)",
|
|
55
|
+
)
|
|
56
|
+
highlight_color: str = Field(
|
|
57
|
+
default="white",
|
|
58
|
+
description="Color for highlighting text (default: 'yellow', options: 'yellow', 'white', 'cyan', 'green')",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@field_validator("video_name")
|
|
62
|
+
@classmethod
|
|
63
|
+
def _validate_video_name(cls, value: str) -> str:
|
|
64
|
+
if not value.strip():
|
|
65
|
+
raise ValueError("video_name must not be empty")
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
@field_validator("font_size")
|
|
69
|
+
@classmethod
|
|
70
|
+
def _validate_font_size(cls, value: int) -> int:
|
|
71
|
+
if value < 20 or value > 120:
|
|
72
|
+
raise ValueError("font_size must be between 20 and 120")
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
@field_validator("words_per_clip")
|
|
76
|
+
@classmethod
|
|
77
|
+
def _validate_words_per_clip(cls, value: int) -> int:
|
|
78
|
+
if value < 1 or value > 10:
|
|
79
|
+
raise ValueError("words_per_clip must be between 1 and 10")
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
def run(self) -> str:
|
|
83
|
+
"""Add animated subtitles to the video using Whisper for timing."""
|
|
84
|
+
videos_dir = get_videos_dir(self.product_name)
|
|
85
|
+
video_path = os.path.join(videos_dir, f"{self.video_name}.mp4")
|
|
86
|
+
|
|
87
|
+
if not os.path.exists(video_path):
|
|
88
|
+
raise FileNotFoundError(
|
|
89
|
+
f"Video file not found: {video_path}. "
|
|
90
|
+
f"Make sure the video exists in the {videos_dir} directory."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
video = VideoFileClip(video_path)
|
|
94
|
+
video_width, video_height = video.size
|
|
95
|
+
|
|
96
|
+
api_key = os.getenv("OPENAI_API_KEY")
|
|
97
|
+
if not api_key:
|
|
98
|
+
raise RuntimeError("OPENAI_API_KEY environment variable is required for subtitles")
|
|
99
|
+
client = OpenAI(api_key=api_key)
|
|
100
|
+
|
|
101
|
+
prompt = (
|
|
102
|
+
"Transcribe the audio of the video into text, make sure to include correct periods and capitalization. "
|
|
103
|
+
"Do not use em dashes, use hyphens instead. "
|
|
104
|
+
f"The original script, use it as a reference to ensure correct spelling: {self.original_script}."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Transcribe with word-level timestamps using OpenAI API
|
|
108
|
+
with open(video_path, "rb") as audio_file:
|
|
109
|
+
transcript = client.audio.transcriptions.create(
|
|
110
|
+
model="whisper-1",
|
|
111
|
+
prompt=prompt,
|
|
112
|
+
file=audio_file,
|
|
113
|
+
response_format="verbose_json",
|
|
114
|
+
timestamp_granularities=["word"],
|
|
115
|
+
temperature=0.0,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Extract words with timestamps from API response and add punctuation
|
|
120
|
+
words_with_timing = []
|
|
121
|
+
|
|
122
|
+
if hasattr(transcript, "words") and transcript.words:
|
|
123
|
+
# Character replacements for Unicode normalization
|
|
124
|
+
char_replacements = {
|
|
125
|
+
'\u2014': '-', # Em dash
|
|
126
|
+
'\u2013': '-', # En dash
|
|
127
|
+
'\u2212': '-', # Minus sign
|
|
128
|
+
'\u2010': '-', # Hyphen
|
|
129
|
+
'\u2011': '-', # Non-breaking hyphen
|
|
130
|
+
'\ufe63': '-', # Small hyphen-minus
|
|
131
|
+
'\uff0d': '-', # Fullwidth hyphen-minus
|
|
132
|
+
'\u2019': "'", # Right single quote
|
|
133
|
+
'\u2018': "'", # Left single quote
|
|
134
|
+
'\u2032': "'", # Prime
|
|
135
|
+
'\u2035': "'" # Reversed prime
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Split full text into words (preserves punctuation)
|
|
139
|
+
full_text_words = transcript.text.split()
|
|
140
|
+
|
|
141
|
+
# Match API words with full text words to get punctuation
|
|
142
|
+
full_text_idx = 0
|
|
143
|
+
|
|
144
|
+
for word_info in transcript.words:
|
|
145
|
+
word = word_info.word.strip()
|
|
146
|
+
|
|
147
|
+
# Try to find matching word in full text (with punctuation)
|
|
148
|
+
final_word = word
|
|
149
|
+
if full_text_idx < len(full_text_words):
|
|
150
|
+
full_word = full_text_words[full_text_idx]
|
|
151
|
+
# Check if the word matches (case-insensitive, ignoring punctuation)
|
|
152
|
+
if word.lower() == full_word.strip('.,!?;:').lower():
|
|
153
|
+
final_word = full_word # Use the version with punctuation
|
|
154
|
+
full_text_idx += 1
|
|
155
|
+
else:
|
|
156
|
+
# Try to find it in the next few words
|
|
157
|
+
for i in range(full_text_idx, min(full_text_idx + 3, len(full_text_words))):
|
|
158
|
+
if word.lower() == full_text_words[i].strip('.,!?;:').lower():
|
|
159
|
+
final_word = full_text_words[i]
|
|
160
|
+
full_text_idx = i + 1
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
# Normalize Unicode characters to ASCII equivalents
|
|
164
|
+
for old_char, new_char in char_replacements.items():
|
|
165
|
+
final_word = final_word.replace(old_char, new_char)
|
|
166
|
+
|
|
167
|
+
words_with_timing.append(
|
|
168
|
+
{
|
|
169
|
+
"word": final_word,
|
|
170
|
+
"start": word_info.start,
|
|
171
|
+
"end": word_info.end,
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if not words_with_timing:
|
|
176
|
+
raise RuntimeError(
|
|
177
|
+
"No words detected in audio. Video may not have speech or audio track."
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
chunks = []
|
|
182
|
+
i = 0
|
|
183
|
+
|
|
184
|
+
while i < len(words_with_timing):
|
|
185
|
+
chunk_words = []
|
|
186
|
+
words_added = 0
|
|
187
|
+
|
|
188
|
+
# Add words until we reach max words_per_clip or hit a sentence ending
|
|
189
|
+
while (
|
|
190
|
+
i + words_added < len(words_with_timing)
|
|
191
|
+
and words_added < self.words_per_clip
|
|
192
|
+
):
|
|
193
|
+
word_data = words_with_timing[i + words_added]
|
|
194
|
+
chunk_words.append(word_data)
|
|
195
|
+
words_added += 1
|
|
196
|
+
|
|
197
|
+
# Check if this word ends a sentence (period, exclamation, question mark)
|
|
198
|
+
word_text = word_data["word"].strip()
|
|
199
|
+
if (
|
|
200
|
+
word_text.endswith(".")
|
|
201
|
+
or word_text.endswith("!")
|
|
202
|
+
or word_text.endswith("?")
|
|
203
|
+
):
|
|
204
|
+
# End chunk here
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
# Create chunk with combined text and timing
|
|
208
|
+
if chunk_words:
|
|
209
|
+
chunk = {
|
|
210
|
+
"text": " ".join([w["word"] for w in chunk_words]),
|
|
211
|
+
"start": chunk_words[0]["start"],
|
|
212
|
+
"end": chunk_words[-1]["end"],
|
|
213
|
+
}
|
|
214
|
+
chunks.append(chunk)
|
|
215
|
+
i += words_added
|
|
216
|
+
else:
|
|
217
|
+
# Safety: should never happen, but just in case
|
|
218
|
+
i += 1
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
if self.position == "center":
|
|
222
|
+
y_position = "center"
|
|
223
|
+
elif self.position == "bottom":
|
|
224
|
+
y_position = video_height - 400 # 400px from bottom
|
|
225
|
+
else: # top
|
|
226
|
+
y_position = 150 # 150px from top
|
|
227
|
+
|
|
228
|
+
# Color mapping (RGB format for PIL)
|
|
229
|
+
color_map = {
|
|
230
|
+
"yellow": (255, 255, 0),
|
|
231
|
+
"white": (255, 255, 255),
|
|
232
|
+
"cyan": (0, 255, 255),
|
|
233
|
+
"green": (144, 238, 144),
|
|
234
|
+
}
|
|
235
|
+
text_color = color_map.get(self.highlight_color, (255, 255, 0))
|
|
236
|
+
|
|
237
|
+
# Helper function to create text image using PIL
|
|
238
|
+
def create_text_image(text, font_size, width, color):
|
|
239
|
+
"""Create an image with text using PIL, with automatic line wrapping."""
|
|
240
|
+
# Get the fonts directory relative to this tool file
|
|
241
|
+
tools_dir = os.path.dirname(os.path.abspath(__file__))
|
|
242
|
+
fonts_dir = os.path.join(tools_dir, "utils", "fonts")
|
|
243
|
+
|
|
244
|
+
# Montserrat font paths - try multiple variations
|
|
245
|
+
font_paths = [
|
|
246
|
+
os.path.join(fonts_dir, "Montserrat-Bold.ttf"),
|
|
247
|
+
os.path.join(fonts_dir, "Montserrat-SemiBold.ttf"),
|
|
248
|
+
os.path.join(fonts_dir, "Montserrat-Regular.ttf"),
|
|
249
|
+
"C:/Windows/Fonts/Montserrat-Bold.ttf",
|
|
250
|
+
"Montserrat-Bold.ttf",
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
# Try to load Montserrat font
|
|
254
|
+
font = None
|
|
255
|
+
for font_path in font_paths:
|
|
256
|
+
try:
|
|
257
|
+
font = ImageFont.truetype(font_path, font_size)
|
|
258
|
+
break
|
|
259
|
+
except Exception:
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
# Fall back to default if no font loaded
|
|
263
|
+
if font is None:
|
|
264
|
+
font = ImageFont.load_default()
|
|
265
|
+
|
|
266
|
+
# Create temporary image to measure text
|
|
267
|
+
temp_img = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
|
|
268
|
+
temp_draw = ImageDraw.Draw(temp_img)
|
|
269
|
+
|
|
270
|
+
# Word wrapping logic
|
|
271
|
+
words = text.split(" ")
|
|
272
|
+
lines = []
|
|
273
|
+
current_line = []
|
|
274
|
+
|
|
275
|
+
max_width = width # Maximum width available for text
|
|
276
|
+
|
|
277
|
+
for word in words:
|
|
278
|
+
# Try adding this word to current line
|
|
279
|
+
test_line = " ".join(current_line + [word])
|
|
280
|
+
bbox = temp_draw.textbbox((0, 0), test_line, font=font)
|
|
281
|
+
line_width = bbox[2] - bbox[0]
|
|
282
|
+
|
|
283
|
+
if line_width <= max_width:
|
|
284
|
+
# Word fits, add it to current line
|
|
285
|
+
current_line.append(word)
|
|
286
|
+
else:
|
|
287
|
+
# Word doesn't fit, start new line
|
|
288
|
+
if current_line:
|
|
289
|
+
lines.append(" ".join(current_line))
|
|
290
|
+
current_line = [word]
|
|
291
|
+
else:
|
|
292
|
+
# Single word is too long, add it anyway
|
|
293
|
+
lines.append(word)
|
|
294
|
+
|
|
295
|
+
# Add remaining words
|
|
296
|
+
if current_line:
|
|
297
|
+
lines.append(" ".join(current_line))
|
|
298
|
+
|
|
299
|
+
# Calculate dimensions for multi-line text
|
|
300
|
+
padding = 20
|
|
301
|
+
line_height = font_size + 10 # Add some spacing between lines
|
|
302
|
+
|
|
303
|
+
# Get max line width
|
|
304
|
+
max_line_width = 0
|
|
305
|
+
for line in lines:
|
|
306
|
+
bbox = temp_draw.textbbox((0, 0), line, font=font)
|
|
307
|
+
line_width = bbox[2] - bbox[0]
|
|
308
|
+
max_line_width = max(max_line_width, line_width)
|
|
309
|
+
|
|
310
|
+
img_width = max_line_width + padding * 2
|
|
311
|
+
img_height = len(lines) * line_height + padding * 2
|
|
312
|
+
|
|
313
|
+
# Create actual image with transparent background
|
|
314
|
+
img = Image.new("RGBA", (img_width, img_height), (0, 0, 0, 0))
|
|
315
|
+
draw = ImageDraw.Draw(img)
|
|
316
|
+
|
|
317
|
+
# Draw each line of text
|
|
318
|
+
stroke_width = 6
|
|
319
|
+
y_offset = padding
|
|
320
|
+
|
|
321
|
+
for line in lines:
|
|
322
|
+
# Get line width for centering
|
|
323
|
+
bbox = draw.textbbox((0, 0), line, font=font)
|
|
324
|
+
line_width = bbox[2] - bbox[0]
|
|
325
|
+
x = (img_width - line_width) // 2
|
|
326
|
+
|
|
327
|
+
# Draw stroke (outline)
|
|
328
|
+
for offset_x in range(-stroke_width, stroke_width + 1):
|
|
329
|
+
for offset_y in range(-stroke_width, stroke_width + 1):
|
|
330
|
+
draw.text(
|
|
331
|
+
(x + offset_x, y_offset + offset_y),
|
|
332
|
+
line,
|
|
333
|
+
font=font,
|
|
334
|
+
fill=(0, 0, 0, 255),
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Draw main text
|
|
338
|
+
draw.text((x, y_offset), line, font=font, fill=(*color, 255))
|
|
339
|
+
|
|
340
|
+
# Move to next line
|
|
341
|
+
y_offset += line_height
|
|
342
|
+
|
|
343
|
+
return np.array(img)
|
|
344
|
+
|
|
345
|
+
subtitle_clips = []
|
|
346
|
+
|
|
347
|
+
for i, chunk in enumerate(chunks):
|
|
348
|
+
# Bold, uppercase for visibility
|
|
349
|
+
text = chunk["text"].upper()
|
|
350
|
+
start_time = chunk["start"]
|
|
351
|
+
duration = chunk["end"] - chunk["start"]
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
# Create text image using PIL
|
|
355
|
+
text_img = create_text_image(
|
|
356
|
+
text, self.font_size, video_width - 100, text_color
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Create ImageClip from the text image
|
|
360
|
+
txt_clip = ImageClip(text_img)
|
|
361
|
+
|
|
362
|
+
# Position and time the clip
|
|
363
|
+
txt_clip = txt_clip.set_position(("center", y_position))
|
|
364
|
+
txt_clip = txt_clip.set_start(start_time)
|
|
365
|
+
txt_clip = txt_clip.set_duration(duration)
|
|
366
|
+
|
|
367
|
+
subtitle_clips.append(txt_clip)
|
|
368
|
+
|
|
369
|
+
except Exception:
|
|
370
|
+
continue
|
|
371
|
+
final_video = CompositeVideoClip([video] + subtitle_clips)
|
|
372
|
+
final_video = final_video.set_duration(video.duration)
|
|
373
|
+
final_video = final_video.set_audio(video.audio)
|
|
374
|
+
|
|
375
|
+
if self.output_name:
|
|
376
|
+
output_name = self.output_name
|
|
377
|
+
else:
|
|
378
|
+
output_name = f"{self.video_name}_subtitled"
|
|
379
|
+
|
|
380
|
+
output_path = os.path.join(videos_dir, f"{output_name}.mp4")
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
final_video.write_videofile(
|
|
384
|
+
output_path,
|
|
385
|
+
codec="libx264",
|
|
386
|
+
audio_codec="aac",
|
|
387
|
+
temp_audiofile="temp-audio.m4a",
|
|
388
|
+
remove_temp=True,
|
|
389
|
+
logger=None,
|
|
390
|
+
fps=video.fps,
|
|
391
|
+
)
|
|
392
|
+
finally:
|
|
393
|
+
# Clean up
|
|
394
|
+
video.close()
|
|
395
|
+
final_video.close()
|
|
396
|
+
for clip in subtitle_clips:
|
|
397
|
+
try:
|
|
398
|
+
clip.close()
|
|
399
|
+
except Exception:
|
|
400
|
+
pass
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
f"Successfully added animated subtitles to {self.video_name}.mp4\n\n"
|
|
404
|
+
f"Output: {output_name}.mp4\n"
|
|
405
|
+
f"Path: {output_path}\n\n"
|
|
406
|
+
f"Details:\n"
|
|
407
|
+
f" - Duration: {video.duration:.1f} seconds\n"
|
|
408
|
+
f" - Subtitle chunks: {len(subtitle_clips)}\n"
|
|
409
|
+
f" - Style: {self.highlight_color.upper()} text, {self.words_per_clip} words per clip\n"
|
|
410
|
+
f" - Transcribed: {transcript.text[:200]}..."
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
if __name__ == "__main__":
|
|
414
|
+
# Test case
|
|
415
|
+
tool = AddSubtitles(
|
|
416
|
+
product_name="Test_Product",
|
|
417
|
+
video_name="herbaluxe_ad_v3",
|
|
418
|
+
original_script="Does your moisturizer still smell like perfume? Mine did, and my skin hated it. So I switched to HerbaLuxe—an aloe‑first daily moisturizer made with organic ingredients. It's fragrance‑free, with no essential oils and a minimal formula. First swipe feels cool and soothing. Sinks in fast—no stickiness. Layers clean under sunscreen. Skin feels calm, not coated. herbaluxe-cosmetics.com",
|
|
419
|
+
words_per_clip=4,
|
|
420
|
+
position="bottom",
|
|
421
|
+
highlight_color="white",
|
|
422
|
+
font_size=60,
|
|
423
|
+
)
|
|
424
|
+
result = tool.run()
|
|
425
|
+
print(result)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Tool for combining multiple images using Google's Gemini 2.5 Flash Image model."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
from typing import Literal
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from google import genai
|
|
9
|
+
from PIL import Image
|
|
10
|
+
from pydantic import Field, field_validator
|
|
11
|
+
|
|
12
|
+
from agency_swarm import BaseTool
|
|
13
|
+
|
|
14
|
+
from .utils.image_utils import (
|
|
15
|
+
get_images_dir,
|
|
16
|
+
MODEL_NAME,
|
|
17
|
+
load_image_by_name,
|
|
18
|
+
extract_image_parts_from_response,
|
|
19
|
+
extract_usage_metadata,
|
|
20
|
+
process_variant_result,
|
|
21
|
+
split_results_and_usage,
|
|
22
|
+
run_parallel_variants,
|
|
23
|
+
compress_image_for_base64,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CombineImages(BaseTool):
|
|
28
|
+
"""Combine multiple images using Google's Gemini 2.5 Flash Image (Nano Banana) model according to the given text instruction.
|
|
29
|
+
|
|
30
|
+
Images are saved to: mnt/{product_name}/generated_images/
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
product_name: str = Field(
|
|
34
|
+
...,
|
|
35
|
+
description="Name of the product these images are for (e.g., 'Acme_Widget_Pro', 'Green_Tea_Extract'). Used to organize files into product-specific folders.",
|
|
36
|
+
)
|
|
37
|
+
image_names: list[str] = Field(
|
|
38
|
+
...,
|
|
39
|
+
description="List of image file names (without extension) or full file paths to combine. Can mix both formats.",
|
|
40
|
+
)
|
|
41
|
+
text_instruction: str = Field(
|
|
42
|
+
...,
|
|
43
|
+
description="Text instruction describing how to combine the images",
|
|
44
|
+
)
|
|
45
|
+
file_name_or_path: str = Field(
|
|
46
|
+
...,
|
|
47
|
+
description="The name (without extension) or full path for the generated combined image file",
|
|
48
|
+
)
|
|
49
|
+
num_variants: int = Field(
|
|
50
|
+
default=1,
|
|
51
|
+
description="Number of image variants to generate (1-4, default is 1)",
|
|
52
|
+
)
|
|
53
|
+
aspect_ratio: Literal["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"] = Field(
|
|
54
|
+
default="1:1",
|
|
55
|
+
description="The aspect ratio of the generated image (default is 1:1)",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@field_validator("image_names")
|
|
59
|
+
@classmethod
|
|
60
|
+
def _validate_image_names(cls, value: list[str]) -> list[str]:
|
|
61
|
+
if not value:
|
|
62
|
+
raise ValueError("image_names must not be empty")
|
|
63
|
+
if len(value) < 2:
|
|
64
|
+
raise ValueError("At least 2 images are required for combining")
|
|
65
|
+
for name in value:
|
|
66
|
+
if not name.strip():
|
|
67
|
+
raise ValueError("Image names must not be empty")
|
|
68
|
+
return value
|
|
69
|
+
|
|
70
|
+
@field_validator("text_instruction")
|
|
71
|
+
@classmethod
|
|
72
|
+
def _instruction_not_blank(cls, value: str) -> str:
|
|
73
|
+
if not value.strip():
|
|
74
|
+
raise ValueError("text_instruction must not be empty")
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
@field_validator("file_name_or_path")
|
|
78
|
+
@classmethod
|
|
79
|
+
def _filename_not_blank(cls, value: str) -> str:
|
|
80
|
+
if not value.strip():
|
|
81
|
+
raise ValueError("file_name_or_path must not be empty")
|
|
82
|
+
return value
|
|
83
|
+
|
|
84
|
+
@field_validator("num_variants")
|
|
85
|
+
@classmethod
|
|
86
|
+
def _validate_num_variants(cls, value: int) -> int:
|
|
87
|
+
if value < 1 or value > 4:
|
|
88
|
+
raise ValueError("num_variants must be between 1 and 4")
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
async def run(self) -> list:
|
|
92
|
+
"""Combine images using the Gemini API."""
|
|
93
|
+
api_key = os.getenv("GOOGLE_API_KEY")
|
|
94
|
+
if not api_key:
|
|
95
|
+
raise ValueError("GOOGLE_API_KEY is not set. Add it to your .env to use image composition.")
|
|
96
|
+
|
|
97
|
+
client = genai.Client(api_key=api_key)
|
|
98
|
+
images_dir = get_images_dir(self.product_name)
|
|
99
|
+
|
|
100
|
+
images = []
|
|
101
|
+
for image_name_or_path in self.image_names:
|
|
102
|
+
path = Path(image_name_or_path).expanduser().resolve()
|
|
103
|
+
if path.exists():
|
|
104
|
+
images.append(Image.open(path))
|
|
105
|
+
else:
|
|
106
|
+
image, _image_path, load_error = load_image_by_name(
|
|
107
|
+
image_name_or_path, images_dir, [".png", ".jpg", ".jpeg"]
|
|
108
|
+
)
|
|
109
|
+
if load_error:
|
|
110
|
+
raise FileNotFoundError(f"Image not found: '{image_name_or_path}' (tried as path and as name in {images_dir})")
|
|
111
|
+
images.append(image)
|
|
112
|
+
|
|
113
|
+
def combine_single_variant(variant_num: int):
|
|
114
|
+
try:
|
|
115
|
+
response = client.models.generate_content(
|
|
116
|
+
model=MODEL_NAME,
|
|
117
|
+
contents=images + [self.text_instruction],
|
|
118
|
+
config=genai.types.GenerateContentConfig(
|
|
119
|
+
image_config=genai.types.ImageConfig(aspect_ratio=self.aspect_ratio),
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
usage_metadata = extract_usage_metadata(response)
|
|
123
|
+
image_parts = extract_image_parts_from_response(response)
|
|
124
|
+
if not image_parts:
|
|
125
|
+
return None
|
|
126
|
+
combined_image = Image.open(io.BytesIO(image_parts[0]))
|
|
127
|
+
result = process_variant_result(
|
|
128
|
+
variant_num,
|
|
129
|
+
combined_image,
|
|
130
|
+
self.file_name_or_path,
|
|
131
|
+
self.num_variants,
|
|
132
|
+
compress_image_for_base64,
|
|
133
|
+
images_dir,
|
|
134
|
+
)
|
|
135
|
+
result["prompt_tokens"] = float(usage_metadata.get("prompt_token_count") or 0)
|
|
136
|
+
result["candidate_tokens"] = float(usage_metadata.get("candidates_token_count") or 0)
|
|
137
|
+
return result
|
|
138
|
+
except Exception:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
raw_results = await run_parallel_variants(combine_single_variant, self.num_variants)
|
|
142
|
+
if not raw_results:
|
|
143
|
+
raise RuntimeError("No variants were successfully generated")
|
|
144
|
+
|
|
145
|
+
results, _usage = split_results_and_usage(raw_results)
|
|
146
|
+
return results
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
# Example usage with Google Gemini 2.5 Flash Image
|
|
150
|
+
import asyncio
|
|
151
|
+
tool = CombineImages(
|
|
152
|
+
product_name="Test_Product",
|
|
153
|
+
image_names=["laptop_image_variant_2", "logo_image_variant_2"],
|
|
154
|
+
text_instruction=(
|
|
155
|
+
"Take the first image of a laptop on a table. Add the logo from the second image into the middle "
|
|
156
|
+
"of the laptop. Remove the background of the logo and make it transparent. Ensure the laptop and "
|
|
157
|
+
"features remain completely unchanged. The logo should look like it's naturally attached."
|
|
158
|
+
),
|
|
159
|
+
file_name_or_path="laptop_with_logo",
|
|
160
|
+
num_variants=2,
|
|
161
|
+
)
|
|
162
|
+
try:
|
|
163
|
+
result = asyncio.run(tool.run())
|
|
164
|
+
print(result)
|
|
165
|
+
except Exception as exc:
|
|
166
|
+
print(f"Image combining failed: {exc}")
|