Please enter the commit message for your changes. Lines starting
with '#' will be ignored, and an empty message aborts the commit. On branch main Your branch is up to date with 'origin/main'. Changes to be committed: new file: .claude/skills/algorithmic-art/.openskills.json new file: .claude/skills/algorithmic-art/LICENSE.txt new file: .claude/skills/algorithmic-art/SKILL.md new file: .claude/skills/algorithmic-art/templates/generator_template.js new file: .claude/skills/algorithmic-art/templates/viewer.html new file: .claude/skills/brand-guidelines/.openskills.json new file: .claude/skills/brand-guidelines/LICENSE.txt new file: .claude/skills/brand-guidelines/SKILL.md new file: .claude/skills/canvas-design/.openskills.json new file: .claude/skills/canvas-design/LICENSE.txt new file: .claude/skills/canvas-design/SKILL.md new file: .claude/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf new file: .claude/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/DMMono-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/Gloock-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf new file: .claude/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf new file: .claude/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf new file: .claude/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf new file: .claude/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf new file: .claude/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/Italiana-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/Jura-Light.ttf new file: .claude/skills/canvas-design/canvas-fonts/Jura-Medium.ttf new file: .claude/skills/canvas-design/canvas-fonts/Jura-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/Lora-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf new file: .claude/skills/canvas-design/canvas-fonts/Lora-Italic.ttf new file: .claude/skills/canvas-design/canvas-fonts/Lora-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/Lora-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/Outfit-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf new file: .claude/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf new file: .claude/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf new file: .claude/skills/canvas-design/canvas-fonts/Tektur-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf new file: .claude/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf new file: .claude/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf new file: .claude/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf new file: .claude/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt new file: .claude/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf new file: .claude/skills/doc-coauthoring/.openskills.json new file: .claude/skills/doc-coauthoring/SKILL.md new file: .claude/skills/docx/.openskills.json new file: .claude/skills/docx/LICENSE.txt new file: .claude/skills/docx/SKILL.md new file: .claude/skills/docx/scripts/__init__.py new file: .claude/skills/docx/scripts/accept_changes.py new file: .claude/skills/docx/scripts/comment.py new file: .claude/skills/docx/scripts/office/helpers/__init__.py new file: .claude/skills/docx/scripts/office/helpers/merge_runs.py new file: .claude/skills/docx/scripts/office/helpers/simplify_redlines.py new file: .claude/skills/docx/scripts/office/pack.py new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd new file: .claude/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd new file: .claude/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd new file: .claude/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd new file: .claude/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd new file: .claude/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd new file: .claude/skills/docx/scripts/office/schemas/mce/mc.xsd new file: .claude/skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd new file: .claude/skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd new file: .claude/skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd new file: .claude/skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd new file: .claude/skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd new file: .claude/skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd new file: .claude/skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd new file: .claude/skills/docx/scripts/office/soffice.py new file: .claude/skills/docx/scripts/office/unpack.py new file: .claude/skills/docx/scripts/office/validate.py new file: .claude/skills/docx/scripts/office/validators/__init__.py new file: .claude/skills/docx/scripts/office/validators/base.py new file: .claude/skills/docx/scripts/office/validators/docx.py new file: .claude/skills/docx/scripts/office/validators/pptx.py new file: .claude/skills/docx/scripts/office/validators/redlining.py new file: .claude/skills/docx/scripts/templates/comments.xml new file: .claude/skills/docx/scripts/templates/commentsExtended.xml new file: .claude/skills/docx/scripts/templates/commentsExtensible.xml new file: .claude/skills/docx/scripts/templates/commentsIds.xml new file: .claude/skills/docx/scripts/templates/people.xml new file: .claude/skills/frontend-design/.openskills.json new file: .claude/skills/frontend-design/LICENSE.txt new file: .claude/skills/frontend-design/SKILL.md new file: .claude/skills/internal-comms/.openskills.json new file: .claude/skills/internal-comms/LICENSE.txt new file: .claude/skills/internal-comms/SKILL.md new file: .claude/skills/internal-comms/examples/3p-updates.md new file: .claude/skills/internal-comms/examples/company-newsletter.md new file: .claude/skills/internal-comms/examples/faq-answers.md new file: .claude/skills/internal-comms/examples/general-comms.md new file: .claude/skills/mcp-builder/.openskills.json new file: .claude/skills/mcp-builder/LICENSE.txt new file: .claude/skills/mcp-builder/SKILL.md new file: .claude/skills/mcp-builder/reference/evaluation.md new file: .claude/skills/mcp-builder/reference/mcp_best_practices.md new file: .claude/skills/mcp-builder/reference/node_mcp_server.md new file: .claude/skills/mcp-builder/reference/python_mcp_server.md new file: .claude/skills/mcp-builder/scripts/connections.py new file: .claude/skills/mcp-builder/scripts/evaluation.py new file: .claude/skills/mcp-builder/scripts/example_evaluation.xml new file: .claude/skills/mcp-builder/scripts/requirements.txt new file: .claude/skills/pdf/.openskills.json new file: .claude/skills/pdf/LICENSE.txt new file: .claude/skills/pdf/SKILL.md new file: .claude/skills/pdf/forms.md new file: .claude/skills/pdf/reference.md new file: .claude/skills/pdf/scripts/check_bounding_boxes.py new file: .claude/skills/pdf/scripts/check_fillable_fields.py new file: .claude/skills/pdf/scripts/convert_pdf_to_images.py new file: .claude/skills/pdf/scripts/create_validation_image.py new file: .claude/skills/pdf/scripts/extract_form_field_info.py new file: .claude/skills/pdf/scripts/extract_form_structure.py new file: .claude/skills/pdf/scripts/fill_fillable_fields.py new file: .claude/skills/pdf/scripts/fill_pdf_form_with_annotations.py new file: .claude/skills/pptx/.openskills.json new file: .claude/skills/pptx/LICENSE.txt new file: .claude/skills/pptx/SKILL.md new file: .claude/skills/pptx/editing.md new file: .claude/skills/pptx/pptxgenjs.md new file: .claude/skills/pptx/scripts/__init__.py new file: .claude/skills/pptx/scripts/add_slide.py new file: .claude/skills/pptx/scripts/clean.py new file: .claude/skills/pptx/scripts/office/helpers/__init__.py new file: .claude/skills/pptx/scripts/office/helpers/merge_runs.py new file: .claude/skills/pptx/scripts/office/helpers/simplify_redlines.py new file: .claude/skills/pptx/scripts/office/pack.py new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd new file: .claude/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd new file: .claude/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd new file: .claude/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd new file: .claude/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd new file: .claude/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd new file: .claude/skills/pptx/scripts/office/schemas/mce/mc.xsd new file: .claude/skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd new file: .claude/skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd new file: .claude/skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd new file: .claude/skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd new file: .claude/skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd new file: .claude/skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd new file: .claude/skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd new file: .claude/skills/pptx/scripts/office/soffice.py new file: .claude/skills/pptx/scripts/office/unpack.py new file: .claude/skills/pptx/scripts/office/validate.py new file: .claude/skills/pptx/scripts/office/validators/__init__.py new file: .claude/skills/pptx/scripts/office/validators/base.py new file: .claude/skills/pptx/scripts/office/validators/docx.py new file: .claude/skills/pptx/scripts/office/validators/pptx.py new file: .claude/skills/pptx/scripts/office/validators/redlining.py new file: .claude/skills/pptx/scripts/thumbnail.py new file: .claude/skills/skill-creator/.openskills.json new file: .claude/skills/skill-creator/LICENSE.txt new file: .claude/skills/skill-creator/SKILL.md new file: .claude/skills/skill-creator/agents/analyzer.md new file: .claude/skills/skill-creator/agents/comparator.md new file: .claude/skills/skill-creator/agents/grader.md new file: .claude/skills/skill-creator/assets/eval_review.html new file: .claude/skills/skill-creator/eval-viewer/generate_review.py new file: .claude/skills/skill-creator/eval-viewer/viewer.html new file: .claude/skills/skill-creator/references/schemas.md new file: .claude/skills/skill-creator/scripts/__init__.py new file: .claude/skills/skill-creator/scripts/aggregate_benchmark.py new file: .claude/skills/skill-creator/scripts/generate_report.py new file: .claude/skills/skill-creator/scripts/improve_description.py new file: .claude/skills/skill-creator/scripts/package_skill.py new file: .claude/skills/skill-creator/scripts/quick_validate.py new file: .claude/skills/skill-creator/scripts/run_eval.py new file: .claude/skills/skill-creator/scripts/run_loop.py new file: .claude/skills/skill-creator/scripts/utils.py new file: .claude/skills/slack-gif-creator/.openskills.json new file: .claude/skills/slack-gif-creator/LICENSE.txt new file: .claude/skills/slack-gif-creator/SKILL.md new file: .claude/skills/slack-gif-creator/core/easing.py new file: .claude/skills/slack-gif-creator/core/frame_composer.py new file: .claude/skills/slack-gif-creator/core/gif_builder.py new file: .claude/skills/slack-gif-creator/core/validators.py new file: .claude/skills/slack-gif-creator/requirements.txt new file: .claude/skills/template/.openskills.json new file: .claude/skills/template/SKILL.md new file: .claude/skills/theme-factory/.openskills.json new file: .claude/skills/theme-factory/LICENSE.txt new file: .claude/skills/theme-factory/SKILL.md new file: .claude/skills/theme-factory/theme-showcase.pdf new file: .claude/skills/theme-factory/themes/arctic-frost.md new file: .claude/skills/theme-factory/themes/botanical-garden.md new file: .claude/skills/theme-factory/themes/desert-rose.md new file: .claude/skills/theme-factory/themes/forest-canopy.md new file: .claude/skills/theme-factory/themes/golden-hour.md new file: .claude/skills/theme-factory/themes/midnight-galaxy.md new file: .claude/skills/theme-factory/themes/modern-minimalist.md new file: .claude/skills/theme-factory/themes/ocean-depths.md new file: .claude/skills/theme-factory/themes/sunset-boulevard.md new file: .claude/skills/theme-factory/themes/tech-innovation.md new file: .claude/skills/web-artifacts-builder/.openskills.json new file: .claude/skills/web-artifacts-builder/LICENSE.txt new file: .claude/skills/web-artifacts-builder/SKILL.md new file: .claude/skills/web-artifacts-builder/scripts/bundle-artifact.sh new file: .claude/skills/web-artifacts-builder/scripts/init-artifact.sh new file: .claude/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz new file: .claude/skills/webapp-testing/.openskills.json new file: .claude/skills/webapp-testing/LICENSE.txt new file: .claude/skills/webapp-testing/SKILL.md new file: .claude/skills/webapp-testing/examples/console_logging.py new file: .claude/skills/webapp-testing/examples/element_discovery.py new file: .claude/skills/webapp-testing/examples/static_html_automation.py new file: .claude/skills/webapp-testing/scripts/with_server.py new file: .claude/skills/xlsx/.openskills.json new file: .claude/skills/xlsx/LICENSE.txt new file: .claude/skills/xlsx/SKILL.md new file: .claude/skills/xlsx/scripts/office/helpers/__init__.py new file: .claude/skills/xlsx/scripts/office/helpers/merge_runs.py new file: .claude/skills/xlsx/scripts/office/helpers/simplify_redlines.py new file: .claude/skills/xlsx/scripts/office/pack.py new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd new file: .claude/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd new file: .claude/skills/xlsx/scripts/office/schemas/mce/mc.xsd new file: .claude/skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd new file: .claude/skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd new file: .claude/skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd new file: .claude/skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd new file: .claude/skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd new file: .claude/skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd new file: .claude/skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd new file: .claude/skills/xlsx/scripts/office/soffice.py new file: .claude/skills/xlsx/scripts/office/unpack.py new file: .claude/skills/xlsx/scripts/office/validate.py new file: .claude/skills/xlsx/scripts/office/validators/__init__.py new file: .claude/skills/xlsx/scripts/office/validators/base.py new file: .claude/skills/xlsx/scripts/office/validators/docx.py new file: .claude/skills/xlsx/scripts/office/validators/pptx.py new file: .claude/skills/xlsx/scripts/office/validators/redlining.py new file: .claude/skills/xlsx/scripts/recalc.py new file: .env.example new file: .gitignore new file: config/mcp.json new file: config/models.json new file: config/personalities.json new file: docs/AGENTS.md new file: docs/AI_IMPLEMENTATION.md new file: docs/AI_INTEGRATION_COMPLETE.md new file: docs/AI_QUICKSTART.md new file: docs/AI_SUMMARY.md new file: docs/CHANGELOG.md new file: docs/CONFIG_GUIDE.md new file: docs/FIXES.md new file: docs/PROJECT_REFACTOR.md new file: docs/README.md new file: docs/README_INDEX.md new file: examples/ai_example.py new file: main.py new file: pytest.ini new file: requirements.txt new file: scripts/migrate_to_vector_db.py new file: skills/cmd_zip_skill/README.md new file: skills/cmd_zip_skill/__init__.py new file: skills/cmd_zip_skill/main.py new file: skills/cmd_zip_skill/skill.json new file: skills/cmd_zip_skill_1772465404375/README.md new file: skills/cmd_zip_skill_1772465404375/__init__.py new file: skills/cmd_zip_skill_1772465404375/main.py new file: skills/cmd_zip_skill_1772465404375/skill.json new file: skills/cmd_zip_skill_1772465434774/README.md new file: skills/cmd_zip_skill_1772465434774/__init__.py new file: skills/cmd_zip_skill_1772465434774/main.py new file: skills/cmd_zip_skill_1772465434774/skill.json new file: skills/cmd_zip_skill_1772465467809/README.md new file: skills/cmd_zip_skill_1772465467809/__init__.py new file: skills/cmd_zip_skill_1772465467809/main.py new file: skills/cmd_zip_skill_1772465467809/skill.json new file: skills/cmd_zip_skill_1772465652075/README.md new file: skills/cmd_zip_skill_1772465652075/__init__.py new file: skills/cmd_zip_skill_1772465652075/main.py new file: skills/cmd_zip_skill_1772465652075/skill.json new file: skills/cmd_zip_skill_1772465685352/README.md new file: skills/cmd_zip_skill_1772465685352/__init__.py new file: skills/cmd_zip_skill_1772465685352/main.py new file: skills/cmd_zip_skill_1772465685352/skill.json new file: skills/cmd_zip_skill_1772465936294/README.md new file: skills/cmd_zip_skill_1772465936294/__init__.py new file: skills/cmd_zip_skill_1772465936294/main.py new file: skills/cmd_zip_skill_1772465936294/skill.json new file: skills/cmd_zip_skill_1772465966322/README.md new file: skills/cmd_zip_skill_1772465966322/__init__.py new file: skills/cmd_zip_skill_1772465966322/main.py new file: skills/cmd_zip_skill_1772465966322/skill.json new file: skills/cmd_zip_skill_1772466071278/README.md new file: skills/cmd_zip_skill_1772466071278/__init__.py new file: skills/cmd_zip_skill_1772466071278/main.py new file: skills/cmd_zip_skill_1772466071278/skill.json new file: skills/skills_creator/README.md new file: skills/skills_creator/__init__.py new file: skills/skills_creator/main.py new file: skills/skills_creator/skill.json new file: src/__init__.py new file: src/ai/__init__.py new file: src/ai/base.py new file: src/ai/client.py new file: src/ai/docs/README.md new file: src/ai/mcp/__init__.py new file: src/ai/mcp/base.py new file: src/ai/mcp/servers/__init__.py new file: src/ai/mcp/servers/filesystem.py new file: src/ai/memory.py new file: src/ai/models/__init__.py new file: src/ai/models/anthropic_model.py new file: src/ai/models/openai_model.py new file: src/ai/personality.py new file: src/ai/skills/__init__.py new file: src/ai/skills/base.py new file: src/ai/task_manager.py new file: src/ai/vector_store/__init__.py new file: src/ai/vector_store/base.py new file: src/ai/vector_store/chroma_store.py new file: src/ai/vector_store/json_store.py new file: src/core/__init__.py new file: src/core/bot.py new file: src/core/config.py new file: src/handlers/__init__.py new file: src/handlers/message_handler.py new file: src/handlers/message_handler_ai.py new file: src/utils/__init__.py new file: src/utils/logger.py new file: start.bat new file: tests/test_ai.py
This commit is contained in:
234
.claude/skills/slack-gif-creator/core/easing.py
Normal file
234
.claude/skills/slack-gif-creator/core/easing.py
Normal file
@@ -0,0 +1,234 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Easing Functions - Timing functions for smooth animations.
|
||||
|
||||
Provides various easing functions for natural motion and timing.
|
||||
All functions take a value t (0.0 to 1.0) and return eased value (0.0 to 1.0).
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
|
||||
def linear(t: float) -> float:
|
||||
"""Linear interpolation (no easing)."""
|
||||
return t
|
||||
|
||||
|
||||
def ease_in_quad(t: float) -> float:
|
||||
"""Quadratic ease-in (slow start, accelerating)."""
|
||||
return t * t
|
||||
|
||||
|
||||
def ease_out_quad(t: float) -> float:
|
||||
"""Quadratic ease-out (fast start, decelerating)."""
|
||||
return t * (2 - t)
|
||||
|
||||
|
||||
def ease_in_out_quad(t: float) -> float:
|
||||
"""Quadratic ease-in-out (slow start and end)."""
|
||||
if t < 0.5:
|
||||
return 2 * t * t
|
||||
return -1 + (4 - 2 * t) * t
|
||||
|
||||
|
||||
def ease_in_cubic(t: float) -> float:
|
||||
"""Cubic ease-in (slow start)."""
|
||||
return t * t * t
|
||||
|
||||
|
||||
def ease_out_cubic(t: float) -> float:
|
||||
"""Cubic ease-out (fast start)."""
|
||||
return (t - 1) * (t - 1) * (t - 1) + 1
|
||||
|
||||
|
||||
def ease_in_out_cubic(t: float) -> float:
|
||||
"""Cubic ease-in-out."""
|
||||
if t < 0.5:
|
||||
return 4 * t * t * t
|
||||
return (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
|
||||
|
||||
|
||||
def ease_in_bounce(t: float) -> float:
|
||||
"""Bounce ease-in (bouncy start)."""
|
||||
return 1 - ease_out_bounce(1 - t)
|
||||
|
||||
|
||||
def ease_out_bounce(t: float) -> float:
|
||||
"""Bounce ease-out (bouncy end)."""
|
||||
if t < 1 / 2.75:
|
||||
return 7.5625 * t * t
|
||||
elif t < 2 / 2.75:
|
||||
t -= 1.5 / 2.75
|
||||
return 7.5625 * t * t + 0.75
|
||||
elif t < 2.5 / 2.75:
|
||||
t -= 2.25 / 2.75
|
||||
return 7.5625 * t * t + 0.9375
|
||||
else:
|
||||
t -= 2.625 / 2.75
|
||||
return 7.5625 * t * t + 0.984375
|
||||
|
||||
|
||||
def ease_in_out_bounce(t: float) -> float:
|
||||
"""Bounce ease-in-out."""
|
||||
if t < 0.5:
|
||||
return ease_in_bounce(t * 2) * 0.5
|
||||
return ease_out_bounce(t * 2 - 1) * 0.5 + 0.5
|
||||
|
||||
|
||||
def ease_in_elastic(t: float) -> float:
|
||||
"""Elastic ease-in (spring effect)."""
|
||||
if t == 0 or t == 1:
|
||||
return t
|
||||
return -math.pow(2, 10 * (t - 1)) * math.sin((t - 1.1) * 5 * math.pi)
|
||||
|
||||
|
||||
def ease_out_elastic(t: float) -> float:
|
||||
"""Elastic ease-out (spring effect)."""
|
||||
if t == 0 or t == 1:
|
||||
return t
|
||||
return math.pow(2, -10 * t) * math.sin((t - 0.1) * 5 * math.pi) + 1
|
||||
|
||||
|
||||
def ease_in_out_elastic(t: float) -> float:
|
||||
"""Elastic ease-in-out."""
|
||||
if t == 0 or t == 1:
|
||||
return t
|
||||
t = t * 2 - 1
|
||||
if t < 0:
|
||||
return -0.5 * math.pow(2, 10 * t) * math.sin((t - 0.1) * 5 * math.pi)
|
||||
return math.pow(2, -10 * t) * math.sin((t - 0.1) * 5 * math.pi) * 0.5 + 1
|
||||
|
||||
|
||||
# Convenience mapping
|
||||
EASING_FUNCTIONS = {
|
||||
"linear": linear,
|
||||
"ease_in": ease_in_quad,
|
||||
"ease_out": ease_out_quad,
|
||||
"ease_in_out": ease_in_out_quad,
|
||||
"bounce_in": ease_in_bounce,
|
||||
"bounce_out": ease_out_bounce,
|
||||
"bounce": ease_in_out_bounce,
|
||||
"elastic_in": ease_in_elastic,
|
||||
"elastic_out": ease_out_elastic,
|
||||
"elastic": ease_in_out_elastic,
|
||||
}
|
||||
|
||||
|
||||
def get_easing(name: str = "linear"):
|
||||
"""Get easing function by name."""
|
||||
return EASING_FUNCTIONS.get(name, linear)
|
||||
|
||||
|
||||
def interpolate(start: float, end: float, t: float, easing: str = "linear") -> float:
|
||||
"""
|
||||
Interpolate between two values with easing.
|
||||
|
||||
Args:
|
||||
start: Start value
|
||||
end: End value
|
||||
t: Progress from 0.0 to 1.0
|
||||
easing: Name of easing function
|
||||
|
||||
Returns:
|
||||
Interpolated value
|
||||
"""
|
||||
ease_func = get_easing(easing)
|
||||
eased_t = ease_func(t)
|
||||
return start + (end - start) * eased_t
|
||||
|
||||
|
||||
def ease_back_in(t: float) -> float:
|
||||
"""Back ease-in (slight overshoot backward before forward motion)."""
|
||||
c1 = 1.70158
|
||||
c3 = c1 + 1
|
||||
return c3 * t * t * t - c1 * t * t
|
||||
|
||||
|
||||
def ease_back_out(t: float) -> float:
|
||||
"""Back ease-out (overshoot forward then settle back)."""
|
||||
c1 = 1.70158
|
||||
c3 = c1 + 1
|
||||
return 1 + c3 * pow(t - 1, 3) + c1 * pow(t - 1, 2)
|
||||
|
||||
|
||||
def ease_back_in_out(t: float) -> float:
|
||||
"""Back ease-in-out (overshoot at both ends)."""
|
||||
c1 = 1.70158
|
||||
c2 = c1 * 1.525
|
||||
if t < 0.5:
|
||||
return (pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
|
||||
return (pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2
|
||||
|
||||
|
||||
def apply_squash_stretch(
|
||||
base_scale: tuple[float, float], intensity: float, direction: str = "vertical"
|
||||
) -> tuple[float, float]:
|
||||
"""
|
||||
Calculate squash and stretch scales for more dynamic animation.
|
||||
|
||||
Args:
|
||||
base_scale: (width_scale, height_scale) base scales
|
||||
intensity: Squash/stretch intensity (0.0-1.0)
|
||||
direction: 'vertical', 'horizontal', or 'both'
|
||||
|
||||
Returns:
|
||||
(width_scale, height_scale) with squash/stretch applied
|
||||
"""
|
||||
width_scale, height_scale = base_scale
|
||||
|
||||
if direction == "vertical":
|
||||
# Compress vertically, expand horizontally (preserve volume)
|
||||
height_scale *= 1 - intensity * 0.5
|
||||
width_scale *= 1 + intensity * 0.5
|
||||
elif direction == "horizontal":
|
||||
# Compress horizontally, expand vertically
|
||||
width_scale *= 1 - intensity * 0.5
|
||||
height_scale *= 1 + intensity * 0.5
|
||||
elif direction == "both":
|
||||
# General squash (both dimensions)
|
||||
width_scale *= 1 - intensity * 0.3
|
||||
height_scale *= 1 - intensity * 0.3
|
||||
|
||||
return (width_scale, height_scale)
|
||||
|
||||
|
||||
def calculate_arc_motion(
|
||||
start: tuple[float, float], end: tuple[float, float], height: float, t: float
|
||||
) -> tuple[float, float]:
|
||||
"""
|
||||
Calculate position along a parabolic arc (natural motion path).
|
||||
|
||||
Args:
|
||||
start: (x, y) starting position
|
||||
end: (x, y) ending position
|
||||
height: Arc height at midpoint (positive = upward)
|
||||
t: Progress (0.0-1.0)
|
||||
|
||||
Returns:
|
||||
(x, y) position along arc
|
||||
"""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
# Linear interpolation for x
|
||||
x = x1 + (x2 - x1) * t
|
||||
|
||||
# Parabolic interpolation for y
|
||||
# y = start + progress * (end - start) + arc_offset
|
||||
# Arc offset peaks at t=0.5
|
||||
arc_offset = 4 * height * t * (1 - t)
|
||||
y = y1 + (y2 - y1) * t - arc_offset
|
||||
|
||||
return (x, y)
|
||||
|
||||
|
||||
# Add new easing functions to the convenience mapping
|
||||
EASING_FUNCTIONS.update(
|
||||
{
|
||||
"back_in": ease_back_in,
|
||||
"back_out": ease_back_out,
|
||||
"back_in_out": ease_back_in_out,
|
||||
"anticipate": ease_back_in, # Alias
|
||||
"overshoot": ease_back_out, # Alias
|
||||
}
|
||||
)
|
||||
176
.claude/skills/slack-gif-creator/core/frame_composer.py
Normal file
176
.claude/skills/slack-gif-creator/core/frame_composer.py
Normal file
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Frame Composer - Utilities for composing visual elements into frames.
|
||||
|
||||
Provides functions for drawing shapes, text, emojis, and compositing elements
|
||||
together to create animation frames.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
|
||||
def create_blank_frame(
|
||||
width: int, height: int, color: tuple[int, int, int] = (255, 255, 255)
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Create a blank frame with solid color background.
|
||||
|
||||
Args:
|
||||
width: Frame width
|
||||
height: Frame height
|
||||
color: RGB color tuple (default: white)
|
||||
|
||||
Returns:
|
||||
PIL Image
|
||||
"""
|
||||
return Image.new("RGB", (width, height), color)
|
||||
|
||||
|
||||
def draw_circle(
|
||||
frame: Image.Image,
|
||||
center: tuple[int, int],
|
||||
radius: int,
|
||||
fill_color: Optional[tuple[int, int, int]] = None,
|
||||
outline_color: Optional[tuple[int, int, int]] = None,
|
||||
outline_width: int = 1,
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Draw a circle on a frame.
|
||||
|
||||
Args:
|
||||
frame: PIL Image to draw on
|
||||
center: (x, y) center position
|
||||
radius: Circle radius
|
||||
fill_color: RGB fill color (None for no fill)
|
||||
outline_color: RGB outline color (None for no outline)
|
||||
outline_width: Outline width in pixels
|
||||
|
||||
Returns:
|
||||
Modified frame
|
||||
"""
|
||||
draw = ImageDraw.Draw(frame)
|
||||
x, y = center
|
||||
bbox = [x - radius, y - radius, x + radius, y + radius]
|
||||
draw.ellipse(bbox, fill=fill_color, outline=outline_color, width=outline_width)
|
||||
return frame
|
||||
|
||||
|
||||
def draw_text(
|
||||
frame: Image.Image,
|
||||
text: str,
|
||||
position: tuple[int, int],
|
||||
color: tuple[int, int, int] = (0, 0, 0),
|
||||
centered: bool = False,
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Draw text on a frame.
|
||||
|
||||
Args:
|
||||
frame: PIL Image to draw on
|
||||
text: Text to draw
|
||||
position: (x, y) position (top-left unless centered=True)
|
||||
color: RGB text color
|
||||
centered: If True, center text at position
|
||||
|
||||
Returns:
|
||||
Modified frame
|
||||
"""
|
||||
draw = ImageDraw.Draw(frame)
|
||||
|
||||
# Uses Pillow's default font.
|
||||
# If the font should be changed for the emoji, add additional logic here.
|
||||
font = ImageFont.load_default()
|
||||
|
||||
if centered:
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
x = position[0] - text_width // 2
|
||||
y = position[1] - text_height // 2
|
||||
position = (x, y)
|
||||
|
||||
draw.text(position, text, fill=color, font=font)
|
||||
return frame
|
||||
|
||||
|
||||
def create_gradient_background(
|
||||
width: int,
|
||||
height: int,
|
||||
top_color: tuple[int, int, int],
|
||||
bottom_color: tuple[int, int, int],
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Create a vertical gradient background.
|
||||
|
||||
Args:
|
||||
width: Frame width
|
||||
height: Frame height
|
||||
top_color: RGB color at top
|
||||
bottom_color: RGB color at bottom
|
||||
|
||||
Returns:
|
||||
PIL Image with gradient
|
||||
"""
|
||||
frame = Image.new("RGB", (width, height))
|
||||
draw = ImageDraw.Draw(frame)
|
||||
|
||||
# Calculate color step for each row
|
||||
r1, g1, b1 = top_color
|
||||
r2, g2, b2 = bottom_color
|
||||
|
||||
for y in range(height):
|
||||
# Interpolate color
|
||||
ratio = y / height
|
||||
r = int(r1 * (1 - ratio) + r2 * ratio)
|
||||
g = int(g1 * (1 - ratio) + g2 * ratio)
|
||||
b = int(b1 * (1 - ratio) + b2 * ratio)
|
||||
|
||||
# Draw horizontal line
|
||||
draw.line([(0, y), (width, y)], fill=(r, g, b))
|
||||
|
||||
return frame
|
||||
|
||||
|
||||
def draw_star(
|
||||
frame: Image.Image,
|
||||
center: tuple[int, int],
|
||||
size: int,
|
||||
fill_color: tuple[int, int, int],
|
||||
outline_color: Optional[tuple[int, int, int]] = None,
|
||||
outline_width: int = 1,
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Draw a 5-pointed star.
|
||||
|
||||
Args:
|
||||
frame: PIL Image to draw on
|
||||
center: (x, y) center position
|
||||
size: Star size (outer radius)
|
||||
fill_color: RGB fill color
|
||||
outline_color: RGB outline color (None for no outline)
|
||||
outline_width: Outline width
|
||||
|
||||
Returns:
|
||||
Modified frame
|
||||
"""
|
||||
import math
|
||||
|
||||
draw = ImageDraw.Draw(frame)
|
||||
x, y = center
|
||||
|
||||
# Calculate star points
|
||||
points = []
|
||||
for i in range(10):
|
||||
angle = (i * 36 - 90) * math.pi / 180 # 36 degrees per point, start at top
|
||||
radius = size if i % 2 == 0 else size * 0.4 # Alternate between outer and inner
|
||||
px = x + radius * math.cos(angle)
|
||||
py = y + radius * math.sin(angle)
|
||||
points.append((px, py))
|
||||
|
||||
# Draw star
|
||||
draw.polygon(points, fill=fill_color, outline=outline_color, width=outline_width)
|
||||
|
||||
return frame
|
||||
269
.claude/skills/slack-gif-creator/core/gif_builder.py
Normal file
269
.claude/skills/slack-gif-creator/core/gif_builder.py
Normal file
@@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GIF Builder - Core module for assembling frames into GIFs optimized for Slack.
|
||||
|
||||
This module provides the main interface for creating GIFs from programmatically
|
||||
generated frames, with automatic optimization for Slack's requirements.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import imageio.v3 as imageio
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class GIFBuilder:
|
||||
"""Builder for creating optimized GIFs from frames."""
|
||||
|
||||
def __init__(self, width: int = 480, height: int = 480, fps: int = 15):
|
||||
"""
|
||||
Initialize GIF builder.
|
||||
|
||||
Args:
|
||||
width: Frame width in pixels
|
||||
height: Frame height in pixels
|
||||
fps: Frames per second
|
||||
"""
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.fps = fps
|
||||
self.frames: list[np.ndarray] = []
|
||||
|
||||
def add_frame(self, frame: np.ndarray | Image.Image):
|
||||
"""
|
||||
Add a frame to the GIF.
|
||||
|
||||
Args:
|
||||
frame: Frame as numpy array or PIL Image (will be converted to RGB)
|
||||
"""
|
||||
if isinstance(frame, Image.Image):
|
||||
frame = np.array(frame.convert("RGB"))
|
||||
|
||||
# Ensure frame is correct size
|
||||
if frame.shape[:2] != (self.height, self.width):
|
||||
pil_frame = Image.fromarray(frame)
|
||||
pil_frame = pil_frame.resize(
|
||||
(self.width, self.height), Image.Resampling.LANCZOS
|
||||
)
|
||||
frame = np.array(pil_frame)
|
||||
|
||||
self.frames.append(frame)
|
||||
|
||||
def add_frames(self, frames: list[np.ndarray | Image.Image]):
|
||||
"""Add multiple frames at once."""
|
||||
for frame in frames:
|
||||
self.add_frame(frame)
|
||||
|
||||
def optimize_colors(
|
||||
self, num_colors: int = 128, use_global_palette: bool = True
|
||||
) -> list[np.ndarray]:
|
||||
"""
|
||||
Reduce colors in all frames using quantization.
|
||||
|
||||
Args:
|
||||
num_colors: Target number of colors (8-256)
|
||||
use_global_palette: Use a single palette for all frames (better compression)
|
||||
|
||||
Returns:
|
||||
List of color-optimized frames
|
||||
"""
|
||||
optimized = []
|
||||
|
||||
if use_global_palette and len(self.frames) > 1:
|
||||
# Create a global palette from all frames
|
||||
# Sample frames to build palette
|
||||
sample_size = min(5, len(self.frames))
|
||||
sample_indices = [
|
||||
int(i * len(self.frames) / sample_size) for i in range(sample_size)
|
||||
]
|
||||
sample_frames = [self.frames[i] for i in sample_indices]
|
||||
|
||||
# Combine sample frames into a single image for palette generation
|
||||
# Flatten each frame to get all pixels, then stack them
|
||||
all_pixels = np.vstack(
|
||||
[f.reshape(-1, 3) for f in sample_frames]
|
||||
) # (total_pixels, 3)
|
||||
|
||||
# Create a properly-shaped RGB image from the pixel data
|
||||
# We'll make a roughly square image from all the pixels
|
||||
total_pixels = len(all_pixels)
|
||||
width = min(512, int(np.sqrt(total_pixels))) # Reasonable width, max 512
|
||||
height = (total_pixels + width - 1) // width # Ceiling division
|
||||
|
||||
# Pad if necessary to fill the rectangle
|
||||
pixels_needed = width * height
|
||||
if pixels_needed > total_pixels:
|
||||
padding = np.zeros((pixels_needed - total_pixels, 3), dtype=np.uint8)
|
||||
all_pixels = np.vstack([all_pixels, padding])
|
||||
|
||||
# Reshape to proper RGB image format (H, W, 3)
|
||||
img_array = (
|
||||
all_pixels[:pixels_needed].reshape(height, width, 3).astype(np.uint8)
|
||||
)
|
||||
combined_img = Image.fromarray(img_array, mode="RGB")
|
||||
|
||||
# Generate global palette
|
||||
global_palette = combined_img.quantize(colors=num_colors, method=2)
|
||||
|
||||
# Apply global palette to all frames
|
||||
for frame in self.frames:
|
||||
pil_frame = Image.fromarray(frame)
|
||||
quantized = pil_frame.quantize(palette=global_palette, dither=1)
|
||||
optimized.append(np.array(quantized.convert("RGB")))
|
||||
else:
|
||||
# Use per-frame quantization
|
||||
for frame in self.frames:
|
||||
pil_frame = Image.fromarray(frame)
|
||||
quantized = pil_frame.quantize(colors=num_colors, method=2, dither=1)
|
||||
optimized.append(np.array(quantized.convert("RGB")))
|
||||
|
||||
return optimized
|
||||
|
||||
def deduplicate_frames(self, threshold: float = 0.9995) -> int:
|
||||
"""
|
||||
Remove duplicate or near-duplicate consecutive frames.
|
||||
|
||||
Args:
|
||||
threshold: Similarity threshold (0.0-1.0). Higher = more strict (0.9995 = nearly identical).
|
||||
Use 0.9995+ to preserve subtle animations, 0.98 for aggressive removal.
|
||||
|
||||
Returns:
|
||||
Number of frames removed
|
||||
"""
|
||||
if len(self.frames) < 2:
|
||||
return 0
|
||||
|
||||
deduplicated = [self.frames[0]]
|
||||
removed_count = 0
|
||||
|
||||
for i in range(1, len(self.frames)):
|
||||
# Compare with previous frame
|
||||
prev_frame = np.array(deduplicated[-1], dtype=np.float32)
|
||||
curr_frame = np.array(self.frames[i], dtype=np.float32)
|
||||
|
||||
# Calculate similarity (normalized)
|
||||
diff = np.abs(prev_frame - curr_frame)
|
||||
similarity = 1.0 - (np.mean(diff) / 255.0)
|
||||
|
||||
# Keep frame if sufficiently different
|
||||
# High threshold (0.9995+) means only remove nearly identical frames
|
||||
if similarity < threshold:
|
||||
deduplicated.append(self.frames[i])
|
||||
else:
|
||||
removed_count += 1
|
||||
|
||||
self.frames = deduplicated
|
||||
return removed_count
|
||||
|
||||
def save(
|
||||
self,
|
||||
output_path: str | Path,
|
||||
num_colors: int = 128,
|
||||
optimize_for_emoji: bool = False,
|
||||
remove_duplicates: bool = False,
|
||||
) -> dict:
|
||||
"""
|
||||
Save frames as optimized GIF for Slack.
|
||||
|
||||
Args:
|
||||
output_path: Where to save the GIF
|
||||
num_colors: Number of colors to use (fewer = smaller file)
|
||||
optimize_for_emoji: If True, optimize for emoji size (128x128, fewer colors)
|
||||
remove_duplicates: If True, remove duplicate consecutive frames (opt-in)
|
||||
|
||||
Returns:
|
||||
Dictionary with file info (path, size, dimensions, frame_count)
|
||||
"""
|
||||
if not self.frames:
|
||||
raise ValueError("No frames to save. Add frames with add_frame() first.")
|
||||
|
||||
output_path = Path(output_path)
|
||||
|
||||
# Remove duplicate frames to reduce file size
|
||||
if remove_duplicates:
|
||||
removed = self.deduplicate_frames(threshold=0.9995)
|
||||
if removed > 0:
|
||||
print(
|
||||
f" Removed {removed} nearly identical frames (preserved subtle animations)"
|
||||
)
|
||||
|
||||
# Optimize for emoji if requested
|
||||
if optimize_for_emoji:
|
||||
if self.width > 128 or self.height > 128:
|
||||
print(
|
||||
f" Resizing from {self.width}x{self.height} to 128x128 for emoji"
|
||||
)
|
||||
self.width = 128
|
||||
self.height = 128
|
||||
# Resize all frames
|
||||
resized_frames = []
|
||||
for frame in self.frames:
|
||||
pil_frame = Image.fromarray(frame)
|
||||
pil_frame = pil_frame.resize((128, 128), Image.Resampling.LANCZOS)
|
||||
resized_frames.append(np.array(pil_frame))
|
||||
self.frames = resized_frames
|
||||
num_colors = min(num_colors, 48) # More aggressive color limit for emoji
|
||||
|
||||
# More aggressive FPS reduction for emoji
|
||||
if len(self.frames) > 12:
|
||||
print(
|
||||
f" Reducing frames from {len(self.frames)} to ~12 for emoji size"
|
||||
)
|
||||
# Keep every nth frame to get close to 12 frames
|
||||
keep_every = max(1, len(self.frames) // 12)
|
||||
self.frames = [
|
||||
self.frames[i] for i in range(0, len(self.frames), keep_every)
|
||||
]
|
||||
|
||||
# Optimize colors with global palette
|
||||
optimized_frames = self.optimize_colors(num_colors, use_global_palette=True)
|
||||
|
||||
# Calculate frame duration in milliseconds
|
||||
frame_duration = 1000 / self.fps
|
||||
|
||||
# Save GIF
|
||||
imageio.imwrite(
|
||||
output_path,
|
||||
optimized_frames,
|
||||
duration=frame_duration,
|
||||
loop=0, # Infinite loop
|
||||
)
|
||||
|
||||
# Get file info
|
||||
file_size_kb = output_path.stat().st_size / 1024
|
||||
file_size_mb = file_size_kb / 1024
|
||||
|
||||
info = {
|
||||
"path": str(output_path),
|
||||
"size_kb": file_size_kb,
|
||||
"size_mb": file_size_mb,
|
||||
"dimensions": f"{self.width}x{self.height}",
|
||||
"frame_count": len(optimized_frames),
|
||||
"fps": self.fps,
|
||||
"duration_seconds": len(optimized_frames) / self.fps,
|
||||
"colors": num_colors,
|
||||
}
|
||||
|
||||
# Print info
|
||||
print(f"\n✓ GIF created successfully!")
|
||||
print(f" Path: {output_path}")
|
||||
print(f" Size: {file_size_kb:.1f} KB ({file_size_mb:.2f} MB)")
|
||||
print(f" Dimensions: {self.width}x{self.height}")
|
||||
print(f" Frames: {len(optimized_frames)} @ {self.fps} fps")
|
||||
print(f" Duration: {info['duration_seconds']:.1f}s")
|
||||
print(f" Colors: {num_colors}")
|
||||
|
||||
# Size info
|
||||
if optimize_for_emoji:
|
||||
print(f" Optimized for emoji (128x128, reduced colors)")
|
||||
if file_size_mb > 1.0:
|
||||
print(f"\n Note: Large file size ({file_size_kb:.1f} KB)")
|
||||
print(" Consider: fewer frames, smaller dimensions, or fewer colors")
|
||||
|
||||
return info
|
||||
|
||||
def clear(self):
|
||||
"""Clear all frames (useful for creating multiple GIFs)."""
|
||||
self.frames = []
|
||||
136
.claude/skills/slack-gif-creator/core/validators.py
Normal file
136
.claude/skills/slack-gif-creator/core/validators.py
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Validators - Check if GIFs meet Slack's requirements.
|
||||
|
||||
These validators help ensure your GIFs meet Slack's size and dimension constraints.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def validate_gif(
|
||||
gif_path: str | Path, is_emoji: bool = True, verbose: bool = True
|
||||
) -> tuple[bool, dict]:
|
||||
"""
|
||||
Validate GIF for Slack (dimensions, size, frame count).
|
||||
|
||||
Args:
|
||||
gif_path: Path to GIF file
|
||||
is_emoji: True for emoji (128x128 recommended), False for message GIF
|
||||
verbose: Print validation details
|
||||
|
||||
Returns:
|
||||
Tuple of (passes: bool, results: dict with all details)
|
||||
"""
|
||||
from PIL import Image
|
||||
|
||||
gif_path = Path(gif_path)
|
||||
|
||||
if not gif_path.exists():
|
||||
return False, {"error": f"File not found: {gif_path}"}
|
||||
|
||||
# Get file size
|
||||
size_bytes = gif_path.stat().st_size
|
||||
size_kb = size_bytes / 1024
|
||||
size_mb = size_kb / 1024
|
||||
|
||||
# Get dimensions and frame info
|
||||
try:
|
||||
with Image.open(gif_path) as img:
|
||||
width, height = img.size
|
||||
|
||||
# Count frames
|
||||
frame_count = 0
|
||||
try:
|
||||
while True:
|
||||
img.seek(frame_count)
|
||||
frame_count += 1
|
||||
except EOFError:
|
||||
pass
|
||||
|
||||
# Get duration
|
||||
try:
|
||||
duration_ms = img.info.get("duration", 100)
|
||||
total_duration = (duration_ms * frame_count) / 1000
|
||||
fps = frame_count / total_duration if total_duration > 0 else 0
|
||||
except:
|
||||
total_duration = None
|
||||
fps = None
|
||||
|
||||
except Exception as e:
|
||||
return False, {"error": f"Failed to read GIF: {e}"}
|
||||
|
||||
# Validate dimensions
|
||||
if is_emoji:
|
||||
optimal = width == height == 128
|
||||
acceptable = width == height and 64 <= width <= 128
|
||||
dim_pass = acceptable
|
||||
else:
|
||||
aspect_ratio = (
|
||||
max(width, height) / min(width, height)
|
||||
if min(width, height) > 0
|
||||
else float("inf")
|
||||
)
|
||||
dim_pass = aspect_ratio <= 2.0 and 320 <= min(width, height) <= 640
|
||||
|
||||
results = {
|
||||
"file": str(gif_path),
|
||||
"passes": dim_pass,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"size_kb": size_kb,
|
||||
"size_mb": size_mb,
|
||||
"frame_count": frame_count,
|
||||
"duration_seconds": total_duration,
|
||||
"fps": fps,
|
||||
"is_emoji": is_emoji,
|
||||
"optimal": optimal if is_emoji else None,
|
||||
}
|
||||
|
||||
# Print if verbose
|
||||
if verbose:
|
||||
print(f"\nValidating {gif_path.name}:")
|
||||
print(
|
||||
f" Dimensions: {width}x{height}"
|
||||
+ (
|
||||
f" ({'optimal' if optimal else 'acceptable'})"
|
||||
if is_emoji and acceptable
|
||||
else ""
|
||||
)
|
||||
)
|
||||
print(
|
||||
f" Size: {size_kb:.1f} KB"
|
||||
+ (f" ({size_mb:.2f} MB)" if size_mb >= 1.0 else "")
|
||||
)
|
||||
print(
|
||||
f" Frames: {frame_count}"
|
||||
+ (f" @ {fps:.1f} fps ({total_duration:.1f}s)" if fps else "")
|
||||
)
|
||||
|
||||
if not dim_pass:
|
||||
print(
|
||||
f" Note: {'Emoji should be 128x128' if is_emoji else 'Unusual dimensions for Slack'}"
|
||||
)
|
||||
|
||||
if size_mb > 5.0:
|
||||
print(f" Note: Large file size - consider fewer frames/colors")
|
||||
|
||||
return dim_pass, results
|
||||
|
||||
|
||||
def is_slack_ready(
|
||||
gif_path: str | Path, is_emoji: bool = True, verbose: bool = True
|
||||
) -> bool:
|
||||
"""
|
||||
Quick check if GIF is ready for Slack.
|
||||
|
||||
Args:
|
||||
gif_path: Path to GIF file
|
||||
is_emoji: True for emoji GIF, False for message GIF
|
||||
verbose: Print feedback
|
||||
|
||||
Returns:
|
||||
True if dimensions are acceptable
|
||||
"""
|
||||
passes, _ = validate_gif(gif_path, is_emoji, verbose)
|
||||
return passes
|
||||
Reference in New Issue
Block a user