yap-studio 1.2.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 (510) hide show
  1. yap_studio/__init__.py +12 -0
  2. yap_studio/__main__.py +8 -0
  3. yap_studio/affiliate.py +4 -0
  4. yap_studio/audio.py +601 -0
  5. yap_studio/audio_chain.py +60 -0
  6. yap_studio/background.py +213 -0
  7. yap_studio/brand_command.py +99 -0
  8. yap_studio/brand_words.py +129 -0
  9. yap_studio/cache.py +28 -0
  10. yap_studio/canvas.py +113 -0
  11. yap_studio/captions.py +496 -0
  12. yap_studio/card_motion.py +219 -0
  13. yap_studio/card_project.py +145 -0
  14. yap_studio/card_stills.py +94 -0
  15. yap_studio/cards.py +2455 -0
  16. yap_studio/cards_video.py +185 -0
  17. yap_studio/checks/__init__.py +5 -0
  18. yap_studio/checks/caption_contrast.py +515 -0
  19. yap_studio/checks/caption_layout.py +281 -0
  20. yap_studio/checks/caption_words.py +207 -0
  21. yap_studio/checks/declared.py +313 -0
  22. yap_studio/checks/detect.py +569 -0
  23. yap_studio/checks/face_clearance.py +133 -0
  24. yap_studio/checks/fillers_gold.py +450 -0
  25. yap_studio/checks/flash.py +225 -0
  26. yap_studio/checks/grade.py +114 -0
  27. yap_studio/checks/joins.py +2121 -0
  28. yap_studio/checks/kept_words.py +133 -0
  29. yap_studio/checks/model.py +152 -0
  30. yap_studio/checks/platform.py +188 -0
  31. yap_studio/checks/relisten.py +368 -0
  32. yap_studio/checks/spans.py +164 -0
  33. yap_studio/checks/structure.py +1055 -0
  34. yap_studio/checks/sync.py +648 -0
  35. yap_studio/cli.py +2023 -0
  36. yap_studio/covers/__init__.py +1 -0
  37. yap_studio/covers/command.py +221 -0
  38. yap_studio/covers/layout.py +532 -0
  39. yap_studio/covers/pick.py +282 -0
  40. yap_studio/covers/render.py +521 -0
  41. yap_studio/covers/wide.py +314 -0
  42. yap_studio/editor_cards.py +168 -0
  43. yap_studio/edl/__init__.py +127 -0
  44. yap_studio/edl/fcpxml.py +239 -0
  45. yap_studio/edl/model.py +312 -0
  46. yap_studio/edl/rangesjson.py +310 -0
  47. yap_studio/edl/xmeml.py +264 -0
  48. yap_studio/edl/yapjson.py +149 -0
  49. yap_studio/elevenlabs.py +181 -0
  50. yap_studio/errors.py +331 -0
  51. yap_studio/faces/__init__.py +104 -0
  52. yap_studio/faces/clearance.py +685 -0
  53. yap_studio/faces/command.py +254 -0
  54. yap_studio/faces/detect.py +402 -0
  55. yap_studio/faces/face_boxes.swift +273 -0
  56. yap_studio/faces/final.py +87 -0
  57. yap_studio/faces/mapping.py +179 -0
  58. yap_studio/faces/model.py +389 -0
  59. yap_studio/faces/placement.py +235 -0
  60. yap_studio/faces/project.py +145 -0
  61. yap_studio/feedback/__init__.py +1 -0
  62. yap_studio/feedback/apply.py +500 -0
  63. yap_studio/feedback/audio.py +145 -0
  64. yap_studio/feedback/batch.py +639 -0
  65. yap_studio/feedback/broll.py +306 -0
  66. yap_studio/feedback/clarify.py +178 -0
  67. yap_studio/feedback/cli.py +346 -0
  68. yap_studio/feedback/edits.py +649 -0
  69. yap_studio/feedback/keeps.py +286 -0
  70. yap_studio/feedback/locate.py +89 -0
  71. yap_studio/feedback/moments.py +118 -0
  72. yap_studio/feedback/outline.py +614 -0
  73. yap_studio/feedback/pins.py +366 -0
  74. yap_studio/feedback/release.py +321 -0
  75. yap_studio/feedback/removed.py +81 -0
  76. yap_studio/feedback/replies.py +386 -0
  77. yap_studio/feedback/spelling.py +124 -0
  78. yap_studio/feedback/taste.py +152 -0
  79. yap_studio/feedback/versions.py +814 -0
  80. yap_studio/ffmpeg.py +475 -0
  81. yap_studio/file_cards.py +187 -0
  82. yap_studio/fonts/alata-regular.woff2 +0 -0
  83. yap_studio/fonts/anton-sc-regular.woff2 +0 -0
  84. yap_studio/fonts/archivo-black-regular.woff2 +0 -0
  85. yap_studio/fonts/geist-italic-variable.woff2 +0 -0
  86. yap_studio/fonts/geist-variable.woff2 +0 -0
  87. yap_studio/fonts/gelasio-italic-variable.woff2 +0 -0
  88. yap_studio/fonts/gelasio-variable.woff2 +0 -0
  89. yap_studio/fonts/instrument-serif-italic.woff2 +0 -0
  90. yap_studio/fonts/instrument-serif-regular.woff2 +0 -0
  91. yap_studio/fonts/inter-italic-variable.woff2 +0 -0
  92. yap_studio/fonts/inter-variable.woff2 +0 -0
  93. yap_studio/fonts/permanent-marker-regular.woff2 +0 -0
  94. yap_studio/fonts/space-mono-italic.woff2 +0 -0
  95. yap_studio/fonts/space-mono-regular.woff2 +0 -0
  96. yap_studio/fonts/titan-one-regular.woff2 +0 -0
  97. yap_studio/gallery/__init__.py +1 -0
  98. yap_studio/gallery/command.py +339 -0
  99. yap_studio/gallery/cutout.py +84 -0
  100. yap_studio/gallery/detach.py +232 -0
  101. yap_studio/gallery/face_landmarks.swift +78 -0
  102. yap_studio/gallery/live.py +326 -0
  103. yap_studio/gallery/sample-still.jpg +0 -0
  104. yap_studio/gallery/server.py +881 -0
  105. yap_studio/gallery/static/choices.mjs +1216 -0
  106. yap_studio/gallery/static/pick.css +482 -0
  107. yap_studio/gallery/static/pick.html +61 -0
  108. yap_studio/gallery/static/pick.js +2242 -0
  109. yap_studio/gallery/static/shot.jpg +0 -0
  110. yap_studio/gallery/still.py +587 -0
  111. yap_studio/gallery/still_command.py +69 -0
  112. yap_studio/grade.py +407 -0
  113. yap_studio/grade_command.py +124 -0
  114. yap_studio/hdr.py +190 -0
  115. yap_studio/hf_registry/__init__.py +1 -0
  116. yap_studio/hf_registry/cards.py +228 -0
  117. yap_studio/hf_registry/catalog.json +122 -0
  118. yap_studio/hf_registry/catalog.py +308 -0
  119. yap_studio/hf_registry/install.py +255 -0
  120. yap_studio/hf_registry/localize.py +471 -0
  121. yap_studio/homepage/__init__.py +1 -0
  122. yap_studio/homepage/bridge.py +656 -0
  123. yap_studio/homepage/command.py +173 -0
  124. yap_studio/homepage/detach.py +164 -0
  125. yap_studio/homepage/link.py +87 -0
  126. yap_studio/homepage/server.py +625 -0
  127. yap_studio/homepage/state.py +452 -0
  128. yap_studio/homepage/static/home-model.mjs +365 -0
  129. yap_studio/homepage/static/home.css +334 -0
  130. yap_studio/homepage/static/home.html +38 -0
  131. yap_studio/homepage/static/home.js +1079 -0
  132. yap_studio/install_info.py +175 -0
  133. yap_studio/jiggle_cards.py +185 -0
  134. yap_studio/joins/__init__.py +1 -0
  135. yap_studio/joins/assembly.py +39 -0
  136. yap_studio/joins/command.py +57 -0
  137. yap_studio/joins/helper.py +142 -0
  138. yap_studio/joins/plan.py +260 -0
  139. yap_studio/joins/render.py +210 -0
  140. yap_studio/joins/smooth_frames.swift +196 -0
  141. yap_studio/joins/steps.py +183 -0
  142. yap_studio/letters.py +417 -0
  143. yap_studio/local_http.py +20 -0
  144. yap_studio/matte/__init__.py +21 -0
  145. yap_studio/matte/check.py +582 -0
  146. yap_studio/matte/chroma.py +393 -0
  147. yap_studio/matte/command.py +169 -0
  148. yap_studio/matte/composite.py +417 -0
  149. yap_studio/matte/edge_colour.py +212 -0
  150. yap_studio/matte/frames.py +252 -0
  151. yap_studio/matte/hyperframes.py +292 -0
  152. yap_studio/matte/ink.py +20 -0
  153. yap_studio/matte/make.py +452 -0
  154. yap_studio/matte/model.py +280 -0
  155. yap_studio/matte/person_matte.swift +197 -0
  156. yap_studio/matte/project.py +207 -0
  157. yap_studio/matte/refine.py +247 -0
  158. yap_studio/matte/timing.py +34 -0
  159. yap_studio/matte/vision.py +105 -0
  160. yap_studio/mcp_server.py +112 -0
  161. yap_studio/media/__init__.py +34 -0
  162. yap_studio/media/album.py +353 -0
  163. yap_studio/media/captured.py +325 -0
  164. yap_studio/media/catalog.py +791 -0
  165. yap_studio/media/commands.py +315 -0
  166. yap_studio/media/describe.py +209 -0
  167. yap_studio/media/frame.py +54 -0
  168. yap_studio/media/inventory.py +353 -0
  169. yap_studio/media/select.py +376 -0
  170. yap_studio/media/sheet.py +404 -0
  171. yap_studio/media/sound.py +117 -0
  172. yap_studio/opening_push.py +60 -0
  173. yap_studio/paths.py +456 -0
  174. yap_studio/pictures/__init__.py +1 -0
  175. yap_studio/pictures/attempts.py +71 -0
  176. yap_studio/pictures/command.py +196 -0
  177. yap_studio/pictures/guard.py +208 -0
  178. yap_studio/pictures/make.py +526 -0
  179. yap_studio/pictures/metadata.py +40 -0
  180. yap_studio/pictures/sheet.py +273 -0
  181. yap_studio/pictures/wavespeed.py +372 -0
  182. yap_studio/plan.py +256 -0
  183. yap_studio/planner/__init__.py +1 -0
  184. yap_studio/planner/anchors.py +74 -0
  185. yap_studio/planner/broll.py +681 -0
  186. yap_studio/planner/broll_placement.py +30 -0
  187. yap_studio/planner/broll_timing.py +45 -0
  188. yap_studio/planner/cards.py +1388 -0
  189. yap_studio/planner/command.py +236 -0
  190. yap_studio/planner/number_behind.py +281 -0
  191. yap_studio/planner/number_spoken.py +49 -0
  192. yap_studio/platforms.json +131 -0
  193. yap_studio/process.py +75 -0
  194. yap_studio/product_feedback.py +387 -0
  195. yap_studio/project/__init__.py +181 -0
  196. yap_studio/project/anchors.py +254 -0
  197. yap_studio/project/edit_format.py +93 -0
  198. yap_studio/project/lock.py +153 -0
  199. yap_studio/project/model.py +940 -0
  200. yap_studio/project/receipts.py +542 -0
  201. yap_studio/project/schemas/cut-words.schema.json +78 -0
  202. yap_studio/project/schemas/edit.schema.json +169 -0
  203. yap_studio/project/schemas/project.schema.json +487 -0
  204. yap_studio/project/schemas/removals.schema.json +84 -0
  205. yap_studio/project/schemas/text-hook.schema.json +78 -0
  206. yap_studio/project/schemas/timeline-fragment.schema.json +3128 -0
  207. yap_studio/project/schemas/timeline.schema.json +3145 -0
  208. yap_studio/project/schemas/words.schema.json +101 -0
  209. yap_studio/project/status.py +889 -0
  210. yap_studio/project/word_fixes.py +39 -0
  211. yap_studio/receipt.py +80 -0
  212. yap_studio/registry.py +205 -0
  213. yap_studio/render/__init__.py +1 -0
  214. yap_studio/render/alpha.py +98 -0
  215. yap_studio/render/beats.py +112 -0
  216. yap_studio/render/camera3d.py +2319 -0
  217. yap_studio/render/camera3d_clearance.py +112 -0
  218. yap_studio/render/camera3d_fallback.py +136 -0
  219. yap_studio/render/camera3d_geometry.py +77 -0
  220. yap_studio/render/camera3d_look.py +395 -0
  221. yap_studio/render/camera3d_matte.py +54 -0
  222. yap_studio/render/camera3d_motion.py +113 -0
  223. yap_studio/render/fonts.py +307 -0
  224. yap_studio/render/hyperframes.py +1075 -0
  225. yap_studio/render/layer_checks.py +315 -0
  226. yap_studio/render/moment_clones.py +453 -0
  227. yap_studio/render/moments.py +531 -0
  228. yap_studio/render/renderer-lock/package-lock.json +1933 -0
  229. yap_studio/render/renderer-lock/package.json +11 -0
  230. yap_studio/render/text_metrics.json +10442 -0
  231. yap_studio/render/text_metrics.py +94 -0
  232. yap_studio/render/user_fonts.py +310 -0
  233. yap_studio/report.py +595 -0
  234. yap_studio/review/__init__.py +85 -0
  235. yap_studio/review/captions.py +61 -0
  236. yap_studio/review/detach.py +224 -0
  237. yap_studio/review/export.py +403 -0
  238. yap_studio/review/findings.py +349 -0
  239. yap_studio/review/launch.py +108 -0
  240. yap_studio/review/media.py +262 -0
  241. yap_studio/review/model.py +456 -0
  242. yap_studio/review/outline_server.py +339 -0
  243. yap_studio/review/phone.py +178 -0
  244. yap_studio/review/rounds.py +162 -0
  245. yap_studio/review/send.py +77 -0
  246. yap_studio/review/series.py +247 -0
  247. yap_studio/review/server.py +1428 -0
  248. yap_studio/review/static/app.css +445 -0
  249. yap_studio/review/static/app.mjs +2893 -0
  250. yap_studio/review/static/index.html +269 -0
  251. yap_studio/review/static/list.html +27 -0
  252. yap_studio/review/static/outline-view.mjs +63 -0
  253. yap_studio/review/static/outline.html +52 -0
  254. yap_studio/review/static/outline.mjs +359 -0
  255. yap_studio/review/static/taste.mjs +111 -0
  256. yap_studio/review/static/text-hook.mjs +70 -0
  257. yap_studio/review/static/timeline.mjs +588 -0
  258. yap_studio/review/store.py +922 -0
  259. yap_studio/review/video_map.py +122 -0
  260. yap_studio/setup_checks.py +75 -0
  261. yap_studio/sfx/__init__.py +376 -0
  262. yap_studio/sfx/bleep.py +469 -0
  263. yap_studio/sfx/commands.py +77 -0
  264. yap_studio/sfx/kits/extras.json +70 -0
  265. yap_studio/sfx/kits/made.json +311 -0
  266. yap_studio/sfx/kits/soft.json +43 -0
  267. yap_studio/sfx/made.py +250 -0
  268. yap_studio/sfx/place.py +356 -0
  269. yap_studio/sfx/swear-words.json +48 -0
  270. yap_studio/stages/__init__.py +1 -0
  271. yap_studio/stages/assemble.py +1759 -0
  272. yap_studio/stages/assemble_audio.py +653 -0
  273. yap_studio/stages/assemble_grade.py +62 -0
  274. yap_studio/stages/assemble_picture.py +1467 -0
  275. yap_studio/stages/behind_window.py +58 -0
  276. yap_studio/stages/broll_framing.py +93 -0
  277. yap_studio/stages/broll_limits.py +109 -0
  278. yap_studio/stages/broll_match.py +385 -0
  279. yap_studio/stages/broll_split.py +213 -0
  280. yap_studio/stages/captions.py +2010 -0
  281. yap_studio/stages/captions_3d.py +348 -0
  282. yap_studio/stages/coverage.py +210 -0
  283. yap_studio/stages/cut.py +1466 -0
  284. yap_studio/stages/edges.py +752 -0
  285. yap_studio/stages/fillers.py +2322 -0
  286. yap_studio/stages/graphics.py +1048 -0
  287. yap_studio/stages/green_screen.py +355 -0
  288. yap_studio/stages/ingest.py +816 -0
  289. yap_studio/stages/music.py +650 -0
  290. yap_studio/stages/number_graphics.py +157 -0
  291. yap_studio/stages/number_visibility.py +69 -0
  292. yap_studio/stages/rebase.py +789 -0
  293. yap_studio/stages/repeats.py +140 -0
  294. yap_studio/stages/run.py +336 -0
  295. yap_studio/stages/run_detach.py +407 -0
  296. yap_studio/stages/take_levels.py +227 -0
  297. yap_studio/stages/tall_box.py +144 -0
  298. yap_studio/stages/timeline.py +1527 -0
  299. yap_studio/stages/transitions.py +148 -0
  300. yap_studio/stages/voiceover.py +59 -0
  301. yap_studio/stock/__init__.py +1 -0
  302. yap_studio/stock/command.py +135 -0
  303. yap_studio/stock/credits.py +67 -0
  304. yap_studio/stock/fetch.py +118 -0
  305. yap_studio/stock/get.py +273 -0
  306. yap_studio/stock/guard.py +144 -0
  307. yap_studio/stock/metadata.py +53 -0
  308. yap_studio/stock/pexels.py +191 -0
  309. yap_studio/stock/provider.py +158 -0
  310. yap_studio/stock/sheet.py +451 -0
  311. yap_studio/studio/__init__.py +5 -0
  312. yap_studio/studio/commands.py +70 -0
  313. yap_studio/studio/export.py +409 -0
  314. yap_studio/style/PROVENANCE.json +141 -0
  315. yap_studio/style/__init__.py +63 -0
  316. yap_studio/style/answers.py +358 -0
  317. yap_studio/style/library.py +415 -0
  318. yap_studio/style/loader.py +316 -0
  319. yap_studio/style/presets/candy.json +42 -0
  320. yap_studio/style/presets/clean_minimal.json +26 -0
  321. yap_studio/style/presets/cobalt.json +42 -0
  322. yap_studio/style/presets/editorial_serif.json +43 -0
  323. yap_studio/style/presets/electric.json +29 -0
  324. yap_studio/style/presets/headline.json +43 -0
  325. yap_studio/style/presets/hf_bold.json +35 -0
  326. yap_studio/style/presets/hf_editorial.json +35 -0
  327. yap_studio/style/presets/hf_neutral.json +35 -0
  328. yap_studio/style/presets/marker.json +42 -0
  329. yap_studio/style/presets/mono.json +42 -0
  330. yap_studio/style/presets/one_word.json +44 -0
  331. yap_studio/style/presets/warm_editorial.json +28 -0
  332. yap_studio/style/schema-v1.json +1095 -0
  333. yap_studio/style/site_colors.py +463 -0
  334. yap_studio/taste/__init__.py +82 -0
  335. yap_studio/taste/check.py +242 -0
  336. yap_studio/taste/cli.py +249 -0
  337. yap_studio/taste/frames.py +296 -0
  338. yap_studio/taste/inbox.py +269 -0
  339. yap_studio/taste/learn.py +1237 -0
  340. yap_studio/taste/learned.py +119 -0
  341. yap_studio/taste/measure_pacing.py +306 -0
  342. yap_studio/taste/measure_panels.py +360 -0
  343. yap_studio/taste/measure_shapes.py +377 -0
  344. yap_studio/taste/measure_text.py +488 -0
  345. yap_studio/taste/measure_words.py +357 -0
  346. yap_studio/taste/model.py +585 -0
  347. yap_studio/taste/render.py +162 -0
  348. yap_studio/taste/resolve.py +132 -0
  349. yap_studio/taste/store.py +436 -0
  350. yap_studio/templates/PROVENANCE.json +519 -0
  351. yap_studio/templates/big_number/preview.json +6 -0
  352. yap_studio/templates/big_number/template.html +96 -0
  353. yap_studio/templates/big_number/template.json +289 -0
  354. yap_studio/templates/callout_circle/preview.json +8 -0
  355. yap_studio/templates/callout_circle/template.html +82 -0
  356. yap_studio/templates/callout_circle/template.json +196 -0
  357. yap_studio/templates/camera3d/PROVENANCE.json +42 -0
  358. yap_studio/templates/camera3d/cam3d.js +360 -0
  359. yap_studio/templates/camera3d/finish.js +35 -0
  360. yap_studio/templates/camera3d/metrics.json +312 -0
  361. yap_studio/templates/camera3d/tables.js +29 -0
  362. yap_studio/templates/captions/template.html +1077 -0
  363. yap_studio/templates/captions/template.json +396 -0
  364. yap_studio/templates/card-motion-drawn.js +35 -0
  365. yap_studio/templates/card-motion-host.js +100 -0
  366. yap_studio/templates/chapter/template.html +74 -0
  367. yap_studio/templates/chapter/template.json +67 -0
  368. yap_studio/templates/chart/preview.json +20 -0
  369. yap_studio/templates/chart/template.html +88 -0
  370. yap_studio/templates/chart/template.json +202 -0
  371. yap_studio/templates/checklist/preview.json +9 -0
  372. yap_studio/templates/checklist/template.html +141 -0
  373. yap_studio/templates/checklist/template.json +148 -0
  374. yap_studio/templates/choices/preview.json +1 -0
  375. yap_studio/templates/choices/template.html +102 -0
  376. yap_studio/templates/choices/template.json +239 -0
  377. yap_studio/templates/comment_cta/preview.json +6 -0
  378. yap_studio/templates/comment_cta/template.html +140 -0
  379. yap_studio/templates/comment_cta/template.json +198 -0
  380. yap_studio/templates/comment_drop/preview.json +6 -0
  381. yap_studio/templates/comment_drop/template.html +118 -0
  382. yap_studio/templates/comment_drop/template.json +214 -0
  383. yap_studio/templates/cover/template.html +104 -0
  384. yap_studio/templates/edit_timeline/preview.json +13 -0
  385. yap_studio/templates/edit_timeline/template.html +142 -0
  386. yap_studio/templates/edit_timeline/template.json +242 -0
  387. yap_studio/templates/file_drop/preview.json +5 -0
  388. yap_studio/templates/file_drop/template.html +124 -0
  389. yap_studio/templates/file_drop/template.json +264 -0
  390. yap_studio/templates/flow/preview.json +1 -0
  391. yap_studio/templates/flow/template.html +326 -0
  392. yap_studio/templates/flow/template.json +184 -0
  393. yap_studio/templates/glow-shadow.js +9 -0
  394. yap_studio/templates/gravity_drop/preview.json +18 -0
  395. yap_studio/templates/gravity_drop/template.html +63 -0
  396. yap_studio/templates/gravity_drop/template.json +141 -0
  397. yap_studio/templates/icon_strike/preview.json +6 -0
  398. yap_studio/templates/icon_strike/template.html +144 -0
  399. yap_studio/templates/icon_strike/template.json +193 -0
  400. yap_studio/templates/image_card/preview.json +6 -0
  401. yap_studio/templates/image_card/template.html +231 -0
  402. yap_studio/templates/image_card/template.json +244 -0
  403. yap_studio/templates/image_compare/preview.json +8 -0
  404. yap_studio/templates/image_compare/template.html +131 -0
  405. yap_studio/templates/image_compare/template.json +231 -0
  406. yap_studio/templates/jiggle_delete/preview.json +18 -0
  407. yap_studio/templates/jiggle_delete/template.html +118 -0
  408. yap_studio/templates/jiggle_delete/template.json +217 -0
  409. yap_studio/templates/letters/template.html +128 -0
  410. yap_studio/templates/letters/template.json +66 -0
  411. yap_studio/templates/name_tag/preview.json +6 -0
  412. yap_studio/templates/name_tag/template.html +74 -0
  413. yap_studio/templates/name_tag/template.json +148 -0
  414. yap_studio/templates/playing_grid/preview.json +1 -0
  415. yap_studio/templates/playing_grid/template.html +94 -0
  416. yap_studio/templates/playing_grid/template.json +182 -0
  417. yap_studio/templates/progress_stat/preview.json +7 -0
  418. yap_studio/templates/progress_stat/template.html +75 -0
  419. yap_studio/templates/progress_stat/template.json +160 -0
  420. yap_studio/templates/prompt_bar/preview.json +6 -0
  421. yap_studio/templates/prompt_bar/template.html +134 -0
  422. yap_studio/templates/prompt_bar/template.json +183 -0
  423. yap_studio/templates/quote/preview.json +7 -0
  424. yap_studio/templates/quote/template.html +79 -0
  425. yap_studio/templates/quote/template.json +166 -0
  426. yap_studio/templates/reel_feed/preview.json +1 -0
  427. yap_studio/templates/reel_feed/template.html +172 -0
  428. yap_studio/templates/reel_feed/template.json +271 -0
  429. yap_studio/templates/specs_list/preview.json +19 -0
  430. yap_studio/templates/specs_list/template.html +157 -0
  431. yap_studio/templates/specs_list/template.json +188 -0
  432. yap_studio/templates/stat/preview.json +8 -0
  433. yap_studio/templates/stat/template.html +305 -0
  434. yap_studio/templates/stat/template.json +290 -0
  435. yap_studio/templates/steps/preview.json +1 -0
  436. yap_studio/templates/steps/template.html +129 -0
  437. yap_studio/templates/steps/template.json +144 -0
  438. yap_studio/templates/storyboard/preview.json +59 -0
  439. yap_studio/templates/storyboard/template.html +107 -0
  440. yap_studio/templates/storyboard/template.json +188 -0
  441. yap_studio/templates/success_check/preview.json +5 -0
  442. yap_studio/templates/success_check/template.html +73 -0
  443. yap_studio/templates/success_check/template.json +129 -0
  444. yap_studio/templates/terminal/preview.json +12 -0
  445. yap_studio/templates/terminal/template.html +149 -0
  446. yap_studio/templates/terminal/template.json +229 -0
  447. yap_studio/templates/text_tag/preview.json +7 -0
  448. yap_studio/templates/text_tag/template.html +281 -0
  449. yap_studio/templates/text_tag/template.json +229 -0
  450. yap_studio/templates/title/template.html +541 -0
  451. yap_studio/templates/title/template.json +269 -0
  452. yap_studio/templates/transfer/preview.json +7 -0
  453. yap_studio/templates/transfer/template.html +244 -0
  454. yap_studio/templates/transfer/template.json +347 -0
  455. yap_studio/templates/transition_cinematic_zoom/template.html +106 -0
  456. yap_studio/templates/transition_cinematic_zoom/template.json +29 -0
  457. yap_studio/templates/transition_light_leak/template.html +106 -0
  458. yap_studio/templates/transition_light_leak/template.json +29 -0
  459. yap_studio/templates/transition_whip_pan/template.html +106 -0
  460. yap_studio/templates/transition_whip_pan/template.json +29 -0
  461. yap_studio/templates/video_card/preview.json +1 -0
  462. yap_studio/templates/video_card/template.html +101 -0
  463. yap_studio/templates/video_card/template.json +183 -0
  464. yap_studio/templates/wide_titles/template.html +39 -0
  465. yap_studio/templates/wide_titles/template.json +114 -0
  466. yap_studio/templates_command.py +410 -0
  467. yap_studio/text_hook.py +103 -0
  468. yap_studio/text_rules.py +406 -0
  469. yap_studio/tools.py +227 -0
  470. yap_studio/transcript/__init__.py +46 -0
  471. yap_studio/transcript/backends.py +941 -0
  472. yap_studio/transcript/child.py +52 -0
  473. yap_studio/transcript/engine_child.py +213 -0
  474. yap_studio/transcript/isolated.py +191 -0
  475. yap_studio/transcript/mappers.py +183 -0
  476. yap_studio/transcript/model.py +95 -0
  477. yap_studio/transcript/shared.py +283 -0
  478. yap_studio/transcript/take_job.py +176 -0
  479. yap_studio/transcript/words.py +130 -0
  480. yap_studio/verify.py +1025 -0
  481. yap_studio/video.py +551 -0
  482. yap_studio/video_type.py +225 -0
  483. yap_studio/video_type_command.py +145 -0
  484. yap_studio/watch/__init__.py +90 -0
  485. yap_studio/watch/command.py +198 -0
  486. yap_studio/watch/findings.py +527 -0
  487. yap_studio/watch/images.py +271 -0
  488. yap_studio/watch/measure.py +264 -0
  489. yap_studio/watch/pack.py +1038 -0
  490. yap_studio/watch/scan.py +429 -0
  491. yap_studio/wide_cards.py +600 -0
  492. yap_studio/wide_titles.py +116 -0
  493. yap_studio/youtube_chapters.py +70 -0
  494. yap_studio/zoom_curve.py +289 -0
  495. yap_studio-1.2.0.dist-info/METADATA +346 -0
  496. yap_studio-1.2.0.dist-info/RECORD +510 -0
  497. yap_studio-1.2.0.dist-info/WHEEL +4 -0
  498. yap_studio-1.2.0.dist-info/entry_points.txt +2 -0
  499. yap_studio-1.2.0.dist-info/licenses/LICENSE +202 -0
  500. yap_studio-1.2.0.dist-info/licenses/LICENSES/Apache-2.0.txt +202 -0
  501. yap_studio-1.2.0.dist-info/licenses/LICENSES/OFL-alata.txt +93 -0
  502. yap_studio-1.2.0.dist-info/licenses/LICENSES/OFL-antonsc.txt +93 -0
  503. yap_studio-1.2.0.dist-info/licenses/LICENSES/OFL-archivoblack.txt +93 -0
  504. yap_studio-1.2.0.dist-info/licenses/LICENSES/OFL-geist.txt +93 -0
  505. yap_studio-1.2.0.dist-info/licenses/LICENSES/OFL-gelasio.txt +93 -0
  506. yap_studio-1.2.0.dist-info/licenses/LICENSES/OFL-instrumentserif.txt +93 -0
  507. yap_studio-1.2.0.dist-info/licenses/LICENSES/OFL-inter.txt +93 -0
  508. yap_studio-1.2.0.dist-info/licenses/LICENSES/OFL-spacemono.txt +93 -0
  509. yap_studio-1.2.0.dist-info/licenses/LICENSES/OFL-titanone.txt +94 -0
  510. yap_studio-1.2.0.dist-info/licenses/NOTICE +56 -0
