@thanhvn14/csvibe 0.1.3 → 0.1.5
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/.github/agents/schemas/base-output.schema.json +88 -0
- package/.github/agents/schemas/brainstorm-output.schema.json +88 -0
- package/.github/agents/schemas/scout-output.schema.json +60 -0
- package/.github/agents/scripts/fetch-copilot-tools.js +245 -0
- package/.github/agents/scripts/lib/parse-agent-file.js +275 -0
- package/.github/agents/scripts/package-lock.json +78 -0
- package/.github/agents/scripts/package.json +22 -0
- package/.github/agents/scripts/schemas/agent-frontmatter.schema.json +83 -0
- package/.github/agents/scripts/validate-agent-all.js +157 -0
- package/.github/agents/scripts/validate-agent-frontmatter.js +96 -0
- package/.github/agents/scripts/validate-agent-handoffs.js +169 -0
- package/.github/agents/scripts/validate-agent-output.js +157 -0
- package/.github/agents/scripts/validate-agent-tools.js +278 -0
- package/.github/skills/.env.example +100 -0
- package/.github/skills/.install-state.json +23 -0
- package/.github/skills/README.md +149 -0
- package/.github/skills/ai-multimodal/.env.example +204 -0
- package/.github/skills/ai-multimodal/scripts/.coverage +0 -0
- package/.github/skills/ai-multimodal/scripts/check_setup.py +305 -0
- package/.github/skills/ai-multimodal/scripts/document_converter.py +395 -0
- package/.github/skills/ai-multimodal/scripts/gemini_batch_process.py +1184 -0
- package/.github/skills/ai-multimodal/scripts/media_optimizer.py +506 -0
- package/.github/skills/ai-multimodal/scripts/requirements.txt +26 -0
- package/.github/skills/better-auth/scripts/.coverage +0 -0
- package/.github/skills/better-auth/scripts/better_auth_init.py +521 -0
- package/.github/skills/better-auth/scripts/requirements.txt +15 -0
- package/.github/skills/chrome-devtools/scripts/README.md +272 -0
- package/.github/skills/chrome-devtools/scripts/__tests__/selector.test.js +210 -0
- package/.github/skills/chrome-devtools/scripts/aria-snapshot.js +362 -0
- package/.github/skills/chrome-devtools/scripts/click.js +83 -0
- package/.github/skills/chrome-devtools/scripts/console.js +79 -0
- package/.github/skills/chrome-devtools/scripts/evaluate.js +53 -0
- package/.github/skills/chrome-devtools/scripts/fill.js +76 -0
- package/.github/skills/chrome-devtools/scripts/inject-auth.js +229 -0
- package/.github/skills/chrome-devtools/scripts/install-deps.sh +181 -0
- package/.github/skills/chrome-devtools/scripts/install.sh +83 -0
- package/.github/skills/chrome-devtools/scripts/lib/browser.js +318 -0
- package/.github/skills/chrome-devtools/scripts/lib/selector.js +178 -0
- package/.github/skills/chrome-devtools/scripts/navigate.js +54 -0
- package/.github/skills/chrome-devtools/scripts/network.js +106 -0
- package/.github/skills/chrome-devtools/scripts/package-lock.json +1589 -0
- package/.github/skills/chrome-devtools/scripts/package.json +16 -0
- package/.github/skills/chrome-devtools/scripts/performance.js +149 -0
- package/.github/skills/chrome-devtools/scripts/screenshot.js +198 -0
- package/.github/skills/chrome-devtools/scripts/select-ref.js +131 -0
- package/.github/skills/chrome-devtools/scripts/snapshot.js +135 -0
- package/.github/skills/common/README.md +120 -0
- package/.github/skills/common/api_key_helper.py +411 -0
- package/.github/skills/common/api_key_rotator.py +248 -0
- package/.github/skills/databases/scripts/.coverage +0 -0
- package/.github/skills/databases/scripts/db_backup.py +502 -0
- package/.github/skills/databases/scripts/db_migrate.py +425 -0
- package/.github/skills/databases/scripts/db_performance_check.py +456 -0
- package/.github/skills/databases/scripts/requirements.txt +20 -0
- package/.github/skills/debugging/scripts/find-polluter.sh +63 -0
- package/.github/skills/devops/.env.example +76 -0
- package/.github/skills/devops/scripts/cloudflare_deploy.py +269 -0
- package/.github/skills/devops/scripts/docker_optimize.py +331 -0
- package/.github/skills/devops/scripts/requirements.txt +20 -0
- package/.github/skills/docs-seeker/.env.example +15 -0
- package/.github/skills/docs-seeker/package.json +25 -0
- package/.github/skills/docs-seeker/scripts/analyze-llms-txt.js +211 -0
- package/.github/skills/docs-seeker/scripts/detect-topic.js +172 -0
- package/.github/skills/docs-seeker/scripts/fetch-docs.js +213 -0
- package/.github/skills/docs-seeker/scripts/utils/env-loader.js +94 -0
- package/.github/skills/document-skills/docx/LICENSE.txt +30 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/mce/mc.xsd +75 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/.github/skills/document-skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/.github/skills/document-skills/docx/ooxml/scripts/pack.py +159 -0
- package/.github/skills/document-skills/docx/ooxml/scripts/unpack.py +29 -0
- package/.github/skills/document-skills/docx/ooxml/scripts/validate.py +69 -0
- package/.github/skills/document-skills/docx/ooxml/scripts/validation/__init__.py +15 -0
- package/.github/skills/document-skills/docx/ooxml/scripts/validation/base.py +951 -0
- package/.github/skills/document-skills/docx/ooxml/scripts/validation/docx.py +274 -0
- package/.github/skills/document-skills/docx/ooxml/scripts/validation/pptx.py +315 -0
- package/.github/skills/document-skills/docx/ooxml/scripts/validation/redlining.py +279 -0
- package/.github/skills/document-skills/docx/scripts/__init__.py +1 -0
- package/.github/skills/document-skills/docx/scripts/document.py +1276 -0
- package/.github/skills/document-skills/docx/scripts/templates/comments.xml +3 -0
- package/.github/skills/document-skills/docx/scripts/templates/commentsExtended.xml +3 -0
- package/.github/skills/document-skills/docx/scripts/templates/commentsExtensible.xml +3 -0
- package/.github/skills/document-skills/docx/scripts/templates/commentsIds.xml +3 -0
- package/.github/skills/document-skills/docx/scripts/templates/people.xml +3 -0
- package/.github/skills/document-skills/docx/scripts/utilities.py +374 -0
- package/.github/skills/document-skills/pdf/LICENSE.txt +30 -0
- package/.github/skills/document-skills/pdf/scripts/check_bounding_boxes.py +70 -0
- package/.github/skills/document-skills/pdf/scripts/check_bounding_boxes_test.py +226 -0
- package/.github/skills/document-skills/pdf/scripts/check_fillable_fields.py +12 -0
- package/.github/skills/document-skills/pdf/scripts/convert_pdf_to_images.py +35 -0
- package/.github/skills/document-skills/pdf/scripts/create_validation_image.py +41 -0
- package/.github/skills/document-skills/pdf/scripts/extract_form_field_info.py +152 -0
- package/.github/skills/document-skills/pdf/scripts/fill_fillable_fields.py +114 -0
- package/.github/skills/document-skills/pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
- package/.github/skills/document-skills/pptx/LICENSE.txt +30 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/mce/mc.xsd +75 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/.github/skills/document-skills/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/.github/skills/document-skills/pptx/ooxml/scripts/pack.py +159 -0
- package/.github/skills/document-skills/pptx/ooxml/scripts/unpack.py +29 -0
- package/.github/skills/document-skills/pptx/ooxml/scripts/validate.py +69 -0
- package/.github/skills/document-skills/pptx/ooxml/scripts/validation/__init__.py +15 -0
- package/.github/skills/document-skills/pptx/ooxml/scripts/validation/base.py +951 -0
- package/.github/skills/document-skills/pptx/ooxml/scripts/validation/docx.py +274 -0
- package/.github/skills/document-skills/pptx/ooxml/scripts/validation/pptx.py +315 -0
- package/.github/skills/document-skills/pptx/ooxml/scripts/validation/redlining.py +279 -0
- package/.github/skills/document-skills/pptx/scripts/html2pptx.js +979 -0
- package/.github/skills/document-skills/pptx/scripts/inventory.py +1020 -0
- package/.github/skills/document-skills/pptx/scripts/rearrange.py +231 -0
- package/.github/skills/document-skills/pptx/scripts/replace.py +385 -0
- package/.github/skills/document-skills/pptx/scripts/thumbnail.py +450 -0
- package/.github/skills/document-skills/xlsx/LICENSE.txt +30 -0
- package/.github/skills/document-skills/xlsx/recalc.py +190 -0
- package/.github/skills/install.ps1 +1220 -0
- package/.github/skills/install.sh +1032 -0
- package/.github/skills/markdown-novel-viewer/assets/directory-browser.css +215 -0
- package/.github/skills/markdown-novel-viewer/assets/favicon.png +0 -0
- package/.github/skills/markdown-novel-viewer/assets/novel-theme.css +818 -0
- package/.github/skills/markdown-novel-viewer/assets/reader.js +262 -0
- package/.github/skills/markdown-novel-viewer/assets/template.html +80 -0
- package/.github/skills/markdown-novel-viewer/package-lock.json +146 -0
- package/.github/skills/markdown-novel-viewer/package.json +15 -0
- package/.github/skills/markdown-novel-viewer/scripts/lib/http-server.cjs +434 -0
- package/.github/skills/markdown-novel-viewer/scripts/lib/markdown-renderer.cjs +272 -0
- package/.github/skills/markdown-novel-viewer/scripts/lib/plan-navigator.cjs +509 -0
- package/.github/skills/markdown-novel-viewer/scripts/lib/port-finder.cjs +48 -0
- package/.github/skills/markdown-novel-viewer/scripts/lib/process-mgr.cjs +150 -0
- package/.github/skills/markdown-novel-viewer/scripts/server.cjs +411 -0
- package/.github/skills/mcp-builder/LICENSE.txt +202 -0
- package/.github/skills/mcp-builder/scripts/connections.py +151 -0
- package/.github/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/.github/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/.github/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/.github/skills/mcp-management/README.md +219 -0
- package/.github/skills/mcp-management/assets/tools.json +3146 -0
- package/.github/skills/mcp-management/package-lock.json +6 -0
- package/.github/skills/mcp-management/scripts/.env.example +10 -0
- package/.github/skills/mcp-management/scripts/cli.ts +195 -0
- package/.github/skills/mcp-management/scripts/dist/analyze-tools.js +70 -0
- package/.github/skills/mcp-management/scripts/dist/cli.js +160 -0
- package/.github/skills/mcp-management/scripts/dist/mcp-client.js +183 -0
- package/.github/skills/mcp-management/scripts/mcp-client.ts +230 -0
- package/.github/skills/mcp-management/scripts/package.json +20 -0
- package/.github/skills/media-processing/scripts/README.md +111 -0
- package/.github/skills/media-processing/scripts/batch-remove-background.sh +124 -0
- package/.github/skills/media-processing/scripts/batch_resize.py +342 -0
- package/.github/skills/media-processing/scripts/media_convert.py +311 -0
- package/.github/skills/media-processing/scripts/remove-background.sh +96 -0
- package/.github/skills/media-processing/scripts/remove-bg-node.js +158 -0
- package/.github/skills/media-processing/scripts/requirements.txt +24 -0
- package/.github/skills/media-processing/scripts/video_optimize.py +414 -0
- package/.github/skills/payment-integration/README.md +185 -0
- package/.github/skills/payment-integration/scripts/.env.example +20 -0
- package/.github/skills/payment-integration/scripts/checkout-helper.js +244 -0
- package/.github/skills/payment-integration/scripts/package.json +17 -0
- package/.github/skills/payment-integration/scripts/polar-webhook-verify.js +202 -0
- package/.github/skills/payment-integration/scripts/sepay-webhook-verify.js +193 -0
- package/.github/skills/payment-integration/scripts/test-scripts.js +237 -0
- package/.github/skills/plans-kanban/assets/dashboard-template.html +119 -0
- package/.github/skills/plans-kanban/assets/dashboard.css +1594 -0
- package/.github/skills/plans-kanban/assets/dashboard.js +596 -0
- package/.github/skills/plans-kanban/assets/favicon.png +0 -0
- package/.github/skills/plans-kanban/package-lock.json +123 -0
- package/.github/skills/plans-kanban/package.json +13 -0
- package/.github/skills/plans-kanban/scripts/lib/dashboard-renderer.cjs +884 -0
- package/.github/skills/plans-kanban/scripts/lib/http-server.cjs +310 -0
- package/.github/skills/plans-kanban/scripts/lib/plan-metadata-extractor.cjs +489 -0
- package/.github/skills/plans-kanban/scripts/lib/plan-parser.cjs +175 -0
- package/.github/skills/plans-kanban/scripts/lib/plan-scanner.cjs +272 -0
- package/.github/skills/plans-kanban/scripts/lib/port-finder.cjs +48 -0
- package/.github/skills/plans-kanban/scripts/lib/process-mgr.cjs +128 -0
- package/.github/skills/plans-kanban/scripts/server.cjs +260 -0
- package/.github/skills/repomix/scripts/.coverage +0 -0
- package/.github/skills/repomix/scripts/README.md +179 -0
- package/.github/skills/repomix/scripts/repomix_batch.py +455 -0
- package/.github/skills/repomix/scripts/repos.example.json +15 -0
- package/.github/skills/repomix/scripts/requirements.txt +15 -0
- package/.github/skills/scout-validation/scripts/lib/broad-pattern-detector.cjs +124 -0
- package/.github/skills/scout-validation/scripts/lib/path-checker.cjs +66 -0
- package/.github/skills/scout-validation/scripts/lib/schema-validator.cjs +45 -0
- package/.github/skills/scout-validation/scripts/package.json +11 -0
- package/.github/skills/scout-validation/scripts/validate-scout-output.cjs +219 -0
- package/.github/skills/scout-validation/test/broad-pattern-output.json +18 -0
- package/.github/skills/scout-validation/test/invalid-path-output.json +18 -0
- package/.github/skills/scout-validation/test/valid-scout-output.json +26 -0
- package/.github/skills/sequential-thinking/.env.example +8 -0
- package/.github/skills/sequential-thinking/README.md +183 -0
- package/.github/skills/sequential-thinking/package.json +31 -0
- package/.github/skills/sequential-thinking/scripts/format-thought.js +159 -0
- package/.github/skills/sequential-thinking/scripts/process-thought.js +236 -0
- package/.github/skills/shopify/README.md +66 -0
- package/.github/skills/shopify/scripts/.coverage +0 -0
- package/.github/skills/shopify/scripts/requirements.txt +19 -0
- package/.github/skills/shopify/scripts/shopify_init.py +423 -0
- package/.github/skills/skill-creator/LICENSE.txt +202 -0
- package/.github/skills/skill-creator/scripts/init_skill.py +303 -0
- package/.github/skills/skill-creator/scripts/package_skill.py +110 -0
- package/.github/skills/skill-creator/scripts/quick_validate.py +65 -0
- package/.github/skills/ui-styling/LICENSE.txt +202 -0
- package/.github/skills/ui-styling/canvas-fonts/ArsenalSC-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/BigShoulders-Bold.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/BigShoulders-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/BigShoulders-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/Boldonse-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/Boldonse-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/CrimsonPro-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/DMMono-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/DMMono-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/EricaOne-OFL.txt +94 -0
- package/.github/skills/ui-styling/canvas-fonts/EricaOne-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/GeistMono-Bold.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/GeistMono-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/GeistMono-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/Gloock-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/Gloock-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/IBMPlexMono-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/InstrumentSans-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/Italiana-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/Italiana-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/Jura-Light.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/Jura-Medium.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/Jura-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/LibreBaskerville-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/Lora-Bold.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/Lora-BoldItalic.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/Lora-Italic.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/Lora-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/Lora-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/NationalPark-Bold.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/NationalPark-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/NationalPark-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/Outfit-Bold.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/Outfit-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/Outfit-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/PixelifySans-Medium.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/PixelifySans-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/PoiretOne-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/PoiretOne-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/RedHatMono-Bold.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/RedHatMono-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/RedHatMono-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/Silkscreen-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/Silkscreen-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/SmoochSans-Medium.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/SmoochSans-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/Tektur-Medium.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/Tektur-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/Tektur-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/WorkSans-Bold.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/WorkSans-Italic.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/WorkSans-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/WorkSans-Regular.ttf +0 -0
- package/.github/skills/ui-styling/canvas-fonts/YoungSerif-OFL.txt +93 -0
- package/.github/skills/ui-styling/canvas-fonts/YoungSerif-Regular.ttf +0 -0
- package/.github/skills/ui-styling/scripts/.coverage +0 -0
- package/.github/skills/ui-styling/scripts/requirements.txt +17 -0
- package/.github/skills/ui-styling/scripts/shadcn_add.py +292 -0
- package/.github/skills/ui-styling/scripts/tailwind_config_gen.py +456 -0
- package/.github/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.github/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.github/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.github/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.github/skills/ui-ux-pro-max/data/prompts.csv +24 -0
- package/.github/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.github/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +51 -0
- package/.github/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.github/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.github/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.github/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.github/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.github/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.github/skills/ui-ux-pro-max/data/styles.csv +59 -0
- package/.github/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.github/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.github/skills/ui-ux-pro-max/scripts/core.py +236 -0
- package/.github/skills/ui-ux-pro-max/scripts/search.py +76 -0
- package/.github/skills/web-frameworks/scripts/.coverage +0 -0
- package/.github/skills/web-frameworks/scripts/__init__.py +0 -0
- package/.github/skills/web-frameworks/scripts/nextjs_init.py +547 -0
- package/.github/skills/web-frameworks/scripts/requirements.txt +16 -0
- package/.github/skills/web-frameworks/scripts/turborepo_migrate.py +394 -0
- package/dist/config/constants.d.ts +3 -0
- package/dist/config/constants.d.ts.map +1 -1
- package/dist/config/constants.js +5 -1
- package/dist/config/constants.js.map +1 -1
- package/dist/domains/github/github-client.d.ts +5 -0
- package/dist/domains/github/github-client.d.ts.map +1 -1
- package/dist/domains/github/github-client.js +44 -0
- package/dist/domains/github/github-client.js.map +1 -1
- package/dist/utils/downloader.d.ts +3 -1
- package/dist/utils/downloader.d.ts.map +1 -1
- package/dist/utils/downloader.js +48 -11
- package/dist/utils/downloader.js.map +1 -1
- package/dist/utils/scaffolder.d.ts.map +1 -1
- package/dist/utils/scaffolder.js +2 -0
- package/dist/utils/scaffolder.js.map +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,1184 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Batch process multiple media files using Gemini API.
|
|
4
|
+
|
|
5
|
+
Supports all Gemini modalities:
|
|
6
|
+
- Audio: Transcription, analysis, summarization
|
|
7
|
+
- Image: Captioning, detection, OCR, analysis
|
|
8
|
+
- Video: Summarization, Q&A, scene detection
|
|
9
|
+
- Document: PDF extraction, structured output
|
|
10
|
+
- Generation: Image creation via Imagen 4 or Nano Banana (Gemini native)
|
|
11
|
+
- Nano Banana Flash (gemini-2.5-flash-image): Speed/volume
|
|
12
|
+
- Nano Banana Pro (gemini-3-pro-image-preview): Quality/4K text/reasoning
|
|
13
|
+
- Imagen 4 (imagen-4.0-*): Production-grade generation
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import List, Dict, Any, Optional
|
|
23
|
+
import csv
|
|
24
|
+
import shutil
|
|
25
|
+
|
|
26
|
+
# Import centralized environment resolver
|
|
27
|
+
sys.path.insert(0, str(Path.home() / '.claude' / 'scripts'))
|
|
28
|
+
try:
|
|
29
|
+
from resolve_env import resolve_env
|
|
30
|
+
CENTRALIZED_RESOLVER_AVAILABLE = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
# Fallback if centralized resolver not available
|
|
33
|
+
CENTRALIZED_RESOLVER_AVAILABLE = False
|
|
34
|
+
try:
|
|
35
|
+
from dotenv import load_dotenv
|
|
36
|
+
except ImportError:
|
|
37
|
+
load_dotenv = None
|
|
38
|
+
|
|
39
|
+
# Import key rotation support
|
|
40
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'common'))
|
|
41
|
+
try:
|
|
42
|
+
from api_key_rotator import KeyRotator, is_rate_limit_error
|
|
43
|
+
from api_key_helper import find_all_api_keys
|
|
44
|
+
KEY_ROTATION_AVAILABLE = True
|
|
45
|
+
except ImportError:
|
|
46
|
+
KEY_ROTATION_AVAILABLE = False
|
|
47
|
+
KeyRotator = None
|
|
48
|
+
is_rate_limit_error = None
|
|
49
|
+
find_all_api_keys = None
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
from google import genai
|
|
53
|
+
from google.genai import types
|
|
54
|
+
except ImportError:
|
|
55
|
+
print("Error: google-genai package not installed")
|
|
56
|
+
print("Install with: pip install google-genai")
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Image generation model configuration
|
|
61
|
+
# Default: gemini-2.5-flash-image (Nano Banana Flash - fast, cost-effective)
|
|
62
|
+
# Alternative: imagen-4.0-generate-001 (production quality)
|
|
63
|
+
# All image generation requires billing - no completely free option exists
|
|
64
|
+
IMAGE_MODEL_DEFAULT = 'gemini-2.5-flash-image' # Nano Banana Flash (~$1/1M tokens)
|
|
65
|
+
IMAGE_MODEL_FALLBACK = 'gemini-2.5-flash-image' # Fallback if Imagen fails (billing)
|
|
66
|
+
IMAGEN_MODELS = {
|
|
67
|
+
'imagen-4.0-generate-001',
|
|
68
|
+
'imagen-4.0-ultra-generate-001',
|
|
69
|
+
'imagen-4.0-fast-generate-001',
|
|
70
|
+
}
|
|
71
|
+
# Video models have no fallback - Veo always requires billing
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def find_api_key() -> Optional[str]:
|
|
75
|
+
"""Find Gemini API key using centralized resolver or fallback.
|
|
76
|
+
|
|
77
|
+
Uses ~/.claude/scripts/resolve_env.py for consistent resolution across all skills.
|
|
78
|
+
Falls back to local resolution if centralized resolver not available.
|
|
79
|
+
|
|
80
|
+
Priority order (highest to lowest):
|
|
81
|
+
1. process.env (runtime environment variables)
|
|
82
|
+
2. PROJECT/.claude/skills/ai-multimodal/.env (skill-specific)
|
|
83
|
+
3. PROJECT/.claude/skills/.env (shared skills)
|
|
84
|
+
4. PROJECT/.claude/.env (project global)
|
|
85
|
+
5. ~/.claude/skills/ai-multimodal/.env (user skill-specific)
|
|
86
|
+
6. ~/.claude/skills/.env (user shared)
|
|
87
|
+
7. ~/.claude/.env (user global)
|
|
88
|
+
"""
|
|
89
|
+
if CENTRALIZED_RESOLVER_AVAILABLE:
|
|
90
|
+
# Use centralized resolver (recommended)
|
|
91
|
+
return resolve_env('GEMINI_API_KEY', skill='ai-multimodal')
|
|
92
|
+
|
|
93
|
+
# Fallback: Local resolution (legacy)
|
|
94
|
+
api_key = os.getenv('GEMINI_API_KEY')
|
|
95
|
+
if api_key:
|
|
96
|
+
return api_key
|
|
97
|
+
|
|
98
|
+
if load_dotenv:
|
|
99
|
+
script_dir = Path(__file__).parent
|
|
100
|
+
skill_dir = script_dir.parent
|
|
101
|
+
skills_dir = skill_dir.parent
|
|
102
|
+
claude_dir = skills_dir.parent
|
|
103
|
+
|
|
104
|
+
env_files = [
|
|
105
|
+
claude_dir / '.env',
|
|
106
|
+
skills_dir / '.env',
|
|
107
|
+
skill_dir / '.env',
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
for env_file in env_files:
|
|
111
|
+
if env_file.exists():
|
|
112
|
+
load_dotenv(env_file, override=True)
|
|
113
|
+
|
|
114
|
+
api_key = os.getenv('GEMINI_API_KEY')
|
|
115
|
+
if api_key:
|
|
116
|
+
return api_key
|
|
117
|
+
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_default_model(task: str) -> str:
|
|
122
|
+
"""Get default model for task from environment or fallback.
|
|
123
|
+
|
|
124
|
+
Priority:
|
|
125
|
+
1. Environment variable for specific capability
|
|
126
|
+
2. Legacy GEMINI_MODEL variable
|
|
127
|
+
3. Hard-coded defaults
|
|
128
|
+
"""
|
|
129
|
+
if task == 'generate': # Image generation
|
|
130
|
+
model = os.getenv('IMAGE_GEN_MODEL')
|
|
131
|
+
if model:
|
|
132
|
+
return model
|
|
133
|
+
# Fallback to legacy
|
|
134
|
+
model = os.getenv('GEMINI_IMAGE_GEN_MODEL')
|
|
135
|
+
if model:
|
|
136
|
+
return model
|
|
137
|
+
# Default to Nano Banana Flash (fast, cost-effective)
|
|
138
|
+
# Alternative: imagen-4.0-generate-001 for production quality
|
|
139
|
+
return 'gemini-2.5-flash-image'
|
|
140
|
+
|
|
141
|
+
elif task == 'generate-video':
|
|
142
|
+
model = os.getenv('VIDEO_GEN_MODEL')
|
|
143
|
+
if model:
|
|
144
|
+
return model
|
|
145
|
+
return 'veo-3.1-generate-preview' # New default
|
|
146
|
+
|
|
147
|
+
elif task in ['analyze', 'transcribe', 'extract']:
|
|
148
|
+
model = os.getenv('MULTIMODAL_MODEL')
|
|
149
|
+
if model:
|
|
150
|
+
return model
|
|
151
|
+
# Fallback to legacy
|
|
152
|
+
model = os.getenv('GEMINI_MODEL')
|
|
153
|
+
if model:
|
|
154
|
+
return model
|
|
155
|
+
return 'gemini-2.5-flash' # Existing default
|
|
156
|
+
|
|
157
|
+
return 'gemini-2.5-flash'
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def validate_model_task_combination(model: str, task: str) -> None:
|
|
161
|
+
"""Validate model is compatible with task.
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
ValueError: If combination is invalid
|
|
165
|
+
"""
|
|
166
|
+
# Video generation requires Veo
|
|
167
|
+
if task == 'generate-video':
|
|
168
|
+
if not model.startswith('veo-'):
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"Video generation requires Veo model, got '{model}'\n"
|
|
171
|
+
f"Valid models: veo-3.1-generate-preview, veo-3.1-fast-generate-preview, "
|
|
172
|
+
f"veo-3.0-generate-001, veo-3.0-fast-generate-001"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Image generation models
|
|
176
|
+
if task == 'generate':
|
|
177
|
+
valid_image_models = [
|
|
178
|
+
'imagen-4.0-generate-001',
|
|
179
|
+
'imagen-4.0-ultra-generate-001',
|
|
180
|
+
'imagen-4.0-fast-generate-001',
|
|
181
|
+
'gemini-3-pro-image-preview',
|
|
182
|
+
'gemini-2.5-flash-image',
|
|
183
|
+
'gemini-2.5-flash-image-preview',
|
|
184
|
+
]
|
|
185
|
+
if model not in valid_image_models:
|
|
186
|
+
# Allow gemini models for analysis-based generation (backward compat)
|
|
187
|
+
if not model.startswith('gemini-'):
|
|
188
|
+
raise ValueError(
|
|
189
|
+
f"Image generation requires Imagen/Gemini image model, got '{model}'\n"
|
|
190
|
+
f"Valid models: {', '.join(valid_image_models)}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def infer_task_from_file(file_path: str) -> str:
|
|
195
|
+
"""Infer task type from file extension.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
'transcribe' for audio files
|
|
199
|
+
'analyze' for image/video/document files
|
|
200
|
+
"""
|
|
201
|
+
ext = Path(file_path).suffix.lower()
|
|
202
|
+
|
|
203
|
+
audio_extensions = {'.mp3', '.wav', '.aac', '.flac', '.ogg', '.aiff', '.m4a'}
|
|
204
|
+
image_extensions = {'.jpg', '.jpeg', '.png', '.webp', '.heic', '.heif', '.gif', '.bmp'}
|
|
205
|
+
video_extensions = {'.mp4', '.mpeg', '.mov', '.avi', '.flv', '.mpg', '.webm', '.wmv', '.3gpp', '.mkv'}
|
|
206
|
+
document_extensions = {'.pdf', '.txt', '.html', '.md', '.doc', '.docx'}
|
|
207
|
+
|
|
208
|
+
if ext in audio_extensions:
|
|
209
|
+
return 'transcribe'
|
|
210
|
+
elif ext in image_extensions:
|
|
211
|
+
return 'analyze'
|
|
212
|
+
elif ext in video_extensions:
|
|
213
|
+
return 'analyze'
|
|
214
|
+
elif ext in document_extensions:
|
|
215
|
+
return 'extract'
|
|
216
|
+
|
|
217
|
+
# Default to analyze for unknown types
|
|
218
|
+
return 'analyze'
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_mime_type(file_path: str) -> str:
|
|
222
|
+
"""Determine MIME type from file extension."""
|
|
223
|
+
ext = Path(file_path).suffix.lower()
|
|
224
|
+
|
|
225
|
+
mime_types = {
|
|
226
|
+
# Audio
|
|
227
|
+
'.mp3': 'audio/mp3',
|
|
228
|
+
'.wav': 'audio/wav',
|
|
229
|
+
'.aac': 'audio/aac',
|
|
230
|
+
'.flac': 'audio/flac',
|
|
231
|
+
'.ogg': 'audio/ogg',
|
|
232
|
+
'.aiff': 'audio/aiff',
|
|
233
|
+
# Image
|
|
234
|
+
'.jpg': 'image/jpeg',
|
|
235
|
+
'.jpeg': 'image/jpeg',
|
|
236
|
+
'.png': 'image/png',
|
|
237
|
+
'.webp': 'image/webp',
|
|
238
|
+
'.heic': 'image/heic',
|
|
239
|
+
'.heif': 'image/heif',
|
|
240
|
+
# Video
|
|
241
|
+
'.mp4': 'video/mp4',
|
|
242
|
+
'.mpeg': 'video/mpeg',
|
|
243
|
+
'.mov': 'video/quicktime',
|
|
244
|
+
'.avi': 'video/x-msvideo',
|
|
245
|
+
'.flv': 'video/x-flv',
|
|
246
|
+
'.mpg': 'video/mpeg',
|
|
247
|
+
'.webm': 'video/webm',
|
|
248
|
+
'.wmv': 'video/x-ms-wmv',
|
|
249
|
+
'.3gpp': 'video/3gpp',
|
|
250
|
+
# Document
|
|
251
|
+
'.pdf': 'application/pdf',
|
|
252
|
+
'.txt': 'text/plain',
|
|
253
|
+
'.html': 'text/html',
|
|
254
|
+
'.md': 'text/markdown',
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return mime_types.get(ext, 'application/octet-stream')
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def upload_file(client: genai.Client, file_path: str, verbose: bool = False) -> Any:
|
|
261
|
+
"""Upload file to Gemini File API."""
|
|
262
|
+
if verbose:
|
|
263
|
+
print(f"Uploading {file_path}...")
|
|
264
|
+
|
|
265
|
+
myfile = client.files.upload(file=file_path)
|
|
266
|
+
|
|
267
|
+
# Wait for processing (video/audio files need processing)
|
|
268
|
+
mime_type = get_mime_type(file_path)
|
|
269
|
+
if mime_type.startswith('video/') or mime_type.startswith('audio/'):
|
|
270
|
+
max_wait = 300 # 5 minutes
|
|
271
|
+
elapsed = 0
|
|
272
|
+
while myfile.state.name == 'PROCESSING' and elapsed < max_wait:
|
|
273
|
+
time.sleep(2)
|
|
274
|
+
myfile = client.files.get(name=myfile.name)
|
|
275
|
+
elapsed += 2
|
|
276
|
+
if verbose and elapsed % 10 == 0:
|
|
277
|
+
print(f" Processing... {elapsed}s")
|
|
278
|
+
|
|
279
|
+
if myfile.state.name == 'FAILED':
|
|
280
|
+
raise ValueError(f"File processing failed: {file_path}")
|
|
281
|
+
|
|
282
|
+
if myfile.state.name == 'PROCESSING':
|
|
283
|
+
raise TimeoutError(f"Processing timeout after {max_wait}s: {file_path}")
|
|
284
|
+
|
|
285
|
+
if verbose:
|
|
286
|
+
print(f" Uploaded: {myfile.name}")
|
|
287
|
+
|
|
288
|
+
return myfile
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _is_billing_error(error: Exception) -> bool:
|
|
292
|
+
"""Check if error is due to billing/access restrictions."""
|
|
293
|
+
error_str = str(error).lower()
|
|
294
|
+
billing_indicators = [
|
|
295
|
+
'billing',
|
|
296
|
+
'billed users',
|
|
297
|
+
'payment',
|
|
298
|
+
'access denied',
|
|
299
|
+
'not authorized',
|
|
300
|
+
'permission denied',
|
|
301
|
+
]
|
|
302
|
+
return any(indicator in error_str for indicator in billing_indicators)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _is_free_tier_quota_error(error: Exception) -> bool:
|
|
306
|
+
"""Check if error indicates free tier has zero quota for this model.
|
|
307
|
+
|
|
308
|
+
Free tier users have NO access to image/video generation models.
|
|
309
|
+
The API returns 'limit: 0' or 'RESOURCE_EXHAUSTED' with quota details.
|
|
310
|
+
"""
|
|
311
|
+
error_str = str(error)
|
|
312
|
+
# Check for zero quota indicators
|
|
313
|
+
return (
|
|
314
|
+
'RESOURCE_EXHAUSTED' in error_str and
|
|
315
|
+
('limit: 0' in error_str or 'free_tier' in error_str.lower())
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
FREE_TIER_NO_ACCESS_MSG = """
|
|
320
|
+
[FREE TIER LIMITATION] Image/Video generation is NOT available on free tier.
|
|
321
|
+
|
|
322
|
+
Free tier users have zero quota (limit: 0) for:
|
|
323
|
+
- All Imagen models (imagen-4.0-*)
|
|
324
|
+
- All Veo models (veo-*)
|
|
325
|
+
- Gemini image models (gemini-*-image, gemini-*-image-preview)
|
|
326
|
+
|
|
327
|
+
To use image/video generation:
|
|
328
|
+
1. Enable billing: https://aistudio.google.com/apikey
|
|
329
|
+
2. Or use Google Cloud $300 free credits: https://cloud.google.com/free
|
|
330
|
+
|
|
331
|
+
STOP: Do not retry image/video generation on free tier - it will always fail.
|
|
332
|
+
""".strip()
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def generate_image_imagen4(
|
|
336
|
+
client,
|
|
337
|
+
prompt: str,
|
|
338
|
+
model: str,
|
|
339
|
+
num_images: int = 1,
|
|
340
|
+
aspect_ratio: str = '1:1',
|
|
341
|
+
size: str = '1K',
|
|
342
|
+
verbose: bool = False
|
|
343
|
+
) -> Dict[str, Any]:
|
|
344
|
+
"""Generate image using Imagen 4 models.
|
|
345
|
+
|
|
346
|
+
Returns special status 'billing_required' if model needs billing,
|
|
347
|
+
allowing caller to fallback to free-tier generate_content API.
|
|
348
|
+
"""
|
|
349
|
+
try:
|
|
350
|
+
# Build config based on model (Fast doesn't support imageSize)
|
|
351
|
+
config_params = {
|
|
352
|
+
'numberOfImages': num_images,
|
|
353
|
+
'aspectRatio': aspect_ratio
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# Only Standard and Ultra support imageSize parameter
|
|
357
|
+
if 'fast' not in model.lower() and model.startswith('imagen-'):
|
|
358
|
+
config_params['imageSize'] = size
|
|
359
|
+
|
|
360
|
+
gen_config = types.GenerateImagesConfig(**config_params)
|
|
361
|
+
|
|
362
|
+
if verbose:
|
|
363
|
+
print(f" Generating with: {model}")
|
|
364
|
+
print(f" Config: {num_images} images, {aspect_ratio}", end='')
|
|
365
|
+
if 'fast' not in model.lower() and model.startswith('imagen-'):
|
|
366
|
+
print(f", {size}")
|
|
367
|
+
else:
|
|
368
|
+
print()
|
|
369
|
+
|
|
370
|
+
response = client.models.generate_images(
|
|
371
|
+
model=model,
|
|
372
|
+
prompt=prompt,
|
|
373
|
+
config=gen_config
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Save images
|
|
377
|
+
generated_files = []
|
|
378
|
+
for i, generated_image in enumerate(response.generated_images):
|
|
379
|
+
# Find project root
|
|
380
|
+
script_dir = Path(__file__).parent
|
|
381
|
+
project_root = script_dir
|
|
382
|
+
for parent in [script_dir] + list(script_dir.parents):
|
|
383
|
+
if (parent / '.git').exists() or (parent / '.claude').exists():
|
|
384
|
+
project_root = parent
|
|
385
|
+
break
|
|
386
|
+
|
|
387
|
+
output_dir = project_root / 'docs' / 'assets'
|
|
388
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
389
|
+
output_file = output_dir / f"imagen4_generated_{int(time.time())}_{i}.png"
|
|
390
|
+
|
|
391
|
+
with open(output_file, 'wb') as f:
|
|
392
|
+
f.write(generated_image.image.image_bytes)
|
|
393
|
+
generated_files.append(str(output_file))
|
|
394
|
+
|
|
395
|
+
if verbose:
|
|
396
|
+
print(f" Saved: {output_file}")
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
'status': 'success',
|
|
400
|
+
'generated_images': generated_files,
|
|
401
|
+
'model': model
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
except Exception as e:
|
|
405
|
+
# Return special status for billing errors so caller can fallback
|
|
406
|
+
if _is_billing_error(e) and model in IMAGEN_MODELS:
|
|
407
|
+
return {
|
|
408
|
+
'status': 'billing_required',
|
|
409
|
+
'original_model': model,
|
|
410
|
+
'error': str(e)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if verbose:
|
|
414
|
+
print(f" Error: {str(e)}")
|
|
415
|
+
import traceback
|
|
416
|
+
traceback.print_exc()
|
|
417
|
+
return {
|
|
418
|
+
'status': 'error',
|
|
419
|
+
'error': str(e)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def generate_video_veo(
|
|
424
|
+
client,
|
|
425
|
+
prompt: str,
|
|
426
|
+
model: str,
|
|
427
|
+
resolution: str = '1080p',
|
|
428
|
+
aspect_ratio: str = '16:9',
|
|
429
|
+
reference_images: Optional[List[str]] = None,
|
|
430
|
+
verbose: bool = False
|
|
431
|
+
) -> Dict[str, Any]:
|
|
432
|
+
"""Generate video using Veo models.
|
|
433
|
+
|
|
434
|
+
For image-to-video with first/last frames (Veo 3.1):
|
|
435
|
+
- First reference image becomes the opening frame (image parameter)
|
|
436
|
+
- Second reference image becomes the closing frame (last_frame config)
|
|
437
|
+
- Model interpolates between them to create smooth video
|
|
438
|
+
"""
|
|
439
|
+
try:
|
|
440
|
+
# Build config with snake_case for Python SDK
|
|
441
|
+
config_params = {
|
|
442
|
+
'aspect_ratio': aspect_ratio,
|
|
443
|
+
'resolution': resolution
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
# Prepare first frame and last frame images
|
|
447
|
+
first_frame = None
|
|
448
|
+
last_frame = None
|
|
449
|
+
|
|
450
|
+
if reference_images:
|
|
451
|
+
import mimetypes
|
|
452
|
+
|
|
453
|
+
def load_image(img_path_str: str) -> types.Image:
|
|
454
|
+
"""Load image file as types.Image with bytes and mime type."""
|
|
455
|
+
img_path = Path(img_path_str)
|
|
456
|
+
image_bytes = img_path.read_bytes()
|
|
457
|
+
mime_type, _ = mimetypes.guess_type(str(img_path))
|
|
458
|
+
if not mime_type:
|
|
459
|
+
mime_type = 'image/png'
|
|
460
|
+
return types.Image(
|
|
461
|
+
image_bytes=image_bytes,
|
|
462
|
+
mime_type=mime_type
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# First image = opening frame
|
|
466
|
+
if len(reference_images) >= 1:
|
|
467
|
+
first_frame = load_image(reference_images[0])
|
|
468
|
+
|
|
469
|
+
# Second image = closing frame (last_frame in config)
|
|
470
|
+
if len(reference_images) >= 2:
|
|
471
|
+
last_frame = load_image(reference_images[1])
|
|
472
|
+
config_params['last_frame'] = last_frame
|
|
473
|
+
|
|
474
|
+
gen_config = types.GenerateVideosConfig(**config_params)
|
|
475
|
+
|
|
476
|
+
if verbose:
|
|
477
|
+
print(f" Generating video with Veo: {model}")
|
|
478
|
+
print(f" Config: {resolution}, {aspect_ratio}")
|
|
479
|
+
if first_frame:
|
|
480
|
+
print(f" First frame: provided")
|
|
481
|
+
if last_frame:
|
|
482
|
+
print(f" Last frame: provided (interpolation mode)")
|
|
483
|
+
|
|
484
|
+
start = time.time()
|
|
485
|
+
|
|
486
|
+
if verbose:
|
|
487
|
+
print(f" Starting video generation (this may take 11s-6min)...")
|
|
488
|
+
|
|
489
|
+
# Call generate_videos with image parameter for first frame
|
|
490
|
+
operation = client.models.generate_videos(
|
|
491
|
+
model=model,
|
|
492
|
+
prompt=prompt,
|
|
493
|
+
image=first_frame, # First frame as opening image
|
|
494
|
+
config=gen_config
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
# Poll operation until complete
|
|
498
|
+
poll_count = 0
|
|
499
|
+
while not operation.done:
|
|
500
|
+
poll_count += 1
|
|
501
|
+
if verbose and poll_count % 3 == 0: # Update every 30s
|
|
502
|
+
elapsed = time.time() - start
|
|
503
|
+
print(f" Still generating... ({elapsed:.0f}s elapsed)")
|
|
504
|
+
time.sleep(10)
|
|
505
|
+
operation = client.operations.get(operation)
|
|
506
|
+
|
|
507
|
+
duration = time.time() - start
|
|
508
|
+
|
|
509
|
+
# Access generated video from operation response
|
|
510
|
+
generated_video = operation.response.generated_videos[0]
|
|
511
|
+
|
|
512
|
+
# Download the video file first
|
|
513
|
+
client.files.download(file=generated_video.video)
|
|
514
|
+
|
|
515
|
+
# Save video
|
|
516
|
+
script_dir = Path(__file__).parent
|
|
517
|
+
project_root = script_dir
|
|
518
|
+
for parent in [script_dir] + list(script_dir.parents):
|
|
519
|
+
if (parent / '.git').exists() or (parent / '.claude').exists():
|
|
520
|
+
project_root = parent
|
|
521
|
+
break
|
|
522
|
+
|
|
523
|
+
output_dir = project_root / 'docs' / 'assets'
|
|
524
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
525
|
+
output_file = output_dir / f"veo_generated_{int(time.time())}.mp4"
|
|
526
|
+
|
|
527
|
+
# Now save to file
|
|
528
|
+
generated_video.video.save(str(output_file))
|
|
529
|
+
|
|
530
|
+
file_size = output_file.stat().st_size / (1024 * 1024) # MB
|
|
531
|
+
|
|
532
|
+
if verbose:
|
|
533
|
+
print(f" Generated in {duration:.1f}s")
|
|
534
|
+
print(f" File size: {file_size:.2f} MB")
|
|
535
|
+
print(f" Saved: {output_file}")
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
'status': 'success',
|
|
539
|
+
'generated_video': str(output_file),
|
|
540
|
+
'generation_time': duration,
|
|
541
|
+
'file_size_mb': file_size,
|
|
542
|
+
'model': model
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
except Exception as e:
|
|
546
|
+
if verbose:
|
|
547
|
+
print(f" Error: {str(e)}")
|
|
548
|
+
import traceback
|
|
549
|
+
traceback.print_exc()
|
|
550
|
+
return {
|
|
551
|
+
'status': 'error',
|
|
552
|
+
'error': str(e)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def process_file(
|
|
557
|
+
client: genai.Client,
|
|
558
|
+
file_path: Optional[str],
|
|
559
|
+
prompt: str,
|
|
560
|
+
model: str,
|
|
561
|
+
task: str,
|
|
562
|
+
format_output: str,
|
|
563
|
+
aspect_ratio: Optional[str] = None,
|
|
564
|
+
image_size: Optional[str] = None,
|
|
565
|
+
verbose: bool = False,
|
|
566
|
+
max_retries: int = 3
|
|
567
|
+
) -> Dict[str, Any]:
|
|
568
|
+
"""Process a single file with retry logic.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
image_size: Image size for Nano Banana models (1K, 2K, 4K). Must be uppercase K.
|
|
572
|
+
Note: Not all models support image_size - only pass when explicitly needed.
|
|
573
|
+
"""
|
|
574
|
+
|
|
575
|
+
for attempt in range(max_retries):
|
|
576
|
+
try:
|
|
577
|
+
# For generation tasks without input files
|
|
578
|
+
if task == 'generate' and not file_path:
|
|
579
|
+
content = [prompt]
|
|
580
|
+
else:
|
|
581
|
+
# Process input file
|
|
582
|
+
file_path = Path(file_path)
|
|
583
|
+
# Determine if we need File API
|
|
584
|
+
file_size = file_path.stat().st_size
|
|
585
|
+
use_file_api = file_size > 20 * 1024 * 1024 # >20MB
|
|
586
|
+
|
|
587
|
+
if use_file_api:
|
|
588
|
+
# Upload to File API
|
|
589
|
+
myfile = upload_file(client, str(file_path), verbose)
|
|
590
|
+
content = [prompt, myfile]
|
|
591
|
+
else:
|
|
592
|
+
# Inline data
|
|
593
|
+
with open(file_path, 'rb') as f:
|
|
594
|
+
file_bytes = f.read()
|
|
595
|
+
|
|
596
|
+
mime_type = get_mime_type(str(file_path))
|
|
597
|
+
content = [
|
|
598
|
+
prompt,
|
|
599
|
+
types.Part.from_bytes(data=file_bytes, mime_type=mime_type)
|
|
600
|
+
]
|
|
601
|
+
|
|
602
|
+
# Configure request
|
|
603
|
+
config_args = {}
|
|
604
|
+
if task == 'generate':
|
|
605
|
+
# Nano Banana requires fully uppercase 'IMAGE' per API spec
|
|
606
|
+
config_args['response_modalities'] = ['IMAGE']
|
|
607
|
+
# Build image_config with aspect_ratio and/or image_size
|
|
608
|
+
image_config_args = {}
|
|
609
|
+
if aspect_ratio:
|
|
610
|
+
image_config_args['aspect_ratio'] = aspect_ratio
|
|
611
|
+
if image_size:
|
|
612
|
+
# image_size must be uppercase K (1K, 2K, 4K)
|
|
613
|
+
image_config_args['image_size'] = image_size
|
|
614
|
+
if image_config_args:
|
|
615
|
+
config_args['image_config'] = types.ImageConfig(**image_config_args)
|
|
616
|
+
|
|
617
|
+
if format_output == 'json':
|
|
618
|
+
config_args['response_mime_type'] = 'application/json'
|
|
619
|
+
|
|
620
|
+
config = types.GenerateContentConfig(**config_args) if config_args else None
|
|
621
|
+
|
|
622
|
+
# Generate content
|
|
623
|
+
response = client.models.generate_content(
|
|
624
|
+
model=model,
|
|
625
|
+
contents=content,
|
|
626
|
+
config=config
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
# Extract response
|
|
630
|
+
result = {
|
|
631
|
+
'file': str(file_path) if file_path else 'generated',
|
|
632
|
+
'status': 'success',
|
|
633
|
+
'response': response.text if hasattr(response, 'text') else None
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
# Handle image output
|
|
637
|
+
if task == 'generate' and hasattr(response, 'candidates'):
|
|
638
|
+
for i, part in enumerate(response.candidates[0].content.parts):
|
|
639
|
+
if part.inline_data:
|
|
640
|
+
# Determine output directory - use project root docs/assets
|
|
641
|
+
if file_path:
|
|
642
|
+
output_dir = Path(file_path).parent
|
|
643
|
+
base_name = Path(file_path).stem
|
|
644
|
+
else:
|
|
645
|
+
# Find project root (look for .git or .claude directory)
|
|
646
|
+
script_dir = Path(__file__).parent
|
|
647
|
+
project_root = script_dir
|
|
648
|
+
for parent in [script_dir] + list(script_dir.parents):
|
|
649
|
+
if (parent / '.git').exists() or (parent / '.claude').exists():
|
|
650
|
+
project_root = parent
|
|
651
|
+
break
|
|
652
|
+
|
|
653
|
+
output_dir = project_root / 'docs' / 'assets'
|
|
654
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
655
|
+
base_name = "generated"
|
|
656
|
+
|
|
657
|
+
output_file = output_dir / f"{base_name}_generated_{i}.png"
|
|
658
|
+
with open(output_file, 'wb') as f:
|
|
659
|
+
f.write(part.inline_data.data)
|
|
660
|
+
result['generated_image'] = str(output_file)
|
|
661
|
+
if verbose:
|
|
662
|
+
print(f" Saved image to: {output_file}")
|
|
663
|
+
|
|
664
|
+
return result
|
|
665
|
+
|
|
666
|
+
except Exception as e:
|
|
667
|
+
# Don't retry on billing/free tier errors - they won't resolve
|
|
668
|
+
if _is_billing_error(e) or _is_free_tier_quota_error(e):
|
|
669
|
+
return {
|
|
670
|
+
'file': str(file_path) if file_path else 'generated',
|
|
671
|
+
'status': 'error',
|
|
672
|
+
'error': str(e)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
# Check if this is a rate limit error (candidate for key rotation)
|
|
676
|
+
is_rate_limited = (
|
|
677
|
+
KEY_ROTATION_AVAILABLE and
|
|
678
|
+
is_rate_limit_error and
|
|
679
|
+
is_rate_limit_error(e)
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
if attempt == max_retries - 1:
|
|
683
|
+
return {
|
|
684
|
+
'file': str(file_path) if file_path else 'generated',
|
|
685
|
+
'status': 'error',
|
|
686
|
+
'error': str(e),
|
|
687
|
+
'rate_limited': is_rate_limited # Flag for caller to handle rotation
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
wait_time = 2 ** attempt
|
|
691
|
+
if verbose:
|
|
692
|
+
print(f" Retry {attempt + 1} after {wait_time}s: {e}")
|
|
693
|
+
time.sleep(wait_time)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def batch_process(
|
|
697
|
+
files: List[str],
|
|
698
|
+
prompt: str,
|
|
699
|
+
model: str,
|
|
700
|
+
task: str,
|
|
701
|
+
format_output: str,
|
|
702
|
+
aspect_ratio: Optional[str] = None,
|
|
703
|
+
num_images: int = 1,
|
|
704
|
+
size: str = '1K',
|
|
705
|
+
resolution: str = '1080p',
|
|
706
|
+
reference_images: Optional[List[str]] = None,
|
|
707
|
+
output_file: Optional[str] = None,
|
|
708
|
+
verbose: bool = False,
|
|
709
|
+
dry_run: bool = False
|
|
710
|
+
) -> List[Dict[str, Any]]:
|
|
711
|
+
"""Batch process multiple files with automatic key rotation."""
|
|
712
|
+
|
|
713
|
+
# Initialize key rotator or fall back to single key
|
|
714
|
+
rotator = None
|
|
715
|
+
api_key = None
|
|
716
|
+
|
|
717
|
+
if KEY_ROTATION_AVAILABLE and find_all_api_keys:
|
|
718
|
+
all_keys = find_all_api_keys()
|
|
719
|
+
if all_keys:
|
|
720
|
+
if len(all_keys) > 1:
|
|
721
|
+
rotator = KeyRotator(keys=all_keys, verbose=verbose)
|
|
722
|
+
api_key = rotator.get_key()
|
|
723
|
+
if verbose:
|
|
724
|
+
print(f"✓ Key rotation enabled with {len(all_keys)} keys", file=sys.stderr)
|
|
725
|
+
else:
|
|
726
|
+
api_key = all_keys[0]
|
|
727
|
+
if verbose:
|
|
728
|
+
print(f"✓ Using single API key: {api_key[:8]}...", file=sys.stderr)
|
|
729
|
+
|
|
730
|
+
# Fallback to original single-key lookup
|
|
731
|
+
if not api_key:
|
|
732
|
+
api_key = find_api_key()
|
|
733
|
+
|
|
734
|
+
if not api_key:
|
|
735
|
+
print("Error: GEMINI_API_KEY not found")
|
|
736
|
+
print("\nSetup options:")
|
|
737
|
+
print("1. Run setup checker: python scripts/check_setup.py")
|
|
738
|
+
print("2. Show hierarchy: python ~/.claude/scripts/resolve_env.py --show-hierarchy --skill ai-multimodal")
|
|
739
|
+
print("3. Quick setup: export GEMINI_API_KEY='your-key'")
|
|
740
|
+
print("4. Create .env: cd ~/.claude/skills/ai-multimodal && cp .env.example .env")
|
|
741
|
+
print("\nFor key rotation, add multiple keys:")
|
|
742
|
+
print(" GEMINI_API_KEY=key1")
|
|
743
|
+
print(" GEMINI_API_KEY_2=key2")
|
|
744
|
+
print(" GEMINI_API_KEY_3=key3")
|
|
745
|
+
sys.exit(1)
|
|
746
|
+
|
|
747
|
+
if dry_run:
|
|
748
|
+
print("DRY RUN MODE - No API calls will be made")
|
|
749
|
+
print(f"Files to process: {len(files)}")
|
|
750
|
+
print(f"Model: {model}")
|
|
751
|
+
print(f"Task: {task}")
|
|
752
|
+
print(f"Prompt: {prompt}")
|
|
753
|
+
if rotator:
|
|
754
|
+
print(f"API keys available: {rotator.key_count}")
|
|
755
|
+
return []
|
|
756
|
+
|
|
757
|
+
# Create client with current key
|
|
758
|
+
client = genai.Client(api_key=api_key)
|
|
759
|
+
results = []
|
|
760
|
+
|
|
761
|
+
def get_client_with_rotation(error: Optional[Exception] = None) -> Optional[genai.Client]:
|
|
762
|
+
"""Get client, rotating key if rate limited."""
|
|
763
|
+
nonlocal client, api_key
|
|
764
|
+
|
|
765
|
+
if error and rotator and is_rate_limit_error and is_rate_limit_error(error):
|
|
766
|
+
# Try to rotate to next key
|
|
767
|
+
if rotator.mark_rate_limited(str(error)):
|
|
768
|
+
new_key = rotator.get_key()
|
|
769
|
+
if new_key:
|
|
770
|
+
api_key = new_key
|
|
771
|
+
client = genai.Client(api_key=api_key)
|
|
772
|
+
return client
|
|
773
|
+
# All keys exhausted
|
|
774
|
+
return None
|
|
775
|
+
return client
|
|
776
|
+
|
|
777
|
+
# For generation tasks without input files, process once
|
|
778
|
+
if task == 'generate' and not files:
|
|
779
|
+
if verbose:
|
|
780
|
+
print(f"\nGenerating image from prompt...")
|
|
781
|
+
|
|
782
|
+
# Use Imagen 4 API for imagen models
|
|
783
|
+
if model.startswith('imagen-') or model in IMAGEN_MODELS:
|
|
784
|
+
result = generate_image_imagen4(
|
|
785
|
+
client=client,
|
|
786
|
+
prompt=prompt,
|
|
787
|
+
model=model,
|
|
788
|
+
num_images=num_images,
|
|
789
|
+
aspect_ratio=aspect_ratio or '1:1',
|
|
790
|
+
size=size or '1K', # Default to 1K for Imagen models
|
|
791
|
+
verbose=verbose
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
# Silent fallback to cheaper model if Imagen billing required
|
|
795
|
+
if result.get('status') == 'billing_required':
|
|
796
|
+
if verbose:
|
|
797
|
+
print(f" Falling back to: {IMAGE_MODEL_FALLBACK}")
|
|
798
|
+
result = process_file(
|
|
799
|
+
client=client,
|
|
800
|
+
file_path=None,
|
|
801
|
+
prompt=prompt,
|
|
802
|
+
model=IMAGE_MODEL_FALLBACK,
|
|
803
|
+
task=task,
|
|
804
|
+
format_output=format_output,
|
|
805
|
+
aspect_ratio=aspect_ratio,
|
|
806
|
+
image_size=size,
|
|
807
|
+
verbose=verbose
|
|
808
|
+
)
|
|
809
|
+
# Check if free tier (zero quota) - stop immediately with clear message
|
|
810
|
+
error_str = result.get('error', '')
|
|
811
|
+
if result.get('status') == 'error':
|
|
812
|
+
if _is_free_tier_quota_error(Exception(error_str)):
|
|
813
|
+
result['error'] = FREE_TIER_NO_ACCESS_MSG
|
|
814
|
+
elif _is_billing_error(Exception(error_str)):
|
|
815
|
+
result['error'] = (
|
|
816
|
+
"Image generation requires billing. Enable billing at: "
|
|
817
|
+
"https://aistudio.google.com/apikey or use Google Cloud credits."
|
|
818
|
+
)
|
|
819
|
+
else:
|
|
820
|
+
# Nano Banana (Flash/Pro) or other models via generate_content API
|
|
821
|
+
result = process_file(
|
|
822
|
+
client=client,
|
|
823
|
+
file_path=None,
|
|
824
|
+
prompt=prompt,
|
|
825
|
+
model=model,
|
|
826
|
+
task=task,
|
|
827
|
+
format_output=format_output,
|
|
828
|
+
aspect_ratio=aspect_ratio,
|
|
829
|
+
image_size=size,
|
|
830
|
+
verbose=verbose
|
|
831
|
+
)
|
|
832
|
+
# Check for free tier error
|
|
833
|
+
if result.get('status') == 'error':
|
|
834
|
+
error_str = result.get('error', '')
|
|
835
|
+
if _is_free_tier_quota_error(Exception(error_str)):
|
|
836
|
+
result['error'] = FREE_TIER_NO_ACCESS_MSG
|
|
837
|
+
|
|
838
|
+
results.append(result)
|
|
839
|
+
|
|
840
|
+
if verbose:
|
|
841
|
+
status = result.get('status', 'unknown')
|
|
842
|
+
print(f" Status: {status}")
|
|
843
|
+
|
|
844
|
+
elif task == 'generate-video' and not files:
|
|
845
|
+
if verbose:
|
|
846
|
+
print(f"\nGenerating video from prompt...")
|
|
847
|
+
|
|
848
|
+
result = generate_video_veo(
|
|
849
|
+
client=client,
|
|
850
|
+
prompt=prompt,
|
|
851
|
+
model=model,
|
|
852
|
+
resolution=resolution,
|
|
853
|
+
aspect_ratio=aspect_ratio or '16:9',
|
|
854
|
+
reference_images=reference_images,
|
|
855
|
+
verbose=verbose
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
# Check for free tier error - video gen has NO free tier access
|
|
859
|
+
if result.get('status') == 'error':
|
|
860
|
+
error_str = result.get('error', '')
|
|
861
|
+
if _is_free_tier_quota_error(Exception(error_str)) or _is_billing_error(Exception(error_str)):
|
|
862
|
+
result['error'] = FREE_TIER_NO_ACCESS_MSG
|
|
863
|
+
|
|
864
|
+
results.append(result)
|
|
865
|
+
|
|
866
|
+
if verbose:
|
|
867
|
+
status = result.get('status', 'unknown')
|
|
868
|
+
print(f" Status: {status}")
|
|
869
|
+
else:
|
|
870
|
+
# Process input files with key rotation support
|
|
871
|
+
for i, file_path in enumerate(files, 1):
|
|
872
|
+
if verbose:
|
|
873
|
+
print(f"\n[{i}/{len(files)}] Processing: {file_path}")
|
|
874
|
+
|
|
875
|
+
# Try processing with key rotation on rate limit
|
|
876
|
+
max_rotation_attempts = rotator.key_count if rotator else 1
|
|
877
|
+
result = None
|
|
878
|
+
|
|
879
|
+
for rotation_attempt in range(max_rotation_attempts):
|
|
880
|
+
result = process_file(
|
|
881
|
+
client=client,
|
|
882
|
+
file_path=file_path,
|
|
883
|
+
prompt=prompt,
|
|
884
|
+
model=model,
|
|
885
|
+
task=task,
|
|
886
|
+
format_output=format_output,
|
|
887
|
+
aspect_ratio=aspect_ratio,
|
|
888
|
+
image_size=size,
|
|
889
|
+
verbose=verbose
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
# Check if rate limited and can rotate
|
|
893
|
+
if (result.get('rate_limited') and rotator and
|
|
894
|
+
rotation_attempt < max_rotation_attempts - 1):
|
|
895
|
+
new_client = get_client_with_rotation(Exception(result.get('error', '')))
|
|
896
|
+
if new_client:
|
|
897
|
+
client = new_client
|
|
898
|
+
if verbose:
|
|
899
|
+
print(f" Retrying with rotated key...")
|
|
900
|
+
continue
|
|
901
|
+
else:
|
|
902
|
+
# All keys exhausted - mark result with clear error
|
|
903
|
+
if verbose:
|
|
904
|
+
print(f" ⚠ All API keys exhausted (on cooldown)", file=sys.stderr)
|
|
905
|
+
result['error'] = "All API keys exhausted (rate limited). Try again later."
|
|
906
|
+
break
|
|
907
|
+
|
|
908
|
+
results.append(result)
|
|
909
|
+
|
|
910
|
+
if verbose:
|
|
911
|
+
status = result.get('status', 'unknown')
|
|
912
|
+
print(f" Status: {status}")
|
|
913
|
+
|
|
914
|
+
# Save results
|
|
915
|
+
if output_file:
|
|
916
|
+
save_results(results, output_file, format_output)
|
|
917
|
+
|
|
918
|
+
return results
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def print_results(results: List[Dict[str, Any]], task: str) -> None:
|
|
922
|
+
"""Print results to stdout for LLM workflows.
|
|
923
|
+
|
|
924
|
+
Always prints actual results (not just success/fail counts) so LLMs
|
|
925
|
+
can continue processing based on the output.
|
|
926
|
+
"""
|
|
927
|
+
if not results:
|
|
928
|
+
return
|
|
929
|
+
|
|
930
|
+
print("\n=== RESULTS ===\n")
|
|
931
|
+
|
|
932
|
+
for result in results:
|
|
933
|
+
file_name = result.get('file', 'generated')
|
|
934
|
+
status = result.get('status', 'unknown')
|
|
935
|
+
|
|
936
|
+
print(f"[{file_name}]")
|
|
937
|
+
print(f"Status: {status}")
|
|
938
|
+
|
|
939
|
+
if status == 'success':
|
|
940
|
+
# Print task-specific output
|
|
941
|
+
if task in ['analyze', 'transcribe', 'extract']:
|
|
942
|
+
response = result.get('response')
|
|
943
|
+
if response:
|
|
944
|
+
print(f"Result:\n{response}")
|
|
945
|
+
|
|
946
|
+
elif task == 'generate':
|
|
947
|
+
# Image generation
|
|
948
|
+
generated_images = result.get('generated_images', [])
|
|
949
|
+
if generated_images:
|
|
950
|
+
print(f"Generated images: {len(generated_images)}")
|
|
951
|
+
for img in generated_images:
|
|
952
|
+
print(f" - {img}")
|
|
953
|
+
else:
|
|
954
|
+
generated_image = result.get('generated_image')
|
|
955
|
+
if generated_image:
|
|
956
|
+
print(f"Generated image: {generated_image}")
|
|
957
|
+
|
|
958
|
+
elif task == 'generate-video':
|
|
959
|
+
generated_video = result.get('generated_video')
|
|
960
|
+
if generated_video:
|
|
961
|
+
print(f"Generated video: {generated_video}")
|
|
962
|
+
gen_time = result.get('generation_time')
|
|
963
|
+
if gen_time:
|
|
964
|
+
print(f"Generation time: {gen_time:.1f}s")
|
|
965
|
+
file_size = result.get('file_size_mb')
|
|
966
|
+
if file_size:
|
|
967
|
+
print(f"File size: {file_size:.2f} MB")
|
|
968
|
+
|
|
969
|
+
elif status == 'error':
|
|
970
|
+
error = result.get('error', 'Unknown error')
|
|
971
|
+
print(f"Error: {error}")
|
|
972
|
+
|
|
973
|
+
print() # Blank line between results
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def save_results(results: List[Dict[str, Any]], output_file: str, format_output: str):
|
|
977
|
+
"""Save results to file."""
|
|
978
|
+
output_path = Path(output_file)
|
|
979
|
+
|
|
980
|
+
# Special handling for image generation - if output has image extension, copy the generated image
|
|
981
|
+
image_extensions = {'.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp'}
|
|
982
|
+
video_extensions = {'.mp4', '.mov', '.avi', '.webm'}
|
|
983
|
+
|
|
984
|
+
if output_path.suffix.lower() in image_extensions and len(results) == 1:
|
|
985
|
+
# Ensure output directory exists
|
|
986
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
987
|
+
|
|
988
|
+
# Check for multiple generated images
|
|
989
|
+
generated_images = results[0].get('generated_images')
|
|
990
|
+
if generated_images:
|
|
991
|
+
# Copy first image to the specified output location
|
|
992
|
+
shutil.copy2(generated_images[0], output_path)
|
|
993
|
+
return
|
|
994
|
+
|
|
995
|
+
# Legacy single image field
|
|
996
|
+
generated_image = results[0].get('generated_image')
|
|
997
|
+
if generated_image:
|
|
998
|
+
shutil.copy2(generated_image, output_path)
|
|
999
|
+
return
|
|
1000
|
+
else:
|
|
1001
|
+
# Don't write text reports to image files - save error as .txt instead
|
|
1002
|
+
output_path = output_path.with_suffix('.error.txt')
|
|
1003
|
+
output_path.parent.mkdir(parents=True, exist_ok=True) # Ensure directory exists
|
|
1004
|
+
print(f"Warning: Generation failed, saving error report to: {output_path}")
|
|
1005
|
+
|
|
1006
|
+
if output_path.suffix.lower() in video_extensions and len(results) == 1:
|
|
1007
|
+
# Ensure output directory exists
|
|
1008
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1009
|
+
|
|
1010
|
+
generated_video = results[0].get('generated_video')
|
|
1011
|
+
if generated_video:
|
|
1012
|
+
shutil.copy2(generated_video, output_path)
|
|
1013
|
+
return
|
|
1014
|
+
else:
|
|
1015
|
+
output_path = output_path.with_suffix('.error.txt')
|
|
1016
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1017
|
+
print(f"Warning: Video generation failed, saving error report to: {output_path}")
|
|
1018
|
+
|
|
1019
|
+
if format_output == 'json':
|
|
1020
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
1021
|
+
json.dump(results, f, indent=2)
|
|
1022
|
+
elif format_output == 'csv':
|
|
1023
|
+
with open(output_path, 'w', newline='', encoding='utf-8') as f:
|
|
1024
|
+
fieldnames = ['file', 'status', 'response', 'error']
|
|
1025
|
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
1026
|
+
writer.writeheader()
|
|
1027
|
+
for result in results:
|
|
1028
|
+
writer.writerow({
|
|
1029
|
+
'file': result.get('file', ''),
|
|
1030
|
+
'status': result.get('status', ''),
|
|
1031
|
+
'response': result.get('response', ''),
|
|
1032
|
+
'error': result.get('error', '')
|
|
1033
|
+
})
|
|
1034
|
+
else: # markdown
|
|
1035
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
1036
|
+
f.write("# Batch Processing Results\n\n")
|
|
1037
|
+
for i, result in enumerate(results, 1):
|
|
1038
|
+
f.write(f"## {i}. {result.get('file', 'Unknown')}\n\n")
|
|
1039
|
+
f.write(f"**Status**: {result.get('status', 'unknown')}\n\n")
|
|
1040
|
+
if result.get('response'):
|
|
1041
|
+
f.write(f"**Response**:\n\n{result['response']}\n\n")
|
|
1042
|
+
if result.get('error'):
|
|
1043
|
+
f.write(f"**Error**: {result['error']}\n\n")
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
def main():
|
|
1047
|
+
parser = argparse.ArgumentParser(
|
|
1048
|
+
description='Batch process media files with Gemini API',
|
|
1049
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1050
|
+
epilog="""
|
|
1051
|
+
Examples:
|
|
1052
|
+
# Transcribe multiple audio files
|
|
1053
|
+
%(prog)s --files *.mp3 --task transcribe --model gemini-2.5-flash
|
|
1054
|
+
|
|
1055
|
+
# Analyze images
|
|
1056
|
+
%(prog)s --files *.jpg --task analyze --prompt "Describe this image" \\
|
|
1057
|
+
--model gemini-2.5-flash
|
|
1058
|
+
|
|
1059
|
+
# Process PDFs to JSON
|
|
1060
|
+
%(prog)s --files *.pdf --task extract --prompt "Extract data as JSON" \\
|
|
1061
|
+
--format json --output results.json
|
|
1062
|
+
|
|
1063
|
+
# Generate images with Nano Banana Flash (fast)
|
|
1064
|
+
%(prog)s --task generate --prompt "A mountain landscape at sunset" \\
|
|
1065
|
+
--model gemini-2.5-flash-image --aspect-ratio 16:9 --size 2K
|
|
1066
|
+
|
|
1067
|
+
# Generate images with Nano Banana Pro (4K text, reasoning)
|
|
1068
|
+
%(prog)s --task generate --prompt "Travel poster with text 'EXPLORE'" \\
|
|
1069
|
+
--model gemini-3-pro-image-preview --aspect-ratio 3:4 --size 4K
|
|
1070
|
+
|
|
1071
|
+
# Generate images with Imagen 4 (production quality)
|
|
1072
|
+
%(prog)s --task generate --prompt "Product photo of coffee mug" \\
|
|
1073
|
+
--model imagen-4.0-ultra-generate-001 --aspect-ratio 1:1 --size 2K
|
|
1074
|
+
"""
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
parser.add_argument('--files', nargs='*', help='Input files to process')
|
|
1078
|
+
parser.add_argument('--task',
|
|
1079
|
+
choices=['transcribe', 'analyze', 'extract', 'generate', 'generate-video'],
|
|
1080
|
+
help='Task to perform (auto-detected from file type if not specified)')
|
|
1081
|
+
parser.add_argument('--prompt', help='Prompt for analysis/generation')
|
|
1082
|
+
parser.add_argument('--model',
|
|
1083
|
+
help='Model to use (default: auto-detected from task and env vars)')
|
|
1084
|
+
parser.add_argument('--format', dest='format_output', default='text',
|
|
1085
|
+
choices=['text', 'json', 'csv', 'markdown'],
|
|
1086
|
+
help='Output format (default: text)')
|
|
1087
|
+
|
|
1088
|
+
# Image generation options
|
|
1089
|
+
# All 10 aspect ratios supported by Nano Banana / Imagen 4
|
|
1090
|
+
parser.add_argument('--aspect-ratio',
|
|
1091
|
+
choices=['1:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9'],
|
|
1092
|
+
help='Aspect ratio for image/video generation')
|
|
1093
|
+
parser.add_argument('--num-images', type=int, default=1,
|
|
1094
|
+
help='Number of images to generate (1-4, default: 1)')
|
|
1095
|
+
# 4K available for Nano Banana Pro (gemini-3-pro-image-preview)
|
|
1096
|
+
# Note: Not all models support --size, only use when needed
|
|
1097
|
+
parser.add_argument('--size', choices=['1K', '2K', '4K'], default=None,
|
|
1098
|
+
help='Image size - 1K/2K for Imagen 4, 1K/2K/4K for Nano Banana (optional)')
|
|
1099
|
+
|
|
1100
|
+
# Video generation options
|
|
1101
|
+
parser.add_argument('--resolution', choices=['720p', '1080p'], default='1080p',
|
|
1102
|
+
help='Video resolution (default: 1080p)')
|
|
1103
|
+
parser.add_argument('--reference-images', nargs='+',
|
|
1104
|
+
help='Reference images for video generation (max 3)')
|
|
1105
|
+
|
|
1106
|
+
parser.add_argument('--output', help='Output file for results')
|
|
1107
|
+
parser.add_argument('--verbose', '-v', action='store_true',
|
|
1108
|
+
help='Verbose output')
|
|
1109
|
+
parser.add_argument('--dry-run', action='store_true',
|
|
1110
|
+
help='Show what would be done without making API calls')
|
|
1111
|
+
|
|
1112
|
+
args = parser.parse_args()
|
|
1113
|
+
|
|
1114
|
+
# Auto-detect task from file type if not specified
|
|
1115
|
+
if not args.task:
|
|
1116
|
+
if args.files and len(args.files) > 0:
|
|
1117
|
+
args.task = infer_task_from_file(args.files[0])
|
|
1118
|
+
if args.verbose:
|
|
1119
|
+
print(f"Auto-detected task: {args.task} (from file extension)")
|
|
1120
|
+
else:
|
|
1121
|
+
parser.error("--task required when no input files provided")
|
|
1122
|
+
|
|
1123
|
+
# Auto-detect model if not specified
|
|
1124
|
+
if not args.model:
|
|
1125
|
+
args.model = get_default_model(args.task)
|
|
1126
|
+
if args.verbose:
|
|
1127
|
+
print(f"Auto-detected model: {args.model}")
|
|
1128
|
+
|
|
1129
|
+
# Validate model/task combination
|
|
1130
|
+
try:
|
|
1131
|
+
validate_model_task_combination(args.model, args.task)
|
|
1132
|
+
except ValueError as e:
|
|
1133
|
+
parser.error(str(e))
|
|
1134
|
+
|
|
1135
|
+
# Validate arguments
|
|
1136
|
+
if args.task not in ['generate', 'generate-video'] and not args.files:
|
|
1137
|
+
parser.error("--files required for non-generation tasks")
|
|
1138
|
+
|
|
1139
|
+
if args.task in ['generate', 'generate-video'] and not args.prompt:
|
|
1140
|
+
parser.error("--prompt required for generation tasks")
|
|
1141
|
+
|
|
1142
|
+
if args.task not in ['generate', 'generate-video'] and not args.prompt:
|
|
1143
|
+
# Set default prompts
|
|
1144
|
+
if args.task == 'transcribe':
|
|
1145
|
+
args.prompt = 'Generate a transcript with timestamps'
|
|
1146
|
+
elif args.task == 'analyze':
|
|
1147
|
+
args.prompt = 'Analyze this content'
|
|
1148
|
+
elif args.task == 'extract':
|
|
1149
|
+
args.prompt = 'Extract key information'
|
|
1150
|
+
|
|
1151
|
+
# Process files
|
|
1152
|
+
files = args.files or []
|
|
1153
|
+
results = batch_process(
|
|
1154
|
+
files=files,
|
|
1155
|
+
prompt=args.prompt,
|
|
1156
|
+
model=args.model,
|
|
1157
|
+
task=args.task,
|
|
1158
|
+
format_output=args.format_output,
|
|
1159
|
+
aspect_ratio=args.aspect_ratio,
|
|
1160
|
+
num_images=args.num_images,
|
|
1161
|
+
size=args.size,
|
|
1162
|
+
resolution=args.resolution,
|
|
1163
|
+
reference_images=args.reference_images,
|
|
1164
|
+
output_file=args.output,
|
|
1165
|
+
verbose=args.verbose,
|
|
1166
|
+
dry_run=args.dry_run
|
|
1167
|
+
)
|
|
1168
|
+
|
|
1169
|
+
# Print results and summary
|
|
1170
|
+
if not args.dry_run and results:
|
|
1171
|
+
# Always print actual results for LLM workflows
|
|
1172
|
+
print_results(results, args.task)
|
|
1173
|
+
|
|
1174
|
+
# Print summary
|
|
1175
|
+
success = sum(1 for r in results if r.get('status') == 'success')
|
|
1176
|
+
failed = len(results) - success
|
|
1177
|
+
print(f"{'='*50}")
|
|
1178
|
+
print(f"Summary: {len(results)} processed, {success} success, {failed} failed")
|
|
1179
|
+
if args.output:
|
|
1180
|
+
print(f"Results saved to: {args.output}")
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
if __name__ == '__main__':
|
|
1184
|
+
main()
|