cutan 0.0.2__tar.gz

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 (324) hide show
  1. cutan-0.0.2/.gitattributes +1 -0
  2. cutan-0.0.2/.gitignore +176 -0
  3. cutan-0.0.2/CLAUDE.md +42 -0
  4. cutan-0.0.2/LICENSE +21 -0
  5. cutan-0.0.2/PKG-INFO +64 -0
  6. cutan-0.0.2/README.md +35 -0
  7. cutan-0.0.2/conftest.py +265 -0
  8. cutan-0.0.2/cutan/__init__.py +80 -0
  9. cutan-0.0.2/cutan/audio/__init__.py +50 -0
  10. cutan-0.0.2/cutan/audio/injectable_lipsync.py +123 -0
  11. cutan-0.0.2/cutan/audio/offline_lipsync.py +126 -0
  12. cutan-0.0.2/cutan/audio/rhubarb_lipsync.py +152 -0
  13. cutan-0.0.2/cutan/audio/whisper_lipsync.py +143 -0
  14. cutan-0.0.2/cutan/bench.py +244 -0
  15. cutan-0.0.2/cutan/characters/__init__.py +125 -0
  16. cutan-0.0.2/cutan/characters/brows.py +504 -0
  17. cutan-0.0.2/cutan/characters/checks.py +610 -0
  18. cutan-0.0.2/cutan/characters/cli.py +629 -0
  19. cutan-0.0.2/cutan/characters/colour_roles.py +176 -0
  20. cutan-0.0.2/cutan/characters/dicebear.py +238 -0
  21. cutan-0.0.2/cutan/characters/drawn.py +94 -0
  22. cutan-0.0.2/cutan/characters/factory.py +2237 -0
  23. cutan-0.0.2/cutan/characters/idle.py +220 -0
  24. cutan-0.0.2/cutan/characters/licenses.py +385 -0
  25. cutan-0.0.2/cutan/characters/methods.py +708 -0
  26. cutan-0.0.2/cutan/characters/mouth_set.py +235 -0
  27. cutan-0.0.2/cutan/characters/play.py +1244 -0
  28. cutan-0.0.2/cutan/characters/promote.py +189 -0
  29. cutan-0.0.2/cutan/characters/record.py +167 -0
  30. cutan-0.0.2/cutan/characters/registration.py +239 -0
  31. cutan-0.0.2/cutan/characters/schema.py +921 -0
  32. cutan-0.0.2/cutan/characters/silhouette.py +155 -0
  33. cutan-0.0.2/cutan/characters/svg_utils.py +346 -0
  34. cutan-0.0.2/cutan/characters/validate.py +946 -0
  35. cutan-0.0.2/cutan/characters/vocabulary.py +194 -0
  36. cutan-0.0.2/cutan/compile/__init__.py +1 -0
  37. cutan-0.0.2/cutan/compile/coarticulate.py +445 -0
  38. cutan-0.0.2/cutan/compile/gaze.py +147 -0
  39. cutan-0.0.2/cutan/compile/lowering.py +109 -0
  40. cutan-0.0.2/cutan/compile/passes.py +2581 -0
  41. cutan-0.0.2/cutan/conftest.py +8 -0
  42. cutan-0.0.2/cutan/data/dicebear_9x_styles.json +38 -0
  43. cutan-0.0.2/cutan/expression/__init__.py +80 -0
  44. cutan-0.0.2/cutan/expression/axes.py +122 -0
  45. cutan-0.0.2/cutan/expression/binding.py +326 -0
  46. cutan-0.0.2/cutan/expression/blendshapes.py +125 -0
  47. cutan-0.0.2/cutan/expression/presets.py +203 -0
  48. cutan-0.0.2/cutan/expression/provider.py +210 -0
  49. cutan-0.0.2/cutan/expression/registration.py +187 -0
  50. cutan-0.0.2/cutan/genre.py +220 -0
  51. cutan-0.0.2/cutan/impacts/__init__.py +89 -0
  52. cutan-0.0.2/cutan/impacts/cli.py +166 -0
  53. cutan-0.0.2/cutan/impacts/clip.py +603 -0
  54. cutan-0.0.2/cutan/impacts/objects.py +255 -0
  55. cutan-0.0.2/cutan/impacts/performance.py +257 -0
  56. cutan-0.0.2/cutan/impacts/stroke.py +252 -0
  57. cutan-0.0.2/cutan/impacts/truth.py +368 -0
  58. cutan-0.0.2/cutan/library.py +318 -0
  59. cutan-0.0.2/cutan/nw.py +58 -0
  60. cutan-0.0.2/cutan/runtime/__init__.py +1 -0
  61. cutan-0.0.2/cutan/runtime/visuals.js +129 -0
  62. cutan-0.0.2/cutan/verify/__init__.py +1 -0
  63. cutan-0.0.2/cutan/verify/style.py +1194 -0
  64. cutan-0.0.2/examples/character_gallery/.gitignore +4 -0
  65. cutan-0.0.2/examples/character_gallery/README.md +70 -0
  66. cutan-0.0.2/examples/character_gallery/build.py +276 -0
  67. cutan-0.0.2/examples/character_gallery/cartoon/.gitignore +6 -0
  68. cutan-0.0.2/examples/character_gallery/cartoon/scene.md +66 -0
  69. cutan-0.0.2/examples/character_gallery/videos/cartoon.mp4 +0 -0
  70. cutan-0.0.2/examples/park_bench_cartoon/README.md +43 -0
  71. cutan-0.0.2/examples/park_bench_cartoon/ir/scene.json +121 -0
  72. cutan-0.0.2/examples/park_bench_cartoon/scene.md +66 -0
  73. cutan-0.0.2/examples/promote_demo/README.md +81 -0
  74. cutan-0.0.2/examples/promote_demo/assets/characters/raw_maya/raw_maya.svg +88 -0
  75. cutan-0.0.2/examples/promote_demo/build.py +51 -0
  76. cutan-0.0.2/examples/promote_demo/ir/scene.json +64 -0
  77. cutan-0.0.2/examples/promote_demo/scene.md +39 -0
  78. cutan-0.0.2/examples/single_character/README.md +11 -0
  79. cutan-0.0.2/examples/single_character/ir/scene.json +102 -0
  80. cutan-0.0.2/examples/single_character/scene.md +29 -0
  81. cutan-0.0.2/examples/walk_demo/ir/scene.json +204 -0
  82. cutan-0.0.2/examples/walk_demo/scene.md +82 -0
  83. cutan-0.0.2/misc/CHANGELOG.md +6 -0
  84. cutan-0.0.2/misc/bench/corpus/aa_probe/assets/characters/probe-rig/character.json +10 -0
  85. cutan-0.0.2/misc/bench/corpus/aa_probe/scene.md +49 -0
  86. cutan-0.0.2/misc/bench/corpus/dialogue/assets/characters/talker-rig/character.json +10 -0
  87. cutan-0.0.2/misc/bench/corpus/dialogue/ir/scene.json +163 -0
  88. cutan-0.0.2/misc/bench/corpus/dialogue/scene.md +36 -0
  89. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/character.json +822 -0
  90. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/face.svg +5 -0
  91. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/arm_l.svg +2 -0
  92. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/arm_r.svg +2 -0
  93. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/brow_l.svg +2 -0
  94. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/brow_r.svg +2 -0
  95. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/eye_l_closed.svg +2 -0
  96. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/eye_l_open.svg +2 -0
  97. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/eye_r_closed.svg +2 -0
  98. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/eye_r_open.svg +2 -0
  99. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/head.svg +5 -0
  100. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/leg_l.svg +2 -0
  101. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/leg_r.svg +2 -0
  102. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_a.svg +2 -0
  103. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_a_happy.svg +2 -0
  104. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_a_sad.svg +2 -0
  105. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_b.svg +2 -0
  106. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_b_happy.svg +2 -0
  107. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_b_sad.svg +2 -0
  108. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_c.svg +2 -0
  109. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_c_happy.svg +2 -0
  110. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_c_sad.svg +2 -0
  111. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_d.svg +2 -0
  112. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_d_happy.svg +2 -0
  113. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_d_sad.svg +2 -0
  114. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_e.svg +2 -0
  115. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_e_happy.svg +2 -0
  116. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_e_sad.svg +2 -0
  117. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_f.svg +2 -0
  118. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_f_happy.svg +2 -0
  119. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_f_sad.svg +2 -0
  120. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_g.svg +2 -0
  121. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_g_happy.svg +2 -0
  122. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_g_sad.svg +2 -0
  123. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_h.svg +2 -0
  124. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_h_happy.svg +2 -0
  125. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_h_sad.svg +2 -0
  126. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_x.svg +2 -0
  127. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_x_happy.svg +2 -0
  128. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/mouth/mouth_x_sad.svg +2 -0
  129. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/pupil_l.svg +2 -0
  130. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/pupil_r.svg +2 -0
  131. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/sclera_l.svg +2 -0
  132. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/sclera_r.svg +2 -0
  133. cutan-0.0.2/misc/bench/corpus/expressions/assets/characters/face/parts/torso.svg +2 -0
  134. cutan-0.0.2/misc/bench/corpus/expressions/scene.md +212 -0
  135. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/character.json +465 -0
  136. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/arm_l.svg +1 -0
  137. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/arm_r.svg +1 -0
  138. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/brow_l.svg +1 -0
  139. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/brow_r.svg +1 -0
  140. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/eye_l_open.svg +1 -0
  141. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/eye_r_open.svg +1 -0
  142. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/head.svg +1 -0
  143. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/leg_l.svg +1 -0
  144. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/leg_r.svg +1 -0
  145. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/mouth/mouth_a.svg +1 -0
  146. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/mouth/mouth_b.svg +1 -0
  147. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/mouth/mouth_c.svg +1 -0
  148. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/mouth/mouth_d.svg +1 -0
  149. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/mouth/mouth_e.svg +1 -0
  150. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/mouth/mouth_f.svg +1 -0
  151. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/mouth/mouth_g.svg +1 -0
  152. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/mouth/mouth_h.svg +1 -0
  153. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/mouth/mouth_x.svg +1 -0
  154. cutan-0.0.2/misc/bench/corpus/graded_field/assets/characters/graded-field-rig/parts/torso.svg +1 -0
  155. cutan-0.0.2/misc/bench/corpus/graded_field/scene.md +60 -0
  156. cutan-0.0.2/misc/bench/corpus/multi_shot/assets/characters/ada-rig/character.json +10 -0
  157. cutan-0.0.2/misc/bench/corpus/multi_shot/scene.md +64 -0
  158. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/character.json +465 -0
  159. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/arm_l.svg +1 -0
  160. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/arm_r.svg +1 -0
  161. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/brow_l.svg +1 -0
  162. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/brow_r.svg +1 -0
  163. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/eye_l_open.svg +1 -0
  164. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/eye_r_open.svg +1 -0
  165. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/head.svg +1 -0
  166. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/leg_l.svg +1 -0
  167. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/leg_r.svg +1 -0
  168. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/mouth/mouth_a.svg +1 -0
  169. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/mouth/mouth_b.svg +1 -0
  170. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/mouth/mouth_c.svg +1 -0
  171. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/mouth/mouth_d.svg +1 -0
  172. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/mouth/mouth_e.svg +1 -0
  173. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/mouth/mouth_f.svg +1 -0
  174. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/mouth/mouth_g.svg +1 -0
  175. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/mouth/mouth_h.svg +1 -0
  176. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/mouth/mouth_x.svg +1 -0
  177. cutan-0.0.2/misc/bench/corpus/saturated_outline/assets/characters/saturated-rig/parts/torso.svg +1 -0
  178. cutan-0.0.2/misc/bench/corpus/saturated_outline/scene.md +34 -0
  179. cutan-0.0.2/misc/bench/golden/aa_probe/bless-chromium140.0.7339.16.json +46 -0
  180. cutan-0.0.2/misc/bench/golden/aa_probe/f0000-chromium140.0.7339.16.png +0 -0
  181. cutan-0.0.2/misc/bench/golden/aa_probe/f0006-chromium140.0.7339.16.png +0 -0
  182. cutan-0.0.2/misc/bench/golden/dialogue/bless-chromium140.0.7339.16.json +46 -0
  183. cutan-0.0.2/misc/bench/golden/dialogue/f0000-chromium140.0.7339.16.png +0 -0
  184. cutan-0.0.2/misc/bench/golden/dialogue/f0014-chromium140.0.7339.16.png +0 -0
  185. cutan-0.0.2/misc/bench/golden/expressions/bless-chromium140.0.7339.16.json +113 -0
  186. cutan-0.0.2/misc/bench/golden/expressions/f0003-chromium140.0.7339.16.png +0 -0
  187. cutan-0.0.2/misc/bench/golden/expressions/f0009-chromium140.0.7339.16.png +0 -0
  188. cutan-0.0.2/misc/bench/golden/expressions/f0015-chromium140.0.7339.16.png +0 -0
  189. cutan-0.0.2/misc/bench/golden/expressions/f0021-chromium140.0.7339.16.png +0 -0
  190. cutan-0.0.2/misc/bench/golden/expressions/f0027-chromium140.0.7339.16.png +0 -0
  191. cutan-0.0.2/misc/bench/golden/expressions/f0033-chromium140.0.7339.16.png +0 -0
  192. cutan-0.0.2/misc/bench/golden/expressions/f0039-chromium140.0.7339.16.png +0 -0
  193. cutan-0.0.2/misc/bench/golden/expressions/f0045-chromium140.0.7339.16.png +0 -0
  194. cutan-0.0.2/misc/bench/golden/graded_field/bless-chromium140.0.7339.16.json +46 -0
  195. cutan-0.0.2/misc/bench/golden/graded_field/f0000-chromium140.0.7339.16.png +0 -0
  196. cutan-0.0.2/misc/bench/golden/graded_field/f0004-chromium140.0.7339.16.png +0 -0
  197. cutan-0.0.2/misc/bench/golden/multi_shot/bless-chromium140.0.7339.16.json +47 -0
  198. cutan-0.0.2/misc/bench/golden/multi_shot/f0000-chromium140.0.7339.16.png +0 -0
  199. cutan-0.0.2/misc/bench/golden/multi_shot/f0006-chromium140.0.7339.16.png +0 -0
  200. cutan-0.0.2/misc/bench/golden/promote_demo/bless-chromium140.0.7339.16.json +46 -0
  201. cutan-0.0.2/misc/bench/golden/promote_demo/f0000-chromium140.0.7339.16.png +0 -0
  202. cutan-0.0.2/misc/bench/golden/promote_demo/f0070-chromium140.0.7339.16.png +0 -0
  203. cutan-0.0.2/misc/bench/golden/saturated_outline/bless-chromium140.0.7339.16.json +46 -0
  204. cutan-0.0.2/misc/bench/golden/saturated_outline/f0000-chromium140.0.7339.16.png +0 -0
  205. cutan-0.0.2/misc/bench/golden/saturated_outline/f0006-chromium140.0.7339.16.png +0 -0
  206. cutan-0.0.2/misc/bench/golden/single_character/bless-chromium140.0.7339.16.json +46 -0
  207. cutan-0.0.2/misc/bench/golden/single_character/f0000-chromium140.0.7339.16.png +0 -0
  208. cutan-0.0.2/misc/bench/golden/single_character/f0024-chromium140.0.7339.16.png +0 -0
  209. cutan-0.0.2/misc/demos/README.md +56 -0
  210. cutan-0.0.2/misc/demos/build_demos.py +3283 -0
  211. cutan-0.0.2/pyproject.toml +200 -0
  212. cutan-0.0.2/tests/__init__.py +0 -0
  213. cutan-0.0.2/tests/_bench_compat.py +24 -0
  214. cutan-0.0.2/tests/_child_guard/sitecustomize.py +101 -0
  215. cutan-0.0.2/tests/_emotion_frames.py +63 -0
  216. cutan-0.0.2/tests/_fake_subprocess.py +135 -0
  217. cutan-0.0.2/tests/_lipsync_strips.py +81 -0
  218. cutan-0.0.2/tests/_node.py +117 -0
  219. cutan-0.0.2/tests/_render_seam.py +97 -0
  220. cutan-0.0.2/tests/_support.py +60 -0
  221. cutan-0.0.2/tests/_vision_cassettes.py +144 -0
  222. cutan-0.0.2/tests/conftest.py +1002 -0
  223. cutan-0.0.2/tests/fixtures/art/invisible_head.svg +19 -0
  224. cutan-0.0.2/tests/fixtures/characters/gale/character.json +594 -0
  225. cutan-0.0.2/tests/fixtures/characters/gale/parts/arm_l.svg +1 -0
  226. cutan-0.0.2/tests/fixtures/characters/gale/parts/arm_r.svg +1 -0
  227. cutan-0.0.2/tests/fixtures/characters/gale/parts/brow_l.svg +1 -0
  228. cutan-0.0.2/tests/fixtures/characters/gale/parts/brow_r.svg +1 -0
  229. cutan-0.0.2/tests/fixtures/characters/gale/parts/eye_l_closed.svg +1 -0
  230. cutan-0.0.2/tests/fixtures/characters/gale/parts/eye_l_open.svg +1 -0
  231. cutan-0.0.2/tests/fixtures/characters/gale/parts/eye_r_closed.svg +1 -0
  232. cutan-0.0.2/tests/fixtures/characters/gale/parts/eye_r_open.svg +1 -0
  233. cutan-0.0.2/tests/fixtures/characters/gale/parts/hand_fist.svg +1 -0
  234. cutan-0.0.2/tests/fixtures/characters/gale/parts/hand_palm.svg +1 -0
  235. cutan-0.0.2/tests/fixtures/characters/gale/parts/hand_point.svg +1 -0
  236. cutan-0.0.2/tests/fixtures/characters/gale/parts/head.svg +1 -0
  237. cutan-0.0.2/tests/fixtures/characters/gale/parts/leg_l.svg +1 -0
  238. cutan-0.0.2/tests/fixtures/characters/gale/parts/leg_r.svg +1 -0
  239. cutan-0.0.2/tests/fixtures/characters/gale/parts/mouth/mouth_a.svg +1 -0
  240. cutan-0.0.2/tests/fixtures/characters/gale/parts/mouth/mouth_b.svg +1 -0
  241. cutan-0.0.2/tests/fixtures/characters/gale/parts/mouth/mouth_c.svg +1 -0
  242. cutan-0.0.2/tests/fixtures/characters/gale/parts/mouth/mouth_d.svg +1 -0
  243. cutan-0.0.2/tests/fixtures/characters/gale/parts/mouth/mouth_e.svg +1 -0
  244. cutan-0.0.2/tests/fixtures/characters/gale/parts/mouth/mouth_f.svg +1 -0
  245. cutan-0.0.2/tests/fixtures/characters/gale/parts/mouth/mouth_g.svg +1 -0
  246. cutan-0.0.2/tests/fixtures/characters/gale/parts/mouth/mouth_h.svg +1 -0
  247. cutan-0.0.2/tests/fixtures/characters/gale/parts/mouth/mouth_x.svg +1 -0
  248. cutan-0.0.2/tests/fixtures/characters/gale/parts/torso.svg +1 -0
  249. cutan-0.0.2/tests/fixtures/characters/gale/parts/torso_left.svg +1 -0
  250. cutan-0.0.2/tests/fixtures/characters/gale/parts/torso_right.svg +1 -0
  251. cutan-0.0.2/tests/fixtures/runtime_visual_demo.js +9 -0
  252. cutan-0.0.2/tests/fixtures/vision_frames/emotions/afraid.png +0 -0
  253. cutan-0.0.2/tests/fixtures/vision_frames/emotions/angry.png +0 -0
  254. cutan-0.0.2/tests/fixtures/vision_frames/emotions/happy.png +0 -0
  255. cutan-0.0.2/tests/fixtures/vision_frames/emotions/neutral.png +0 -0
  256. cutan-0.0.2/tests/fixtures/vision_frames/emotions/sad.png +0 -0
  257. cutan-0.0.2/tests/fixtures/vision_frames/emotions/skeptical.png +0 -0
  258. cutan-0.0.2/tests/fixtures/vision_frames/emotions/surprised.png +0 -0
  259. cutan-0.0.2/tests/fixtures/vision_frames/emotions/thinking.png +0 -0
  260. cutan-0.0.2/tests/fixtures/vision_frames/lipsync/after/f0_frame001.png +0 -0
  261. cutan-0.0.2/tests/fixtures/vision_frames/lipsync/after/f1_frame004.png +0 -0
  262. cutan-0.0.2/tests/fixtures/vision_frames/lipsync/after/f2_frame007.png +0 -0
  263. cutan-0.0.2/tests/fixtures/vision_frames/lipsync/after/f3_frame010.png +0 -0
  264. cutan-0.0.2/tests/fixtures/vision_frames/lipsync/after/f4_frame013.png +0 -0
  265. cutan-0.0.2/tests/fixtures/vision_frames/lipsync/after/f5_frame016.png +0 -0
  266. cutan-0.0.2/tests/fixtures/vision_frames/lipsync/after/f6_frame019.png +0 -0
  267. cutan-0.0.2/tests/fixtures/vision_frames/lipsync/after/f7_frame022.png +0 -0
  268. cutan-0.0.2/tests/fixtures/vision_frames/lipsync/before/f0_frame001.png +0 -0
  269. cutan-0.0.2/tests/fixtures/vision_frames/lipsync/before/f1_frame004.png +0 -0
  270. cutan-0.0.2/tests/fixtures/vision_frames/lipsync/before/f2_frame007.png +0 -0
  271. cutan-0.0.2/tests/fixtures/vision_frames/lipsync/before/f3_frame010.png +0 -0
  272. cutan-0.0.2/tests/fixtures/vision_frames/lipsync/before/f4_frame013.png +0 -0
  273. cutan-0.0.2/tests/fixtures/vision_frames/lipsync/before/f5_frame016.png +0 -0
  274. cutan-0.0.2/tests/fixtures/vision_frames/lipsync/before/f6_frame019.png +0 -0
  275. cutan-0.0.2/tests/fixtures/vision_frames/lipsync/before/f7_frame022.png +0 -0
  276. cutan-0.0.2/tests/test_art_contract.py +242 -0
  277. cutan-0.0.2/tests/test_bench_capture.py +283 -0
  278. cutan-0.0.2/tests/test_bench_corpus.py +483 -0
  279. cutan-0.0.2/tests/test_blinks.py +489 -0
  280. cutan-0.0.2/tests/test_brow_acting.py +405 -0
  281. cutan-0.0.2/tests/test_carved_art_gaps.py +563 -0
  282. cutan-0.0.2/tests/test_character_variety.py +460 -0
  283. cutan-0.0.2/tests/test_character_voices.py +150 -0
  284. cutan-0.0.2/tests/test_characters.py +545 -0
  285. cutan-0.0.2/tests/test_coarticulation.py +524 -0
  286. cutan-0.0.2/tests/test_cutout_compile_visemes.py +217 -0
  287. cutan-0.0.2/tests/test_cutout_render.py +216 -0
  288. cutan-0.0.2/tests/test_demo_framing.py +109 -0
  289. cutan-0.0.2/tests/test_demo_gallery_contracts.py +53 -0
  290. cutan-0.0.2/tests/test_dialogue_emotion.py +72 -0
  291. cutan-0.0.2/tests/test_emotion_gaze_render.py +111 -0
  292. cutan-0.0.2/tests/test_emotion_judge.py +143 -0
  293. cutan-0.0.2/tests/test_examples_spend_gate.py +282 -0
  294. cutan-0.0.2/tests/test_expression.py +243 -0
  295. cutan-0.0.2/tests/test_expression_compose.py +661 -0
  296. cutan-0.0.2/tests/test_expression_goldens.py +68 -0
  297. cutan-0.0.2/tests/test_gaze.py +442 -0
  298. cutan-0.0.2/tests/test_impacts.py +498 -0
  299. cutan-0.0.2/tests/test_injectable_lipsync.py +107 -0
  300. cutan-0.0.2/tests/test_methods_compile.py +429 -0
  301. cutan-0.0.2/tests/test_multi_character_render.py +113 -0
  302. cutan-0.0.2/tests/test_nw.py +55 -0
  303. cutan-0.0.2/tests/test_offline_lipsync.py +68 -0
  304. cutan-0.0.2/tests/test_placeholder_limb_pivot.py +119 -0
  305. cutan-0.0.2/tests/test_play.py +507 -0
  306. cutan-0.0.2/tests/test_play_extent.py +219 -0
  307. cutan-0.0.2/tests/test_play_presets.py +445 -0
  308. cutan-0.0.2/tests/test_provider_swap.py +130 -0
  309. cutan-0.0.2/tests/test_pure_pose.py +409 -0
  310. cutan-0.0.2/tests/test_rhubarb_lipsync.py +33 -0
  311. cutan-0.0.2/tests/test_rhubarb_recognizer.py +190 -0
  312. cutan-0.0.2/tests/test_rig_builder_extraction.py +200 -0
  313. cutan-0.0.2/tests/test_rig_fidelity.py +223 -0
  314. cutan-0.0.2/tests/test_rig_layout.py +138 -0
  315. cutan-0.0.2/tests/test_scaffold.py +41 -0
  316. cutan-0.0.2/tests/test_skill_rig_paths.py +102 -0
  317. cutan-0.0.2/tests/test_style_lint.py +379 -0
  318. cutan-0.0.2/tests/test_style_specs.py +404 -0
  319. cutan-0.0.2/tests/test_swap_channels.py +965 -0
  320. cutan-0.0.2/tests/test_turnaround.py +751 -0
  321. cutan-0.0.2/tests/test_validate_whole_character_swaps.py +110 -0
  322. cutan-0.0.2/tests/test_walk_preset.py +248 -0
  323. cutan-0.0.2/tests/test_whisper_lipsync.py +100 -0
  324. cutan-0.0.2/tests/test_word_timings.py +215 -0