yap_studio/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """Yap Studio."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import PackageNotFoundError, version
6
+
7
+ try:
8
+ __version__ = version("yap-studio")
9
+ except PackageNotFoundError:
10
+ __version__ = "0+unknown"
11
+
12
+ __all__ = ["__version__"]
yap_studio/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Run Yap Studio as a module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from yap_studio.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(main())
@@ -0,0 +1,4 @@
1
+ """Affiliate links used in creator-facing setup messages."""
2
+
3
+ ELEVENLABS_AFFILIATE_URL = "https://littlemight.com/recommend/elevenlabs"
4
+ WAVESPEED_AFFILIATE_URL = "https://littlemight.com/recommends/wavespeed"
yap_studio/audio.py ADDED
@@ -0,0 +1,601 @@
1
+ """Audio decoding and signal measurements on the container clock."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import json
7
+ import math
8
+ import os
9
+ from dataclasses import asdict, dataclass, replace
10
+ from fractions import Fraction
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import numpy as np
15
+
16
+ from yap_studio import ffmpeg
17
+ from yap_studio.errors import InputError, ProcessingError, ValidationError
18
+
19
+
20
+ def _samples(seconds: float, sr: int) -> int:
21
+ return math.floor(seconds * sr + 0.5)
22
+
23
+
24
+ def _validate_rate(sr: int) -> None:
25
+ if sr <= 0:
26
+ raise ValidationError("sr", "must be positive")
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class Discontinuity:
31
+ """A break in the audio stream's own timestamps.
32
+
33
+ ``samples`` is positive for a gap (the next frame starts later than the
34
+ previous one ends) and negative for an overlap. ``time_s`` is the container
35
+ time where the previous frame's timestamps end. ``position`` counts decoded
36
+ samples before the break, at the stream's native rate.
37
+ """
38
+
39
+ frame: int
40
+ time_s: float
41
+ position: int
42
+ samples: int
43
+ ms: float
44
+
45
+ def to_dict(self) -> dict[str, Any]:
46
+ """Return a JSON-compatible representation."""
47
+ return {**asdict(self), "kind": "gap" if self.samples > 0 else "overlap"}
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class Timeline:
52
+ """Decoded audio frames compared with their container timestamps.
53
+
54
+ ``decoded_samples`` is the audio content: every decoded sample, in order.
55
+ ``span_samples`` runs from the first frame's timestamp to the end of the
56
+ last frame by its timestamp. Breaks no larger than ``tolerance_samples``
57
+ (one sample, or one tick of a coarser stream time base) are rounding.
58
+
59
+ ``fills`` places the content on the container clock the way a player that
60
+ never drops decoded audio does: each frame starts at its timestamp or where
61
+ the previous frame ends, whichever is later. Each entry is a decoded sample
62
+ position and the silence inserted before it. Overlaps insert nothing, so the
63
+ sound after an overlap runs late by its size.
64
+ """
65
+
66
+ sample_rate: int
67
+ frames: int
68
+ decoded_samples: int
69
+ span_samples: int
70
+ tolerance_samples: int
71
+ discontinuities: tuple[Discontinuity, ...]
72
+ fills: tuple[tuple[int, int], ...] = ()
73
+
74
+ @property
75
+ def fill_samples(self) -> int:
76
+ """Return the silence inserted to keep frames on their timestamps."""
77
+ return sum(count for _, count in self.fills)
78
+
79
+ @property
80
+ def gap_samples(self) -> int:
81
+ """Return the total of all gaps, in native samples."""
82
+ return sum(item.samples for item in self.discontinuities if item.samples > 0)
83
+
84
+ @property
85
+ def overlap_samples(self) -> int:
86
+ """Return the total of all overlaps as a positive number of native samples."""
87
+ return -sum(item.samples for item in self.discontinuities if item.samples < 0)
88
+
89
+ def to_dict(self) -> dict[str, Any]:
90
+ """Return a JSON-compatible representation."""
91
+ return {
92
+ "sample_rate": self.sample_rate,
93
+ "frames": self.frames,
94
+ "decoded_samples": self.decoded_samples,
95
+ "span_samples": self.span_samples,
96
+ "tolerance_samples": self.tolerance_samples,
97
+ "gap_samples": self.gap_samples,
98
+ "overlap_samples": self.overlap_samples,
99
+ "fill_samples": self.fill_samples,
100
+ "discontinuities": [item.to_dict() for item in self.discontinuities],
101
+ }
102
+
103
+
104
+ def _parse_frames(text: str) -> tuple[Fraction | None, list[tuple[int | None, int]]]:
105
+ time_base: Fraction | None = None
106
+ frames: list[tuple[int | None, int]] = []
107
+ for line in text.splitlines():
108
+ fields = line.strip().split(",")
109
+ if fields[0] == "stream" and len(fields) >= 2:
110
+ try:
111
+ time_base = Fraction(fields[1])
112
+ except (ValueError, ZeroDivisionError):
113
+ time_base = None
114
+ elif fields[0] == "frame" and len(fields) >= 3:
115
+ try:
116
+ count = int(fields[2])
117
+ except ValueError:
118
+ continue
119
+ try:
120
+ pts = int(fields[1])
121
+ except ValueError:
122
+ pts = None
123
+ frames.append((pts, count))
124
+ return time_base, frames
125
+
126
+
127
+ @functools.lru_cache(maxsize=32)
128
+ def _timeline(
129
+ path: str,
130
+ size: int,
131
+ modified_ns: int,
132
+ inode: int,
133
+ changed_ns: int,
134
+ stream_index: int,
135
+ sample_rate: int,
136
+ format_start_s: float,
137
+ ) -> Timeline:
138
+ """Probe decoded frame timestamps once per file version."""
139
+ # Cache keys only: a rewritten or replaced file is probed again, even at the same size and time.
140
+ del size, modified_ns, inode, changed_ns
141
+ result = ffmpeg.run(
142
+ [
143
+ "ffprobe",
144
+ "-v",
145
+ "error",
146
+ "-select_streams",
147
+ str(stream_index),
148
+ "-show_entries",
149
+ "stream=time_base:frame=pts,nb_samples",
150
+ "-of",
151
+ "csv=p=1",
152
+ path,
153
+ ],
154
+ timeout_s=ffmpeg.DEFAULT_TIMEOUT_S,
155
+ )
156
+ output = result.stdout
157
+ if isinstance(output, bytes):
158
+ output = output.decode("utf-8", errors="replace")
159
+ time_base, frames = _parse_frames(output or "")
160
+ ticks = time_base * sample_rate if time_base else Fraction(1)
161
+ tolerance = max(1, math.ceil(ticks))
162
+ breaks: list[Discontinuity] = []
163
+ fills: list[tuple[int, int]] = []
164
+ decoded = 0
165
+ first: Fraction | None = None
166
+ end: Fraction | None = None
167
+ played: Fraction | None = None
168
+ for index, (pts, count) in enumerate(frames):
169
+ stamp = Fraction(pts) * ticks if pts is not None and time_base else None
170
+ if played is not None and stamp is not None and stamp - played > tolerance:
171
+ silence = round(stamp - played)
172
+ fills.append((decoded, silence))
173
+ played += silence
174
+ if end is None:
175
+ start = stamp if stamp is not None else Fraction(0)
176
+ first = start
177
+ played = start
178
+ elif stamp is None:
179
+ start = end
180
+ else:
181
+ delta = stamp - end
182
+ if abs(delta) > tolerance:
183
+ size_samples = round(delta)
184
+ breaks.append(
185
+ Discontinuity(
186
+ frame=index,
187
+ time_s=float(end / sample_rate) - format_start_s,
188
+ position=decoded,
189
+ samples=size_samples,
190
+ ms=size_samples * 1000 / sample_rate,
191
+ )
192
+ )
193
+ start = stamp
194
+ else:
195
+ # Rounded timestamps stay on the running clock.
196
+ start = end
197
+ end = start + count
198
+ played += count
199
+ decoded += count
200
+ span = round(end - first) if end is not None and first is not None else 0
201
+ return Timeline(
202
+ sample_rate=sample_rate,
203
+ frames=len(frames),
204
+ decoded_samples=decoded,
205
+ span_samples=span,
206
+ tolerance_samples=tolerance,
207
+ discontinuities=tuple(breaks),
208
+ fills=tuple(fills),
209
+ )
210
+
211
+
212
+ def _clock_for(source: Path, info: ffmpeg.MediaInfo) -> Timeline:
213
+ status = source.stat()
214
+ return _timeline(
215
+ str(source.resolve()),
216
+ status.st_size,
217
+ status.st_mtime_ns,
218
+ status.st_ino,
219
+ status.st_ctime_ns,
220
+ info.audio_index,
221
+ info.sample_rate,
222
+ info.format_start_s,
223
+ )
224
+
225
+
226
+ def timeline(path: str | os.PathLike[str]) -> Timeline:
227
+ """Compare the selected audio stream's decoded frames with their timestamps."""
228
+ source = Path(path)
229
+ info = ffmpeg.probe(source, count_frames=False)
230
+ if not info.has_audio or info.audio_index is None or info.sample_rate is None:
231
+ raise InputError(str(source), "no audio stream")
232
+ return _clock_for(source, info)
233
+
234
+
235
+ def _fill_gaps(
236
+ audio: np.ndarray, clock: Timeline, native_rate: int, output_rate: int, source: Path
237
+ ) -> np.ndarray:
238
+ """Insert the timeline's silences into sequentially decoded audio."""
239
+ if not clock.fills:
240
+ return audio
241
+ scale = Fraction(output_rate, native_rate)
242
+ expected = round(clock.decoded_samples * scale)
243
+ # Resampling can round the stream length by a sample or two.
244
+ slack = 0 if output_rate == native_rate else 4
245
+ if abs(len(audio) - expected) > slack:
246
+ raise ProcessingError(
247
+ f"decode {source}",
248
+ 1,
249
+ f"decoded {len(audio)} audio samples but the frame timestamps describe "
250
+ f"{expected}; the audio cannot be placed on the container clock",
251
+ code="audio_clock_mismatch",
252
+ )
253
+ pieces: list[np.ndarray] = []
254
+ previous = 0
255
+ for position, count in clock.fills:
256
+ at = min(len(audio), round(position * scale))
257
+ pieces.append(audio[previous:at])
258
+ pieces.append(np.zeros((round(count * scale), *audio.shape[1:]), dtype=np.float32))
259
+ previous = at
260
+ pieces.append(audio[previous:])
261
+ return np.concatenate(pieces, axis=0)
262
+
263
+
264
+ @dataclass(frozen=True)
265
+ class Located:
266
+ """A signal location and the confidence measurements for its peak."""
267
+
268
+ time_s: float
269
+ corr: float
270
+ ambiguity: float
271
+
272
+ def to_dict(self) -> dict[str, Any]:
273
+ """Return a JSON-compatible representation."""
274
+ return asdict(self)
275
+
276
+
277
+ def _selected_audio_info(source: Path, stream: int | None) -> ffmpeg.MediaInfo:
278
+ """Probe facts for an audio ordinal, retaining the container clock."""
279
+ if stream is not None and (
280
+ isinstance(stream, bool) or not isinstance(stream, int) or stream < 0
281
+ ):
282
+ raise ValidationError("stream", "must be a non-negative audio ordinal")
283
+ info = ffmpeg.probe(source, count_frames=False)
284
+ if stream is None:
285
+ return info
286
+ result = ffmpeg.run([
287
+ "ffprobe", "-v", "error", "-select_streams", f"a:{stream}",
288
+ "-show_streams", "-of", "json", str(source),
289
+ ], timeout_s=ffmpeg.PROBE_TIMEOUT_S)
290
+ streams = json.loads(result.stdout).get("streams", [])
291
+ if not streams:
292
+ raise InputError(str(source), "no audio stream for the selected ordinal")
293
+ selected = streams[0]
294
+ start = ffmpeg._optional_float(selected.get("start_time"))
295
+ return replace(
296
+ info,
297
+ has_audio=True,
298
+ audio_index=ffmpeg._optional_int(selected.get("index")),
299
+ audio_codec=selected.get("codec_name"),
300
+ sample_rate=ffmpeg._optional_int(selected.get("sample_rate")),
301
+ channels=ffmpeg._optional_int(selected.get("channels")),
302
+ audio_start_s=start,
303
+ audio_offset_s=round((start or 0.0) - info.format_start_s, 6),
304
+ audio_duration_s=ffmpeg._optional_float(selected.get("duration")),
305
+ )
306
+
307
+
308
+ def decode(
309
+ path: str | os.PathLike[str],
310
+ *,
311
+ stream: int | None = None,
312
+ sr: int | None = None,
313
+ mono: bool = True,
314
+ start_s: float | None = None,
315
+ dur_s: float | None = None,
316
+ ) -> tuple[int, np.ndarray]:
317
+ """Decode the selected audio stream on the container clock.
318
+
319
+ ``stream`` is an audio ordinal (``0:a:N``); None selects the first stream.
320
+ Sample zero is container time zero, and sample N plays at N / sr for the
321
+ whole stream. Where the audio timestamps jump ahead, the gap is filled with
322
+ silence so later sound stays on its timestamps. Where they step back, the
323
+ decoded sound keeps playing in order and nothing is dropped, the way a
324
+ player that never discards decoded audio plays it; the sound after such an
325
+ overlap runs late by its size. ``timeline`` reports both kinds of break.
326
+ Raises ProcessingError when the decoded length does not match the frame
327
+ timestamps, because the gaps could not then be placed.
328
+ """
329
+ source = Path(path)
330
+ info = _selected_audio_info(source, stream)
331
+ if (
332
+ not info.has_audio
333
+ or info.audio_index is None
334
+ or info.sample_rate is None
335
+ or info.channels is None
336
+ ):
337
+ raise InputError(str(source), "no audio stream")
338
+ output_rate = info.sample_rate if sr is None else sr
339
+ _validate_rate(output_rate)
340
+ if start_s is not None and start_s < 0:
341
+ raise ValidationError("start_s", "must be non-negative")
342
+ if dur_s is not None and dur_s < 0:
343
+ raise ValidationError("dur_s", "must be non-negative")
344
+
345
+ command = [
346
+ "ffmpeg",
347
+ "-loglevel",
348
+ "error",
349
+ "-i",
350
+ str(source),
351
+ "-map",
352
+ f"0:{info.audio_index}",
353
+ "-vn",
354
+ ]
355
+ channels = 1 if mono else info.channels
356
+ if mono:
357
+ command.extend(["-ac", "1"])
358
+ if sr is not None:
359
+ command.extend(["-ar", str(output_rate)])
360
+ command.extend(["-f", "f32le", "-"])
361
+ result = ffmpeg.run(command, timeout_s=ffmpeg.DEFAULT_TIMEOUT_S, binary=True)
362
+ audio = np.frombuffer(result.stdout, dtype="<f4").copy()
363
+ if channels > 1:
364
+ audio = audio[: len(audio) - len(audio) % channels].reshape(-1, channels)
365
+ # The raw PCM pipe drops timestamps; restore gaps from the frame timestamps.
366
+ audio = _fill_gaps(audio, _clock_for(source, info), info.sample_rate, output_rate, source)
367
+
368
+ offset = _samples(info.audio_offset_s, output_rate)
369
+ if offset > 0:
370
+ shape = (offset,) if mono else (offset, channels)
371
+ audio = np.concatenate((np.zeros(shape, dtype=np.float32), audio), axis=0)
372
+ elif offset < 0:
373
+ audio = audio[min(-offset, len(audio)) :]
374
+
375
+ first = _samples(start_s or 0.0, output_rate)
376
+ if dur_s is None:
377
+ return output_rate, audio[first:].copy()
378
+ count = _samples(dur_s, output_rate)
379
+ shape = (count,) if mono else (count, channels)
380
+ sliced = np.zeros(shape, dtype=np.float32)
381
+ available = max(0, min(count, len(audio) - first))
382
+ if available:
383
+ sliced[:available] = audio[first : first + available]
384
+ return output_rate, sliced
385
+
386
+
387
+ def raised_cosine(n: int) -> np.ndarray:
388
+ """Return a half raised-cosine ramp from zero to one."""
389
+ if n < 0:
390
+ raise ValidationError("n", "must be non-negative")
391
+ if n == 0:
392
+ return np.empty(0, dtype=np.float32)
393
+ return (0.5 - 0.5 * np.cos(np.linspace(0, np.pi, n))).astype(np.float32)
394
+
395
+
396
+ def rms_db(sig: np.ndarray, center_s: float, sr: int, half_s: float = 0.005) -> float:
397
+ """Measure RMS level in a window centered at a time."""
398
+ _validate_rate(sr)
399
+ if half_s < 0:
400
+ raise ValidationError("half_s", "must be non-negative")
401
+ first = max(0, _samples(center_s - half_s, sr))
402
+ last = _samples(center_s + half_s, sr)
403
+ segment = np.asarray(sig)[first:last]
404
+ if len(segment) == 0:
405
+ return -120.0
406
+ rms = np.sqrt(np.mean(segment.astype(np.float64) ** 2))
407
+ return float(20 * np.log10(rms + 1e-9))
408
+
409
+
410
+ def click_db(m: np.ndarray, j: int, sr: int) -> float:
411
+ """Measure join sharpness against nearby high-frequency signal peaks."""
412
+ _validate_rate(sr)
413
+ signal = np.asarray(m)
414
+ if signal.ndim != 1:
415
+ raise ValidationError("m", "must be a mono array")
416
+ if not 0 <= j < len(signal):
417
+ raise ValidationError("j", "must index the signal")
418
+ difference = np.abs(np.diff(signal.astype(np.float64)))
419
+ w2, w100, w5 = int(0.002 * sr), int(0.1 * sr), int(0.005 * sr)
420
+ near_values = difference[max(0, j - w2) : min(len(difference), j + w2)]
421
+ if not len(near_values):
422
+ return -120.0
423
+ near = near_values.max()
424
+ low, high = max(0, j - w100), min(len(difference), j + w100)
425
+ mask = np.ones(high - low, dtype=bool)
426
+ mask[max(0, j - w5 - low) : max(0, j + w5 - low)] = False
427
+ reference_values = difference[low:high][mask]
428
+ reference = (np.percentile(reference_values, 99.5) if len(reference_values) else 0.0) + 1e-12
429
+ return float(20 * np.log10(near / reference + 1e-12))
430
+
431
+
432
+ def pause_at(
433
+ m: np.ndarray,
434
+ j: int,
435
+ sr: int,
436
+ thr_db: float = -45.0,
437
+ step_s: float = 0.01,
438
+ ) -> float:
439
+ """Measure the run of quiet windows touching a sample position."""
440
+ _validate_rate(sr)
441
+ signal = np.asarray(m)
442
+ if signal.ndim != 1:
443
+ raise ValidationError("m", "must be a mono array")
444
+ if not 0 <= j < len(signal):
445
+ raise ValidationError("j", "must index the signal")
446
+ step = _samples(step_s, sr)
447
+ if step <= 0:
448
+ raise ValidationError("step_s", "must span at least one sample")
449
+ half = step // 2
450
+
451
+ def level(index: int) -> float:
452
+ return rms_db(signal, index / sr, sr, half / sr)
453
+
454
+ left = 0
455
+ index = j
456
+ while index - step > 0 and level(index - half) < thr_db:
457
+ left += step
458
+ index -= step
459
+ right = 0
460
+ index = j
461
+ while index + step < len(signal) and level(index + half) < thr_db:
462
+ right += step
463
+ index += step
464
+ return (left + right) / sr
465
+
466
+
467
+ def splice(
468
+ x: np.ndarray,
469
+ pieces_samples: list[tuple[int, int]],
470
+ fade: int,
471
+ ) -> tuple[np.ndarray, list[int]]:
472
+ """Concatenate ranges with non-overlapping raised-cosine fades."""
473
+ signal = np.asarray(x)
474
+ if signal.ndim not in {1, 2}:
475
+ raise ValidationError("x", "must have one or two dimensions")
476
+ if fade < 0:
477
+ raise ValidationError("fade", "must be non-negative")
478
+ if not pieces_samples:
479
+ return signal[:0].copy(), []
480
+ ramp = raised_cosine(fade)
481
+ if signal.ndim == 2:
482
+ ramp = ramp[:, None]
483
+ output: list[np.ndarray] = []
484
+ joins: list[int] = []
485
+ accumulated = 0
486
+ for index, (start, end) in enumerate(pieces_samples):
487
+ if start < 0 or end < start or end > len(signal):
488
+ raise ValidationError("pieces_samples", "ranges must be ordered within x")
489
+ segment = signal[start:end].copy()
490
+ if fade and index > 0 and len(segment) >= fade:
491
+ segment[:fade] *= ramp
492
+ if fade and index < len(pieces_samples) - 1 and len(segment) >= fade:
493
+ segment[-fade:] *= ramp[::-1]
494
+ output.append(segment)
495
+ accumulated += len(segment)
496
+ if index < len(pieces_samples) - 1:
497
+ joins.append(accumulated)
498
+ return np.concatenate(output), joins
499
+
500
+
501
+ def _fft_valid_correlation(hay: np.ndarray, needle: np.ndarray) -> np.ndarray:
502
+ size = len(hay) + len(needle) - 1
503
+ nfft = 1 << size.bit_length()
504
+ convolution = np.fft.irfft(
505
+ np.fft.rfft(hay, nfft) * np.fft.rfft(needle[::-1], nfft), nfft
506
+ )[:size]
507
+ return convolution[len(needle) - 1 : len(hay)]
508
+
509
+
510
+ def _whiten(signal: np.ndarray) -> np.ndarray:
511
+ """First-difference and robustly compress magnitude so tones do not dominate."""
512
+ difference = np.diff(signal)
513
+ return np.sign(difference) * np.sqrt(np.sqrt(np.abs(difference)))
514
+
515
+
516
+ def locate(
517
+ needle: np.ndarray,
518
+ hay: np.ndarray,
519
+ sr: int,
520
+ *,
521
+ expect_s: float,
522
+ search_s: float,
523
+ ) -> Located:
524
+ """Locate audio with a whitened, energy-normalised correlation."""
525
+ _validate_rate(sr)
526
+ if search_s < 0:
527
+ raise ValidationError("search_s", "must be non-negative")
528
+ query = np.asarray(needle, dtype=np.float64)
529
+ signal = np.asarray(hay, dtype=np.float64)
530
+ if query.ndim != 1 or signal.ndim != 1:
531
+ raise ValidationError("audio", "needle and hay must be mono arrays")
532
+ if len(query) < 2:
533
+ raise ValidationError("needle", "must contain at least two samples")
534
+ whitened_query = _whiten(query)
535
+ query_energy = float(np.dot(whitened_query, whitened_query))
536
+ if query_energy < 1e-12:
537
+ return Located(expect_s, 0.0, 1.0)
538
+
539
+ first = max(0, _samples(expect_s - search_s, sr))
540
+ last = min(len(signal) - len(query), _samples(expect_s + search_s, sr))
541
+ if last < first:
542
+ raise ValidationError("hay", "search window cannot fit the needle")
543
+ # One extra original sample is needed because first differences preserve start indices.
544
+ region = signal[first : last + len(query)]
545
+ whitened_hay = _whiten(region)
546
+ products = _fft_valid_correlation(whitened_hay, whitened_query)
547
+ squared = whitened_hay * whitened_hay
548
+ cumulative = np.concatenate(([0.0], np.cumsum(squared)))
549
+ width = len(whitened_query)
550
+ energies = cumulative[width:] - cumulative[:-width]
551
+ correlations = products / (np.sqrt(energies * query_energy) + 1e-12)
552
+
553
+ best = int(np.argmax(correlations))
554
+ peak = float(correlations[best])
555
+ fraction = 0.0
556
+ if 0 < best < len(correlations) - 1:
557
+ left, center, right = correlations[best - 1 : best + 2]
558
+ denominator = left - 2.0 * center + right
559
+ if abs(denominator) > 1e-15:
560
+ fraction = float(np.clip(0.5 * (left - right) / denominator, -1.0, 1.0))
561
+
562
+ local = np.ones(len(correlations), dtype=bool)
563
+ if len(correlations) > 1:
564
+ local[1:] &= correlations[1:] >= correlations[:-1]
565
+ local[:-1] &= correlations[:-1] >= correlations[1:]
566
+ exclusion = _samples(0.005, sr)
567
+ indices = np.flatnonzero(local & (np.abs(np.arange(len(correlations)) - best) > exclusion))
568
+ second = float(np.max(correlations[indices])) if len(indices) else 0.0
569
+ ambiguity = float(np.clip(second / peak, 0.0, 1.0)) if peak > 0 else 1.0
570
+ return Located((first + best + fraction) / sr, peak, ambiguity)
571
+
572
+
573
+ def lag(
574
+ a: np.ndarray,
575
+ b: np.ndarray,
576
+ sr: int,
577
+ max_lag_s: float = 0.5,
578
+ ) -> tuple[float, float]:
579
+ """Return lag of b against a, positive when b plays late, and peak correlation."""
580
+ _validate_rate(sr)
581
+ if max_lag_s < 0:
582
+ raise ValidationError("max_lag_s", "must be non-negative")
583
+ first = np.asarray(a, dtype=np.float64)
584
+ second = np.asarray(b, dtype=np.float64)
585
+ if first.ndim != 1 or second.ndim != 1 or not len(first) or not len(second):
586
+ raise ValidationError("audio", "a and b must be non-empty mono arrays")
587
+ first = first - first.mean()
588
+ second = second - second.mean()
589
+ nfft = 1 << (len(first) + len(second) - 1).bit_length()
590
+ correlation = np.fft.irfft(
591
+ np.fft.rfft(second, nfft) * np.conj(np.fft.rfft(first, nfft)), nfft
592
+ )
593
+ radius = min(_samples(max_lag_s, sr), len(correlation) // 2)
594
+ candidates = (
595
+ correlation[:1]
596
+ if radius == 0
597
+ else np.concatenate((correlation[-radius:], correlation[: radius + 1]))
598
+ )
599
+ index = int(np.argmax(candidates)) - radius
600
+ energy = np.sqrt(np.dot(first, first) * np.dot(second, second)) + 1e-12
601
+ return index / sr, float(candidates.max() / energy)
@@ -0,0 +1,60 @@
1
+ """Fixed speech treatments at 48 kHz, with unity makeup gain and no timing shift."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from yap_studio.errors import YapError
6
+
7
+ # Each entry is a complete, fixed treatment. The two-pole highpass cuts rumble;
8
+ # denoising is limited to 6 dB; compression uses no lookahead and no makeup gain.
9
+ # afftdn delays audio by two 12.5 ms blocks at 48 kHz. Feed 1200 trailing samples
10
+ # before removing that delay, so the final input samples are retained as well.
11
+ # The room repair uses a mild downward expander, capped at about 6 dB reduction.
12
+ # It lowers quiet room tails without trying to remove reverberation from speech.
13
+ # Proximity reduces boomy bass from a close mic.
14
+ # Its 100 Hz highpass replaces the 80 Hz highpass.
15
+ # The broadcast compressor replaces the light compressor rather than stacking it.
16
+ VOICE_FILTERS: dict[str, str] = {
17
+ "none": "",
18
+ "rumble": "highpass=f=80:p=2",
19
+ "proximity": "highpass=f=100:p=2,equalizer=f=200:t=q:w=1:g=-3",
20
+ "proximity_strong": "highpass=f=100:p=2,equalizer=f=200:t=q:w=0.8:g=-5",
21
+ "room": "agate=threshold=0.02:ratio=1.3:range=0.5:attack=10:release=200:makeup=1",
22
+ "harshness": "deesser=i=0.2:m=0.25:f=0.5,highshelf=f=5500:g=-2",
23
+ "clean": (
24
+ "highpass=f=80:p=2,apad=pad_len=1200,afftdn=nr=6:nf=-50,"
25
+ "atrim=start_sample=1200,asetpts=N/SR/TB,"
26
+ "acompressor=threshold=0.125:ratio=1.5:attack=15:release=150:makeup=1"
27
+ ),
28
+ "broadcast": (
29
+ "highpass=f=80:p=2,apad=pad_len=1200,afftdn=nr=6:nf=-50,"
30
+ "atrim=start_sample=1200,asetpts=N/SR/TB,equalizer=f=3000:t=q:w=0.7:g=1.5,"
31
+ "acompressor=threshold=0.1:ratio=2.5:attack=10:release=150:makeup=1"
32
+ ),
33
+ "warm": (
34
+ "highpass=f=80:p=2,equalizer=f=200:t=q:w=0.7:g=1.5,highshelf=f=5500:g=-2,"
35
+ "acompressor=threshold=0.125:ratio=1.5:attack=15:release=150:makeup=1"
36
+ ),
37
+ }
38
+
39
+
40
+ def voice_filters(preset: str, *, rumble: bool = False, room: bool = False,
41
+ harshness: bool = False, proximity: str = "off") -> str:
42
+ """Apply repairs in fixed order before the preset, omitting repeated filters.
43
+
44
+ All chains expect 48 kHz audio. An empty treatment returns an empty string.
45
+ """
46
+ if preset not in {"none", "clean", "broadcast", "warm"}:
47
+ raise YapError("Choose voice preset none, clean, broadcast or warm.",
48
+ error_type="audio_error", code="voice_chain_invalid",
49
+ suggested_action="Correct the voice preset in the style.")
50
+ if proximity not in ("off", "gentle", "strong"):
51
+ raise YapError("Choose proximity repair off, gentle or strong.",
52
+ error_type="audio_error", code="voice_chain_invalid",
53
+ suggested_action="Correct the proximity repair in the style.")
54
+ proximity_name = "proximity_strong" if proximity == "strong" else "proximity"
55
+ names = [name for name, enabled in (("rumble", rumble),
56
+ (proximity_name, proximity != "off"), ("room", room),
57
+ ("harshness", harshness)) if enabled]
58
+ filters = [part for name in [*names, preset] for part in VOICE_FILTERS[name].split(",")
59
+ if part and not (proximity != "off" and part == "highpass=f=80:p=2")]
60
+ return ",".join(dict.fromkeys(filters))