magic_hour 0.9.5__py3-none-any.whl → 0.44.0__py3-none-any.whl

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 (264) hide show
  1. magic_hour/README.md +34 -0
  2. magic_hour/__init__.py +1 -1
  3. magic_hour/client.py +8 -17
  4. magic_hour/environment.py +13 -1
  5. magic_hour/helpers/__init__.py +4 -0
  6. magic_hour/helpers/download.py +77 -0
  7. magic_hour/helpers/logger.py +8 -0
  8. magic_hour/resources/v1/README.md +32 -0
  9. magic_hour/resources/v1/ai_clothes_changer/README.md +94 -5
  10. magic_hour/resources/v1/ai_clothes_changer/client.py +161 -16
  11. magic_hour/resources/v1/ai_face_editor/README.md +195 -0
  12. magic_hour/resources/v1/ai_face_editor/__init__.py +4 -0
  13. magic_hour/resources/v1/ai_face_editor/client.py +324 -0
  14. magic_hour/resources/v1/ai_gif_generator/README.md +116 -0
  15. magic_hour/resources/v1/ai_gif_generator/__init__.py +4 -0
  16. magic_hour/resources/v1/ai_gif_generator/client.py +257 -0
  17. magic_hour/resources/v1/ai_headshot_generator/README.md +81 -3
  18. magic_hour/resources/v1/ai_headshot_generator/client.py +167 -18
  19. magic_hour/resources/v1/ai_image_editor/README.md +125 -0
  20. magic_hour/resources/v1/ai_image_editor/__init__.py +4 -0
  21. magic_hour/resources/v1/ai_image_editor/client.py +290 -0
  22. magic_hour/resources/v1/ai_image_generator/README.md +99 -5
  23. magic_hour/resources/v1/ai_image_generator/client.py +170 -24
  24. magic_hour/resources/v1/ai_image_upscaler/README.md +89 -3
  25. magic_hour/resources/v1/ai_image_upscaler/client.py +173 -20
  26. magic_hour/resources/v1/ai_meme_generator/README.md +129 -0
  27. magic_hour/resources/v1/ai_meme_generator/__init__.py +4 -0
  28. magic_hour/resources/v1/ai_meme_generator/client.py +253 -0
  29. magic_hour/resources/v1/ai_photo_editor/README.md +119 -4
  30. magic_hour/resources/v1/ai_photo_editor/client.py +199 -18
  31. magic_hour/resources/v1/ai_qr_code_generator/README.md +84 -3
  32. magic_hour/resources/v1/ai_qr_code_generator/client.py +140 -18
  33. magic_hour/resources/v1/ai_talking_photo/README.md +137 -0
  34. magic_hour/resources/v1/ai_talking_photo/__init__.py +4 -0
  35. magic_hour/resources/v1/ai_talking_photo/client.py +326 -0
  36. magic_hour/resources/v1/ai_voice_cloner/README.md +62 -0
  37. magic_hour/resources/v1/ai_voice_cloner/__init__.py +4 -0
  38. magic_hour/resources/v1/ai_voice_cloner/client.py +272 -0
  39. magic_hour/resources/v1/ai_voice_generator/README.md +112 -0
  40. magic_hour/resources/v1/ai_voice_generator/__init__.py +4 -0
  41. magic_hour/resources/v1/ai_voice_generator/client.py +241 -0
  42. magic_hour/resources/v1/animation/README.md +128 -6
  43. magic_hour/resources/v1/animation/client.py +247 -22
  44. magic_hour/resources/v1/audio_projects/README.md +135 -0
  45. magic_hour/resources/v1/audio_projects/__init__.py +12 -0
  46. magic_hour/resources/v1/audio_projects/client.py +310 -0
  47. magic_hour/resources/v1/audio_projects/client_test.py +520 -0
  48. magic_hour/resources/v1/auto_subtitle_generator/README.md +128 -0
  49. magic_hour/resources/v1/auto_subtitle_generator/__init__.py +4 -0
  50. magic_hour/resources/v1/auto_subtitle_generator/client.py +346 -0
  51. magic_hour/resources/v1/client.py +75 -1
  52. magic_hour/resources/v1/face_detection/README.md +157 -0
  53. magic_hour/resources/v1/face_detection/__init__.py +12 -0
  54. magic_hour/resources/v1/face_detection/client.py +380 -0
  55. magic_hour/resources/v1/face_swap/README.md +137 -9
  56. magic_hour/resources/v1/face_swap/client.py +329 -38
  57. magic_hour/resources/v1/face_swap_photo/README.md +118 -3
  58. magic_hour/resources/v1/face_swap_photo/client.py +199 -14
  59. magic_hour/resources/v1/files/README.md +39 -0
  60. magic_hour/resources/v1/files/client.py +351 -1
  61. magic_hour/resources/v1/files/client_test.py +414 -0
  62. magic_hour/resources/v1/files/upload_urls/README.md +38 -17
  63. magic_hour/resources/v1/files/upload_urls/client.py +38 -34
  64. magic_hour/resources/v1/image_background_remover/README.md +96 -5
  65. magic_hour/resources/v1/image_background_remover/client.py +151 -16
  66. magic_hour/resources/v1/image_projects/README.md +82 -10
  67. magic_hour/resources/v1/image_projects/__init__.py +10 -2
  68. magic_hour/resources/v1/image_projects/client.py +154 -16
  69. magic_hour/resources/v1/image_projects/client_test.py +527 -0
  70. magic_hour/resources/v1/image_to_video/README.md +96 -11
  71. magic_hour/resources/v1/image_to_video/client.py +282 -38
  72. magic_hour/resources/v1/lip_sync/README.md +112 -9
  73. magic_hour/resources/v1/lip_sync/client.py +288 -34
  74. magic_hour/resources/v1/photo_colorizer/README.md +107 -0
  75. magic_hour/resources/v1/photo_colorizer/__init__.py +4 -0
  76. magic_hour/resources/v1/photo_colorizer/client.py +248 -0
  77. magic_hour/resources/v1/text_to_video/README.md +96 -7
  78. magic_hour/resources/v1/text_to_video/client.py +204 -18
  79. magic_hour/resources/v1/video_projects/README.md +81 -9
  80. magic_hour/resources/v1/video_projects/__init__.py +10 -2
  81. magic_hour/resources/v1/video_projects/client.py +151 -14
  82. magic_hour/resources/v1/video_projects/client_test.py +527 -0
  83. magic_hour/resources/v1/video_to_video/README.md +119 -15
  84. magic_hour/resources/v1/video_to_video/client.py +299 -46
  85. magic_hour/types/models/__init__.py +92 -56
  86. magic_hour/types/models/v1_ai_clothes_changer_create_response.py +33 -0
  87. magic_hour/types/models/v1_ai_face_editor_create_response.py +33 -0
  88. magic_hour/types/models/v1_ai_gif_generator_create_response.py +33 -0
  89. magic_hour/types/models/v1_ai_headshot_generator_create_response.py +33 -0
  90. magic_hour/types/models/v1_ai_image_editor_create_response.py +33 -0
  91. magic_hour/types/models/v1_ai_image_generator_create_response.py +33 -0
  92. magic_hour/types/models/v1_ai_image_upscaler_create_response.py +33 -0
  93. magic_hour/types/models/v1_ai_meme_generator_create_response.py +33 -0
  94. magic_hour/types/models/v1_ai_photo_editor_create_response.py +33 -0
  95. magic_hour/types/models/v1_ai_qr_code_generator_create_response.py +33 -0
  96. magic_hour/types/models/v1_ai_talking_photo_create_response.py +35 -0
  97. magic_hour/types/models/v1_ai_voice_cloner_create_response.py +27 -0
  98. magic_hour/types/models/v1_ai_voice_generator_create_response.py +27 -0
  99. magic_hour/types/models/v1_animation_create_response.py +35 -0
  100. magic_hour/types/models/v1_audio_projects_get_response.py +72 -0
  101. magic_hour/types/models/v1_audio_projects_get_response_downloads_item.py +19 -0
  102. magic_hour/types/models/{get_v1_image_projects_id_response_error.py → v1_audio_projects_get_response_error.py} +2 -2
  103. magic_hour/types/models/v1_auto_subtitle_generator_create_response.py +35 -0
  104. magic_hour/types/models/v1_face_detection_create_response.py +25 -0
  105. magic_hour/types/models/v1_face_detection_get_response.py +45 -0
  106. magic_hour/types/models/v1_face_detection_get_response_faces_item.py +25 -0
  107. magic_hour/types/models/v1_face_swap_create_response.py +35 -0
  108. magic_hour/types/models/v1_face_swap_photo_create_response.py +33 -0
  109. magic_hour/types/models/v1_files_upload_urls_create_response.py +24 -0
  110. magic_hour/types/models/{post_v1_files_upload_urls_response_items_item.py → v1_files_upload_urls_create_response_items_item.py} +2 -2
  111. magic_hour/types/models/v1_image_background_remover_create_response.py +33 -0
  112. magic_hour/types/models/{get_v1_image_projects_id_response.py → v1_image_projects_get_response.py} +20 -18
  113. magic_hour/types/models/{get_v1_video_projects_id_response_downloads_item.py → v1_image_projects_get_response_downloads_item.py} +1 -1
  114. magic_hour/types/models/{get_v1_video_projects_id_response_error.py → v1_image_projects_get_response_error.py} +2 -2
  115. magic_hour/types/models/v1_image_to_video_create_response.py +35 -0
  116. magic_hour/types/models/v1_lip_sync_create_response.py +35 -0
  117. magic_hour/types/models/v1_photo_colorizer_create_response.py +33 -0
  118. magic_hour/types/models/v1_text_to_video_create_response.py +35 -0
  119. magic_hour/types/models/{get_v1_video_projects_id_response.py → v1_video_projects_get_response.py} +26 -23
  120. magic_hour/types/models/{get_v1_video_projects_id_response_download.py → v1_video_projects_get_response_download.py} +1 -1
  121. magic_hour/types/models/{get_v1_image_projects_id_response_downloads_item.py → v1_video_projects_get_response_downloads_item.py} +1 -1
  122. magic_hour/types/models/v1_video_projects_get_response_error.py +25 -0
  123. magic_hour/types/models/v1_video_to_video_create_response.py +35 -0
  124. magic_hour/types/params/__init__.py +422 -176
  125. magic_hour/types/params/v1_ai_clothes_changer_create_body.py +40 -0
  126. magic_hour/types/params/v1_ai_clothes_changer_create_body_assets.py +58 -0
  127. magic_hour/types/params/v1_ai_clothes_changer_generate_body_assets.py +33 -0
  128. magic_hour/types/params/v1_ai_face_editor_create_body.py +52 -0
  129. magic_hour/types/params/v1_ai_face_editor_create_body_assets.py +33 -0
  130. magic_hour/types/params/v1_ai_face_editor_create_body_style.py +137 -0
  131. magic_hour/types/params/v1_ai_face_editor_generate_body_assets.py +17 -0
  132. magic_hour/types/params/v1_ai_gif_generator_create_body.py +47 -0
  133. magic_hour/types/params/{post_v1_ai_image_generator_body_style.py → v1_ai_gif_generator_create_body_style.py} +5 -5
  134. magic_hour/types/params/v1_ai_headshot_generator_create_body.py +49 -0
  135. magic_hour/types/params/v1_ai_headshot_generator_create_body_assets.py +33 -0
  136. magic_hour/types/params/v1_ai_headshot_generator_create_body_style.py +27 -0
  137. magic_hour/types/params/v1_ai_headshot_generator_generate_body_assets.py +17 -0
  138. magic_hour/types/params/v1_ai_image_editor_create_body.py +49 -0
  139. magic_hour/types/params/v1_ai_image_editor_create_body_assets.py +47 -0
  140. magic_hour/types/params/v1_ai_image_editor_create_body_style.py +41 -0
  141. magic_hour/types/params/v1_ai_image_editor_generate_body_assets.py +28 -0
  142. magic_hour/types/params/{post_v1_ai_image_generator_body.py → v1_ai_image_generator_create_body.py} +17 -11
  143. magic_hour/types/params/v1_ai_image_generator_create_body_style.py +127 -0
  144. magic_hour/types/params/v1_ai_image_upscaler_create_body.py +59 -0
  145. magic_hour/types/params/v1_ai_image_upscaler_create_body_assets.py +33 -0
  146. magic_hour/types/params/{post_v1_ai_image_upscaler_body_style.py → v1_ai_image_upscaler_create_body_style.py} +4 -4
  147. magic_hour/types/params/v1_ai_image_upscaler_generate_body_assets.py +17 -0
  148. magic_hour/types/params/v1_ai_meme_generator_create_body.py +37 -0
  149. magic_hour/types/params/v1_ai_meme_generator_create_body_style.py +73 -0
  150. magic_hour/types/params/{post_v1_ai_photo_editor_body.py → v1_ai_photo_editor_create_body.py} +15 -15
  151. magic_hour/types/params/v1_ai_photo_editor_create_body_assets.py +33 -0
  152. magic_hour/types/params/{post_v1_ai_photo_editor_body_style.py → v1_ai_photo_editor_create_body_style.py} +20 -4
  153. magic_hour/types/params/v1_ai_photo_editor_generate_body_assets.py +17 -0
  154. magic_hour/types/params/v1_ai_qr_code_generator_create_body.py +45 -0
  155. magic_hour/types/params/{post_v1_ai_qr_code_generator_body_style.py → v1_ai_qr_code_generator_create_body_style.py} +4 -4
  156. magic_hour/types/params/v1_ai_talking_photo_create_body.py +68 -0
  157. magic_hour/types/params/v1_ai_talking_photo_create_body_assets.py +46 -0
  158. magic_hour/types/params/v1_ai_talking_photo_create_body_style.py +44 -0
  159. magic_hour/types/params/v1_ai_talking_photo_generate_body_assets.py +26 -0
  160. magic_hour/types/params/v1_ai_voice_cloner_create_body.py +49 -0
  161. magic_hour/types/params/v1_ai_voice_cloner_create_body_assets.py +33 -0
  162. magic_hour/types/params/v1_ai_voice_cloner_create_body_style.py +28 -0
  163. magic_hour/types/params/v1_ai_voice_cloner_generate_body_assets.py +28 -0
  164. magic_hour/types/params/v1_ai_voice_generator_create_body.py +40 -0
  165. magic_hour/types/params/v1_ai_voice_generator_create_body_style.py +440 -0
  166. magic_hour/types/params/{post_v1_animation_body.py → v1_animation_create_body.py} +16 -16
  167. magic_hour/types/params/{post_v1_animation_body_assets.py → v1_animation_create_body_assets.py} +15 -5
  168. magic_hour/types/params/{post_v1_animation_body_style.py → v1_animation_create_body_style.py} +13 -10
  169. magic_hour/types/params/v1_animation_generate_body_assets.py +39 -0
  170. magic_hour/types/params/v1_auto_subtitle_generator_create_body.py +78 -0
  171. magic_hour/types/params/v1_auto_subtitle_generator_create_body_assets.py +33 -0
  172. magic_hour/types/params/v1_auto_subtitle_generator_create_body_style.py +56 -0
  173. magic_hour/types/params/v1_auto_subtitle_generator_create_body_style_custom_config.py +86 -0
  174. magic_hour/types/params/v1_auto_subtitle_generator_generate_body_assets.py +17 -0
  175. magic_hour/types/params/v1_face_detection_create_body.py +44 -0
  176. magic_hour/types/params/v1_face_detection_create_body_assets.py +33 -0
  177. magic_hour/types/params/v1_face_detection_generate_body_assets.py +17 -0
  178. magic_hour/types/params/v1_face_swap_create_body.py +92 -0
  179. magic_hour/types/params/v1_face_swap_create_body_assets.py +91 -0
  180. magic_hour/types/params/v1_face_swap_create_body_assets_face_mappings_item.py +44 -0
  181. magic_hour/types/params/v1_face_swap_create_body_style.py +33 -0
  182. magic_hour/types/params/v1_face_swap_generate_body_assets.py +56 -0
  183. magic_hour/types/params/v1_face_swap_generate_body_assets_face_mappings_item.py +25 -0
  184. magic_hour/types/params/v1_face_swap_photo_create_body.py +40 -0
  185. magic_hour/types/params/v1_face_swap_photo_create_body_assets.py +76 -0
  186. magic_hour/types/params/v1_face_swap_photo_create_body_assets_face_mappings_item.py +44 -0
  187. magic_hour/types/params/v1_face_swap_photo_generate_body_assets.py +47 -0
  188. magic_hour/types/params/v1_face_swap_photo_generate_body_assets_face_mappings_item.py +25 -0
  189. magic_hour/types/params/v1_files_upload_urls_create_body.py +36 -0
  190. magic_hour/types/params/v1_files_upload_urls_create_body_items_item.py +38 -0
  191. magic_hour/types/params/v1_image_background_remover_create_body.py +40 -0
  192. magic_hour/types/params/v1_image_background_remover_create_body_assets.py +49 -0
  193. magic_hour/types/params/v1_image_background_remover_generate_body_assets.py +27 -0
  194. magic_hour/types/params/v1_image_to_video_create_body.py +101 -0
  195. magic_hour/types/params/v1_image_to_video_create_body_assets.py +33 -0
  196. magic_hour/types/params/v1_image_to_video_create_body_style.py +53 -0
  197. magic_hour/types/params/v1_image_to_video_generate_body_assets.py +17 -0
  198. magic_hour/types/params/v1_lip_sync_create_body.py +100 -0
  199. magic_hour/types/params/{post_v1_lip_sync_body_assets.py → v1_lip_sync_create_body_assets.py} +15 -5
  200. magic_hour/types/params/v1_lip_sync_create_body_style.py +37 -0
  201. magic_hour/types/params/v1_lip_sync_generate_body_assets.py +36 -0
  202. magic_hour/types/params/v1_photo_colorizer_create_body.py +40 -0
  203. magic_hour/types/params/v1_photo_colorizer_create_body_assets.py +33 -0
  204. magic_hour/types/params/v1_photo_colorizer_generate_body_assets.py +17 -0
  205. magic_hour/types/params/v1_text_to_video_create_body.py +78 -0
  206. magic_hour/types/params/v1_text_to_video_create_body_style.py +43 -0
  207. magic_hour/types/params/v1_video_to_video_create_body.py +101 -0
  208. magic_hour/types/params/{post_v1_video_to_video_body_assets.py → v1_video_to_video_create_body_assets.py} +9 -4
  209. magic_hour/types/params/{post_v1_video_to_video_body_style.py → v1_video_to_video_create_body_style.py} +68 -26
  210. magic_hour/types/params/v1_video_to_video_generate_body_assets.py +27 -0
  211. magic_hour-0.44.0.dist-info/METADATA +328 -0
  212. magic_hour-0.44.0.dist-info/RECORD +231 -0
  213. magic_hour/core/__init__.py +0 -52
  214. magic_hour/core/api_error.py +0 -56
  215. magic_hour/core/auth.py +0 -314
  216. magic_hour/core/base_client.py +0 -618
  217. magic_hour/core/binary_response.py +0 -23
  218. magic_hour/core/query.py +0 -106
  219. magic_hour/core/request.py +0 -156
  220. magic_hour/core/response.py +0 -293
  221. magic_hour/core/type_utils.py +0 -28
  222. magic_hour/core/utils.py +0 -55
  223. magic_hour/types/models/post_v1_ai_clothes_changer_response.py +0 -25
  224. magic_hour/types/models/post_v1_ai_headshot_generator_response.py +0 -25
  225. magic_hour/types/models/post_v1_ai_image_generator_response.py +0 -25
  226. magic_hour/types/models/post_v1_ai_image_upscaler_response.py +0 -25
  227. magic_hour/types/models/post_v1_ai_photo_editor_response.py +0 -25
  228. magic_hour/types/models/post_v1_ai_qr_code_generator_response.py +0 -25
  229. magic_hour/types/models/post_v1_animation_response.py +0 -25
  230. magic_hour/types/models/post_v1_face_swap_photo_response.py +0 -25
  231. magic_hour/types/models/post_v1_face_swap_response.py +0 -25
  232. magic_hour/types/models/post_v1_files_upload_urls_response.py +0 -21
  233. magic_hour/types/models/post_v1_image_background_remover_response.py +0 -25
  234. magic_hour/types/models/post_v1_image_to_video_response.py +0 -25
  235. magic_hour/types/models/post_v1_lip_sync_response.py +0 -25
  236. magic_hour/types/models/post_v1_text_to_video_response.py +0 -25
  237. magic_hour/types/models/post_v1_video_to_video_response.py +0 -25
  238. magic_hour/types/params/post_v1_ai_clothes_changer_body.py +0 -40
  239. magic_hour/types/params/post_v1_ai_clothes_changer_body_assets.py +0 -45
  240. magic_hour/types/params/post_v1_ai_headshot_generator_body.py +0 -40
  241. magic_hour/types/params/post_v1_ai_headshot_generator_body_assets.py +0 -28
  242. magic_hour/types/params/post_v1_ai_image_upscaler_body.py +0 -57
  243. magic_hour/types/params/post_v1_ai_image_upscaler_body_assets.py +0 -28
  244. magic_hour/types/params/post_v1_ai_photo_editor_body_assets.py +0 -28
  245. magic_hour/types/params/post_v1_ai_qr_code_generator_body.py +0 -45
  246. magic_hour/types/params/post_v1_face_swap_body.py +0 -72
  247. magic_hour/types/params/post_v1_face_swap_body_assets.py +0 -52
  248. magic_hour/types/params/post_v1_face_swap_photo_body.py +0 -40
  249. magic_hour/types/params/post_v1_face_swap_photo_body_assets.py +0 -36
  250. magic_hour/types/params/post_v1_files_upload_urls_body.py +0 -31
  251. magic_hour/types/params/post_v1_files_upload_urls_body_items_item.py +0 -38
  252. magic_hour/types/params/post_v1_image_background_remover_body.py +0 -40
  253. magic_hour/types/params/post_v1_image_background_remover_body_assets.py +0 -28
  254. magic_hour/types/params/post_v1_image_to_video_body.py +0 -73
  255. magic_hour/types/params/post_v1_image_to_video_body_assets.py +0 -28
  256. magic_hour/types/params/post_v1_image_to_video_body_style.py +0 -37
  257. magic_hour/types/params/post_v1_lip_sync_body.py +0 -80
  258. magic_hour/types/params/post_v1_text_to_video_body.py +0 -57
  259. magic_hour/types/params/post_v1_text_to_video_body_style.py +0 -28
  260. magic_hour/types/params/post_v1_video_to_video_body.py +0 -93
  261. magic_hour-0.9.5.dist-info/METADATA +0 -133
  262. magic_hour-0.9.5.dist-info/RECORD +0 -132
  263. {magic_hour-0.9.5.dist-info → magic_hour-0.44.0.dist-info}/LICENSE +0 -0
  264. {magic_hour-0.9.5.dist-info → magic_hour-0.44.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,520 @@
1
+ import datetime
2
+ import pytest
3
+ import httpx
4
+ from pathlib import Path
5
+ from typing import Any, Generator, Literal, Union, List
6
+ from unittest.mock import Mock, AsyncMock
7
+
8
+ from magic_hour.types import models
9
+ from magic_hour.resources.v1.audio_projects.client import (
10
+ AudioProjectsClient,
11
+ AsyncAudioProjectsClient,
12
+ )
13
+
14
+
15
+ class DummyResponse(models.V1AudioProjectsGetResponse):
16
+ """Helper response with defaults"""
17
+
18
+ def __init__(
19
+ self,
20
+ *,
21
+ status: Literal[
22
+ "complete", "queued", "rendering", "error", "canceled"
23
+ ] = "complete",
24
+ download_url: Union[str, None] = None,
25
+ error: Union[str, None] = None,
26
+ ):
27
+ # Create error object if error string is provided
28
+ error_obj = None
29
+ if error:
30
+ error_obj = models.V1AudioProjectsGetResponseError(
31
+ code="TEST_ERROR", message=error
32
+ )
33
+
34
+ super().__init__(
35
+ id="test-id",
36
+ created_at=datetime.datetime.now().isoformat(),
37
+ credits_charged=0,
38
+ downloads=[
39
+ models.V1AudioProjectsGetResponseDownloadsItem(
40
+ url=download_url, expires_at="2024-01-01T00:00:00Z"
41
+ )
42
+ ]
43
+ if download_url
44
+ else [],
45
+ enabled=True,
46
+ error=error_obj,
47
+ name="test-name",
48
+ status=status,
49
+ type="test-type",
50
+ )
51
+
52
+
53
+ @pytest.fixture
54
+ def mock_base_client() -> Generator[Mock, None, None]:
55
+ yield Mock()
56
+
57
+
58
+ @pytest.fixture
59
+ def mock_async_base_client() -> Generator[AsyncMock, None, None]:
60
+ yield AsyncMock()
61
+
62
+
63
+ def test_delete_calls_base_client(mock_base_client: Mock) -> None:
64
+ client = AudioProjectsClient(base_client=mock_base_client)
65
+ client.delete(id="123")
66
+
67
+ mock_base_client.request.assert_called_once()
68
+ call = mock_base_client.request.call_args[1]
69
+ assert call["method"] == "DELETE"
70
+ assert "/v1/audio-projects/123" in call["path"]
71
+
72
+
73
+ def test_get_calls_base_client(mock_base_client: Mock) -> None:
74
+ client = AudioProjectsClient(base_client=mock_base_client)
75
+ mock_base_client.request.return_value = DummyResponse()
76
+
77
+ resp = client.get(id="abc")
78
+
79
+ mock_base_client.request.assert_called_once()
80
+ assert isinstance(resp, models.V1AudioProjectsGetResponse)
81
+ assert resp.id == "test-id"
82
+
83
+
84
+ def test_check_result_no_wait_no_download(mock_base_client: Mock) -> None:
85
+ client = AudioProjectsClient(base_client=mock_base_client)
86
+ mock_base_client.request.return_value = DummyResponse(status="queued")
87
+
88
+ resp = client.check_result(
89
+ id="xyz",
90
+ wait_for_completion=False,
91
+ download_outputs=False,
92
+ )
93
+
94
+ assert resp.downloaded_paths is None
95
+
96
+
97
+ def test_check_result_wait_until_complete(
98
+ monkeypatch: Any, mock_base_client: Mock
99
+ ) -> None:
100
+ client = AudioProjectsClient(base_client=mock_base_client)
101
+
102
+ # First calls return queued, then complete
103
+ mock_base_client.request.side_effect = [
104
+ DummyResponse(status="queued"),
105
+ DummyResponse(status="queued"),
106
+ DummyResponse(status="complete"),
107
+ ]
108
+
109
+ monkeypatch.setattr("time.sleep", lambda _: None) # type: ignore
110
+
111
+ resp = client.check_result(
112
+ id="xyz", wait_for_completion=True, download_outputs=False
113
+ )
114
+
115
+ assert resp.status == "complete"
116
+ assert resp.downloaded_paths is None
117
+
118
+
119
+ def test_check_result_download_outputs(
120
+ tmp_path: Path, mock_base_client: Mock, monkeypatch: Any
121
+ ) -> None:
122
+ client = AudioProjectsClient(base_client=mock_base_client)
123
+
124
+ file_url = "https://example.com/file.mp3"
125
+ mock_base_client.request.return_value = DummyResponse(
126
+ status="complete",
127
+ download_url=file_url,
128
+ )
129
+
130
+ # Create a mock response for httpx
131
+ mock_request = httpx.Request("GET", "https://example.com/file.mp3")
132
+ mock_response = httpx.Response(200, content=b"fake mp3", request=mock_request)
133
+
134
+ # Mock the httpx.Client class
135
+ class MockClient:
136
+ def __init__(self):
137
+ pass
138
+
139
+ def __enter__(self) -> "MockClient":
140
+ return self
141
+
142
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
143
+ pass
144
+
145
+ def get(self, url: str) -> httpx.Response:
146
+ return mock_response
147
+
148
+ monkeypatch.setattr(httpx, "Client", MockClient)
149
+
150
+ resp = client.check_result(
151
+ id="xyz",
152
+ wait_for_completion=True,
153
+ download_outputs=True,
154
+ download_directory=str(tmp_path),
155
+ )
156
+
157
+ assert resp.status == "complete"
158
+ assert resp.downloaded_paths
159
+ saved_file = Path(resp.downloaded_paths[0])
160
+ assert saved_file.exists()
161
+ assert saved_file.read_bytes() == b"fake mp3"
162
+
163
+
164
+ def test_check_result_error_status(mock_base_client: Mock) -> None:
165
+ client = AudioProjectsClient(base_client=mock_base_client)
166
+ mock_base_client.request.return_value = DummyResponse(status="error", error="Boom!")
167
+
168
+ resp = client.check_result(
169
+ id="err", wait_for_completion=True, download_outputs=False
170
+ )
171
+ assert resp.status == "error"
172
+ assert resp.error is not None
173
+ assert resp.error.message == "Boom!"
174
+ assert resp.downloaded_paths is None
175
+
176
+
177
+ def test_check_result_canceled_status(mock_base_client: Mock) -> None:
178
+ client = AudioProjectsClient(base_client=mock_base_client)
179
+ mock_base_client.request.return_value = DummyResponse(status="canceled")
180
+
181
+ resp = client.check_result(
182
+ id="cancel", wait_for_completion=True, download_outputs=False
183
+ )
184
+ assert resp.status == "canceled"
185
+ assert resp.downloaded_paths is None
186
+
187
+
188
+ def test_check_result_poll_interval_default(
189
+ mock_base_client: Mock, monkeypatch: Any
190
+ ) -> None:
191
+ client = AudioProjectsClient(base_client=mock_base_client)
192
+
193
+ # First calls return queued, then complete
194
+ mock_base_client.request.side_effect = [
195
+ DummyResponse(status="queued"),
196
+ DummyResponse(status="complete"),
197
+ ]
198
+
199
+ # Mock time.sleep to track calls
200
+ sleep_calls: List[float] = []
201
+
202
+ def mock_sleep(seconds: float) -> None:
203
+ sleep_calls.append(seconds)
204
+
205
+ monkeypatch.setattr("time.sleep", mock_sleep)
206
+
207
+ resp = client.check_result(
208
+ id="xyz", wait_for_completion=True, download_outputs=False
209
+ )
210
+
211
+ assert resp.status == "complete"
212
+ # Should have slept once with default interval (0.5)
213
+ assert len(sleep_calls) == 1
214
+ assert sleep_calls[0] == 0.5
215
+
216
+
217
+ def test_check_result_poll_interval_custom(
218
+ mock_base_client: Mock, monkeypatch: Any
219
+ ) -> None:
220
+ client = AudioProjectsClient(base_client=mock_base_client)
221
+
222
+ # Set custom poll interval
223
+ monkeypatch.setenv("MAGIC_HOUR_POLL_INTERVAL", "1.0")
224
+
225
+ # First calls return queued, then complete
226
+ mock_base_client.request.side_effect = [
227
+ DummyResponse(status="queued"),
228
+ DummyResponse(status="complete"),
229
+ ]
230
+
231
+ # Mock time.sleep to track calls
232
+ sleep_calls: List[float] = []
233
+
234
+ def mock_sleep(seconds: float) -> None:
235
+ sleep_calls.append(seconds)
236
+
237
+ monkeypatch.setattr("time.sleep", mock_sleep)
238
+
239
+ resp = client.check_result(
240
+ id="xyz", wait_for_completion=True, download_outputs=False
241
+ )
242
+
243
+ assert resp.status == "complete"
244
+ # Should have slept once with custom interval (1.0)
245
+ assert len(sleep_calls) == 1
246
+ assert sleep_calls[0] == 1.0
247
+
248
+
249
+ def test_check_result_poll_interval_multiple_polls(
250
+ mock_base_client: Mock, monkeypatch: Any
251
+ ) -> None:
252
+ client = AudioProjectsClient(base_client=mock_base_client)
253
+
254
+ # Set custom poll interval
255
+ monkeypatch.setenv("MAGIC_HOUR_POLL_INTERVAL", "0.1")
256
+
257
+ # Multiple calls return queued before complete
258
+ mock_base_client.request.side_effect = [
259
+ DummyResponse(status="queued"),
260
+ DummyResponse(status="queued"),
261
+ DummyResponse(status="queued"),
262
+ DummyResponse(status="complete"),
263
+ ]
264
+
265
+ # Mock time.sleep to track calls
266
+ sleep_calls: List[float] = []
267
+
268
+ def mock_sleep(seconds: float) -> None:
269
+ sleep_calls.append(seconds)
270
+
271
+ monkeypatch.setattr("time.sleep", mock_sleep)
272
+
273
+ resp = client.check_result(
274
+ id="xyz", wait_for_completion=True, download_outputs=False
275
+ )
276
+
277
+ assert resp.status == "complete"
278
+ # Should have slept 3 times with custom interval (0.1)
279
+ assert len(sleep_calls) == 3
280
+ assert all(sleep_time == 0.1 for sleep_time in sleep_calls)
281
+
282
+
283
+ @pytest.mark.asyncio
284
+ async def test_async_delete_calls_base_client(
285
+ mock_async_base_client: AsyncMock,
286
+ ) -> None:
287
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
288
+ await client.delete(id="456")
289
+
290
+ mock_async_base_client.request.assert_called_once()
291
+ call = mock_async_base_client.request.call_args[1]
292
+ assert call["method"] == "DELETE"
293
+ assert "/v1/audio-projects/456" in call["path"]
294
+
295
+
296
+ @pytest.mark.asyncio
297
+ async def test_async_get_calls_base_client(mock_async_base_client: AsyncMock) -> None:
298
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
299
+ mock_async_base_client.request.return_value = DummyResponse()
300
+
301
+ resp = await client.get(id="zzz")
302
+
303
+ mock_async_base_client.request.assert_called_once()
304
+ assert isinstance(resp, models.V1AudioProjectsGetResponse)
305
+ assert resp.id == "test-id"
306
+
307
+
308
+ @pytest.mark.asyncio
309
+ async def test_async_check_result_no_wait_no_download(
310
+ mock_async_base_client: AsyncMock,
311
+ ) -> None:
312
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
313
+ mock_async_base_client.request.return_value = DummyResponse(status="queued")
314
+
315
+ resp = await client.check_result(
316
+ id="xyz",
317
+ wait_for_completion=False,
318
+ download_outputs=False,
319
+ )
320
+
321
+ assert resp.downloaded_paths is None
322
+
323
+
324
+ @pytest.mark.asyncio
325
+ async def test_async_check_result_wait_until_complete(
326
+ mock_async_base_client: AsyncMock, monkeypatch: Any
327
+ ) -> None:
328
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
329
+
330
+ # First calls return queued, then complete
331
+ mock_async_base_client.request.side_effect = [
332
+ DummyResponse(status="queued"),
333
+ DummyResponse(status="queued"),
334
+ DummyResponse(status="complete"),
335
+ ]
336
+
337
+ monkeypatch.setattr("time.sleep", lambda _: None) # type: ignore
338
+
339
+ resp = await client.check_result(
340
+ id="xyz", wait_for_completion=True, download_outputs=False
341
+ )
342
+
343
+ assert resp.status == "complete"
344
+ assert resp.downloaded_paths is None
345
+
346
+
347
+ @pytest.mark.asyncio
348
+ async def test_async_check_result_download_outputs(
349
+ tmp_path: Path, mock_async_base_client: AsyncMock, monkeypatch: Any
350
+ ) -> None:
351
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
352
+
353
+ file_url = "https://example.com/file.mp3"
354
+ mock_async_base_client.request.return_value = DummyResponse(
355
+ status="complete",
356
+ download_url=file_url,
357
+ )
358
+
359
+ # Create a mock response for httpx
360
+ mock_request = httpx.Request("GET", "https://example.com/file.mp3")
361
+ mock_response = httpx.Response(200, content=b"fake mp3", request=mock_request)
362
+
363
+ # Mock the httpx.AsyncClient class
364
+ class MockAsyncClient:
365
+ def __init__(self):
366
+ pass
367
+
368
+ async def __aenter__(self) -> "MockAsyncClient":
369
+ return self
370
+
371
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
372
+ pass
373
+
374
+ async def get(self, url: str) -> httpx.Response:
375
+ return mock_response
376
+
377
+ monkeypatch.setattr(httpx, "AsyncClient", MockAsyncClient)
378
+
379
+ resp = await client.check_result(
380
+ id="xyz",
381
+ wait_for_completion=True,
382
+ download_outputs=True,
383
+ download_directory=str(tmp_path),
384
+ )
385
+
386
+ assert resp.status == "complete"
387
+ assert resp.downloaded_paths
388
+ saved_file = Path(resp.downloaded_paths[0])
389
+ assert saved_file.exists()
390
+ assert saved_file.read_bytes() == b"fake mp3"
391
+
392
+
393
+ @pytest.mark.asyncio
394
+ async def test_async_check_result_error_status(
395
+ mock_async_base_client: AsyncMock,
396
+ ) -> None:
397
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
398
+ mock_async_base_client.request.return_value = DummyResponse(
399
+ status="error", error="Boom!"
400
+ )
401
+
402
+ resp = await client.check_result(
403
+ id="err", wait_for_completion=True, download_outputs=False
404
+ )
405
+ assert resp.status == "error"
406
+ assert resp.error is not None
407
+ assert resp.error.message == "Boom!"
408
+ assert resp.downloaded_paths is None
409
+
410
+
411
+ @pytest.mark.asyncio
412
+ async def test_async_check_result_canceled_status(
413
+ mock_async_base_client: AsyncMock,
414
+ ) -> None:
415
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
416
+ mock_async_base_client.request.return_value = DummyResponse(status="canceled")
417
+
418
+ resp = await client.check_result(
419
+ id="cancel", wait_for_completion=True, download_outputs=False
420
+ )
421
+ assert resp.status == "canceled"
422
+ assert resp.downloaded_paths is None
423
+
424
+
425
+ @pytest.mark.asyncio
426
+ async def test_async_check_result_poll_interval_default(
427
+ mock_async_base_client: AsyncMock, monkeypatch: Any
428
+ ) -> None:
429
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
430
+
431
+ # First calls return queued, then complete
432
+ mock_async_base_client.request.side_effect = [
433
+ DummyResponse(status="queued"),
434
+ DummyResponse(status="complete"),
435
+ ]
436
+
437
+ # Mock time.sleep to track calls
438
+ sleep_calls: List[float] = []
439
+
440
+ def mock_sleep(seconds: float) -> None:
441
+ sleep_calls.append(seconds)
442
+
443
+ monkeypatch.setattr("time.sleep", mock_sleep)
444
+
445
+ resp = await client.check_result(
446
+ id="xyz", wait_for_completion=True, download_outputs=False
447
+ )
448
+
449
+ assert resp.status == "complete"
450
+ # Should have slept once with default interval (0.5)
451
+ assert len(sleep_calls) == 1
452
+ assert sleep_calls[0] == 0.5
453
+
454
+
455
+ @pytest.mark.asyncio
456
+ async def test_async_check_result_poll_interval_custom(
457
+ mock_async_base_client: AsyncMock, monkeypatch: Any
458
+ ) -> None:
459
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
460
+
461
+ # Set custom poll interval
462
+ monkeypatch.setenv("MAGIC_HOUR_POLL_INTERVAL", "2.0")
463
+
464
+ # First calls return queued, then complete
465
+ mock_async_base_client.request.side_effect = [
466
+ DummyResponse(status="queued"),
467
+ DummyResponse(status="complete"),
468
+ ]
469
+
470
+ # Mock time.sleep to track calls
471
+ sleep_calls: List[float] = []
472
+
473
+ def mock_sleep(seconds: float) -> None:
474
+ sleep_calls.append(seconds)
475
+
476
+ monkeypatch.setattr("time.sleep", mock_sleep)
477
+
478
+ resp = await client.check_result(
479
+ id="xyz", wait_for_completion=True, download_outputs=False
480
+ )
481
+
482
+ assert resp.status == "complete"
483
+ # Should have slept once with custom interval (2.0)
484
+ assert len(sleep_calls) == 1
485
+ assert sleep_calls[0] == 2.0
486
+
487
+
488
+ @pytest.mark.asyncio
489
+ async def test_async_check_result_poll_interval_multiple_polls(
490
+ mock_async_base_client: AsyncMock, monkeypatch: Any
491
+ ) -> None:
492
+ client = AsyncAudioProjectsClient(base_client=mock_async_base_client)
493
+
494
+ # Set custom poll interval
495
+ monkeypatch.setenv("MAGIC_HOUR_POLL_INTERVAL", "0.3")
496
+
497
+ # Multiple calls return queued before complete
498
+ mock_async_base_client.request.side_effect = [
499
+ DummyResponse(status="queued"),
500
+ DummyResponse(status="queued"),
501
+ DummyResponse(status="queued"),
502
+ DummyResponse(status="complete"),
503
+ ]
504
+
505
+ # Mock time.sleep to track calls
506
+ sleep_calls: List[float] = []
507
+
508
+ def mock_sleep(seconds: float) -> None:
509
+ sleep_calls.append(seconds)
510
+
511
+ monkeypatch.setattr("time.sleep", mock_sleep)
512
+
513
+ resp = await client.check_result(
514
+ id="xyz", wait_for_completion=True, download_outputs=False
515
+ )
516
+
517
+ assert resp.status == "complete"
518
+ # Should have slept 3 times with custom interval (0.3)
519
+ assert len(sleep_calls) == 3
520
+ assert all(sleep_time == 0.3 for sleep_time in sleep_calls)
@@ -0,0 +1,128 @@
1
+ # v1.auto_subtitle_generator
2
+
3
+ ## Module Functions
4
+
5
+ <!-- CUSTOM DOCS START -->
6
+
7
+ ### Auto Subtitle Generator Generate Workflow <a name="generate"></a>
8
+
9
+ The workflow performs the following action
10
+
11
+ 1. upload local assets to Magic Hour storage. So you can pass in a local path instead of having to upload files yourself
12
+ 2. trigger a generation
13
+ 3. poll for a completion status. This is configurable
14
+ 4. if success, download the output to local directory
15
+
16
+ > [!TIP]
17
+ > This is the recommended way to use the SDK unless you have specific needs where it is necessary to split up the actions.
18
+
19
+ #### Parameters
20
+
21
+ In Additional to the parameters listed in the `.create` section below, `.generate` introduces 3 new parameters:
22
+
23
+ - `wait_for_completion` (bool, default True): Whether to wait for the project to complete.
24
+ - `download_outputs` (bool, default True): Whether to download the generated files
25
+ - `download_directory` (str, optional): Directory to save downloaded files (defaults to current directory)
26
+
27
+ #### Synchronous Client
28
+
29
+ ```python
30
+ from magic_hour import Client
31
+ from os import getenv
32
+
33
+ client = Client(token=getenv("API_TOKEN"))
34
+ res = client.v1.auto_subtitle_generator.generate(
35
+ assets={"video_file_path": "/path/to/1234.mp4"},
36
+ end_seconds=15.0,
37
+ start_seconds=0.0,
38
+ style={},
39
+ name="Auto Subtitle video",
40
+ wait_for_completion=True,
41
+ download_outputs=True,
42
+ download_directory="outputs"
43
+ )
44
+ ```
45
+
46
+ #### Asynchronous Client
47
+
48
+ ```python
49
+ from magic_hour import AsyncClient
50
+ from os import getenv
51
+
52
+ client = AsyncClient(token=getenv("API_TOKEN"))
53
+ res = await client.v1.auto_subtitle_generator.generate(
54
+ assets={"video_file_path": "/path/to/1234.mp4"},
55
+ end_seconds=15.0,
56
+ start_seconds=0.0,
57
+ style={},
58
+ name="Auto Subtitle video",
59
+ wait_for_completion=True,
60
+ download_outputs=True,
61
+ download_directory="outputs"
62
+ )
63
+ ```
64
+
65
+ <!-- CUSTOM DOCS END -->
66
+
67
+ ### Auto Subtitle Generator <a name="create"></a>
68
+
69
+ Automatically generate subtitles for your video in multiple languages.
70
+
71
+ **API Endpoint**: `POST /v1/auto-subtitle-generator`
72
+
73
+ #### Parameters
74
+
75
+ | Parameter | Required | Description | Example |
76
+ | -------------------- | :------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
77
+ | `assets` | ✓ | Provide the assets for auto subtitle generator | `{"video_file_path": "api-assets/id/1234.mp4"}` |
78
+ | `└─ video_file_path` | ✓ | This is the video used to add subtitles. This value is either - a direct URL to the video file - `file_path` field from the response of the [upload urls API](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls). Please refer to the [Input File documentation](https://docs.magichour.ai/api-reference/files/generate-asset-upload-urls#input-file) to learn more. | `"api-assets/id/1234.mp4"` |
79
+ | `end_seconds` | ✓ | The end time of the input video in seconds. This value is used to trim the input video. The value must be greater than 0.1, and more than the start_seconds. | `15.0` |
80
+ | `start_seconds` | ✓ | The start time of the input video in seconds. This value is used to trim the input video. The value must be greater than 0. | `0.0` |
81
+ | `style` | ✓ | Style of the subtitle. At least one of `.style.template` or `.style.custom_config` must be provided. * If only `.style.template` is provided, default values for the template will be used. * If both are provided, the fields in `.style.custom_config` will be used to overwrite the fields in `.style.template`. * If only `.style.custom_config` is provided, then all fields in `.style.custom_config` will be used. To use custom config only, the following `custom_config` params are required: * `.style.custom_config.font` * `.style.custom_config.text_color` * `.style.custom_config.vertical_position` * `.style.custom_config.horizontal_position` | `{}` |
82
+ | `└─ custom_config` | ✗ | Custom subtitle configuration. | `{"font": "Noto Sans", "font_size": 24.0, "font_style": "normal", "highlighted_text_color": "#FFD700", "horizontal_position": "center", "stroke_color": "#000000", "stroke_width": 1.0, "text_color": "#FFFFFF", "vertical_position": "bottom"}` |
83
+ | `└─ template` | ✗ | Preset subtitle templates. Please visit https://magichour.ai/create/auto-subtitle-generator to see the style of the existing templates. | `"cinematic"` |
84
+ | `name` | ✗ | The name of video. This value is mainly used for your own identification of the video. | `"Auto Subtitle video"` |
85
+
86
+ #### Synchronous Client
87
+
88
+ ```python
89
+ from magic_hour import Client
90
+ from os import getenv
91
+
92
+ client = Client(token=getenv("API_TOKEN"))
93
+ res = client.v1.auto_subtitle_generator.create(
94
+ assets={"video_file_path": "api-assets/id/1234.mp4"},
95
+ end_seconds=15.0,
96
+ start_seconds=0.0,
97
+ style={},
98
+ name="Auto Subtitle video",
99
+ )
100
+ ```
101
+
102
+ #### Asynchronous Client
103
+
104
+ ```python
105
+ from magic_hour import AsyncClient
106
+ from os import getenv
107
+
108
+ client = AsyncClient(token=getenv("API_TOKEN"))
109
+ res = await client.v1.auto_subtitle_generator.create(
110
+ assets={"video_file_path": "api-assets/id/1234.mp4"},
111
+ end_seconds=15.0,
112
+ start_seconds=0.0,
113
+ style={},
114
+ name="Auto Subtitle video",
115
+ )
116
+ ```
117
+
118
+ #### Response
119
+
120
+ ##### Type
121
+
122
+ [V1AutoSubtitleGeneratorCreateResponse](/magic_hour/types/models/v1_auto_subtitle_generator_create_response.py)
123
+
124
+ ##### Example
125
+
126
+ ```python
127
+ {"credits_charged": 450, "estimated_frame_cost": 450, "id": "cuid-example"}
128
+ ```
@@ -0,0 +1,4 @@
1
+ from .client import AsyncAutoSubtitleGeneratorClient, AutoSubtitleGeneratorClient
2
+
3
+
4
+ __all__ = ["AsyncAutoSubtitleGeneratorClient", "AutoSubtitleGeneratorClient"]