diffusers-workflow 0.4.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 (260) hide show
  1. diffusers_workflow-0.4.0.dist-info/METADATA +318 -0
  2. diffusers_workflow-0.4.0.dist-info/RECORD +260 -0
  3. diffusers_workflow-0.4.0.dist-info/WHEEL +5 -0
  4. diffusers_workflow-0.4.0.dist-info/entry_points.txt +7 -0
  5. diffusers_workflow-0.4.0.dist-info/licenses/LICENSE +201 -0
  6. diffusers_workflow-0.4.0.dist-info/top_level.txt +2 -0
  7. dw/__init__.py +440 -0
  8. dw/adapter_compatibility.py +226 -0
  9. dw/arguments.py +1231 -0
  10. dw/assessment_rules.py +159 -0
  11. dw/assets.py +130 -0
  12. dw/cache_blocks.json +16 -0
  13. dw/cache_blocks.py +146 -0
  14. dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
  15. dw/content_types.py +150 -0
  16. dw/dissolve_frame_errors.py +121 -0
  17. dw/docs/ACCELERATION.md +352 -0
  18. dw/docs/AGENT_LOOP.md +95 -0
  19. dw/docs/DEPENDENCIES.md +91 -0
  20. dw/docs/IP_ADAPTER.md +109 -0
  21. dw/docs/LORAS.md +131 -0
  22. dw/docs/MCP.md +517 -0
  23. dw/docs/PROMPT_WEIGHTING.md +78 -0
  24. dw/docs/QUANTIZATION.md +230 -0
  25. dw/docs/RECIPES_24GB.md +201 -0
  26. dw/docs/RELEASING.md +195 -0
  27. dw/docs/REMOTE.md +140 -0
  28. dw/docs/REPL_COMMANDS.md +121 -0
  29. dw/docs/REPL_WORKER_GUIDE.md +51 -0
  30. dw/docs/SECURITY.md +272 -0
  31. dw/docs/SECURITY_QUICKREF.md +112 -0
  32. dw/docs/SERVER.md +679 -0
  33. dw/docs/TASKS.md +1741 -0
  34. dw/docs/TESTING.md +71 -0
  35. dw/docs/WORKFLOW_GUIDE.md +2038 -0
  36. dw/docs/WORKSPACES.md +316 -0
  37. dw/download_watch.py +335 -0
  38. dw/elision.py +306 -0
  39. dw/events.py +275 -0
  40. dw/for_each.py +409 -0
  41. dw/host_memory.py +258 -0
  42. dw/host_memory_projection.py +230 -0
  43. dw/hub_cache.py +432 -0
  44. dw/introspection.py +1228 -0
  45. dw/kernel_availability.py +208 -0
  46. dw/locations.py +599 -0
  47. dw/log_setup.py +45 -0
  48. dw/loudness.py +82 -0
  49. dw/media_audio.py +217 -0
  50. dw/media_frames.py +367 -0
  51. dw/media_info.py +297 -0
  52. dw/pipeline_processors/chain.py +821 -0
  53. dw/pipeline_processors/config_objects.py +237 -0
  54. dw/pipeline_processors/pipeline.py +2297 -0
  55. dw/pipeline_processors/remote.py +46 -0
  56. dw/plan.py +920 -0
  57. dw/previous_results.py +411 -0
  58. dw/probe_paths.py +59 -0
  59. dw/prompt_schema.json +48 -0
  60. dw/prompt_weighting.py +378 -0
  61. dw/prompts.py +159 -0
  62. dw/realize.py +250 -0
  63. dw/reference_limits.py +215 -0
  64. dw/reference_names.py +125 -0
  65. dw/repl.py +338 -0
  66. dw/repl_commands.py +836 -0
  67. dw/repl_worker.py +159 -0
  68. dw/result.py +1720 -0
  69. dw/result_fps.py +82 -0
  70. dw/run.py +162 -0
  71. dw/runs.py +768 -0
  72. dw/scalar_result_validation.py +97 -0
  73. dw/schema.py +283 -0
  74. dw/security.py +1038 -0
  75. dw/select_validation.py +115 -0
  76. dw/serve.py +277 -0
  77. dw/server/__init__.py +2 -0
  78. dw/server/app.py +4586 -0
  79. dw/server/assess.py +132 -0
  80. dw/server/catalog_shape.py +487 -0
  81. dw/server/enhancers.py +129 -0
  82. dw/server/exports.py +480 -0
  83. dw/server/guides.py +257 -0
  84. dw/server/jobs.py +1561 -0
  85. dw/server/mcp_mount.py +95 -0
  86. dw/server/netinfo.py +124 -0
  87. dw/server/observed_cost.py +379 -0
  88. dw/server/sysinfo.py +71 -0
  89. dw/server/ui/assets/abap-08VXUWAP.js +1 -0
  90. dw/server/ui/assets/apex-BWPQTe0t.js +1 -0
  91. dw/server/ui/assets/azcli-Bc_sGQ0U.js +1 -0
  92. dw/server/ui/assets/bat-i0X4ZdIN.js +1 -0
  93. dw/server/ui/assets/bicep-B5-_aFwp.js +2 -0
  94. dw/server/ui/assets/cameligo-DMUM7wLl.js +1 -0
  95. dw/server/ui/assets/clojure-Cm7r79vr.js +1 -0
  96. dw/server/ui/assets/codicon-Brq4_Ui5.ttf +0 -0
  97. dw/server/ui/assets/coffee-Ba7i2nA0.js +1 -0
  98. dw/server/ui/assets/cpp-C7h46wYY.js +1 -0
  99. dw/server/ui/assets/csharp-BKxtCVv1.js +1 -0
  100. dw/server/ui/assets/csp-bTuwJoIa.js +1 -0
  101. dw/server/ui/assets/css-DIMkf-bt.js +3 -0
  102. dw/server/ui/assets/css.worker-B3ciXF_0.js +93 -0
  103. dw/server/ui/assets/cssMode-CPznxfY8.js +1 -0
  104. dw/server/ui/assets/cypher-CVaqCwHa.js +1 -0
  105. dw/server/ui/assets/dart-onAF5SnQ.js +1 -0
  106. dw/server/ui/assets/dockerfile-DZFCIeNp.js +1 -0
  107. dw/server/ui/assets/ecl-D05T4iGw.js +1 -0
  108. dw/server/ui/assets/editor-jjEx9u7D.css +1 -0
  109. dw/server/ui/assets/editor.api-CpWcotrd.js +847 -0
  110. dw/server/ui/assets/editor.worker-q-txB4vs.js +30 -0
  111. dw/server/ui/assets/elixir-6RTg0lbw.js +1 -0
  112. dw/server/ui/assets/flow9-C5_-GSwl.js +1 -0
  113. dw/server/ui/assets/freemarker2-CXtRM8N4.js +3 -0
  114. dw/server/ui/assets/fsharp-C8Ef5oNN.js +1 -0
  115. dw/server/ui/assets/go-C-y9NEjX.js +1 -0
  116. dw/server/ui/assets/graphql-fmXr3nnJ.js +1 -0
  117. dw/server/ui/assets/handlebars-N7x-6NMY.js +1 -0
  118. dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
  119. dw/server/ui/assets/html-PhsdjHSr.js +1 -0
  120. dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
  121. dw/server/ui/assets/htmlMode-Dgj0SEok.js +1 -0
  122. dw/server/ui/assets/index-3Vw6WAPW.css +1 -0
  123. dw/server/ui/assets/index-DgrYhQd9.js +43 -0
  124. dw/server/ui/assets/ini-sBoK_t0W.js +1 -0
  125. dw/server/ui/assets/java-BEtHBSE6.js +1 -0
  126. dw/server/ui/assets/javascript-BJqN9Qhv.js +1 -0
  127. dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
  128. dw/server/ui/assets/jsonMode-DbM4SWSv.js +7 -0
  129. dw/server/ui/assets/julia-Bri6UV-V.js +1 -0
  130. dw/server/ui/assets/kotlin-BOotOW0E.js +1 -0
  131. dw/server/ui/assets/less-B9JPFI3C.js +2 -0
  132. dw/server/ui/assets/lexon-CfSJPG6W.js +1 -0
  133. dw/server/ui/assets/liquid-BWr8lEc4.js +1 -0
  134. dw/server/ui/assets/lspLanguageFeatures-C1iGuDyZ.js +4 -0
  135. dw/server/ui/assets/lua-CsQS60Ue.js +1 -0
  136. dw/server/ui/assets/m3-D-oSqn_W.js +1 -0
  137. dw/server/ui/assets/markdown-Cimd5fb3.js +1 -0
  138. dw/server/ui/assets/mdx-DAdMi_0p.js +1 -0
  139. dw/server/ui/assets/mips-CIPQ_RoX.js +1 -0
  140. dw/server/ui/assets/monaco--ixms01u.css +1 -0
  141. dw/server/ui/assets/monaco-BGCeEqaw.js +56 -0
  142. dw/server/ui/assets/msdax-DauUninz.js +1 -0
  143. dw/server/ui/assets/mysql-SOo6toE5.js +1 -0
  144. dw/server/ui/assets/objective-c-FvmIjYaQ.js +1 -0
  145. dw/server/ui/assets/pascal-DrH0SRf2.js +1 -0
  146. dw/server/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
  147. dw/server/ui/assets/perl-oz_6vUea.js +1 -0
  148. dw/server/ui/assets/pgsql-DTj74zXo.js +1 -0
  149. dw/server/ui/assets/php-nr791fC2.js +1 -0
  150. dw/server/ui/assets/pla-CopQ2nXW.js +1 -0
  151. dw/server/ui/assets/postiats-43DmfD33.js +1 -0
  152. dw/server/ui/assets/powerquery-D3hlyOfw.js +1 -0
  153. dw/server/ui/assets/powershell-DmHpPYUd.js +1 -0
  154. dw/server/ui/assets/protobuf-C531GsRP.js +2 -0
  155. dw/server/ui/assets/pug-Z5eAx3Zn.js +1 -0
  156. dw/server/ui/assets/python-Bcn70HdC.js +1 -0
  157. dw/server/ui/assets/qsharp-DkqhCAOL.js +1 -0
  158. dw/server/ui/assets/r-BwWrilGY.js +1 -0
  159. dw/server/ui/assets/razor-D1HmNnby.js +1 -0
  160. dw/server/ui/assets/redis-ClamHrr6.js +1 -0
  161. dw/server/ui/assets/redshift-DT7zqm-g.js +1 -0
  162. dw/server/ui/assets/restructuredtext-BYgofb2h.js +1 -0
  163. dw/server/ui/assets/ruby-DezsRK8O.js +1 -0
  164. dw/server/ui/assets/rust-DdL9SqIa.js +1 -0
  165. dw/server/ui/assets/sb-CcwsVR0C.js +1 -0
  166. dw/server/ui/assets/scala-DHpiXF5c.js +1 -0
  167. dw/server/ui/assets/scheme-BeGwcela.js +1 -0
  168. dw/server/ui/assets/scss-gp-XZpBa.js +3 -0
  169. dw/server/ui/assets/shell-CC2rA5mh.js +1 -0
  170. dw/server/ui/assets/solidity-BEEn4gHE.js +1 -0
  171. dw/server/ui/assets/sophia-CRfGWb83.js +1 -0
  172. dw/server/ui/assets/sparql-D_Lu-MrJ.js +1 -0
  173. dw/server/ui/assets/sql-NEE52Syq.js +1 -0
  174. dw/server/ui/assets/st-DbInun42.js +1 -0
  175. dw/server/ui/assets/swift-Bxkupp3x.js +1 -0
  176. dw/server/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
  177. dw/server/ui/assets/tcl-DISqw1ZD.js +1 -0
  178. dw/server/ui/assets/ts.worker-D7T1-Ig5.js +67738 -0
  179. dw/server/ui/assets/tsMode-D6u0XmOW.js +11 -0
  180. dw/server/ui/assets/twig-De2hgUGE.js +1 -0
  181. dw/server/ui/assets/typescript-BU6v-LMV.js +1 -0
  182. dw/server/ui/assets/typespec-B8J7ngcE.js +1 -0
  183. dw/server/ui/assets/vb-DV3o63ZY.js +1 -0
  184. dw/server/ui/assets/wgsl-DpFanUEy.js +298 -0
  185. dw/server/ui/assets/workers-Cn7cTUKr.js +1 -0
  186. dw/server/ui/assets/xml--0LP2Lwk.js +1 -0
  187. dw/server/ui/assets/yaml-mpBg9jnt.js +1 -0
  188. dw/server/ui/index.html +17 -0
  189. dw/server/updater.py +192 -0
  190. dw/settings.py +98 -0
  191. dw/shot_span_preflight.py +116 -0
  192. dw/shots.py +359 -0
  193. dw/slice_preflight.py +148 -0
  194. dw/step.py +187 -0
  195. dw/step_cache.py +442 -0
  196. dw/subfolders.py +107 -0
  197. dw/task_domains.py +307 -0
  198. dw/tasks/assess.py +826 -0
  199. dw/tasks/audio_transcription.py +88 -0
  200. dw/tasks/audio_utils.py +1862 -0
  201. dw/tasks/background_remover.py +43 -0
  202. dw/tasks/borders.py +113 -0
  203. dw/tasks/compose_text.py +74 -0
  204. dw/tasks/concat_videos.py +300 -0
  205. dw/tasks/depth_estimator.py +54 -0
  206. dw/tasks/diffusion_upscale.py +109 -0
  207. dw/tasks/dissolve_videos.py +342 -0
  208. dw/tasks/format_messages.py +24 -0
  209. dw/tasks/gather.py +173 -0
  210. dw/tasks/grade.py +97 -0
  211. dw/tasks/image_to_text.py +43 -0
  212. dw/tasks/image_utils.py +764 -0
  213. dw/tasks/interpolate_frames.py +252 -0
  214. dw/tasks/judge.py +68 -0
  215. dw/tasks/model_cache.py +55 -0
  216. dw/tasks/pair_audio.py +268 -0
  217. dw/tasks/qr_code.py +19 -0
  218. dw/tasks/restore_faces.py +175 -0
  219. dw/tasks/rife_model.py +192 -0
  220. dw/tasks/segment.py +121 -0
  221. dw/tasks/select.py +111 -0
  222. dw/tasks/speech_generation.py +228 -0
  223. dw/tasks/stabilize.py +129 -0
  224. dw/tasks/task.py +920 -0
  225. dw/tasks/tensor_image.py +57 -0
  226. dw/tasks/text_generation.py +169 -0
  227. dw/tasks/text_sections.py +80 -0
  228. dw/tasks/upscale.py +203 -0
  229. dw/tasks/video_utils.py +624 -0
  230. dw/tasks/zoe_depth.py +71 -0
  231. dw/teacache.py +381 -0
  232. dw/teacache_models.json +99 -0
  233. dw/test.py +29 -0
  234. dw/type_helpers.py +231 -0
  235. dw/validate.py +68 -0
  236. dw/variable_constraints.py +444 -0
  237. dw/variables.py +443 -0
  238. dw/video_extensions.py +141 -0
  239. dw/vram_estimate.py +116 -0
  240. dw/worker.py +764 -0
  241. dw/workflow.py +2007 -0
  242. dw/workflow_schema.json +1346 -0
  243. dw/workflow_sources.py +383 -0
  244. dw/workflows/h3_context_ir.json +57 -0
  245. dw/workflows/test.json +31 -0
  246. dw/workspace.py +730 -0
  247. dw_mcp/__init__.py +6 -0
  248. dw_mcp/__main__.py +133 -0
  249. dw_mcp/assets.py +336 -0
  250. dw_mcp/authoring.py +114 -0
  251. dw_mcp/catalog.py +360 -0
  252. dw_mcp/client.py +486 -0
  253. dw_mcp/diagnose.py +371 -0
  254. dw_mcp/exports.py +84 -0
  255. dw_mcp/guides.py +35 -0
  256. dw_mcp/media.py +638 -0
  257. dw_mcp/models.py +97 -0
  258. dw_mcp/prompts.py +104 -0
  259. dw_mcp/server.py +1343 -0
  260. dw_mcp/workspaces.py +212 -0