@@ -0,0 +1 @@
1
+ *.ipynb linguist-documentation
cutan-0.0.2/.gitignore ADDED
@@ -0,0 +1,176 @@
1
+ .claude/handoffs/
2
+ .claude/scratch/
3
+
4
+ # Byte-compiled / optimized / DLL files
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+
9
+
10
+ .DS_Store
11
+ # C extensions
12
+ *.so
13
+
14
+ # TLS certificates
15
+ ## Ignore all PEM files anywhere
16
+ *.pem
17
+ ## Also ignore any certs directory
18
+ certs/
19
+
20
+ # Distribution / packaging
21
+ .Python
22
+ build/
23
+ develop-eggs/
24
+ dist/
25
+ downloads/
26
+ eggs/
27
+ .eggs/
28
+ lib/
29
+ lib64/
30
+ # ANCHORED. Unanchored, `parts/` matches at any depth — and this repo's
31
+ # character rigs keep their SVGs in `assets/characters/<ref>/parts/`, so the
32
+ # template's buildout rule silently excluded every one of them (measured:
33
+ # `git ls-files | grep parts/` returned ZERO). That is the an#33 failure shape
34
+ # exactly: a fixture that renders from a build product nobody tracks, and a
35
+ # clean checkout that draws a different picture with no error. Buildout's
36
+ # directory is at the repo root, so anchoring loses nothing.
37
+ /parts/
38
+ sdist/
39
+ var/
40
+ wheels/
41
+ *.egg-info/
42
+ .installed.cfg
43
+ *.egg
44
+ MANIFEST
45
+ _build
46
+
47
+ # PyInstaller
48
+ # Usually these files are written by a python script from a template
49
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
50
+ *.manifest
51
+ *.spec
52
+
53
+ # Installer logs
54
+ pip-log.txt
55
+ pip-delete-this-directory.txt
56
+
57
+ # Unit test / coverage reports
58
+ htmlcov/
59
+ .tox/
60
+ .coverage
61
+ .coverage.*
62
+ .cache
63
+ nosetests.xml
64
+ coverage.xml
65
+ *.cover
66
+ .hypothesis/
67
+ .pytest_cache/
68
+
69
+ # Translations
70
+ *.mo
71
+ *.pot
72
+
73
+ # Django stuff:
74
+ *.log
75
+ local_settings.py
76
+ db.sqlite3
77
+
78
+ # Flask stuff:
79
+ instance/
80
+ .webassets-cache
81
+
82
+ # Scrapy stuff:
83
+ .scrapy
84
+
85
+ # Sphinx documentation
86
+ docs/_build/
87
+ docs/*
88
+
89
+ # PyBuilder
90
+ target/
91
+
92
+ # Jupyter Notebook
93
+ .ipynb_checkpoints
94
+
95
+ # pyenv
96
+ .python-version
97
+
98
+ # celery beat schedule file
99
+ celerybeat-schedule
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+
126
+ # PyCharm
127
+ .idea
128
+
129
+ # an per-project ephemera (renders, intermediates, agent state)
130
+ examples/*/output/
131
+ examples/*/artifacts/
132
+ examples/*/assets/
133
+ examples/*/.an/
134
+ examples/*/.anima/
135
+ examples/*/an.toml
136
+ examples/*/anima.toml
137
+
138
+ # The promote_demo example's input SVG is the whole point of the example
139
+ # — keep it tracked even though examples/*/assets/ is otherwise ignored.
140
+ !examples/promote_demo/assets/
141
+ !examples/promote_demo/assets/characters/
142
+ !examples/promote_demo/assets/characters/raw_maya/
143
+ !examples/promote_demo/assets/characters/raw_maya/*.svg
144
+ # But re-exclude the promote target — it's regenerated by build.py.
145
+ examples/promote_demo/assets/characters/maya-promoted/
146
+
147
+ # Agent worktrees (adversarial reviewers run isolated here so they cannot
148
+ # mutate the shared checkout). Never committed, and excluded from tooling
149
+ # below or ruff/pytest walk three extra copies of the whole repo.
150
+ .claude/worktrees/
151
+
152
+ # Session-boundary handoffs. Local working notes, not project docs — anything
153
+ # that should outlive a session belongs in an issue or misc/docs/ instead.
154
+ .claude/handoffs/
155
+
156
+ # an bench corpus (an#38): the fixtures are COMMITTED WHOLE — their pixels have
157
+ # to be a function of the repo alone — so only the per-render ephemera is
158
+ # ignored here. Note there is deliberately no `assets/` rule: that is what
159
+ # `examples/*/assets/` has, and it is why the corpus does not live there.
160
+ misc/bench/corpus/*/output/
161
+ misc/bench/corpus/*/artifacts/
162
+ misc/bench/corpus/*/.an/
163
+ misc/bench/corpus/*/ir/
164
+ # …except `dialogue`'s IR, which is the fixture: the bench renders with
165
+ # `auto_audio=False`, so the visemes a speaking scene needs must already be in
166
+ # its committed `ir/scene.json` (offline provider, deterministic), exactly as
167
+ # `examples/single_character/ir/scene.json` does. Without this carve-out the
168
+ # scene would be mute on a clean checkout and its "mid-line" golden a lie (an#96).
169
+ !misc/bench/corpus/dialogue/ir/
170
+ !misc/bench/corpus/dialogue/ir/scene.json
171
+
172
+ # `misc/demos/build_demos.py`'s output. Regenerable in a couple of minutes from
173
+ # the committed script, and the media is published as release assets rather than
174
+ # committed — `misc/` ships in the sdist, so a megabyte of GIFs would ride along
175
+ # with every `pip install`.
176
+ misc/demos/out/
cutan-0.0.2/CLAUDE.md ADDED
@@ -0,0 +1,42 @@
1
+ # Working in the cutan repo
2
+
3
+ `cutan` is the cut-out animation genre of [`an`](https://github.com/thorwhalen/an), split out of it per ADR 0001 (an#225, epic an#231). It depends on `an`; `an` never imports it (except through the registered hooks, `an.genres`).
4
+
5
+ ## Read first
6
+
7
+ - `an`'s `CLAUDE.md` and `misc/docs/architecture_as_built.md` (§0a: core and genre, the seams this package plugs into). Decisions and the plan: `misc/docs/adr/0001-core-genre-split.md` and `misc/docs/plan_core_and_cutan_2026-10.md` in `an`.
8
+ - This repo's skills (`.claude/skills/`): `cutan` (downstream: authoring cut-out scenes), `cutan-style`, `cutan-art-package`, and `cutan-dev-{expression,lipsync,rig-contract,swap-channels}` for the areas of the same names. Read the matching `cutan-dev-*` before touching a face, lip-sync, the rig contract or swap channels; for a pixel, an encode flag or the camera, the `an-dev-render-pipeline` and `an-dev-stage` skills in `an`.
9
+
10
+ ## Layout
11
+
12
+ ```
13
+ cutan/genre.py CUTOUT, the Genre object (entry point cutout_animation = "cutan.genre:CUTOUT"); nw.py: the nw declaration
14
+ cutan/characters/ descriptor schema (CharacterDescriptor), factory, play resolution, methods, vocabulary, checks, CLI
15
+ cutan/expression/ expression presets, axes, bindings, the face provider
16
+ cutan/compile/ passes.py (the cut-out passes over an.stage.compile), lowering.py (play/expression lowering, swap declaration), coarticulate.py, gaze.py
17
+ cutan/audio/ lip-sync providers (offline, rhubarb, whisper, word timings)
18
+ cutan/impacts/ impact choreography and ground truth; cutan/verify/style.py: the style lint; cutan/library.py: the character analyser
19
+ cutan/runtime/ visuals.js: the procedural mouth and eye, registered through the stage runtime's anRegisterVisual
20
+ cutan/bench.py CUTOUT_FIXTURES and run_bench() over this repo's misc/bench/ corpus
21
+ tests/ examples/ misc/bench/{corpus,golden}/ misc/demos/ .claude/skills/
22
+ ```
23
+
24
+ ## Rules
25
+
26
+ - **Persisted identifiers never change** (`cutan/__init__.py` lists them): `renderer: cutout`, the genre slug `cutout_animation`, the `CharacterDescriptor` document kind and its `schema_version`, the `character` asset kind and `characters` store, the action kinds `play` and `expression`, the compiled document's wire shape, the provenance provider string `"an character factory"` (licence `cc0-1.0`), the lip-sync provider names, the swap set names (`viseme`, `eyelid`, `view`, `hands`, `body_facing`) and the visual kinds `mouth` and `eye`.
27
+ - **The pixel gate stays with the core.** The corpus contract hashes (`tests/test_expression_compose.py`) and the blessed golden frames (`misc/bench/golden/`) must not move without a bless record that says why. A PR here that can change a pixel is exercised against `an` main in CI. Never write that a rendering behaviour is "verified in CI" without saying where.
28
+ - **Import `an` freely; `an` imports nothing of `cutan`.** If the core needs something from this package, add a registry or hook in `an` first (a service, `ActionKind.lowering`, `EntityKind.swap_declaration`, a compile pass, a runtime script). The compile passes may import `an.stage.compile` helpers (underscore names included): a genre depending on the core is allowed.
29
+ - **A change to any module here changes the core's shot-cache key** (`an.stage.cache_key.genre_code_modules` keys every module of this package), so a cut-out shot re-renders after an upgrade. That is intended.
30
+ - `cutan.REQUIRED_AN_API_LEVEL` is the lowest `an.genres.API_LEVEL` this package runs against (instead of a version pin: `an`'s version is assigned by CI at merge). Raise it in the same change that uses a new seam.
31
+ - Local packages carry no version pins: `an`, not `an>=…`.
32
+ - One `CLI`: `cutan` adds no console script. Its commands are `an character …` and `an impacts …`.
33
+ - Shims in `an` (`an.characters`, `an.expression`, `an.impacts`, `an.audio.*_lipsync`, `an.verify.style`, `an.library.character`, `an.genres.cutout`, `an.genre`, `an.adapters.cutout.{coarticulate,gaze}`) are removed when no package under `$PP` imports an old path and 14 days have passed (checked by a script, not by memory).
34
+ - Decisions of the move (the lead's, 2026-10-02): the bench *runner* stays in `an` (generic); skill names `cutan-style` and `cutan-dev-*`, with stubs of the old names left in `an` for one cycle; `an.genres.API_LEVEL` instead of a version floor.
35
+
36
+ ## Tests
37
+
38
+ `pytest` from the repo root runs the tests and the doctests (`--doctest-modules` over `cutan/`, as CI does). The render tests are gated like `an`'s (`tests/conftest.py`): they need a headless browser and `ffmpeg`, and skip with a stated reason where those are absent. Tests that load a committed project in place depend on `scene.md` and `ir/scene.json` having the same age, which a fresh checkout gives them.
39
+
40
+ ## Data
41
+
42
+ The genre's library root is `~/.local/share/cutan` (`CUTAN_HOME` overrides it), resolved by `an.library.root.library_root(package="cutan")`. Nothing in this repository holds assets or project data beyond the committed corpus and examples.
cutan-0.0.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thor Whalen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
cutan-0.0.2/PKG-INFO ADDED
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.5
2
+ Name: cutan
3
+ Version: 0.0.2
4
+ Summary: Cut-out animation for an: rigged characters, faces, lip-sync and styles, as a genre package on the an core.
5
+ Project-URL: Homepage, https://github.com/thorwhalen/cutan
6
+ Project-URL: Repository, https://github.com/thorwhalen/cutan
7
+ Project-URL: Documentation, https://thorwhalen.github.io/cutan
8
+ Author: Thor Whalen
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: an,animation,cut-out,cutout,lip-sync
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Multimedia :: Graphics
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: an
17
+ Requires-Dist: numpy
18
+ Requires-Dist: pillow
19
+ Requires-Dist: pyyaml
20
+ Provides-Extra: dev
21
+ Requires-Dist: an[stage]; extra == 'dev'
22
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
23
+ Requires-Dist: pytest>=7.0; extra == 'dev'
24
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
25
+ Provides-Extra: docs
26
+ Requires-Dist: sphinx-rtd-theme>=1.0; extra == 'docs'
27
+ Requires-Dist: sphinx>=6.0; extra == 'docs'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # cutan
31
+
32
+ Cut-out animation for [`an`](https://github.com/thorwhalen/an): rigged characters, faces and expressions, lip-sync, swap sets and views, impacts, and cut-out styles, as a genre package on the `an` core.
33
+
34
+ ```bash
35
+ pip install "an[cutout]" # an + the stage + cutan
36
+ an character new maya --offline
37
+ ```
38
+
39
+ `an` finds `cutan` through the `an.genres` entry point and loads it with `an.genres.load()` (the CLI and `an.load(project)` do). The scene format, rendering and the asset library are `an`'s; see its README.
40
+
41
+ ## What is here
42
+
43
+ `cutan` registers with the core:
44
+
45
+ - the `character` entity kind and the `play` and `expression` actions, the `[emotion]` dialogue sugar and the semantic checks that go with them (`cutan.genre`, `cutan.characters`, `cutan.expression`);
46
+ - the rig, face, viseme, blink, gaze and swap-pose compile passes over the `an.stage` compiler (`cutan.compile`), and the mouth and eye visuals of the stage runtime (`cutan/runtime/visuals.js`);
47
+ - the lip-sync providers `offline`, `rhubarb` and `whisper` (`cutan.audio`);
48
+ - locomotion, speech, blink and turn methods with their requirements and defaults, and the cut-out vocabulary;
49
+ - the `an character …` and `an impacts …` command namespaces;
50
+ - the style lint (`python -m cutan.verify.style`) and the style specs of the `cutan-style` skill.
51
+
52
+ The cut-out bench corpus (`misc/bench/`), `examples/` and the demo gallery (`misc/demos/`) live here too; `cutan.bench.run_bench()` runs the corpus through `an`'s bench runner.
53
+
54
+ ## Names that never change
55
+
56
+ Moving the code renamed nothing that is stored. Scenes keep `renderer: cutout`, the genre keeps its slug `cutout_animation`, character descriptors keep their document kind and version, and asset ids keep their `character.` prefix. These names are in `cutan/__init__.py`. The old `an.characters`, `an.expression`, `an.impacts`, `an.audio.offline_lipsync`-style import paths still work in `an`, with a `MovedModuleWarning`.
57
+
58
+ ## Where data lives
59
+
60
+ The genre's asset library and agent-made projects live under `~/.local/share/cutan` by default. Set `CUTAN_HOME` to move them. The core's own library (voices, sounds, fonts) stays under `an`'s root, and a project reads both as one search path.
61
+
62
+ ## Working on cutan
63
+
64
+ Read `CLAUDE.md`. In short: `pip install -e . --no-deps` next to an editable `an`, then `pytest`. The tests that render need a headless browser (`playwright install chromium`) and `ffmpeg`.
cutan-0.0.2/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # cutan
2
+
3
+ Cut-out animation for [`an`](https://github.com/thorwhalen/an): rigged characters, faces and expressions, lip-sync, swap sets and views, impacts, and cut-out styles, as a genre package on the `an` core.
4
+
5
+ ```bash
6
+ pip install "an[cutout]" # an + the stage + cutan
7
+ an character new maya --offline
8
+ ```
9
+
10
+ `an` finds `cutan` through the `an.genres` entry point and loads it with `an.genres.load()` (the CLI and `an.load(project)` do). The scene format, rendering and the asset library are `an`'s; see its README.
11
+
12
+ ## What is here
13
+
14
+ `cutan` registers with the core:
15
+
16
+ - the `character` entity kind and the `play` and `expression` actions, the `[emotion]` dialogue sugar and the semantic checks that go with them (`cutan.genre`, `cutan.characters`, `cutan.expression`);
17
+ - the rig, face, viseme, blink, gaze and swap-pose compile passes over the `an.stage` compiler (`cutan.compile`), and the mouth and eye visuals of the stage runtime (`cutan/runtime/visuals.js`);
18
+ - the lip-sync providers `offline`, `rhubarb` and `whisper` (`cutan.audio`);
19
+ - locomotion, speech, blink and turn methods with their requirements and defaults, and the cut-out vocabulary;
20
+ - the `an character …` and `an impacts …` command namespaces;
21
+ - the style lint (`python -m cutan.verify.style`) and the style specs of the `cutan-style` skill.
22
+
23
+ The cut-out bench corpus (`misc/bench/`), `examples/` and the demo gallery (`misc/demos/`) live here too; `cutan.bench.run_bench()` runs the corpus through `an`'s bench runner.
24
+
25
+ ## Names that never change
26
+
27
+ Moving the code renamed nothing that is stored. Scenes keep `renderer: cutout`, the genre keeps its slug `cutout_animation`, character descriptors keep their document kind and version, and asset ids keep their `character.` prefix. These names are in `cutan/__init__.py`. The old `an.characters`, `an.expression`, `an.impacts`, `an.audio.offline_lipsync`-style import paths still work in `an`, with a `MovedModuleWarning`.
28
+
29
+ ## Where data lives
30
+
31
+ The genre's asset library and agent-made projects live under `~/.local/share/cutan` by default. Set `CUTAN_HOME` to move them. The core's own library (voices, sounds, fonts) stays under `an`'s root, and a project reads both as one search path.
32
+
33
+ ## Working on cutan
34
+
35
+ Read `CLAUDE.md`. In short: `pip install -e . --no-deps` next to an editable `an`, then `pytest`. The tests that render need a headless browser (`playwright install chromium`) and `ffmpeg`.
@@ -0,0 +1,265 @@
1
+ """Repository-wide pytest setup: load the genres, as the real entry points do.
2
+
3
+ Genres are discovered explicitly (ADR 0001 decision 3): importing ``an``
4
+ registers none, and ``an.load(project)`` / the CLI call
5
+ :func:`an.genres.load`. Most tests and doctests build scenes with the cut-out
6
+ genre's kinds (``play``, ``expression``, ``character``, ``[emotion]``)
7
+ without going through either, so the session calls the same
8
+ :func:`an.genres.load` once, here — the in-distribution genres are found
9
+ whatever the installed metadata says. Tests of the unloaded core use
10
+ :func:`an.genres.without_genres`; the entry points themselves are held by
11
+ subprocess tests that run with no pre-registration
12
+ (``tests/test_open_document_model.py``).
13
+
14
+ At the repository root so it reaches both ``tests/`` and the doctests under
15
+ ``an/`` (CI's ``--doctest-modules``).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from pathlib import Path
21
+
22
+
23
+ def pytest_configure(config): # noqa: D103 — a pytest hook
24
+ from an.genres import load
25
+
26
+ load()
27
+ _install_real_home_guard()
28
+ _redirect_account_home_for_the_session()
29
+
30
+
31
+ #: What the session guard found: stack traces of writes that reached the real
32
+ #: account's data folder (review-269 B3).
33
+ _REAL_HOME_WRITES: list[str] = []
34
+ _REAL_DATA_SNAPSHOT: dict = {}
35
+
36
+
37
+ def _real_data_dirs():
38
+ from an.library import registry
39
+ import sys
40
+
41
+ from an.library.root import POSIX_DATA_DEFAULT, WINDOWS_DATA_DEFAULT
42
+
43
+ defaults = (
44
+ WINDOWS_DATA_DEFAULT if sys.platform.startswith("win") else POSIX_DATA_DEFAULT
45
+ )
46
+ data = registry._account_home().joinpath(*defaults)
47
+ return [data / "an", data / "cutan"]
48
+
49
+
50
+ def _snapshot(dirs) -> dict:
51
+ out = {}
52
+ for d in dirs:
53
+ if not d.exists():
54
+ continue
55
+ for p in d.rglob("*"):
56
+ try:
57
+ st = p.lstat()
58
+ except OSError:
59
+ continue
60
+ out[str(p)] = (st.st_mtime_ns, st.st_size)
61
+ return out
62
+
63
+
64
+ def _install_real_home_guard() -> None:
65
+ """Fail the run if this process writes into the real account's data folder.
66
+
67
+ Two layers, installed before any fixture runs:
68
+
69
+ - a tripwire, in this process: every library-store write and every
70
+ machine-registry path that lands under the real data folder is recorded
71
+ with its stack, and fails the session (``pytest_sessionfinish``);
72
+ - a snapshot of ``<data>/an`` and ``<data>/cutan`` before and after the
73
+ session. On CI (``CI`` set) any difference fails the run. On a developer
74
+ machine other sessions (an end-to-end render, an agent publishing) write
75
+ there concurrently, so a difference is reported, and fails only with
76
+ ``AN_TEST_HOME_GUARD=strict``.
77
+ """
78
+ import traceback
79
+
80
+ from an.library import registry, stores
81
+
82
+ data_dirs = _real_data_dirs()
83
+ real_prefixes = tuple(str(d) for d in data_dirs) # not their parent: on Windows
84
+ # the temp dir (home of the redirect) sits inside `AppData/Local`.
85
+ _REAL_DATA_SNAPSHOT["dirs"] = data_dirs
86
+ _REAL_DATA_SNAPSHOT["home"] = registry._account_home()
87
+ _REAL_DATA_SNAPSHOT["before"] = _snapshot(data_dirs)
88
+
89
+ def note(path) -> None:
90
+ if str(path).startswith(real_prefixes):
91
+ _REAL_HOME_WRITES.append(
92
+ f"{path}\n" + "".join(traceback.format_stack(limit=30))
93
+ )
94
+
95
+ temp, delete = stores.LocalFiles._temp, stores.LocalFiles.__delitem__
96
+
97
+ def guarded_temp(self, path, data):
98
+ note(path)
99
+ return temp(self, path, data)
100
+
101
+ def guarded_delete(self, key):
102
+ note(self._path(key))
103
+ return delete(self, key)
104
+
105
+ stores.LocalFiles._temp = guarded_temp
106
+ stores.LocalFiles.__delitem__ = guarded_delete
107
+ registry_dir = registry.machine_registry_dir
108
+
109
+ def guarded_registry_dir():
110
+ out = registry_dir()
111
+ note(out)
112
+ return out
113
+
114
+ registry.machine_registry_dir = guarded_registry_dir
115
+
116
+
117
+ def pytest_collection_modifyitems(config, items): # noqa: D103 — a pytest hook
118
+ # Every test gets a fresh registry, so every publish is a "first use": the
119
+ # RegistryWarning would fire once per publishing test and bury real
120
+ # warnings (review-269 N2). `pytest.warns` still sees it.
121
+ mark = pytest.mark.filterwarnings("ignore::an.library.registry.RegistryWarning")
122
+ for item in items:
123
+ item.add_marker(mark)
124
+
125
+
126
+ def pytest_sessionfinish(session, exitstatus): # noqa: D103 — a pytest hook
127
+ import os
128
+
129
+ reporter = session.config.pluginmanager.get_plugin("terminalreporter")
130
+
131
+ def say(text: str) -> None:
132
+ if reporter is not None:
133
+ reporter.write_line(text)
134
+
135
+ child_log = _CHILD_STATE.get("path")
136
+ if child_log is not None and child_log.exists() and child_log.stat().st_size:
137
+ say(
138
+ "REAL-HOME GUARD: a CHILD python process reached the real data folder "
139
+ "despite the redirect (an#302):\n"
140
+ + child_log.read_text(encoding="utf-8")[:4000]
141
+ )
142
+ session.exitstatus = 1
143
+ if _REAL_HOME_WRITES:
144
+ say(
145
+ f"REAL-HOME GUARD: {len(_REAL_HOME_WRITES)} write(s) or registry access(es) "
146
+ "reached the real data folder from this test process; first one:\n"
147
+ + _REAL_HOME_WRITES[0]
148
+ )
149
+ session.exitstatus = 1
150
+ if "before" in _REAL_DATA_SNAPSHOT:
151
+ after = _snapshot(_REAL_DATA_SNAPSHOT["dirs"])
152
+ before = _REAL_DATA_SNAPSHOT["before"]
153
+ changed = sorted(
154
+ k for k in set(before) | set(after) if before.get(k) != after.get(k)
155
+ )
156
+ if changed:
157
+ strict = (
158
+ os.environ.get("CI") or os.environ.get("AN_TEST_HOME_GUARD") == "strict"
159
+ )
160
+ say(
161
+ f"REAL-HOME GUARD: {len(changed)} path(s) under the real data folder "
162
+ "changed during the session"
163
+ + (
164
+ ""
165
+ if strict
166
+ else " (another process? set AN_TEST_HOME_GUARD=strict to fail on it)"
167
+ )
168
+ + ":\n "
169
+ + "\n ".join(changed[:20])
170
+ )
171
+ if strict:
172
+ session.exitstatus = 1
173
+
174
+
175
+ def _redirect_account_home_for_the_session() -> None:
176
+ """Point the machine registry into a temp folder for the WHOLE session.
177
+
178
+ The per-test fixture below gives each test a fresh registry, but a
179
+ module- or session-scoped fixture runs before it: one that draws a
180
+ character (the factory records what it draws, an#269) or publishes would
181
+ otherwise write the developer's real ``~/.local/share/an/registry``. This
182
+ session-wide redirect is what those fixtures see; each test still gets
183
+ its own fresh one.
184
+ """
185
+ import tempfile
186
+ from an.library import registry
187
+
188
+ home = Path(tempfile.mkdtemp(prefix="an-session-account-home-"))
189
+ registry._account_home = lambda: home
190
+ _redirect_children_too(home)
191
+
192
+
193
+ #: Environment variable naming the redirected account home for child pythons.
194
+ CHILD_HOME_ENV: str = "AN_TEST_ACCOUNT_HOME"
195
+ #: Environment variable naming the file a child appends to if it reaches the real data folder.
196
+ CHILD_LOG_ENV: str = "AN_TEST_REAL_HOME_LOG"
197
+ #: The folder whose ``sitecustomize`` applies the redirect inside a child (an#302).
198
+ CHILD_GUARD_DIR = Path(__file__).parent / "tests" / "_child_guard"
199
+
200
+
201
+ def _redirect_children_too(home) -> None:
202
+ """Make every child python process the tests spawn honour the same redirect.
203
+
204
+ The monkeypatch above lives in THIS interpreter; a child (a CLI test, a
205
+ ``[sys.executable, "-c", ...]`` probe) is a new one and, left alone, would
206
+ write the developer's real registry (an#302). The library reads no
207
+ environment variable for that location, so the redirect rides on
208
+ ``tests/_child_guard/sitecustomize.py`` instead, put on each child's
209
+ ``PYTHONPATH`` by wrapping ``subprocess.Popen`` — whatever ``env=`` the
210
+ test built, including one that replaces ``PYTHONPATH`` wholesale.
211
+ A child that reaches the real folder regardless logs itself to
212
+ ``CHILD_LOG_ENV``, and :func:`pytest_sessionfinish` fails the run on it.
213
+ """
214
+ import os
215
+ import subprocess
216
+
217
+ log = home / "child-real-home-writes.log"
218
+ os.environ[CHILD_HOME_ENV] = str(home)
219
+ os.environ[CHILD_LOG_ENV] = str(log)
220
+ _CHILD_STATE["path"] = log
221
+ _CHILD_STATE["home"] = home
222
+ init = subprocess.Popen.__init__
223
+
224
+ def popen_init(self, *args, **kwargs):
225
+ env = dict(os.environ if kwargs.get("env") is None else kwargs["env"])
226
+ # The CURRENT home: the per-test fixture swaps it, like the in-process one.
227
+ env[CHILD_HOME_ENV] = str(_CHILD_STATE["home"])
228
+ env.setdefault(CHILD_LOG_ENV, str(log))
229
+ existing = env.get("PYTHONPATH")
230
+ env["PYTHONPATH"] = os.pathsep.join(
231
+ [str(CHILD_GUARD_DIR)] + ([existing] if existing else [])
232
+ )
233
+ kwargs["env"] = env
234
+ return init(self, *args, **kwargs)
235
+
236
+ subprocess.Popen.__init__ = popen_init
237
+
238
+
239
+ #: Where the child-process tripwire writes (set by :func:`_redirect_children_too`).
240
+ _CHILD_STATE: dict = {}
241
+
242
+
243
+ import pytest
244
+
245
+
246
+ @pytest.fixture(autouse=True)
247
+ def _isolated_library_registry(tmp_path_factory, monkeypatch):
248
+ """Point the machine's library registry and statement memory into a fresh temp folder.
249
+
250
+ The registry (:mod:`an.library.registry`, an#249) lives under the account's
251
+ home as the OS records it, deliberately ignoring every environment variable,
252
+ so the ``AN_HOME`` / ``XDG_DATA_HOME`` redirection the library tests use
253
+ cannot move it. Every test and doctest that publishes would otherwise
254
+ append to the developer's real registry — and one test's private bytes
255
+ would sit in the next test's rights floor. Fresh per test, for both
256
+ reasons. The account home is what is replaced, so the real path logic
257
+ (``machine_registry_path``) still runs, and a test can hold it to its
258
+ independence from the environment.
259
+ """
260
+ from an.library import registry
261
+
262
+ home = tmp_path_factory.mktemp("account-home")
263
+ monkeypatch.setattr(registry, "_account_home", lambda: home)
264
+ # Children get the same fresh registry (an#302), restored afterwards.
265
+ monkeypatch.setitem(_CHILD_STATE, "home", home)