@thanhvn14/csvibe 0.1.4 → 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.
Files changed (392) hide show
  1. package/.github/agents/schemas/base-output.schema.json +88 -0
  2. package/.github/agents/schemas/brainstorm-output.schema.json +88 -0
  3. package/.github/agents/schemas/scout-output.schema.json +60 -0
  4. package/.github/agents/scripts/fetch-copilot-tools.js +245 -0
  5. package/.github/agents/scripts/lib/parse-agent-file.js +275 -0
  6. package/.github/agents/scripts/package-lock.json +78 -0
  7. package/.github/agents/scripts/package.json +22 -0
  8. package/.github/agents/scripts/schemas/agent-frontmatter.schema.json +83 -0
  9. package/.github/agents/scripts/validate-agent-all.js +157 -0
  10. package/.github/agents/scripts/validate-agent-frontmatter.js +96 -0
  11. package/.github/agents/scripts/validate-agent-handoffs.js +169 -0
  12. package/.github/agents/scripts/validate-agent-output.js +157 -0
  13. package/.github/agents/scripts/validate-agent-tools.js +278 -0
  14. package/.github/skills/.env.example +100 -0
  15. package/.github/skills/.install-state.json +23 -0
  16. package/.github/skills/README.md +149 -0
  17. package/.github/skills/ai-multimodal/.env.example +204 -0
  18. package/.github/skills/ai-multimodal/scripts/.coverage +0 -0
  19. package/.github/skills/ai-multimodal/scripts/check_setup.py +305 -0
  20. package/.github/skills/ai-multimodal/scripts/document_converter.py +395 -0
  21. package/.github/skills/ai-multimodal/scripts/gemini_batch_process.py +1184 -0
  22. package/.github/skills/ai-multimodal/scripts/media_optimizer.py +506 -0
  23. package/.github/skills/ai-multimodal/scripts/requirements.txt +26 -0
  24. package/.github/skills/better-auth/scripts/.coverage +0 -0
  25. package/.github/skills/better-auth/scripts/better_auth_init.py +521 -0
  26. package/.github/skills/better-auth/scripts/requirements.txt +15 -0
  27. package/.github/skills/chrome-devtools/scripts/README.md +272 -0
  28. package/.github/skills/chrome-devtools/scripts/__tests__/selector.test.js +210 -0
  29. package/.github/skills/chrome-devtools/scripts/aria-snapshot.js +362 -0
  30. package/.github/skills/chrome-devtools/scripts/click.js +83 -0
  31. package/.github/skills/chrome-devtools/scripts/console.js +79 -0
  32. package/.github/skills/chrome-devtools/scripts/evaluate.js +53 -0
  33. package/.github/skills/chrome-devtools/scripts/fill.js +76 -0
  34. package/.github/skills/chrome-devtools/scripts/inject-auth.js +229 -0
  35. package/.github/skills/chrome-devtools/scripts/install-deps.sh +181 -0
  36. package/.github/skills/chrome-devtools/scripts/install.sh +83 -0
  37. package/.github/skills/chrome-devtools/scripts/lib/browser.js +318 -0
  38. package/.github/skills/chrome-devtools/scripts/lib/selector.js +178 -0
  39. package/.github/skills/chrome-devtools/scripts/navigate.js +54 -0
  40. package/.github/skills/chrome-devtools/scripts/network.js +106 -0
  41. package/.github/skills/chrome-devtools/scripts/package-lock.json +1589 -0
  42. package/.github/skills/chrome-devtools/scripts/package.json +16 -0
  43. package/.github/skills/chrome-devtools/scripts/performance.js +149 -0
  44. package/.github/skills/chrome-devtools/scripts/screenshot.js +198 -0
  45. package/.github/skills/chrome-devtools/scripts/select-ref.js +131 -0
  46. package/.github/skills/chrome-devtools/scripts/snapshot.js +135 -0
  47. package/.github/skills/common/README.md +120 -0
  48. package/.github/skills/common/api_key_helper.py +411 -0
  49. package/.github/skills/common/api_key_rotator.py +248 -0
  50. package/.github/skills/databases/scripts/.coverage +0 -0
  51. package/.github/skills/databases/scripts/db_backup.py +502 -0
  52. package/.github/skills/databases/scripts/db_migrate.py +425 -0
  53. package/.github/skills/databases/scripts/db_performance_check.py +456 -0
  54. package/.github/skills/databases/scripts/requirements.txt +20 -0
  55. package/.github/skills/debugging/scripts/find-polluter.sh +63 -0
  56. package/.github/skills/devops/.env.example +76 -0
  57. package/.github/skills/devops/scripts/cloudflare_deploy.py +269 -0
  58. package/.github/skills/devops/scripts/docker_optimize.py +331 -0
  59. package/.github/skills/devops/scripts/requirements.txt +20 -0
  60. package/.github/skills/docs-seeker/.env.example +15 -0
  61. package/.github/skills/docs-seeker/package.json +25 -0
  62. package/.github/skills/docs-seeker/scripts/analyze-llms-txt.js +211 -0
  63. package/.github/skills/docs-seeker/scripts/detect-topic.js +172 -0
  64. package/.github/skills/docs-seeker/scripts/fetch-docs.js +213 -0
  65. package/.github/skills/docs-seeker/scripts/utils/env-loader.js +94 -0
  66. package/.github/skills/document-skills/docx/LICENSE.txt +30 -0
  67. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  68. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  69. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  70. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  71. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  72. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  73. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  74. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  75. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  76. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  77. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  78. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  79. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  80. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  81. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  82. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  83. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  84. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  85. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  86. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  87. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  88. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  89. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  90. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  91. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  92. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  93. package/.github/skills/document-skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  94. package/.github/skills/document-skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  95. package/.github/skills/document-skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  96. package/.github/skills/document-skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  97. package/.github/skills/document-skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  98. package/.github/skills/document-skills/docx/ooxml/schemas/mce/mc.xsd +75 -0
  99. package/.github/skills/document-skills/docx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
  100. package/.github/skills/document-skills/docx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
  101. package/.github/skills/document-skills/docx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
  102. package/.github/skills/document-skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
  103. package/.github/skills/document-skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
  104. package/.github/skills/document-skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  105. package/.github/skills/document-skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
  106. package/.github/skills/document-skills/docx/ooxml/scripts/pack.py +159 -0
  107. package/.github/skills/document-skills/docx/ooxml/scripts/unpack.py +29 -0
  108. package/.github/skills/document-skills/docx/ooxml/scripts/validate.py +69 -0
  109. package/.github/skills/document-skills/docx/ooxml/scripts/validation/__init__.py +15 -0
  110. package/.github/skills/document-skills/docx/ooxml/scripts/validation/base.py +951 -0
  111. package/.github/skills/document-skills/docx/ooxml/scripts/validation/docx.py +274 -0
  112. package/.github/skills/document-skills/docx/ooxml/scripts/validation/pptx.py +315 -0
  113. package/.github/skills/document-skills/docx/ooxml/scripts/validation/redlining.py +279 -0
  114. package/.github/skills/document-skills/docx/scripts/__init__.py +1 -0
  115. package/.github/skills/document-skills/docx/scripts/document.py +1276 -0
  116. package/.github/skills/document-skills/docx/scripts/templates/comments.xml +3 -0
  117. package/.github/skills/document-skills/docx/scripts/templates/commentsExtended.xml +3 -0
  118. package/.github/skills/document-skills/docx/scripts/templates/commentsExtensible.xml +3 -0
  119. package/.github/skills/document-skills/docx/scripts/templates/commentsIds.xml +3 -0
  120. package/.github/skills/document-skills/docx/scripts/templates/people.xml +3 -0
  121. package/.github/skills/document-skills/docx/scripts/utilities.py +374 -0
  122. package/.github/skills/document-skills/pdf/LICENSE.txt +30 -0
  123. package/.github/skills/document-skills/pdf/scripts/check_bounding_boxes.py +70 -0
  124. package/.github/skills/document-skills/pdf/scripts/check_bounding_boxes_test.py +226 -0
  125. package/.github/skills/document-skills/pdf/scripts/check_fillable_fields.py +12 -0
  126. package/.github/skills/document-skills/pdf/scripts/convert_pdf_to_images.py +35 -0
  127. package/.github/skills/document-skills/pdf/scripts/create_validation_image.py +41 -0
  128. package/.github/skills/document-skills/pdf/scripts/extract_form_field_info.py +152 -0
  129. package/.github/skills/document-skills/pdf/scripts/fill_fillable_fields.py +114 -0
  130. package/.github/skills/document-skills/pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
  131. package/.github/skills/document-skills/pptx/LICENSE.txt +30 -0
  132. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  133. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  134. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  135. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  136. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  137. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  138. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  139. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  140. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  141. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  142. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  143. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  144. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  145. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  146. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  147. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  148. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  149. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  150. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  151. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  152. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  153. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  154. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  155. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  156. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  157. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  158. package/.github/skills/document-skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  159. package/.github/skills/document-skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  160. package/.github/skills/document-skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  161. package/.github/skills/document-skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  162. package/.github/skills/document-skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  163. package/.github/skills/document-skills/pptx/ooxml/schemas/mce/mc.xsd +75 -0
  164. package/.github/skills/document-skills/pptx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
  165. package/.github/skills/document-skills/pptx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
  166. package/.github/skills/document-skills/pptx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
  167. package/.github/skills/document-skills/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
  168. package/.github/skills/document-skills/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
  169. package/.github/skills/document-skills/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  170. package/.github/skills/document-skills/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
  171. package/.github/skills/document-skills/pptx/ooxml/scripts/pack.py +159 -0
  172. package/.github/skills/document-skills/pptx/ooxml/scripts/unpack.py +29 -0
  173. package/.github/skills/document-skills/pptx/ooxml/scripts/validate.py +69 -0
  174. package/.github/skills/document-skills/pptx/ooxml/scripts/validation/__init__.py +15 -0
  175. package/.github/skills/document-skills/pptx/ooxml/scripts/validation/base.py +951 -0
  176. package/.github/skills/document-skills/pptx/ooxml/scripts/validation/docx.py +274 -0
  177. package/.github/skills/document-skills/pptx/ooxml/scripts/validation/pptx.py +315 -0
  178. package/.github/skills/document-skills/pptx/ooxml/scripts/validation/redlining.py +279 -0
  179. package/.github/skills/document-skills/pptx/scripts/html2pptx.js +979 -0
  180. package/.github/skills/document-skills/pptx/scripts/inventory.py +1020 -0
  181. package/.github/skills/document-skills/pptx/scripts/rearrange.py +231 -0
  182. package/.github/skills/document-skills/pptx/scripts/replace.py +385 -0
  183. package/.github/skills/document-skills/pptx/scripts/thumbnail.py +450 -0
  184. package/.github/skills/document-skills/xlsx/LICENSE.txt +30 -0
  185. package/.github/skills/document-skills/xlsx/recalc.py +190 -0
  186. package/.github/skills/install.ps1 +1220 -0
  187. package/.github/skills/install.sh +1032 -0
  188. package/.github/skills/markdown-novel-viewer/assets/directory-browser.css +215 -0
  189. package/.github/skills/markdown-novel-viewer/assets/favicon.png +0 -0
  190. package/.github/skills/markdown-novel-viewer/assets/novel-theme.css +818 -0
  191. package/.github/skills/markdown-novel-viewer/assets/reader.js +262 -0
  192. package/.github/skills/markdown-novel-viewer/assets/template.html +80 -0
  193. package/.github/skills/markdown-novel-viewer/package-lock.json +146 -0
  194. package/.github/skills/markdown-novel-viewer/package.json +15 -0
  195. package/.github/skills/markdown-novel-viewer/scripts/lib/http-server.cjs +434 -0
  196. package/.github/skills/markdown-novel-viewer/scripts/lib/markdown-renderer.cjs +272 -0
  197. package/.github/skills/markdown-novel-viewer/scripts/lib/plan-navigator.cjs +509 -0
  198. package/.github/skills/markdown-novel-viewer/scripts/lib/port-finder.cjs +48 -0
  199. package/.github/skills/markdown-novel-viewer/scripts/lib/process-mgr.cjs +150 -0
  200. package/.github/skills/markdown-novel-viewer/scripts/server.cjs +411 -0
  201. package/.github/skills/mcp-builder/LICENSE.txt +202 -0
  202. package/.github/skills/mcp-builder/scripts/connections.py +151 -0
  203. package/.github/skills/mcp-builder/scripts/evaluation.py +373 -0
  204. package/.github/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  205. package/.github/skills/mcp-builder/scripts/requirements.txt +2 -0
  206. package/.github/skills/mcp-management/README.md +219 -0
  207. package/.github/skills/mcp-management/assets/tools.json +3146 -0
  208. package/.github/skills/mcp-management/package-lock.json +6 -0
  209. package/.github/skills/mcp-management/scripts/.env.example +10 -0
  210. package/.github/skills/mcp-management/scripts/cli.ts +195 -0
  211. package/.github/skills/mcp-management/scripts/dist/analyze-tools.js +70 -0
  212. package/.github/skills/mcp-management/scripts/dist/cli.js +160 -0
  213. package/.github/skills/mcp-management/scripts/dist/mcp-client.js +183 -0
  214. package/.github/skills/mcp-management/scripts/mcp-client.ts +230 -0
  215. package/.github/skills/mcp-management/scripts/package.json +20 -0
  216. package/.github/skills/media-processing/scripts/README.md +111 -0
  217. package/.github/skills/media-processing/scripts/batch-remove-background.sh +124 -0
  218. package/.github/skills/media-processing/scripts/batch_resize.py +342 -0
  219. package/.github/skills/media-processing/scripts/media_convert.py +311 -0
  220. package/.github/skills/media-processing/scripts/remove-background.sh +96 -0
  221. package/.github/skills/media-processing/scripts/remove-bg-node.js +158 -0
  222. package/.github/skills/media-processing/scripts/requirements.txt +24 -0
  223. package/.github/skills/media-processing/scripts/video_optimize.py +414 -0
  224. package/.github/skills/payment-integration/README.md +185 -0
  225. package/.github/skills/payment-integration/scripts/.env.example +20 -0
  226. package/.github/skills/payment-integration/scripts/checkout-helper.js +244 -0
  227. package/.github/skills/payment-integration/scripts/package.json +17 -0
  228. package/.github/skills/payment-integration/scripts/polar-webhook-verify.js +202 -0
  229. package/.github/skills/payment-integration/scripts/sepay-webhook-verify.js +193 -0
  230. package/.github/skills/payment-integration/scripts/test-scripts.js +237 -0
  231. package/.github/skills/plans-kanban/assets/dashboard-template.html +119 -0
  232. package/.github/skills/plans-kanban/assets/dashboard.css +1594 -0
  233. package/.github/skills/plans-kanban/assets/dashboard.js +596 -0
  234. package/.github/skills/plans-kanban/assets/favicon.png +0 -0
  235. package/.github/skills/plans-kanban/package-lock.json +123 -0
  236. package/.github/skills/plans-kanban/package.json +13 -0
  237. package/.github/skills/plans-kanban/scripts/lib/dashboard-renderer.cjs +884 -0
  238. package/.github/skills/plans-kanban/scripts/lib/http-server.cjs +310 -0
  239. package/.github/skills/plans-kanban/scripts/lib/plan-metadata-extractor.cjs +489 -0
  240. package/.github/skills/plans-kanban/scripts/lib/plan-parser.cjs +175 -0
  241. package/.github/skills/plans-kanban/scripts/lib/plan-scanner.cjs +272 -0
  242. package/.github/skills/plans-kanban/scripts/lib/port-finder.cjs +48 -0
  243. package/.github/skills/plans-kanban/scripts/lib/process-mgr.cjs +128 -0
  244. package/.github/skills/plans-kanban/scripts/server.cjs +260 -0
  245. package/.github/skills/repomix/scripts/.coverage +0 -0
  246. package/.github/skills/repomix/scripts/README.md +179 -0
  247. package/.github/skills/repomix/scripts/repomix_batch.py +455 -0
  248. package/.github/skills/repomix/scripts/repos.example.json +15 -0
  249. package/.github/skills/repomix/scripts/requirements.txt +15 -0
  250. package/.github/skills/scout-validation/scripts/lib/broad-pattern-detector.cjs +124 -0
  251. package/.github/skills/scout-validation/scripts/lib/path-checker.cjs +66 -0
  252. package/.github/skills/scout-validation/scripts/lib/schema-validator.cjs +45 -0
  253. package/.github/skills/scout-validation/scripts/package.json +11 -0
  254. package/.github/skills/scout-validation/scripts/validate-scout-output.cjs +219 -0
  255. package/.github/skills/scout-validation/test/broad-pattern-output.json +18 -0
  256. package/.github/skills/scout-validation/test/invalid-path-output.json +18 -0
  257. package/.github/skills/scout-validation/test/valid-scout-output.json +26 -0
  258. package/.github/skills/sequential-thinking/.env.example +8 -0
  259. package/.github/skills/sequential-thinking/README.md +183 -0
  260. package/.github/skills/sequential-thinking/package.json +31 -0
  261. package/.github/skills/sequential-thinking/scripts/format-thought.js +159 -0
  262. package/.github/skills/sequential-thinking/scripts/process-thought.js +236 -0
  263. package/.github/skills/shopify/README.md +66 -0
  264. package/.github/skills/shopify/scripts/.coverage +0 -0
  265. package/.github/skills/shopify/scripts/requirements.txt +19 -0
  266. package/.github/skills/shopify/scripts/shopify_init.py +423 -0
  267. package/.github/skills/skill-creator/LICENSE.txt +202 -0
  268. package/.github/skills/skill-creator/scripts/init_skill.py +303 -0
  269. package/.github/skills/skill-creator/scripts/package_skill.py +110 -0
  270. package/.github/skills/skill-creator/scripts/quick_validate.py +65 -0
  271. package/.github/skills/ui-styling/LICENSE.txt +202 -0
  272. package/.github/skills/ui-styling/canvas-fonts/ArsenalSC-OFL.txt +93 -0
  273. package/.github/skills/ui-styling/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
  274. package/.github/skills/ui-styling/canvas-fonts/BigShoulders-Bold.ttf +0 -0
  275. package/.github/skills/ui-styling/canvas-fonts/BigShoulders-OFL.txt +93 -0
  276. package/.github/skills/ui-styling/canvas-fonts/BigShoulders-Regular.ttf +0 -0
  277. package/.github/skills/ui-styling/canvas-fonts/Boldonse-OFL.txt +93 -0
  278. package/.github/skills/ui-styling/canvas-fonts/Boldonse-Regular.ttf +0 -0
  279. package/.github/skills/ui-styling/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
  280. package/.github/skills/ui-styling/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
  281. package/.github/skills/ui-styling/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
  282. package/.github/skills/ui-styling/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
  283. package/.github/skills/ui-styling/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
  284. package/.github/skills/ui-styling/canvas-fonts/CrimsonPro-OFL.txt +93 -0
  285. package/.github/skills/ui-styling/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
  286. package/.github/skills/ui-styling/canvas-fonts/DMMono-OFL.txt +93 -0
  287. package/.github/skills/ui-styling/canvas-fonts/DMMono-Regular.ttf +0 -0
  288. package/.github/skills/ui-styling/canvas-fonts/EricaOne-OFL.txt +94 -0
  289. package/.github/skills/ui-styling/canvas-fonts/EricaOne-Regular.ttf +0 -0
  290. package/.github/skills/ui-styling/canvas-fonts/GeistMono-Bold.ttf +0 -0
  291. package/.github/skills/ui-styling/canvas-fonts/GeistMono-OFL.txt +93 -0
  292. package/.github/skills/ui-styling/canvas-fonts/GeistMono-Regular.ttf +0 -0
  293. package/.github/skills/ui-styling/canvas-fonts/Gloock-OFL.txt +93 -0
  294. package/.github/skills/ui-styling/canvas-fonts/Gloock-Regular.ttf +0 -0
  295. package/.github/skills/ui-styling/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
  296. package/.github/skills/ui-styling/canvas-fonts/IBMPlexMono-OFL.txt +93 -0
  297. package/.github/skills/ui-styling/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
  298. package/.github/skills/ui-styling/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
  299. package/.github/skills/ui-styling/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
  300. package/.github/skills/ui-styling/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
  301. package/.github/skills/ui-styling/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
  302. package/.github/skills/ui-styling/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
  303. package/.github/skills/ui-styling/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
  304. package/.github/skills/ui-styling/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
  305. package/.github/skills/ui-styling/canvas-fonts/InstrumentSans-OFL.txt +93 -0
  306. package/.github/skills/ui-styling/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
  307. package/.github/skills/ui-styling/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
  308. package/.github/skills/ui-styling/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
  309. package/.github/skills/ui-styling/canvas-fonts/Italiana-OFL.txt +93 -0
  310. package/.github/skills/ui-styling/canvas-fonts/Italiana-Regular.ttf +0 -0
  311. package/.github/skills/ui-styling/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
  312. package/.github/skills/ui-styling/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
  313. package/.github/skills/ui-styling/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
  314. package/.github/skills/ui-styling/canvas-fonts/Jura-Light.ttf +0 -0
  315. package/.github/skills/ui-styling/canvas-fonts/Jura-Medium.ttf +0 -0
  316. package/.github/skills/ui-styling/canvas-fonts/Jura-OFL.txt +93 -0
  317. package/.github/skills/ui-styling/canvas-fonts/LibreBaskerville-OFL.txt +93 -0
  318. package/.github/skills/ui-styling/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
  319. package/.github/skills/ui-styling/canvas-fonts/Lora-Bold.ttf +0 -0
  320. package/.github/skills/ui-styling/canvas-fonts/Lora-BoldItalic.ttf +0 -0
  321. package/.github/skills/ui-styling/canvas-fonts/Lora-Italic.ttf +0 -0
  322. package/.github/skills/ui-styling/canvas-fonts/Lora-OFL.txt +93 -0
  323. package/.github/skills/ui-styling/canvas-fonts/Lora-Regular.ttf +0 -0
  324. package/.github/skills/ui-styling/canvas-fonts/NationalPark-Bold.ttf +0 -0
  325. package/.github/skills/ui-styling/canvas-fonts/NationalPark-OFL.txt +93 -0
  326. package/.github/skills/ui-styling/canvas-fonts/NationalPark-Regular.ttf +0 -0
  327. package/.github/skills/ui-styling/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
  328. package/.github/skills/ui-styling/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
  329. package/.github/skills/ui-styling/canvas-fonts/Outfit-Bold.ttf +0 -0
  330. package/.github/skills/ui-styling/canvas-fonts/Outfit-OFL.txt +93 -0
  331. package/.github/skills/ui-styling/canvas-fonts/Outfit-Regular.ttf +0 -0
  332. package/.github/skills/ui-styling/canvas-fonts/PixelifySans-Medium.ttf +0 -0
  333. package/.github/skills/ui-styling/canvas-fonts/PixelifySans-OFL.txt +93 -0
  334. package/.github/skills/ui-styling/canvas-fonts/PoiretOne-OFL.txt +93 -0
  335. package/.github/skills/ui-styling/canvas-fonts/PoiretOne-Regular.ttf +0 -0
  336. package/.github/skills/ui-styling/canvas-fonts/RedHatMono-Bold.ttf +0 -0
  337. package/.github/skills/ui-styling/canvas-fonts/RedHatMono-OFL.txt +93 -0
  338. package/.github/skills/ui-styling/canvas-fonts/RedHatMono-Regular.ttf +0 -0
  339. package/.github/skills/ui-styling/canvas-fonts/Silkscreen-OFL.txt +93 -0
  340. package/.github/skills/ui-styling/canvas-fonts/Silkscreen-Regular.ttf +0 -0
  341. package/.github/skills/ui-styling/canvas-fonts/SmoochSans-Medium.ttf +0 -0
  342. package/.github/skills/ui-styling/canvas-fonts/SmoochSans-OFL.txt +93 -0
  343. package/.github/skills/ui-styling/canvas-fonts/Tektur-Medium.ttf +0 -0
  344. package/.github/skills/ui-styling/canvas-fonts/Tektur-OFL.txt +93 -0
  345. package/.github/skills/ui-styling/canvas-fonts/Tektur-Regular.ttf +0 -0
  346. package/.github/skills/ui-styling/canvas-fonts/WorkSans-Bold.ttf +0 -0
  347. package/.github/skills/ui-styling/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
  348. package/.github/skills/ui-styling/canvas-fonts/WorkSans-Italic.ttf +0 -0
  349. package/.github/skills/ui-styling/canvas-fonts/WorkSans-OFL.txt +93 -0
  350. package/.github/skills/ui-styling/canvas-fonts/WorkSans-Regular.ttf +0 -0
  351. package/.github/skills/ui-styling/canvas-fonts/YoungSerif-OFL.txt +93 -0
  352. package/.github/skills/ui-styling/canvas-fonts/YoungSerif-Regular.ttf +0 -0
  353. package/.github/skills/ui-styling/scripts/.coverage +0 -0
  354. package/.github/skills/ui-styling/scripts/requirements.txt +17 -0
  355. package/.github/skills/ui-styling/scripts/shadcn_add.py +292 -0
  356. package/.github/skills/ui-styling/scripts/tailwind_config_gen.py +456 -0
  357. package/.github/skills/ui-ux-pro-max/data/charts.csv +26 -0
  358. package/.github/skills/ui-ux-pro-max/data/colors.csv +97 -0
  359. package/.github/skills/ui-ux-pro-max/data/landing.csv +31 -0
  360. package/.github/skills/ui-ux-pro-max/data/products.csv +97 -0
  361. package/.github/skills/ui-ux-pro-max/data/prompts.csv +24 -0
  362. package/.github/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  363. package/.github/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +51 -0
  364. package/.github/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  365. package/.github/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  366. package/.github/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  367. package/.github/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  368. package/.github/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  369. package/.github/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  370. package/.github/skills/ui-ux-pro-max/data/styles.csv +59 -0
  371. package/.github/skills/ui-ux-pro-max/data/typography.csv +58 -0
  372. package/.github/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  373. package/.github/skills/ui-ux-pro-max/scripts/core.py +236 -0
  374. package/.github/skills/ui-ux-pro-max/scripts/search.py +76 -0
  375. package/.github/skills/web-frameworks/scripts/.coverage +0 -0
  376. package/.github/skills/web-frameworks/scripts/__init__.py +0 -0
  377. package/.github/skills/web-frameworks/scripts/nextjs_init.py +547 -0
  378. package/.github/skills/web-frameworks/scripts/requirements.txt +16 -0
  379. package/.github/skills/web-frameworks/scripts/turborepo_migrate.py +394 -0
  380. package/dist/config/constants.d.ts +2 -0
  381. package/dist/config/constants.d.ts.map +1 -1
  382. package/dist/config/constants.js +4 -1
  383. package/dist/config/constants.js.map +1 -1
  384. package/dist/domains/github/github-client.d.ts +5 -0
  385. package/dist/domains/github/github-client.d.ts.map +1 -1
  386. package/dist/domains/github/github-client.js +44 -0
  387. package/dist/domains/github/github-client.js.map +1 -1
  388. package/dist/utils/downloader.d.ts +3 -1
  389. package/dist/utils/downloader.d.ts.map +1 -1
  390. package/dist/utils/downloader.js +48 -11
  391. package/dist/utils/downloader.js.map +1 -1
  392. 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()