@@ -0,0 +1,252 @@
1
+ """
2
+ Video frame interpolation via RIFE (Real-Time Intermediate Flow Estimation).
3
+
4
+ Takes a list of video frames and generates intermediate frames to increase
5
+ frame rate. Supports 2x, 4x, and 8x multipliers.
6
+
7
+ Model weights are downloaded from HuggingFace Hub on first use.
8
+ """
9
+
10
+ import logging
11
+ import torch
12
+
13
+ from ..result import AudioVideo
14
+ from ..shots import rescaled_shots
15
+ from .tensor_image import pil_to_float_tensor as _pil_to_tensor, float_tensor_to_pil
16
+ from .video_utils import frames_as_pil_list
17
+
18
+ logger = logging.getLogger("dw")
19
+
20
+ _VALID_MULTIPLIERS = {2, 4, 8}
21
+
22
+
23
+ def interpolate_frames(video, device="cpu", **kwargs):
24
+ """Interpolate between video frames using RIFE to increase frame rate.
25
+
26
+ Args:
27
+ video: The frames - a list of PIL images, a frame array or tensor, or
28
+ an AudioVideo (a concat or dissolve step's output, whose
29
+ soundtrack is dropped here and put back by pair_audio)
30
+ device: Target device ("cuda", "mps", "cpu")
31
+ **kwargs:
32
+ multiplier: Frame count multiplier — 2, 4, or 8 (default: 2)
33
+ model_name: HuggingFace repo with RIFE v4.13 weights (default: auto)
34
+ filename: Weights filename within the repo (default: auto)
35
+
36
+ Returns:
37
+ One AudioVideo holding the interpolated frames and no audio - one
38
+ artifact, where a bare frame list would fan a later step out over
39
+ every frame. pair_audio puts a soundtrack under it.
40
+ """
41
+ multiplier = int(kwargs.get("multiplier", 2))
42
+ model_name = kwargs.get("model_name", None)
43
+ filename = kwargs.get("filename", None)
44
+
45
+ if multiplier not in _VALID_MULTIPLIERS:
46
+ raise ValueError(
47
+ f"multiplier must be one of {sorted(_VALID_MULTIPLIERS)}, got {multiplier}"
48
+ )
49
+
50
+ # An AudioVideo or a frame array unwraps to its frames; a PIL list passes
51
+ # through by identity. The soundtrack does not survive - the frame count
52
+ # changes, so pair_audio is how it comes back
53
+ source_fps = getattr(video, "fps", None)
54
+ source_shots = getattr(video, "shots", None)
55
+ video = frames_as_pil_list(video)
56
+ if len(video) < 2:
57
+ raise ValueError(f"Need at least 2 frames to interpolate, got {len(video)}")
58
+
59
+ logger.info(
60
+ f"Interpolating {len(video)} frames with {multiplier}x multiplier on {device}"
61
+ )
62
+
63
+ model = _load_rife_model(device, model_name, filename)
64
+
65
+ passes = {2: 1, 4: 2, 8: 3}[multiplier]
66
+ frames = list(video)
67
+
68
+ for pass_num in range(passes):
69
+ logger.debug(
70
+ f"Interpolation pass {pass_num + 1}/{passes}: {len(frames)} frames"
71
+ )
72
+ frames = _interpolate_2x(frames, model)
73
+
74
+ logger.info(f"Interpolation complete: {len(video)} -> {len(frames)} frames")
75
+ # Multiplied, not carried: interpolation adds frames between the ones it
76
+ # was given, so playing them back at the source rate would run the clip
77
+ # `multiplier` times long. The rate that keeps the source's duration is
78
+ # the source's times the multiplier (#84)
79
+ # The shots stretch with the frames between them; the track is gone, so
80
+ # their sample side goes with it
81
+ return AudioVideo(
82
+ frames,
83
+ None,
84
+ None,
85
+ fps=source_fps * multiplier if source_fps else None,
86
+ shots=rescaled_shots(source_shots, multiplier),
87
+ )
88
+
89
+
90
+ def _interpolate_2x(frames, model):
91
+ """Single pass of 2x interpolation — insert one frame between each pair."""
92
+ result = [frames[0]]
93
+ for i in range(len(frames) - 1):
94
+ mid_frame = model(frames[i], frames[i + 1])
95
+ result.append(mid_frame)
96
+ result.append(frames[i + 1])
97
+ return result
98
+
99
+
100
+ _DEFAULT_RIFE_REPO = "imaginairy/rife-interpolation"
101
+ _DEFAULT_RIFE_FILENAME = "rife-flownet-4.13.2.safetensors"
102
+
103
+
104
+ def _pad_tensor(t, ph, pw):
105
+ """Zero-pad a (1, 3, H, W) tensor on the bottom/right to (ph, pw)."""
106
+ h, w = t.shape[2], t.shape[3]
107
+ padding = (0, pw - w, 0, ph - h)
108
+ return torch.nn.functional.pad(t, padding)
109
+
110
+
111
+ def _padded_size(h, w, multiple=32):
112
+ """Round (h, w) up to the next multiple (RIFE requires dims divisible by 32)."""
113
+ ph = ((h - 1) // multiple + 1) * multiple
114
+ pw = ((w - 1) // multiple + 1) * multiple
115
+ return ph, pw
116
+
117
+
118
+ def _make_flow_context(ph, pw, device):
119
+ """Build the warp grid and flow divisors for one padded resolution."""
120
+ tenFlow_div = torch.tensor([(pw - 1.0) / 2.0, (ph - 1.0) / 2.0], device=device)
121
+ backwarp_tenGrid = torch.cat(
122
+ [
123
+ torch.linspace(-1.0, 1.0, pw, device=device)
124
+ .view(1, 1, 1, pw)
125
+ .expand(-1, -1, ph, -1),
126
+ torch.linspace(-1.0, 1.0, ph, device=device)
127
+ .view(1, 1, ph, 1)
128
+ .expand(-1, -1, -1, pw),
129
+ ],
130
+ 1,
131
+ )
132
+ timestep = torch.full((1, 1, ph, pw), 0.5, dtype=torch.float32, device=device)
133
+ return tenFlow_div, backwarp_tenGrid, timestep
134
+
135
+
136
+ def _load_rife_model(device, model_name=None, filename=None):
137
+ """Load RIFE model and return a callable that interpolates two frames.
138
+
139
+ Args:
140
+ device: Target device string ("cuda", "mps", "cpu").
141
+ model_name: Optional HuggingFace repo ID containing RIFE v4.13 weights.
142
+ Defaults to imaginairy/rife-interpolation.
143
+ filename: Optional weights filename within the repo. Defaults to
144
+ rife-flownet-4.13.2.safetensors. Both .safetensors and torch
145
+ checkpoint formats (.pkl/.pth) are supported.
146
+
147
+ Returns:
148
+ Callable that takes (frame1: PIL.Image, frame2: PIL.Image) -> PIL.Image
149
+ """
150
+ from .rife_model import IFNet
151
+ from huggingface_hub import hf_hub_download
152
+ from .model_cache import cached_model
153
+
154
+ repo_id = model_name if model_name is not None else _DEFAULT_RIFE_REPO
155
+ weights_file = filename if filename is not None else _DEFAULT_RIFE_FILENAME
156
+
157
+ def load_net():
158
+ model_path = hf_hub_download(repo_id=repo_id, filename=weights_file)
159
+
160
+ logger.info(f"Loading RIFE IFNet v4.13 to {device}")
161
+
162
+ if weights_file.endswith(".safetensors"):
163
+ from safetensors.torch import load_file
164
+
165
+ state_dict = load_file(model_path)
166
+ else:
167
+ state_dict = torch.load(model_path, map_location="cpu", weights_only=True)
168
+
169
+ # Strip "module." prefix that comes from DataParallel-saved checkpoints
170
+ cleaned = {}
171
+ for k, v in state_dict.items():
172
+ cleaned[k.removeprefix("module.")] = v
173
+
174
+ net = IFNet()
175
+ net.load_state_dict(cleaned)
176
+ net.eval()
177
+ net.to(device)
178
+ return net
179
+
180
+ net = cached_model(
181
+ ("interpolate_frames", repo_id, weights_file, str(device)), load_net
182
+ )
183
+
184
+ return _build_inference(net, device)
185
+
186
+
187
+ def _build_inference(net, device):
188
+ """Build the (img1, img2) -> mid_frame callable for a loaded RIFE net.
189
+
190
+ All frames of a video share one padded resolution, so the flow-warp
191
+ context (tenFlow_div, backwarp grid, timestep) is computed once per
192
+ (padded_h, padded_w) and reused for every pair instead of being rebuilt
193
+ on every call.
194
+
195
+ `_interpolate_2x` walks frames pairwise: (f0, f1), (f1, f2), (f2, f3), ...
196
+ — the second frame of one pair is the same PIL object as the first frame
197
+ of the next. This closure carries the padded tensor it produced for a
198
+ pair's second frame forward, so that when that exact frame object shows
199
+ up again as a pair's first frame, its tensor is reused instead of being
200
+ re-derived from the PIL image (re-decoded, re-normalized, re-padded). A
201
+ cache miss (non-matching object, e.g. across separate interpolation
202
+ passes) simply falls back to a fresh conversion, so this is a pure
203
+ optimization with no behavioral effect — the reused tensor is bit-for-bit
204
+ what a fresh conversion of the same frame would produce.
205
+ """
206
+ scale_list = [8, 4, 2, 1]
207
+ flow_context_cache = {}
208
+ carry = {"img": None}
209
+
210
+ def get_flow_context(ph, pw):
211
+ key = (ph, pw)
212
+ ctx = flow_context_cache.get(key)
213
+ if ctx is None:
214
+ ctx = _make_flow_context(ph, pw, device)
215
+ flow_context_cache[key] = ctx
216
+ return ctx
217
+
218
+ def inference(img1, img2):
219
+ """Interpolate a single frame between two input frames."""
220
+ if img1 is carry["img"]:
221
+ t1_padded = carry["tensor"]
222
+ h, w, ph, pw = carry["h"], carry["w"], carry["ph"], carry["pw"]
223
+ else:
224
+ t1 = _pil_to_tensor(img1, device)
225
+ h, w = t1.shape[2], t1.shape[3]
226
+ ph, pw = _padded_size(h, w)
227
+ t1_padded = _pad_tensor(t1, ph, pw)
228
+
229
+ t2 = _pil_to_tensor(img2, device)
230
+ t2_padded = _pad_tensor(t2, ph, pw)
231
+
232
+ # Carry img2's padded tensor forward in case it's the next pair's img1.
233
+ carry["img"] = img2
234
+ carry["tensor"] = t2_padded
235
+ carry["h"], carry["w"], carry["ph"], carry["pw"] = h, w, ph, pw
236
+
237
+ tenFlow_div, backwarp_tenGrid, timestep = get_flow_context(ph, pw)
238
+
239
+ with torch.inference_mode():
240
+ _, _, merged = net(
241
+ t1_padded,
242
+ t2_padded,
243
+ timestep,
244
+ scale_list,
245
+ tenFlow_div,
246
+ backwarp_tenGrid,
247
+ )
248
+
249
+ mid = merged[3][:, :, :h, :w]
250
+ return float_tensor_to_pil(mid)
251
+
252
+ return inference
dw/tasks/judge.py ADDED
@@ -0,0 +1,68 @@
1
+ """
2
+ Judge - score a candidate image against a rubric using a vision-language
3
+ model.
4
+
5
+ The first scorer for the select reducer (docs/proposals/score-and-select.md):
6
+ `select` needs a number per candidate, and this is the cheapest way to get
7
+ one without a purpose-built model - prompt the VLM `image_to_text` already
8
+ loads with a rubric and a scale, and parse its reply to one number. Delegates
9
+ to text_generation.generate_text, the same vision path image_to_text uses,
10
+ rather than keeping a second copy of the pipeline handling.
11
+
12
+ generate_text decodes greedily (do_sample=False - see its module docstring),
13
+ so the same rubric and image already return the same reply every run; there
14
+ is no sampling here for a step seed to thread through.
15
+ """
16
+
17
+ import logging
18
+ import re
19
+
20
+ from .text_generation import generate_text, _DEFAULT_VISION_MODEL
21
+
22
+ logger = logging.getLogger("dw")
23
+
24
+ _DEFAULT_MODEL = _DEFAULT_VISION_MODEL
25
+
26
+ _NUMBER_PATTERN = re.compile(r"-?\d+(?:\.\d+)?")
27
+
28
+
29
+ def _judge_prompt(rubric, scale):
30
+ low, high = scale
31
+ return (
32
+ f"{rubric}\n\n"
33
+ f"Respond with a single number from {low} to {high} and nothing else."
34
+ )
35
+
36
+
37
+ def judge(image, rubric, scale, device="cpu", **kwargs):
38
+ """Score an image against a rubric, returning one number.
39
+
40
+ Args:
41
+ image: PIL Image (or URL/path) to score.
42
+ rubric: The question or criterion to score the image against.
43
+ scale: [low, high] the score is expected to fall within.
44
+ device: Target device ("cuda", "mps", "cpu").
45
+ **kwargs:
46
+ model_name: HuggingFace model ID of a vision-language model
47
+ (default: image_to_text's default vision model).
48
+ max_new_tokens: Max tokens to generate (default: 20).
49
+
50
+ Returns:
51
+ The parsed score as a float.
52
+
53
+ Raises:
54
+ ValueError: the model's reply did not contain a number.
55
+ """
56
+ prompt = _judge_prompt(rubric, scale)
57
+ kwargs.setdefault("model_name", _DEFAULT_MODEL)
58
+ kwargs.setdefault("max_new_tokens", 20)
59
+
60
+ reply = generate_text(prompt, device=device, image=image, **kwargs)
61
+
62
+ match = _NUMBER_PATTERN.search(reply)
63
+ if match is None:
64
+ raise ValueError(f"judge: could not parse a score from the reply: {reply!r}")
65
+
66
+ score = float(match.group())
67
+ logger.info(f"Judged: {score}")
68
+ return score
@@ -0,0 +1,55 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger("dw")
4
+
5
+ # Loaded task models, keyed by whatever identifies a load - typically
6
+ # (task, model_name, device). step.py runs a task handler once per cartesian
7
+ # product iteration; without this, segmenting 20 images would load the same
8
+ # multi-gigabyte checkpoints 20 times over
9
+ _cache = {}
10
+
11
+
12
+ def cached_model(key, factory):
13
+ """Return the model for key, loading it with factory() on first use.
14
+
15
+ Args:
16
+ key: Hashable identity of the load - include the model name and the
17
+ device, plus anything else that changes what factory() builds
18
+ factory: Zero-argument callable performing the actual load
19
+
20
+ Returns:
21
+ The cached or freshly loaded model
22
+ """
23
+ if key not in _cache:
24
+ logger.info(f"Loading task model: {key}")
25
+ _cache[key] = factory()
26
+ else:
27
+ logger.debug(f"Reusing cached task model: {key}")
28
+ return _cache[key]
29
+
30
+
31
+ def clear_model_cache():
32
+ """Release every cached task model.
33
+
34
+ Wired into the worker's memory cleanup - dropping the references here is
35
+ what lets gc and the allocator actually reclaim the weights.
36
+ """
37
+ if _cache:
38
+ logger.info(f"Clearing {len(_cache)} cached task models")
39
+ _cache.clear()
40
+
41
+
42
+ def hf_pipeline_placement(device):
43
+ """How a transformers pipeline is told where to load.
44
+
45
+ A device_map has the loading threads cast their shards straight onto the
46
+ device, which races inside torch's Metal shader cache on MPS. Passing
47
+ `device` instead leaves the load on the CPU and moves the finished model in
48
+ one call on this thread. Every task that builds a transformers pipeline
49
+ spells `**hf_pipeline_placement(device)` so the accommodation lives once.
50
+ """
51
+ from .. import get_device_type
52
+
53
+ if get_device_type(device) == "mps":
54
+ return {"device": device}
55
+ return {"device_map": device}
dw/tasks/pair_audio.py ADDED
@@ -0,0 +1,268 @@
1
+ """Pair a video with an audio track so the two are saved as one file.
2
+
3
+ A pipeline that generates its own soundtrack returns the pair together, and the
4
+ result muxes them into a single mp4. Anything that works on the frames alone -
5
+ a latent upsampler, an interpolator, an upscaler - returns frames without it, so
6
+ the soundtrack has to be carried across the step that dropped it. That is what
7
+ this does: it puts the two back together for the step that saves them.
8
+ """
9
+
10
+ import logging
11
+
12
+ from ..events import emit_warning
13
+ from ..result import AudioVideo
14
+ from ..shots import remeasured_shots
15
+ from .audio_utils import as_channels_samples
16
+
17
+ logger = logging.getLogger("dw")
18
+
19
+ # Only gates the unfitted mismatch warning (no 'fit' given): a track and a
20
+ # cut are frame-aligned by construction here, so a difference smaller than
21
+ # this is rounding rather than a decision anyone can act on - one video frame
22
+ # at 24 fps is 41 ms. An explicit 'fit': 'video' always fits and warns on any
23
+ # nonzero difference - the caller asked for exactness, not a guess at whether
24
+ # the gap matters
25
+ LENGTH_WARN_MS = 100.0
26
+
27
+
28
+ class _Loaded:
29
+ """A waveform read from a file, shaped like the artifact pair_audio expects."""
30
+
31
+ def __init__(self, audio, sample_rate):
32
+ self.audio = audio
33
+ self.sample_rate = sample_rate
34
+
35
+
36
+ def _frame_count(frames):
37
+ """How many frames the video is, or None when that cannot be told cheaply
38
+ (a lazily-decoded reader, an object with no length)."""
39
+ try:
40
+ return len(frames)
41
+ except TypeError:
42
+ shape = getattr(frames, "shape", None)
43
+ return int(shape[0]) if shape else None
44
+
45
+
46
+ def _fit_to_video(waveform, rate, frames, fps, fit):
47
+ """Answer the track that goes with these frames, and say when the two do
48
+ not agree.
49
+
50
+ The lengths of a video and the track laid over it are two numbers a
51
+ workflow used to have to keep equal by hand, and nothing checked: the
52
+ music-video template sliced a soundtrack of a fixed 496 frames while its
53
+ cut followed a `shots` list, so a two-shot run wrote 10.3 s of picture
54
+ into a 20.7 s container and reported `succeeded` with no warnings (#142).
55
+ `fit: "video"` derives the length from the frames instead, and with no
56
+ `fit` the mismatch is at least said out loud.
57
+ """
58
+ from .audio_utils import frames_to_samples, slice_samples
59
+
60
+ if fit not in (None, "video"):
61
+ # Refused rather than ignored: a misspelled 'fit' that quietly did
62
+ # nothing is the silence this argument exists to end
63
+ raise ValueError(
64
+ f"pair_audio: 'fit' takes 'video' or nothing, got {fit!r}. "
65
+ f"'video' cuts or pads the track to the length of the frames"
66
+ )
67
+
68
+ count = _frame_count(frames)
69
+ if not count or not fps or not rate:
70
+ # Nothing to compare against - a frame count or a rate this layer
71
+ # cannot know is not a mismatch
72
+ return waveform
73
+
74
+ wanted = frames_to_samples(count, fps, rate)
75
+ have = waveform.shape[1]
76
+ video_seconds = count / float(fps)
77
+ audio_seconds = have / float(rate)
78
+
79
+ if fit != "video":
80
+ if abs(have - wanted) / float(rate) * 1000.0 < LENGTH_WARN_MS:
81
+ return waveform
82
+ emit_warning(
83
+ f"pair_audio: the track is {audio_seconds:.2f} s and the video it "
84
+ f"is laid over is {video_seconds:.2f} s ({count} frames at "
85
+ f"{fps:g} fps), so the saved file's duration and its frame count "
86
+ f"disagree. Pass 'fit': 'video' to cut or pad the track to the "
87
+ f"frames, or make the track the length of the cut.",
88
+ kind="audio_video_length_mismatch",
89
+ command="pair_audio",
90
+ audio_seconds=audio_seconds,
91
+ video_seconds=video_seconds,
92
+ )
93
+ return waveform
94
+
95
+ if have == wanted:
96
+ # Already exact - nothing to pad, trim or warn about
97
+ return waveform
98
+
99
+ fitted = slice_samples(waveform, 0, wanted)
100
+ # A gap smaller than one video frame cannot line up with anything the cut
101
+ # does - the two rates just don't divide evenly - so it is reported as
102
+ # what it is (a sample count) rather than as a claim on the picture
103
+ # ("the last part of the cut has no soundtrack") that a 1-sample pad does
104
+ # not support. At 24 fps a frame is 41.67 ms; formatting a sub-frame gap
105
+ # to two decimal places of a second is what produced "0.00 s of silence
106
+ # ... has no soundtrack" (#429) - self-contradictory and, followed as
107
+ # written, unfixable, since the gap is smaller than either 'a longer
108
+ # track' or 'fewer frames' can address.
109
+ frame_seconds = 1.0 / float(fps)
110
+ if wanted > have:
111
+ pad_samples = wanted - have
112
+ pad_seconds = pad_samples / float(rate)
113
+ if pad_seconds < frame_seconds:
114
+ emit_warning(
115
+ f"pair_audio: 'fit' padded the {audio_seconds:.2f} s track with "
116
+ f"{pad_samples} sample{'s' if pad_samples != 1 else ''} "
117
+ f"({pad_seconds * 1000:.2f} ms) of silence to reach the "
118
+ f"{video_seconds:.2f} s of video it is laid over - under one "
119
+ f"video frame ({frame_seconds * 1000:.1f} ms), most likely "
120
+ f"ordinary rounding between the track's sample rate and the "
121
+ f"video's frame rate rather than a real gap.",
122
+ kind="audio_padded_to_video",
123
+ command="pair_audio",
124
+ audio_seconds=audio_seconds,
125
+ video_seconds=video_seconds,
126
+ pad_samples=pad_samples,
127
+ )
128
+ else:
129
+ emit_warning(
130
+ f"pair_audio: 'fit' padded the {audio_seconds:.2f} s track with "
131
+ f"{pad_seconds:.2f} s of silence to reach the "
132
+ f"{video_seconds:.2f} s of video it is laid over - the last part "
133
+ f"of the cut has no soundtrack. A longer track, or fewer frames, "
134
+ f"is what covers it.",
135
+ kind="audio_padded_to_video",
136
+ command="pair_audio",
137
+ audio_seconds=audio_seconds,
138
+ video_seconds=video_seconds,
139
+ pad_samples=pad_samples,
140
+ )
141
+ else:
142
+ trimmed_samples = have - wanted
143
+ trimmed_seconds = trimmed_samples / float(rate)
144
+ if trimmed_seconds < frame_seconds:
145
+ emit_warning(
146
+ f"pair_audio: 'fit' trimmed {trimmed_samples} sample"
147
+ f"{'s' if trimmed_samples != 1 else ''} "
148
+ f"({trimmed_seconds * 1000:.2f} ms) off the {audio_seconds:.2f} s "
149
+ f"track to reach the {video_seconds:.2f} s of video it is laid "
150
+ f"over - under one video frame ({frame_seconds * 1000:.1f} ms), "
151
+ f"most likely ordinary rounding between the track's sample rate "
152
+ f"and the video's frame rate rather than lost content.",
153
+ kind="audio_trimmed_to_video",
154
+ command="pair_audio",
155
+ audio_seconds=audio_seconds,
156
+ video_seconds=video_seconds,
157
+ trimmed_seconds=trimmed_seconds,
158
+ trimmed_samples=trimmed_samples,
159
+ )
160
+ else:
161
+ emit_warning(
162
+ f"pair_audio: 'fit' trimmed {trimmed_seconds:.2f} s "
163
+ f"off the {audio_seconds:.2f} s track to reach the "
164
+ f"{video_seconds:.2f} s of video it is laid over - that part of "
165
+ f"the track, whatever it held, is gone from the deliverable. A "
166
+ f"shorter track, or more frames, is what keeps it.",
167
+ kind="audio_trimmed_to_video",
168
+ command="pair_audio",
169
+ audio_seconds=audio_seconds,
170
+ video_seconds=video_seconds,
171
+ trimmed_seconds=trimmed_seconds,
172
+ trimmed_samples=trimmed_samples,
173
+ )
174
+ return fitted
175
+
176
+
177
+ def pair_audio(video, audio, sample_rate=None, fps=None, fit=None):
178
+ """Pair a video's frames with an audio track.
179
+
180
+ Args:
181
+ video: The frames - a frame list, a frame array or tensor, or an
182
+ AudioVideo whose own soundtrack is replaced by this one; the
183
+ frames' own rate is carried through to the output, so set
184
+ `result.fps` only to override it (a loaded file brings its rate
185
+ along; frames that carry none are written at 8 fps)
186
+ audio: The soundtrack - a waveform, an AudioVideo (or any object
187
+ carrying '.audio') to take it from, or the path or URL of an
188
+ audio or video file; the last two bring their sample rate along.
189
+ A mono track is fine - saving as mp4 duplicates it into the two
190
+ channels the audio stream takes, and warns that it did
191
+ sample_rate: Sample rate of the waveform. Required unless `audio`
192
+ carries one; given here it wins, for a track whose rate was
193
+ reported wrong
194
+ fps: The rate the frames play at, when the frames do not carry one -
195
+ only used to work out how long the video is, never written
196
+ fit: "video" cuts or pads the track with silence to the length of the
197
+ frames, warning either way (`audio_padded_to_video` when it pads,
198
+ `audio_trimmed_to_video` when it cuts). This is
199
+ how a soundtrack follows a cut whose length is an argument
200
+ rather than a constant: nothing in a workflow can multiply a
201
+ list's length by a frame count, so a slice written to fit four
202
+ shots stayed 496 frames long when the list held two, and the
203
+ deliverable's audio ran twice as long as its picture with
204
+ `succeeded` and no warnings (#142). Left unset the track is used
205
+ as it is, and a length that disagrees with the frames' is
206
+ warned about rather than passing in silence. The exactness
207
+ `fit` guarantees is of the *waveform handed to the encoder*, not
208
+ of the file a lossy mux (AAC, the only container this saves
209
+ audio+video into) writes: encoding is downstream of this
210
+ function and can still trim or pad the written track by a
211
+ further handful of samples (#428 measured up to ~30, well under
212
+ a millisecond). The save logs that residual; on a video with
213
+ recorded shots it warns (`joined_audio_short_after_mux`, #435)
214
+ only once it reaches a frame. `get_gallery_metadata`'s `media.shots`
215
+ and `assess_output`'s `sync_length` are measured against the
216
+ file as written, not this prediction, so they are the ground
217
+ truth for exactly how long the saved track runs
218
+
219
+ Returns:
220
+ One AudioVideo holding the frames and the track, at the rate the
221
+ frames carry - a file loaded for the `video` argument brings its
222
+ own, so the saved mp4 plays at the rate that went in and
223
+ `result.fps` is only needed to write it at a different one (#104)
224
+
225
+ Raises:
226
+ ValueError: If no waveform was given, or if no sample rate can be
227
+ established for the one that was
228
+ """
229
+ if isinstance(audio, str):
230
+ # A file an earlier run wrote - a score, or a cut whose track is wanted
231
+ from .audio_utils import load_audio
232
+
233
+ audio = _Loaded(*load_audio(audio))
234
+ waveform = getattr(audio, "audio", audio)
235
+ if waveform is None:
236
+ raise ValueError(
237
+ "pair_audio needs an audio track - the video it was given carries none"
238
+ )
239
+
240
+ rate = (
241
+ sample_rate if sample_rate is not None else getattr(audio, "sample_rate", None)
242
+ )
243
+ if rate is None:
244
+ raise ValueError(
245
+ "pair_audio needs 'sample_rate' - the audio it was given does not "
246
+ "carry one of its own"
247
+ )
248
+
249
+ # Frames are left in whatever shape they arrived in - the result saves a frame
250
+ # list, an array and a tensor alike, and converting a long video here would
251
+ # cost a copy of the whole thing for nothing
252
+ frames = video.frames if isinstance(video, AudioVideo) else video
253
+ frame_rate = fps if fps is not None else getattr(video, "fps", None)
254
+ logger.debug(f"Pairing frames with audio at {rate} Hz")
255
+ waveform = _fit_to_video(
256
+ as_channels_samples(waveform), rate, frames, frame_rate, fit
257
+ )
258
+ # The picture's shots survive; their samples are re-measured on the new
259
+ # track, which was laid under whole rather than built shot by shot
260
+ return AudioVideo(
261
+ frames,
262
+ waveform,
263
+ rate,
264
+ fps=getattr(video, "fps", None),
265
+ shots=remeasured_shots(
266
+ getattr(video, "shots", None), frame_rate, rate, waveform.shape[1]
267
+ ),
268
+ )
dw/tasks/qr_code.py ADDED
@@ -0,0 +1,19 @@
1
+ import qrcode
2
+ from .image_utils import resize_resample
3
+
4
+
5
+ def get_qrcode_image(qr_code_contents, height=768, width=768):
6
+ # base the resolution off of size - defaulting to 768
7
+ resolution = max(height, width)
8
+
9
+ qr = qrcode.QRCode(
10
+ version=None,
11
+ error_correction=qrcode.constants.ERROR_CORRECT_H,
12
+ box_size=10,
13
+ border=4,
14
+ )
15
+ qr.add_data(qr_code_contents)
16
+ qr.make(fit=True)
17
+
18
+ qrcode_image = qr.make_image(fill_color="black", back_color="white")
19
+ return resize_resample(qrcode_image, resolution)