@_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,556 @@
|
|
|
1
|
+
"""Video generation tool supporting Sora (OpenAI), Veo (Google Gemini), and Seedance (fal.ai) models."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal, Optional
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import mimetypes
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
import fal_client
|
|
13
|
+
import httpx
|
|
14
|
+
from openai import OpenAI
|
|
15
|
+
from pydantic import Field, field_validator, model_validator
|
|
16
|
+
from google.genai.types import GenerateVideosConfig, Image, VideoGenerationReferenceImage
|
|
17
|
+
from PIL import Image as PILImage
|
|
18
|
+
from io import BytesIO
|
|
19
|
+
|
|
20
|
+
from agency_swarm import BaseTool, ToolOutputText
|
|
21
|
+
|
|
22
|
+
from .utils.video_utils import (
|
|
23
|
+
ensure_not_blank,
|
|
24
|
+
extract_last_frame,
|
|
25
|
+
generate_spritesheet,
|
|
26
|
+
get_gemini_client,
|
|
27
|
+
get_videos_dir,
|
|
28
|
+
is_veo_model,
|
|
29
|
+
is_sora_model,
|
|
30
|
+
is_seedance_model,
|
|
31
|
+
resolve_input_reference,
|
|
32
|
+
validate_resolution,
|
|
33
|
+
save_video_with_metadata,
|
|
34
|
+
save_veo_video_with_metadata,
|
|
35
|
+
)
|
|
36
|
+
from .utils.image_utils import load_image_by_name, get_images_dir
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
VIDEO_GENERATION_TIMEOUT_SECONDS = 300
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _is_transient_network_error(exc: Exception) -> bool:
|
|
44
|
+
if isinstance(exc, (BrokenPipeError, ConnectionResetError, TimeoutError)):
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
message = str(exc).lower()
|
|
48
|
+
transient_markers = (
|
|
49
|
+
"broken pipe",
|
|
50
|
+
"connection reset",
|
|
51
|
+
"timed out",
|
|
52
|
+
"timeout",
|
|
53
|
+
"temporarily unavailable",
|
|
54
|
+
)
|
|
55
|
+
return any(marker in message for marker in transient_markers)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class GenerateVideo(BaseTool):
|
|
59
|
+
"""
|
|
60
|
+
Generates a video using OpenAI Sora, Google Veo, or ByteDance Seedance 1.5 Pro (via fal.ai).
|
|
61
|
+
|
|
62
|
+
Tool is stateless and does not maintain any characters / scenes / etc between calls.
|
|
63
|
+
It does not support variables like [INTERNAL PROMPT] in the prompt.
|
|
64
|
+
|
|
65
|
+
**Important**: Sora 2 and Sora 2 Pro do not support reference images with faces.
|
|
66
|
+
|
|
67
|
+
Videos are saved to: mnt/{product_name}/generated_videos/
|
|
68
|
+
"""
|
|
69
|
+
product_name: str = Field(
|
|
70
|
+
...,
|
|
71
|
+
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.",
|
|
72
|
+
)
|
|
73
|
+
prompt: str = Field(
|
|
74
|
+
...,
|
|
75
|
+
description=(
|
|
76
|
+
"Detailed marketing description of the desired video. Include subjects, "
|
|
77
|
+
"camera motion, lighting, and mood for the video generation model. "
|
|
78
|
+
"Be ware that sometimes including things you DONT want to display in the video "
|
|
79
|
+
"can cause the model to generate them instead. Simply don't mention what you don't want to see in the prompt."
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
name: str = Field(
|
|
83
|
+
...,
|
|
84
|
+
description="The name for the generated video file (without extension)",
|
|
85
|
+
)
|
|
86
|
+
model: Literal[
|
|
87
|
+
"sora-2",
|
|
88
|
+
"sora-2-pro",
|
|
89
|
+
"veo-3.1-generate-preview",
|
|
90
|
+
"veo-3.1-fast-generate-preview",
|
|
91
|
+
"seedance-1.5-pro",
|
|
92
|
+
] = Field(
|
|
93
|
+
...,
|
|
94
|
+
description="Video generation model to use.",
|
|
95
|
+
)
|
|
96
|
+
seconds: int = Field(
|
|
97
|
+
default=8,
|
|
98
|
+
ge=4,
|
|
99
|
+
le=12,
|
|
100
|
+
description=(
|
|
101
|
+
"Clip length in seconds. "
|
|
102
|
+
"Sora: 4, 8, or 12. "
|
|
103
|
+
"Veo: 4, 6, or 8. "
|
|
104
|
+
"Seedance 1.5 Pro: any integer 4–12."
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
first_frame_ref: Optional[str] = Field(
|
|
108
|
+
default=None,
|
|
109
|
+
description=(
|
|
110
|
+
"Optional first frame reference image for image-to-video. Can be: "
|
|
111
|
+
"1) Image name without extension (searches generated_images and generated_videos folders), "
|
|
112
|
+
"2) Full local path, or 3) HTTPS URL."
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
asset_image_ref: Optional[str] = Field(
|
|
116
|
+
default=None,
|
|
117
|
+
description=(
|
|
118
|
+
"Optional asset reference image for Veo (subject/asset guidance). Can be: "
|
|
119
|
+
"1) Image name without extension (searches generated_images and generated_videos folders), "
|
|
120
|
+
"2) Full local path, or 3) HTTPS URL."
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
size: Literal['720x1280', '1280x720', '1024x1792', '1792x1024'] = Field(
|
|
124
|
+
default='1280x720',
|
|
125
|
+
description="Optional resolution in WIDTHxHEIGHT format (e.g. 1280x720). For Sora: exact resolution. For Veo: reference image will be cropped/resized to match this aspect ratio to prevent stretching.",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
@field_validator("prompt")
|
|
129
|
+
@classmethod
|
|
130
|
+
def _prompt_not_blank(cls, value: str) -> str:
|
|
131
|
+
if re.match(r"^\[.*\]", value) or re.match(r"^\[.*?\]\s+.+", value):
|
|
132
|
+
raise ValueError("PROMPT CANNNOT CONTAIN VARIABLES!!! YOU HAVE TO REWRITE THE WHOLE PROMPT FROM SCRATCH!!! THIS TOOL IS STATELESS. STOP BEING LAZY AND REWRITE THE WHOLE PROMPT FOR USER'S FEEDBACK.")
|
|
133
|
+
return ensure_not_blank(value, "prompt")
|
|
134
|
+
|
|
135
|
+
@field_validator("first_frame_ref")
|
|
136
|
+
@classmethod
|
|
137
|
+
def _reference_not_blank(cls, value: Optional[str]) -> Optional[str]:
|
|
138
|
+
if value is not None:
|
|
139
|
+
ensure_not_blank(value, "first_frame_ref")
|
|
140
|
+
return value
|
|
141
|
+
|
|
142
|
+
@field_validator("asset_image_ref")
|
|
143
|
+
@classmethod
|
|
144
|
+
def _asset_reference_not_blank(cls, value: Optional[str]) -> Optional[str]:
|
|
145
|
+
if value is not None:
|
|
146
|
+
ensure_not_blank(value, "asset_image_ref")
|
|
147
|
+
return value
|
|
148
|
+
|
|
149
|
+
@field_validator("size")
|
|
150
|
+
@classmethod
|
|
151
|
+
def _size_format(cls, value: Optional[str]) -> Optional[str]:
|
|
152
|
+
return validate_resolution(value)
|
|
153
|
+
|
|
154
|
+
@model_validator(mode="after")
|
|
155
|
+
def _validate_seconds_for_model(self) -> "GenerateVideo":
|
|
156
|
+
if is_sora_model(self.model) and self.seconds not in {4, 8, 12}:
|
|
157
|
+
raise ValueError("Sora supports only 4, 8, or 12 second clips.")
|
|
158
|
+
if is_veo_model(self.model) and self.seconds not in {4, 6, 8}:
|
|
159
|
+
raise ValueError("Veo supports only 4, 6, or 8 second clips.")
|
|
160
|
+
if is_sora_model(self.model) and self.asset_image_ref is not None:
|
|
161
|
+
raise ValueError("Sora does not support asset_image_ref. Use first_frame_ref instead.")
|
|
162
|
+
if is_seedance_model(self.model) and self.asset_image_ref is not None:
|
|
163
|
+
raise ValueError("Seedance does not support asset_image_ref. Use first_frame_ref instead.")
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
async def run(self) -> list:
|
|
167
|
+
"""Generate a marketing video using the chosen model."""
|
|
168
|
+
if is_seedance_model(self.model):
|
|
169
|
+
return await self._generate_with_seedance(self.model)
|
|
170
|
+
|
|
171
|
+
if is_sora_model(self.model):
|
|
172
|
+
return await self._generate_with_sora(self.model)
|
|
173
|
+
|
|
174
|
+
if is_veo_model(self.model):
|
|
175
|
+
return await self._generate_with_veo(self.model)
|
|
176
|
+
|
|
177
|
+
raise ValueError(f"Unsupported video model: {self.model}")
|
|
178
|
+
|
|
179
|
+
async def _generate_with_sora(self, model: str) -> dict:
|
|
180
|
+
"""Generate video using OpenAI's Sora API."""
|
|
181
|
+
|
|
182
|
+
api_key = os.getenv("OPENAI_API_KEY")
|
|
183
|
+
if not api_key:
|
|
184
|
+
raise RuntimeError(
|
|
185
|
+
"OPENAI_API_KEY environment variable is required for video operations"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
client: OpenAI = OpenAI(api_key=api_key)
|
|
189
|
+
reference_file = None
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
reference_file = resolve_input_reference(
|
|
193
|
+
self.first_frame_ref,
|
|
194
|
+
target_size=self.size if self.first_frame_ref else None,
|
|
195
|
+
product_name=self.product_name
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
request_payload = {
|
|
199
|
+
"prompt": self.prompt,
|
|
200
|
+
"model": model,
|
|
201
|
+
"seconds": str(self.seconds),
|
|
202
|
+
}
|
|
203
|
+
if self.size:
|
|
204
|
+
request_payload["size"] = self.size
|
|
205
|
+
if reference_file is not None:
|
|
206
|
+
request_payload["input_reference"] = reference_file
|
|
207
|
+
|
|
208
|
+
logger.info(f"Submitting video generation request to Sora ({model})...")
|
|
209
|
+
|
|
210
|
+
# Run blocking operation in thread pool to avoid blocking event loop
|
|
211
|
+
loop = asyncio.get_event_loop()
|
|
212
|
+
try:
|
|
213
|
+
video = await loop.run_in_executor(
|
|
214
|
+
None,
|
|
215
|
+
lambda: client.videos.create(**request_payload)
|
|
216
|
+
)
|
|
217
|
+
except Exception as exc:
|
|
218
|
+
if _is_transient_network_error(exc):
|
|
219
|
+
raise RuntimeError(
|
|
220
|
+
f"Sora submission hit a transient network error: {exc}. "
|
|
221
|
+
"Please retry this GenerateVideo call."
|
|
222
|
+
) from exc
|
|
223
|
+
raise
|
|
224
|
+
|
|
225
|
+
started_at = asyncio.get_running_loop().time()
|
|
226
|
+
while getattr(video, "status", None) not in {"completed", "failed", "cancelled"}:
|
|
227
|
+
logger.info("Waiting for Sora video generation to complete...")
|
|
228
|
+
elapsed = asyncio.get_running_loop().time() - started_at
|
|
229
|
+
if elapsed > VIDEO_GENERATION_TIMEOUT_SECONDS:
|
|
230
|
+
raise RuntimeError(
|
|
231
|
+
f"Sora video generation timed out after {VIDEO_GENERATION_TIMEOUT_SECONDS} seconds. Openai server is likely overloaded."
|
|
232
|
+
"Please try again later or use a different model."
|
|
233
|
+
)
|
|
234
|
+
await asyncio.sleep(10)
|
|
235
|
+
try:
|
|
236
|
+
video = await loop.run_in_executor(
|
|
237
|
+
None,
|
|
238
|
+
lambda: client.videos.retrieve(video.id)
|
|
239
|
+
)
|
|
240
|
+
except Exception as exc:
|
|
241
|
+
if not _is_transient_network_error(exc):
|
|
242
|
+
raise
|
|
243
|
+
logger.warning(f"Transient Sora polling error: {exc}. Retrying same video id...")
|
|
244
|
+
|
|
245
|
+
logger.info(f"Video generation status: {video.status}")
|
|
246
|
+
if video.status != "completed":
|
|
247
|
+
raise RuntimeError(
|
|
248
|
+
f"Sora video generation ended with status: {video.status}. Please retry."
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return save_video_with_metadata(client, video.id, self.name, self.product_name)
|
|
252
|
+
|
|
253
|
+
finally:
|
|
254
|
+
if reference_file is not None and hasattr(reference_file, "close"):
|
|
255
|
+
try:
|
|
256
|
+
reference_file.close()
|
|
257
|
+
except Exception:
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
async def _generate_with_veo(self, model: str) -> dict:
|
|
261
|
+
"""Generate video using Google's Veo API with optional references."""
|
|
262
|
+
|
|
263
|
+
client = get_gemini_client()
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
config_kwargs = {"duration_seconds": self.seconds}
|
|
267
|
+
first_frame_image = None
|
|
268
|
+
|
|
269
|
+
# Add aspect_ratio and resolution only when NOT using reference images
|
|
270
|
+
# (these parameters cause "not supported" errors when used with reference images)
|
|
271
|
+
if self.size and not self.first_frame_ref and not self.asset_image_ref:
|
|
272
|
+
width, height = map(int, self.size.split('x'))
|
|
273
|
+
config_kwargs["aspect_ratio"] = "9:16" if width < height else "16:9"
|
|
274
|
+
|
|
275
|
+
if self.asset_image_ref:
|
|
276
|
+
parsed = urlparse(self.asset_image_ref)
|
|
277
|
+
|
|
278
|
+
if parsed.scheme in ("http", "https"):
|
|
279
|
+
raise ValueError("Veo does not support URL reference images. Please use local images.")
|
|
280
|
+
else:
|
|
281
|
+
path = Path(self.asset_image_ref).expanduser().resolve()
|
|
282
|
+
if path.exists():
|
|
283
|
+
image_path = str(path)
|
|
284
|
+
else:
|
|
285
|
+
images_dir = get_images_dir(self.product_name)
|
|
286
|
+
pil_image, image_path, load_error = load_image_by_name(
|
|
287
|
+
self.asset_image_ref, images_dir, [".png", ".jpg", ".jpeg", ".webp"]
|
|
288
|
+
)
|
|
289
|
+
if load_error:
|
|
290
|
+
raise FileNotFoundError(f"Reference image '{self.asset_image_ref}' not found in {images_dir}")
|
|
291
|
+
|
|
292
|
+
logger.info(f"Loading asset reference image for Veo: {image_path}")
|
|
293
|
+
|
|
294
|
+
with PILImage.open(image_path) as img:
|
|
295
|
+
if img.mode != 'RGB':
|
|
296
|
+
img = img.convert('RGB')
|
|
297
|
+
|
|
298
|
+
if self.size:
|
|
299
|
+
target_width, target_height = map(int, self.size.split('x'))
|
|
300
|
+
target_ratio = target_width / target_height
|
|
301
|
+
img_ratio = img.width / img.height
|
|
302
|
+
|
|
303
|
+
# Crop to match aspect ratio (center crop)
|
|
304
|
+
if img_ratio > target_ratio:
|
|
305
|
+
# Image is wider, crop width
|
|
306
|
+
new_width = int(img.height * target_ratio)
|
|
307
|
+
left = (img.width - new_width) // 2
|
|
308
|
+
img = img.crop((left, 0, left + new_width, img.height))
|
|
309
|
+
elif img_ratio < target_ratio:
|
|
310
|
+
# Image is taller, crop height
|
|
311
|
+
new_height = int(img.width / target_ratio)
|
|
312
|
+
top = (img.height - new_height) // 2
|
|
313
|
+
img = img.crop((0, top, img.width, top + new_height))
|
|
314
|
+
|
|
315
|
+
img = img.resize((target_width, target_height), PILImage.Resampling.LANCZOS)
|
|
316
|
+
|
|
317
|
+
buffer = BytesIO()
|
|
318
|
+
img.save(buffer, format='PNG')
|
|
319
|
+
image_bytes = buffer.getvalue()
|
|
320
|
+
mime_type = "image/png"
|
|
321
|
+
|
|
322
|
+
config_kwargs["reference_images"] = [
|
|
323
|
+
VideoGenerationReferenceImage(
|
|
324
|
+
image=Image(
|
|
325
|
+
image_bytes=image_bytes,
|
|
326
|
+
mime_type=mime_type,
|
|
327
|
+
),
|
|
328
|
+
reference_type="asset",
|
|
329
|
+
),
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
if self.first_frame_ref:
|
|
333
|
+
# For first frame, pass as 'image' parameter directly to generate_videos()
|
|
334
|
+
# According to official docs: https://docs.cloud.google.com/vertex-ai/generative-ai/docs/video/generate-videos-from-an-image
|
|
335
|
+
reference_file = resolve_input_reference(
|
|
336
|
+
self.first_frame_ref,
|
|
337
|
+
target_size=self.size if self.first_frame_ref else None,
|
|
338
|
+
product_name=self.product_name,
|
|
339
|
+
)
|
|
340
|
+
first_frame_bytes = reference_file.read()
|
|
341
|
+
mime_type = "image/png"
|
|
342
|
+
if getattr(reference_file, "name", None):
|
|
343
|
+
guessed = mimetypes.guess_type(reference_file.name)[0]
|
|
344
|
+
if guessed:
|
|
345
|
+
mime_type = guessed
|
|
346
|
+
|
|
347
|
+
first_frame_image = Image(
|
|
348
|
+
image_bytes=first_frame_bytes,
|
|
349
|
+
mime_type=mime_type,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
if hasattr(reference_file, "close"):
|
|
353
|
+
try:
|
|
354
|
+
reference_file.close()
|
|
355
|
+
except Exception:
|
|
356
|
+
pass
|
|
357
|
+
|
|
358
|
+
config = GenerateVideosConfig(**config_kwargs)
|
|
359
|
+
|
|
360
|
+
if self.size and self.first_frame_ref:
|
|
361
|
+
logger.info(f"Submitting video generation request to Veo ({model}) - first frame resized to {self.size} (aspect ratio inferred from image)...")
|
|
362
|
+
elif self.size:
|
|
363
|
+
logger.info(f"Submitting video generation request to Veo ({model}) - size: {self.size}...")
|
|
364
|
+
else:
|
|
365
|
+
logger.info(f"Submitting video generation request to Veo ({model})...")
|
|
366
|
+
|
|
367
|
+
# run_in_executor keeps the event loop free while Veo polls
|
|
368
|
+
loop = asyncio.get_event_loop()
|
|
369
|
+
generate_kwargs = {
|
|
370
|
+
"model": model,
|
|
371
|
+
"prompt": self.prompt,
|
|
372
|
+
"config": config,
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
# Add image parameter if first_frame_ref is provided (simple image-to-video)
|
|
376
|
+
if first_frame_image:
|
|
377
|
+
generate_kwargs["image"] = first_frame_image
|
|
378
|
+
|
|
379
|
+
started_at = asyncio.get_running_loop().time()
|
|
380
|
+
try:
|
|
381
|
+
operation = await loop.run_in_executor(
|
|
382
|
+
None,
|
|
383
|
+
lambda: client.models.generate_videos(**generate_kwargs)
|
|
384
|
+
)
|
|
385
|
+
except Exception as exc:
|
|
386
|
+
if _is_transient_network_error(exc):
|
|
387
|
+
raise RuntimeError(
|
|
388
|
+
f"Veo submission hit a transient network error: {exc}. "
|
|
389
|
+
"Please retry this GenerateVideo call."
|
|
390
|
+
) from exc
|
|
391
|
+
raise
|
|
392
|
+
|
|
393
|
+
# Poll the operation status until the video is ready
|
|
394
|
+
while not operation.done:
|
|
395
|
+
logger.info("Waiting for Veo video generation to complete...")
|
|
396
|
+
elapsed = asyncio.get_running_loop().time() - started_at
|
|
397
|
+
if elapsed > VIDEO_GENERATION_TIMEOUT_SECONDS:
|
|
398
|
+
raise RuntimeError(
|
|
399
|
+
f"Veo video generation timed out after {VIDEO_GENERATION_TIMEOUT_SECONDS} seconds."
|
|
400
|
+
)
|
|
401
|
+
await asyncio.sleep(10)
|
|
402
|
+
try:
|
|
403
|
+
operation = await loop.run_in_executor(
|
|
404
|
+
None,
|
|
405
|
+
lambda: client.operations.get(operation)
|
|
406
|
+
)
|
|
407
|
+
except Exception as exc:
|
|
408
|
+
if not _is_transient_network_error(exc):
|
|
409
|
+
raise
|
|
410
|
+
logger.warning(f"Transient Veo polling error: {exc}. Retrying same operation...")
|
|
411
|
+
|
|
412
|
+
logger.info("Video generation complete!")
|
|
413
|
+
|
|
414
|
+
# Download the generated video — retry on transient network errors
|
|
415
|
+
generated_video = operation.response.generated_videos[0]
|
|
416
|
+
MAX_DOWNLOAD_RETRIES = 3
|
|
417
|
+
last_exc: Exception | None = None
|
|
418
|
+
for attempt in range(MAX_DOWNLOAD_RETRIES):
|
|
419
|
+
try:
|
|
420
|
+
output = await loop.run_in_executor(
|
|
421
|
+
None,
|
|
422
|
+
lambda: save_veo_video_with_metadata(
|
|
423
|
+
client, generated_video.video, self.name, self.product_name
|
|
424
|
+
),
|
|
425
|
+
)
|
|
426
|
+
last_exc = None
|
|
427
|
+
break
|
|
428
|
+
except Exception as exc:
|
|
429
|
+
last_exc = exc
|
|
430
|
+
if attempt < MAX_DOWNLOAD_RETRIES - 1 and _is_transient_network_error(exc):
|
|
431
|
+
wait = 5 * (attempt + 1)
|
|
432
|
+
logger.warning(
|
|
433
|
+
f"Transient Veo download error (attempt {attempt + 1}/{MAX_DOWNLOAD_RETRIES}): "
|
|
434
|
+
f"{exc}. Retrying in {wait}s..."
|
|
435
|
+
)
|
|
436
|
+
await asyncio.sleep(wait)
|
|
437
|
+
else:
|
|
438
|
+
raise
|
|
439
|
+
if last_exc is not None:
|
|
440
|
+
raise last_exc
|
|
441
|
+
|
|
442
|
+
return output
|
|
443
|
+
|
|
444
|
+
except RuntimeError:
|
|
445
|
+
raise
|
|
446
|
+
except Exception as e:
|
|
447
|
+
raise RuntimeError(f"Veo video generation failed: {str(e)}")
|
|
448
|
+
|
|
449
|
+
async def _generate_with_seedance(self, model: str) -> list:
|
|
450
|
+
"""Generate video using ByteDance Seedance 1.5 Pro via fal.ai."""
|
|
451
|
+
api_key = os.getenv("FAL_KEY")
|
|
452
|
+
if not api_key:
|
|
453
|
+
raise ValueError("FAL_KEY is not set. Add it to your .env to use video generation.")
|
|
454
|
+
fal = fal_client.SyncClient(key=api_key)
|
|
455
|
+
|
|
456
|
+
duration = str(self.seconds)
|
|
457
|
+
|
|
458
|
+
# Map size → fal.ai resolution label and aspect ratio
|
|
459
|
+
width, height = map(int, self.size.split("x"))
|
|
460
|
+
max_dim = max(width, height)
|
|
461
|
+
if max_dim >= 1080:
|
|
462
|
+
resolution = "1080p"
|
|
463
|
+
elif max_dim >= 720:
|
|
464
|
+
resolution = "720p"
|
|
465
|
+
else:
|
|
466
|
+
resolution = "480p"
|
|
467
|
+
|
|
468
|
+
if width < height:
|
|
469
|
+
aspect_ratio = "9:16"
|
|
470
|
+
elif width > height:
|
|
471
|
+
aspect_ratio = "16:9"
|
|
472
|
+
else:
|
|
473
|
+
aspect_ratio = "1:1"
|
|
474
|
+
|
|
475
|
+
if self.first_frame_ref:
|
|
476
|
+
endpoint = "fal-ai/bytedance/seedance/v1.5/pro/image-to-video"
|
|
477
|
+
image_url = await self._resolve_image_for_fal(self.first_frame_ref, fal)
|
|
478
|
+
arguments: dict = {
|
|
479
|
+
"prompt": self.prompt,
|
|
480
|
+
"image_url": image_url,
|
|
481
|
+
"duration": duration,
|
|
482
|
+
"resolution": resolution,
|
|
483
|
+
}
|
|
484
|
+
else:
|
|
485
|
+
endpoint = "fal-ai/bytedance/seedance/v1.5/pro/text-to-video"
|
|
486
|
+
arguments = {
|
|
487
|
+
"prompt": self.prompt,
|
|
488
|
+
"duration": duration,
|
|
489
|
+
"resolution": resolution,
|
|
490
|
+
"aspect_ratio": aspect_ratio,
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
logger.info(f"Submitting video generation request to Seedance 1.5 Pro (fal.ai, {endpoint})...")
|
|
494
|
+
result = await asyncio.to_thread(
|
|
495
|
+
fal.subscribe, endpoint, arguments=arguments, with_logs=True
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
output_url = (result.get("video") or {}).get("url")
|
|
499
|
+
if not output_url:
|
|
500
|
+
raise RuntimeError(f"Seedance did not return a video URL. Response: {result}")
|
|
501
|
+
|
|
502
|
+
videos_dir = get_videos_dir(self.product_name)
|
|
503
|
+
output_path = os.path.join(videos_dir, f"{self.name}.mp4")
|
|
504
|
+
|
|
505
|
+
logger.info(f"Downloading Seedance video to {output_path}...")
|
|
506
|
+
async with httpx.AsyncClient(timeout=120.0) as http:
|
|
507
|
+
response = await http.get(output_url)
|
|
508
|
+
response.raise_for_status()
|
|
509
|
+
with open(output_path, "wb") as fh:
|
|
510
|
+
fh.write(response.content)
|
|
511
|
+
|
|
512
|
+
spritesheet_path = os.path.join(videos_dir, f"{self.name}_spritesheet.jpg")
|
|
513
|
+
await asyncio.to_thread(generate_spritesheet, output_path, spritesheet_path)
|
|
514
|
+
|
|
515
|
+
last_frame_path = os.path.join(videos_dir, f"{self.name}_last_frame.jpg")
|
|
516
|
+
await asyncio.to_thread(extract_last_frame, output_path, last_frame_path)
|
|
517
|
+
|
|
518
|
+
return [ToolOutputText(type="text", text=f"Video saved to `{self.name}.mp4`\nPath: {output_path}")]
|
|
519
|
+
|
|
520
|
+
async def _resolve_image_for_fal(self, image_ref: str, fal: fal_client.SyncClient) -> str:
|
|
521
|
+
"""Resolve a local path or image name to a fal.ai-accessible URL."""
|
|
522
|
+
parsed = urlparse(image_ref)
|
|
523
|
+
if parsed.scheme in ("http", "https"):
|
|
524
|
+
return image_ref
|
|
525
|
+
|
|
526
|
+
path = Path(image_ref).expanduser().resolve()
|
|
527
|
+
if not path.exists():
|
|
528
|
+
images_dir = get_images_dir(self.product_name)
|
|
529
|
+
_, image_path, err = load_image_by_name(
|
|
530
|
+
image_ref, images_dir, [".png", ".jpg", ".jpeg", ".webp"]
|
|
531
|
+
)
|
|
532
|
+
if err:
|
|
533
|
+
raise FileNotFoundError(
|
|
534
|
+
f"Reference image '{image_ref}' not found in {images_dir}"
|
|
535
|
+
)
|
|
536
|
+
path = Path(image_path)
|
|
537
|
+
|
|
538
|
+
return await asyncio.to_thread(fal.upload_file, str(path))
|
|
539
|
+
|
|
540
|
+
if __name__ == "__main__":
|
|
541
|
+
# Basic test invocation (Sora)
|
|
542
|
+
tool = GenerateVideo(
|
|
543
|
+
product_name = "bird_forest_veo",
|
|
544
|
+
prompt = "Cinematic nature footage: a small colorful songbird (robin-like) flies swiftly through a dense evergreen forest corridor, weaving between mossy trunks and sunlit branches. The camera follows in a smooth tracking shot at bird height, slightly behind and to the side, maintaining the bird in sharp focus while the background streaks with gentle motion blur. Early morning golden light filters through the canopy, creating volumetric rays and floating dust motes; rich greens and warm highlights, high dynamic range, natural filmic contrast. The bird beats its wings rhythmically, occasionally gliding past ferns and hanging vines; leaves flutter from the air wake. Lens: 35mm, shallow depth of field, stabilized gimbal-like motion, realistic textures and feathers. Audio: clear forest ambience with soft wind through needles, subtle wing flaps, distant birdsong.",
|
|
545
|
+
name = "bird_flying_forest_4s_16x9_fast_v2",
|
|
546
|
+
model = "veo-3.1-fast-generate-preview",
|
|
547
|
+
seconds = 4,
|
|
548
|
+
size = "1280x720"
|
|
549
|
+
|
|
550
|
+
)
|
|
551
|
+
try:
|
|
552
|
+
logging.basicConfig(level=logging.INFO)
|
|
553
|
+
result = asyncio.run(tool.run())
|
|
554
|
+
print(result)
|
|
555
|
+
except Exception as exc:
|
|
556
|
+
print(f"Video generation failed: {exc}")
|