@_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,113 @@
|
|
|
1
|
+
"""Tool for combining multiple videos into a single video using ffmpeg."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
from pydantic import Field, field_validator
|
|
8
|
+
|
|
9
|
+
from agency_swarm import BaseTool, ToolOutputText
|
|
10
|
+
|
|
11
|
+
from .utils.video_utils import get_videos_dir, resolve_ffmpeg_executable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CombineVideos(BaseTool):
|
|
15
|
+
"""Combine multiple videos into a single video using instant cut transitions (ffmpeg).
|
|
16
|
+
|
|
17
|
+
Videos are saved to: mnt/{product_name}/generated_videos/
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
product_name: str = Field(
|
|
21
|
+
...,
|
|
22
|
+
description="Name of the product these videos are for (e.g., 'Acme_Widget_Pro', 'Green_Tea_Extract'). Used to locate and save videos in product-specific folders.",
|
|
23
|
+
)
|
|
24
|
+
video_names: list[str] = Field(
|
|
25
|
+
...,
|
|
26
|
+
description="List of video file names (without extension) to combine in order.",
|
|
27
|
+
)
|
|
28
|
+
name: str = Field(
|
|
29
|
+
...,
|
|
30
|
+
description="The name for the combined video file (without extension)",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@field_validator("video_names")
|
|
34
|
+
@classmethod
|
|
35
|
+
def _validate_video_names(cls, value: list[str]) -> list[str]:
|
|
36
|
+
if not value:
|
|
37
|
+
raise ValueError("video_names must not be empty")
|
|
38
|
+
if len(value) < 2:
|
|
39
|
+
raise ValueError("At least 2 videos are required for combining")
|
|
40
|
+
for name in value:
|
|
41
|
+
if not name.strip():
|
|
42
|
+
raise ValueError("Video names must not be empty")
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
@field_validator("name")
|
|
46
|
+
@classmethod
|
|
47
|
+
def _name_not_blank(cls, value: str) -> str:
|
|
48
|
+
if not value.strip():
|
|
49
|
+
raise ValueError("name must not be empty")
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
def run(self) -> list:
|
|
53
|
+
"""Combine videos using ffmpeg concat demuxer."""
|
|
54
|
+
videos_dir = get_videos_dir(self.product_name)
|
|
55
|
+
|
|
56
|
+
video_paths = []
|
|
57
|
+
for video_name in self.video_names:
|
|
58
|
+
video_path = os.path.join(videos_dir, f"{video_name}.mp4")
|
|
59
|
+
if not os.path.exists(video_path):
|
|
60
|
+
raise FileNotFoundError(
|
|
61
|
+
f"Video file not found: {video_path}. "
|
|
62
|
+
f"Make sure the video exists in the {videos_dir} directory."
|
|
63
|
+
)
|
|
64
|
+
video_paths.append(video_path)
|
|
65
|
+
|
|
66
|
+
output_path = os.path.join(videos_dir, f"{self.name}.mp4")
|
|
67
|
+
|
|
68
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8') as f:
|
|
69
|
+
for path in video_paths:
|
|
70
|
+
abs_path = os.path.abspath(path).replace('\\', '/')
|
|
71
|
+
escaped_path = abs_path.replace("'", "'\\''")
|
|
72
|
+
f.write(f"file '{escaped_path}'\n")
|
|
73
|
+
concat_file = f.name
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
ffmpeg_executable = resolve_ffmpeg_executable()
|
|
77
|
+
cmd = [
|
|
78
|
+
ffmpeg_executable,
|
|
79
|
+
'-f', 'concat',
|
|
80
|
+
'-safe', '0',
|
|
81
|
+
'-i', concat_file,
|
|
82
|
+
'-c', 'copy', # copy streams without re-encoding
|
|
83
|
+
'-y',
|
|
84
|
+
output_path
|
|
85
|
+
]
|
|
86
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
87
|
+
if result.returncode != 0:
|
|
88
|
+
raise RuntimeError(f"FFmpeg concatenation failed: {result.stderr}")
|
|
89
|
+
finally:
|
|
90
|
+
try:
|
|
91
|
+
os.unlink(concat_file)
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
lines = [f"Combined {len(self.video_names)} videos:"]
|
|
96
|
+
for i, name in enumerate(self.video_names, 1):
|
|
97
|
+
lines.append(f" {i}. {name}.mp4")
|
|
98
|
+
lines.append(f"\nOutput: {self.name}.mp4")
|
|
99
|
+
lines.append(f"Path: {output_path}")
|
|
100
|
+
|
|
101
|
+
return [ToolOutputText(type="text", text="\n".join(lines))]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
# Example usage
|
|
106
|
+
tool = CombineVideos(
|
|
107
|
+
product_name="Test_Product",
|
|
108
|
+
video_names=["herbaluxe_01_hook_v2","herbaluxe_02_formula","herbaluxe_03_result_consistency_fix","herbaluxe_04_cta"],
|
|
109
|
+
name="x_combine_test",
|
|
110
|
+
)
|
|
111
|
+
result = tool.run()
|
|
112
|
+
print(result)
|
|
113
|
+
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Tool for combining audio from one video with visuals from another video."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import asyncio
|
|
6
|
+
import cv2
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from pydantic import Field, field_validator
|
|
11
|
+
from PIL import Image
|
|
12
|
+
|
|
13
|
+
from agency_swarm import BaseTool, ToolOutputText
|
|
14
|
+
|
|
15
|
+
from .utils.video_utils import (
|
|
16
|
+
get_videos_dir,
|
|
17
|
+
ensure_not_blank,
|
|
18
|
+
generate_spritesheet,
|
|
19
|
+
extract_last_frame,
|
|
20
|
+
create_image_output,
|
|
21
|
+
resolve_ffmpeg_executable,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EditAudio(BaseTool):
|
|
26
|
+
"""
|
|
27
|
+
Combine audio from one video with visuals from another video.
|
|
28
|
+
|
|
29
|
+
Useful for adding b-roll footage over narration, or replacing visuals while keeping original audio.
|
|
30
|
+
Supports padding to offset video timing relative to audio.
|
|
31
|
+
|
|
32
|
+
Videos are saved to: mnt/{product_name}/generated_videos/
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
product_name: str = Field(
|
|
36
|
+
...,
|
|
37
|
+
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.",
|
|
38
|
+
)
|
|
39
|
+
audio_source: str = Field(
|
|
40
|
+
...,
|
|
41
|
+
description=(
|
|
42
|
+
"The video to extract audio from. Can be: "
|
|
43
|
+
"1) Video name without extension (searches generated_videos folder), "
|
|
44
|
+
"2) Full local path to video file."
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
video_source: str = Field(
|
|
48
|
+
...,
|
|
49
|
+
description=(
|
|
50
|
+
"The video to use for visuals/b-roll. Can be: "
|
|
51
|
+
"1) Video name without extension (searches generated_videos folder), "
|
|
52
|
+
"2) Full local path to video file."
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
output_name: str = Field(
|
|
56
|
+
...,
|
|
57
|
+
description="The name for the combined video file (without extension)",
|
|
58
|
+
)
|
|
59
|
+
pad_seconds: float = Field(
|
|
60
|
+
default=0.0,
|
|
61
|
+
description=(
|
|
62
|
+
"Seconds to offset the video relative to audio. "
|
|
63
|
+
"Negative = video starts before audio (e.g., -2.0 = video plays 2s before audio), "
|
|
64
|
+
"Positive = video starts after audio (e.g., 2.0 = video plays 2s after audio starts), "
|
|
65
|
+
"Zero = video and audio start together (default: 0.0)"
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@field_validator("audio_source")
|
|
70
|
+
@classmethod
|
|
71
|
+
def _audio_not_blank(cls, value: str) -> str:
|
|
72
|
+
return ensure_not_blank(value, "audio_source")
|
|
73
|
+
|
|
74
|
+
@field_validator("video_source")
|
|
75
|
+
@classmethod
|
|
76
|
+
def _video_not_blank(cls, value: str) -> str:
|
|
77
|
+
return ensure_not_blank(value, "video_source")
|
|
78
|
+
|
|
79
|
+
@field_validator("output_name")
|
|
80
|
+
@classmethod
|
|
81
|
+
def _output_not_blank(cls, value: str) -> str:
|
|
82
|
+
return ensure_not_blank(value, "output_name")
|
|
83
|
+
|
|
84
|
+
async def run(self) -> list:
|
|
85
|
+
"""Mix audio and video from two different sources."""
|
|
86
|
+
audio_path = self._resolve_video_path(self.audio_source)
|
|
87
|
+
video_path = self._resolve_video_path(self.video_source)
|
|
88
|
+
|
|
89
|
+
videos_dir = get_videos_dir(self.product_name)
|
|
90
|
+
output_path = os.path.join(videos_dir, f"{self.output_name}.mp4")
|
|
91
|
+
|
|
92
|
+
loop = asyncio.get_event_loop()
|
|
93
|
+
await loop.run_in_executor(None, self._mix_audio_video_blocking, audio_path, video_path, output_path)
|
|
94
|
+
|
|
95
|
+
output = []
|
|
96
|
+
|
|
97
|
+
spritesheet_path = os.path.join(videos_dir, f"{self.output_name}_spritesheet.jpg")
|
|
98
|
+
spritesheet = await loop.run_in_executor(None, generate_spritesheet, output_path, spritesheet_path)
|
|
99
|
+
if spritesheet:
|
|
100
|
+
output.extend(create_image_output(spritesheet_path, f"{self.output_name}_spritesheet.jpg"))
|
|
101
|
+
|
|
102
|
+
thumbnail_path = os.path.join(videos_dir, f"{self.output_name}_thumbnail.jpg")
|
|
103
|
+
thumbnail = await loop.run_in_executor(None, self._extract_first_frame, output_path, thumbnail_path)
|
|
104
|
+
if thumbnail:
|
|
105
|
+
output.extend(create_image_output(thumbnail_path, f"{self.output_name}_thumbnail.jpg"))
|
|
106
|
+
|
|
107
|
+
last_frame_path = os.path.join(videos_dir, f"{self.output_name}_last_frame.jpg")
|
|
108
|
+
last_frame = await loop.run_in_executor(None, extract_last_frame, output_path, last_frame_path)
|
|
109
|
+
if last_frame:
|
|
110
|
+
output.extend(create_image_output(last_frame_path, f"{self.output_name}_last_frame.jpg"))
|
|
111
|
+
|
|
112
|
+
pad_info = ""
|
|
113
|
+
if self.pad_seconds != 0.0:
|
|
114
|
+
if self.pad_seconds < 0:
|
|
115
|
+
pad_info = f"\nPadding: Video starts {abs(self.pad_seconds)}s before audio"
|
|
116
|
+
else:
|
|
117
|
+
pad_info = f"\nPadding: Video starts {self.pad_seconds}s after audio"
|
|
118
|
+
|
|
119
|
+
output.append(ToolOutputText(
|
|
120
|
+
type="text",
|
|
121
|
+
text=f"Audio and video mixed successfully!\nSaved to: `{self.output_name}.mp4`\nPath: {output_path}{pad_info}"
|
|
122
|
+
))
|
|
123
|
+
|
|
124
|
+
return output
|
|
125
|
+
|
|
126
|
+
def _resolve_video_path(self, video_ref: str) -> str:
|
|
127
|
+
"""Resolve video reference to full path."""
|
|
128
|
+
# Try as full path first
|
|
129
|
+
path = Path(video_ref).expanduser().resolve()
|
|
130
|
+
|
|
131
|
+
if path.exists():
|
|
132
|
+
return str(path)
|
|
133
|
+
|
|
134
|
+
# Try as video name without extension in generated_videos
|
|
135
|
+
videos_dir = get_videos_dir(self.product_name)
|
|
136
|
+
|
|
137
|
+
for ext in [".mp4", ".mov", ".avi", ".webm"]:
|
|
138
|
+
potential_path = os.path.join(videos_dir, f"{video_ref}{ext}")
|
|
139
|
+
if os.path.exists(potential_path):
|
|
140
|
+
return potential_path
|
|
141
|
+
|
|
142
|
+
raise FileNotFoundError(
|
|
143
|
+
f"Video '{video_ref}' not found in {videos_dir}. "
|
|
144
|
+
f"Tried extensions: .mp4, .mov, .avi, .webm"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def _mix_audio_video_blocking(self, audio_path: str, video_path: str, output_path: str) -> None:
|
|
148
|
+
"""Mix audio and video using ffmpeg (blocking operation)."""
|
|
149
|
+
cap_audio = cv2.VideoCapture(audio_path)
|
|
150
|
+
fps_audio = cap_audio.get(cv2.CAP_PROP_FPS)
|
|
151
|
+
frames_audio = int(cap_audio.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
152
|
+
audio_duration = frames_audio / fps_audio if fps_audio > 0 else 0
|
|
153
|
+
cap_audio.release()
|
|
154
|
+
|
|
155
|
+
cap_video = cv2.VideoCapture(video_path)
|
|
156
|
+
fps_video = cap_video.get(cv2.CAP_PROP_FPS)
|
|
157
|
+
frames_video = int(cap_video.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
158
|
+
video_duration = frames_video / fps_video if fps_video > 0 else 0
|
|
159
|
+
cap_video.release()
|
|
160
|
+
|
|
161
|
+
ffmpeg_executable = resolve_ffmpeg_executable()
|
|
162
|
+
|
|
163
|
+
if self.pad_seconds == 0.0:
|
|
164
|
+
# Simple case: no padding, just combine audio and video
|
|
165
|
+
ffmpeg_cmd = [
|
|
166
|
+
ffmpeg_executable,
|
|
167
|
+
"-y", # Overwrite output file
|
|
168
|
+
"-i", video_path, # Input 0: video source
|
|
169
|
+
"-i", audio_path, # Input 1: audio source
|
|
170
|
+
"-map", "0:v:0", # Use video from input 0
|
|
171
|
+
"-map", "1:a:0", # Use audio from input 1
|
|
172
|
+
"-c:v", "libx264", # Video codec
|
|
173
|
+
"-preset", "medium",
|
|
174
|
+
"-crf", "23",
|
|
175
|
+
"-c:a", "aac", # Audio codec
|
|
176
|
+
"-b:a", "128k",
|
|
177
|
+
"-movflags", "+faststart",
|
|
178
|
+
"-pix_fmt", "yuv420p",
|
|
179
|
+
"-t", str(video_duration), # Use video duration as master timeline
|
|
180
|
+
output_path
|
|
181
|
+
]
|
|
182
|
+
else:
|
|
183
|
+
# Complex case: apply padding offset
|
|
184
|
+
# Negative pad = delay audio (video starts first)
|
|
185
|
+
# Positive pad = delay video (audio starts first)
|
|
186
|
+
|
|
187
|
+
if self.pad_seconds < 0:
|
|
188
|
+
# Video starts before audio - delay audio track
|
|
189
|
+
audio_delay = abs(self.pad_seconds)
|
|
190
|
+
ffmpeg_cmd = [
|
|
191
|
+
ffmpeg_executable,
|
|
192
|
+
"-y",
|
|
193
|
+
"-i", video_path,
|
|
194
|
+
"-i", audio_path,
|
|
195
|
+
"-filter_complex",
|
|
196
|
+
f"[1:a]adelay={int(audio_delay * 1000)}|{int(audio_delay * 1000)}[a]", # Delay audio in milliseconds
|
|
197
|
+
"-map", "0:v:0",
|
|
198
|
+
"-map", "[a]",
|
|
199
|
+
"-c:v", "libx264",
|
|
200
|
+
"-preset", "medium",
|
|
201
|
+
"-crf", "23",
|
|
202
|
+
"-c:a", "aac",
|
|
203
|
+
"-b:a", "128k",
|
|
204
|
+
"-movflags", "+faststart",
|
|
205
|
+
"-pix_fmt", "yuv420p",
|
|
206
|
+
"-t", str(video_duration), # Use video duration as master timeline
|
|
207
|
+
output_path
|
|
208
|
+
]
|
|
209
|
+
else:
|
|
210
|
+
# Audio starts before video - delay video track
|
|
211
|
+
video_delay = self.pad_seconds
|
|
212
|
+
ffmpeg_cmd = [
|
|
213
|
+
ffmpeg_executable,
|
|
214
|
+
"-y",
|
|
215
|
+
"-i", video_path,
|
|
216
|
+
"-i", audio_path,
|
|
217
|
+
"-filter_complex",
|
|
218
|
+
f"[0:v]setpts=PTS+{video_delay}/TB[v]", # Delay video
|
|
219
|
+
"-map", "[v]",
|
|
220
|
+
"-map", "1:a:0",
|
|
221
|
+
"-c:v", "libx264",
|
|
222
|
+
"-preset", "medium",
|
|
223
|
+
"-crf", "23",
|
|
224
|
+
"-c:a", "aac",
|
|
225
|
+
"-b:a", "128k",
|
|
226
|
+
"-movflags", "+faststart",
|
|
227
|
+
"-pix_fmt", "yuv420p",
|
|
228
|
+
"-t", str(video_duration), # Use video duration as master timeline
|
|
229
|
+
output_path
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
subprocess.run(
|
|
234
|
+
ffmpeg_cmd,
|
|
235
|
+
stdout=subprocess.PIPE,
|
|
236
|
+
stderr=subprocess.PIPE,
|
|
237
|
+
text=True,
|
|
238
|
+
check=True
|
|
239
|
+
)
|
|
240
|
+
except subprocess.CalledProcessError as e:
|
|
241
|
+
raise RuntimeError(
|
|
242
|
+
f"ffmpeg failed to mix audio and video. Error: {e.stderr}"
|
|
243
|
+
)
|
|
244
|
+
except FileNotFoundError:
|
|
245
|
+
raise RuntimeError(
|
|
246
|
+
"ffmpeg not found. Please install ffmpeg and ensure it's in your PATH. "
|
|
247
|
+
"Download from: https://ffmpeg.org/download.html"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def _extract_first_frame(self, video_path: str, output_path: str) -> Optional[object]:
|
|
251
|
+
"""Extract the first frame from video as thumbnail."""
|
|
252
|
+
cap = cv2.VideoCapture(video_path)
|
|
253
|
+
ret, frame = cap.read()
|
|
254
|
+
cap.release()
|
|
255
|
+
|
|
256
|
+
if not ret:
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
# Convert BGR to RGB
|
|
260
|
+
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
261
|
+
thumbnail_image = Image.fromarray(frame_rgb)
|
|
262
|
+
thumbnail_image.save(output_path)
|
|
263
|
+
|
|
264
|
+
return thumbnail_image
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
if __name__ == "__main__":
|
|
268
|
+
# Check if test videos exist
|
|
269
|
+
test_dir = Path(__file__).parent.parent.parent / "mnt" / "Test_Product" / "generated_videos"
|
|
270
|
+
video1 = test_dir / "test_video.mp4"
|
|
271
|
+
video2 = test_dir / "test_video_trimmed_last2s.mp4"
|
|
272
|
+
|
|
273
|
+
if not video1.exists() or not video2.exists():
|
|
274
|
+
print("Test videos not found. Skipping test.")
|
|
275
|
+
print(f"Expected: {video1}")
|
|
276
|
+
print(f"Expected: {video2}")
|
|
277
|
+
else:
|
|
278
|
+
# Example: Combine audio from one video with b-roll from another
|
|
279
|
+
# Audio is 4s, b-roll is 26.8s - b-roll continues after audio ends
|
|
280
|
+
# Video starts 5 seconds after audio (audio pre-roll)
|
|
281
|
+
tool = EditAudio(
|
|
282
|
+
product_name="Test_Product",
|
|
283
|
+
audio_source="test_video", # Audio from this video (4s)
|
|
284
|
+
video_source="Ad2_3seg_AI_Employee_UGC_final", # Visuals from this video (26.8s)
|
|
285
|
+
output_name="mixed_video",
|
|
286
|
+
pad_seconds=-3.0, # Video starts 5 seconds after audio begins
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
result = asyncio.run(tool.run())
|
|
291
|
+
print("\nMix complete!")
|
|
292
|
+
for item in result:
|
|
293
|
+
if hasattr(item, 'text'):
|
|
294
|
+
print(item.text)
|
|
295
|
+
except Exception as exc:
|
|
296
|
+
print(f"Audio/video mixing failed: {exc}")
|
|
297
|
+
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Tool for editing images using Google's Gemini 2.5 Flash Image model."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from google import genai
|
|
8
|
+
from pydantic import Field, field_validator
|
|
9
|
+
|
|
10
|
+
from agency_swarm import BaseTool
|
|
11
|
+
|
|
12
|
+
from .utils.image_utils import (
|
|
13
|
+
get_images_dir,
|
|
14
|
+
MODEL_NAME,
|
|
15
|
+
load_image_by_name,
|
|
16
|
+
extract_image_from_response,
|
|
17
|
+
extract_usage_metadata,
|
|
18
|
+
process_variant_result,
|
|
19
|
+
split_results_and_usage,
|
|
20
|
+
run_parallel_variants,
|
|
21
|
+
compress_image_for_base64,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EditImage(BaseTool):
|
|
26
|
+
"""Edit existing images using Google's Gemini 2.5 Flash Image (Nano Banana) model.
|
|
27
|
+
|
|
28
|
+
Images are saved to: mnt/{product_name}/generated_images/
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
product_name: str = Field(
|
|
32
|
+
...,
|
|
33
|
+
description="Name of the product this image is for (e.g., 'Acme_Widget_Pro', 'Green_Tea_Extract'). Used to organize files into product-specific folders.",
|
|
34
|
+
)
|
|
35
|
+
input_image_name: str = Field(
|
|
36
|
+
...,
|
|
37
|
+
description="Name of the existing image file to edit (without extension).",
|
|
38
|
+
)
|
|
39
|
+
edit_prompt: str = Field(
|
|
40
|
+
...,
|
|
41
|
+
description="Text prompt describing the edits to make to the image",
|
|
42
|
+
)
|
|
43
|
+
output_image_name: str = Field(
|
|
44
|
+
...,
|
|
45
|
+
description="The name for the generated edited image file (without extension)",
|
|
46
|
+
)
|
|
47
|
+
num_variants: int = Field(
|
|
48
|
+
default=1,
|
|
49
|
+
description="Number of image variants to generate (1-4, default is 1)",
|
|
50
|
+
)
|
|
51
|
+
aspect_ratio: Literal["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"] = Field(
|
|
52
|
+
default="1:1",
|
|
53
|
+
description="The aspect ratio of the generated image (default is 1:1)",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@field_validator("input_image_name")
|
|
57
|
+
@classmethod
|
|
58
|
+
def _input_name_not_blank(cls, value: str) -> str:
|
|
59
|
+
if not value.strip():
|
|
60
|
+
raise ValueError("input_image_name must not be empty")
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
@field_validator("edit_prompt")
|
|
64
|
+
@classmethod
|
|
65
|
+
def _prompt_not_blank(cls, value: str) -> str:
|
|
66
|
+
if not value.strip():
|
|
67
|
+
raise ValueError("edit_prompt must not be empty")
|
|
68
|
+
return value
|
|
69
|
+
|
|
70
|
+
@field_validator("output_image_name")
|
|
71
|
+
@classmethod
|
|
72
|
+
def _output_name_not_blank(cls, value: str) -> str:
|
|
73
|
+
if not value.strip():
|
|
74
|
+
raise ValueError("output_image_name must not be empty")
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
@field_validator("num_variants")
|
|
78
|
+
@classmethod
|
|
79
|
+
def _validate_num_variants(cls, value: int) -> int:
|
|
80
|
+
if value < 1 or value > 4:
|
|
81
|
+
raise ValueError("num_variants must be between 1 and 4")
|
|
82
|
+
return value
|
|
83
|
+
|
|
84
|
+
async def run(self) -> list:
|
|
85
|
+
"""Edit an image using the Gemini API."""
|
|
86
|
+
api_key = os.getenv("GOOGLE_API_KEY")
|
|
87
|
+
if not api_key:
|
|
88
|
+
raise ValueError("GOOGLE_API_KEY is not set. Add it to your .env to use image editing.")
|
|
89
|
+
|
|
90
|
+
client = genai.Client(api_key=api_key)
|
|
91
|
+
images_dir = get_images_dir(self.product_name)
|
|
92
|
+
|
|
93
|
+
image, _image_path, load_error = load_image_by_name(self.input_image_name, images_dir)
|
|
94
|
+
if load_error:
|
|
95
|
+
raise FileNotFoundError(load_error)
|
|
96
|
+
|
|
97
|
+
def edit_single_variant(variant_num: int):
|
|
98
|
+
try:
|
|
99
|
+
response = client.models.generate_content(
|
|
100
|
+
model=MODEL_NAME,
|
|
101
|
+
contents=[self.edit_prompt, image],
|
|
102
|
+
config=genai.types.GenerateContentConfig(
|
|
103
|
+
image_config=genai.types.ImageConfig(aspect_ratio=self.aspect_ratio),
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
usage_metadata = extract_usage_metadata(response)
|
|
107
|
+
edited_image, _text = extract_image_from_response(response)
|
|
108
|
+
if edited_image is None:
|
|
109
|
+
return None
|
|
110
|
+
result = process_variant_result(
|
|
111
|
+
variant_num,
|
|
112
|
+
edited_image,
|
|
113
|
+
self.output_image_name,
|
|
114
|
+
self.num_variants,
|
|
115
|
+
compress_image_for_base64,
|
|
116
|
+
images_dir,
|
|
117
|
+
)
|
|
118
|
+
result["prompt_tokens"] = float(usage_metadata.get("prompt_token_count") or 0)
|
|
119
|
+
result["candidate_tokens"] = float(usage_metadata.get("candidates_token_count") or 0)
|
|
120
|
+
return result
|
|
121
|
+
except Exception:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
raw_results = await run_parallel_variants(edit_single_variant, self.num_variants)
|
|
125
|
+
if not raw_results:
|
|
126
|
+
raise RuntimeError("No variants were successfully generated")
|
|
127
|
+
|
|
128
|
+
results, _usage = split_results_and_usage(raw_results)
|
|
129
|
+
return results
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
# Example usage with Google Gemini 2.5 Flash Image
|
|
133
|
+
tool = EditImage(
|
|
134
|
+
product_name="Test_Product",
|
|
135
|
+
input_image_name="logo_image_variant_1",
|
|
136
|
+
edit_prompt="Change the logo color from red to blue",
|
|
137
|
+
output_image_name="logo_image_edited",
|
|
138
|
+
num_variants=1,
|
|
139
|
+
)
|
|
140
|
+
try:
|
|
141
|
+
result = asyncio.run(tool.run())
|
|
142
|
+
print(result)
|
|
143
|
+
except Exception as exc:
|
|
144
|
+
print(f"Image editing failed: {exc}")
|