lcode-cli 0.44.0__tar.gz → 0.45.0__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 (202) hide show
  1. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/CHANGELOG.md +15 -1
  2. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/PKG-INFO +1 -1
  3. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/modes.md +14 -2
  4. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/quickstart.md +1 -1
  5. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/__init__.py +1 -1
  6. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/imageopts.py +29 -1
  7. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/media.py +66 -16
  8. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/media.toml +50 -0
  9. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/media_catalog.py +4 -0
  10. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/repl.py +13 -2
  11. lcode_cli-0.45.0/src/lcode/workflows/wan21-fun-camera.json +186 -0
  12. lcode_cli-0.45.0/src/lcode/workflows/wan22-fun-inp-5b.json +159 -0
  13. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_media_catalog.py +1 -0
  14. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_video.py +55 -2
  15. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/.editorconfig +0 -0
  16. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/.github/CODEOWNERS +0 -0
  17. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  18. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  19. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  20. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
  21. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  22. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/.github/dependabot.yml +0 -0
  23. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/.github/workflows/ci.yml +0 -0
  24. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/.github/workflows/docs.yml +0 -0
  25. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/.github/workflows/release.yml +0 -0
  26. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/.gitignore +0 -0
  27. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/.pre-commit-config.yaml +0 -0
  28. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/AGENTS.md +0 -0
  29. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/CODE_OF_CONDUCT.md +0 -0
  30. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/CONTRIBUTING.md +0 -0
  31. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/LICENSE +0 -0
  32. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/README.md +0 -0
  33. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/SECURITY.md +0 -0
  34. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/action.yml +0 -0
  35. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/agents.md +0 -0
  36. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/assets/demo.svg +0 -0
  37. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/assets/extra.css +0 -0
  38. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/automation.md +0 -0
  39. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/bench.md +0 -0
  40. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/changelog.md +0 -0
  41. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/commands.md +0 -0
  42. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/configuration.md +0 -0
  43. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/contributing.md +0 -0
  44. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/editors.md +0 -0
  45. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/git.md +0 -0
  46. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/hooks.md +0 -0
  47. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/how-it-works.md +0 -0
  48. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/index.md +0 -0
  49. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/installation.md +0 -0
  50. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/lsp.md +0 -0
  51. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/mcp.md +0 -0
  52. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/memory.md +0 -0
  53. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/models.md +0 -0
  54. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/sandbox.md +0 -0
  55. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/search.md +0 -0
  56. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/servers.md +0 -0
  57. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/troubleshooting.md +0 -0
  58. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/docs/usage.md +0 -0
  59. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/examples/README.md +0 -0
  60. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/examples/api/fix_tests.py +0 -0
  61. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/examples/commands/review.md +0 -0
  62. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/examples/skills/commit-message/SKILL.md +0 -0
  63. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
  64. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/install.sh +0 -0
  65. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/mkdocs.yml +0 -0
  66. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/overrides/main.html +0 -0
  67. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/pyproject.toml +0 -0
  68. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/scripts/make_logos.py +0 -0
  69. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/scripts/record_demo.py +0 -0
  70. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/scripts/social-preview.html +0 -0
  71. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/__main__.py +0 -0
  72. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/acp.py +0 -0
  73. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/action.py +0 -0
  74. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/agent.py +0 -0
  75. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/api.py +0 -0
  76. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/backends.py +0 -0
  77. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/bench.py +0 -0
  78. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/bench_backend.py +0 -0
  79. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/bench_runtime.py +0 -0
  80. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/catalog.py +0 -0
  81. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/checkpoints.py +0 -0
  82. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/checks.py +0 -0
  83. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/cli.py +0 -0
  84. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/clipboard.py +0 -0
  85. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/cloud.py +0 -0
  86. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/codesearch.py +0 -0
  87. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/comfy.py +0 -0
  88. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/config.py +0 -0
  89. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/context.py +0 -0
  90. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/edits.py +0 -0
  91. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/escalate.py +0 -0
  92. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/extensions.py +0 -0
  93. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/frontmatter.py +0 -0
  94. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/gitflow.py +0 -0
  95. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/hardware.py +0 -0
  96. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/hooks.py +0 -0
  97. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/jobs.py +0 -0
  98. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/limits.py +0 -0
  99. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/lsp.py +0 -0
  100. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/matching.py +0 -0
  101. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/mcp/__init__.py +0 -0
  102. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/mcp/auth.py +0 -0
  103. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/mcp/catalog.py +0 -0
  104. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/mcp/catalog.toml +0 -0
  105. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/mcp/client.py +0 -0
  106. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/mcp/commands.py +0 -0
  107. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/mcp/config.py +0 -0
  108. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/mcp/http.py +0 -0
  109. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/mcp/manager.py +0 -0
  110. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/mcp/protocol.py +0 -0
  111. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/mcp/server.py +0 -0
  112. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/mcp/stdio.py +0 -0
  113. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/memory.py +0 -0
  114. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/models.toml +0 -0
  115. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/modes.py +0 -0
  116. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/notify.py +0 -0
  117. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/ollama.py +0 -0
  118. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/permissions.py +0 -0
  119. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/planning.py +0 -0
  120. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/preview.py +0 -0
  121. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/render.py +0 -0
  122. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/repeats.py +0 -0
  123. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/repomap.py +0 -0
  124. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/residency.py +0 -0
  125. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/review.py +0 -0
  126. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/sandbox.py +0 -0
  127. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/screenshots.py +0 -0
  128. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/secretscan.py +0 -0
  129. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/servers/__init__.py +0 -0
  130. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/servers/encord.py +0 -0
  131. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/servers/valohai.py +0 -0
  132. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/servers/xai.py +0 -0
  133. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/sessions.py +0 -0
  134. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/steer.py +0 -0
  135. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/subagents.py +0 -0
  136. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/tools.py +0 -0
  137. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/unreachable.py +0 -0
  138. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/update.py +0 -0
  139. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/vision.py +0 -0
  140. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/web.py +0 -0
  141. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/workflows/__init__.py +0 -0
  142. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/workflows/flux2-klein-4b-t2i.json +0 -0
  143. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/workflows/remove-background.json +0 -0
  144. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/workflows/upscale-esrgan.json +0 -0
  145. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/workflows/wan22-ti2v-5b.json +0 -0
  146. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/src/lcode/workflows/z-image-turbo-t2i.json +0 -0
  147. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/comfy_standin.py +0 -0
  148. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/conftest.py +0 -0
  149. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/fake_lsp_server.py +0 -0
  150. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/fake_mcp_server.py +0 -0
  151. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_acp.py +0 -0
  152. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_action.py +0 -0
  153. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_agent.py +0 -0
  154. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_api.py +0 -0
  155. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_backends.py +0 -0
  156. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_bench.py +0 -0
  157. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_catalog.py +0 -0
  158. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_checkpoints.py +0 -0
  159. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_checks.py +0 -0
  160. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_cli.py +0 -0
  161. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_clipboard.py +0 -0
  162. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_cloud.py +0 -0
  163. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_codesearch.py +0 -0
  164. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_comfy.py +0 -0
  165. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_config.py +0 -0
  166. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_context.py +0 -0
  167. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_context_picker.py +0 -0
  168. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_escalate.py +0 -0
  169. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_extensions.py +0 -0
  170. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_gitflow.py +0 -0
  171. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_hooks.py +0 -0
  172. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_image_edit.py +0 -0
  173. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_image_generate.py +0 -0
  174. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_jobs.py +0 -0
  175. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_look.py +0 -0
  176. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_lsp.py +0 -0
  177. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_matching.py +0 -0
  178. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_mcp.py +0 -0
  179. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_memory.py +0 -0
  180. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_model_picker.py +0 -0
  181. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_modes.py +0 -0
  182. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_permissions.py +0 -0
  183. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_plan_mode.py +0 -0
  184. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_repeats.py +0 -0
  185. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_repomap.py +0 -0
  186. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_residency.py +0 -0
  187. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_review.py +0 -0
  188. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_robustness.py +0 -0
  189. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_sandbox.py +0 -0
  190. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_screenshots.py +0 -0
  191. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_secretscan.py +0 -0
  192. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_servers.py +0 -0
  193. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_sessions.py +0 -0
  194. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_steer.py +0 -0
  195. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_stuck.py +0 -0
  196. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_subagents.py +0 -0
  197. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_tools.py +0 -0
  198. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_update.py +0 -0
  199. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_vision.py +0 -0
  200. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_web.py +0 -0
  201. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/tests/test_xai.py +0 -0
  202. {lcode_cli-0.44.0 → lcode_cli-0.45.0}/uv.lock +0 -0
@@ -6,6 +6,19 @@ All notable changes to lcode are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.45.0] - 2026-10-24
10
+
11
+ ### Added
12
+
13
+ - **Video mode: first and last frames, camera moves, longer clips** (#154, part 1). See
14
+ [Video mode](https://nasser1941.github.io/lcode/modes/#video-mode).
15
+ - **`@start.png @end.png …`** makes a clip from one picture to the other (Wan 2.2 Fun InP 5B).
16
+ - **`--camera pan-left`** and twelve other moves (pan, tilt, zoom, dolly, rotate), from a
17
+ description or a start picture (Wan 2.1 Fun Camera 1.3B, 16 fps).
18
+ - **`/extend 4 --seconds 3 …`** in video mode continues a clip from its last frame into one
19
+ longer clip.
20
+ - lcode picks the model a job needs, installs it when you agree, and says so.
21
+
9
22
  ## [0.44.0] - 2026-10-24
10
23
 
11
24
  ### Added
@@ -969,7 +982,8 @@ First public release.
969
982
  - `AGENTS.md` project instructions and `/init` to generate them.
970
983
  - One-line installer for Linux and macOS.
971
984
 
972
- [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.44.0...HEAD
985
+ [Unreleased]: https://github.com/nasser1941/lcode/compare/v0.45.0...HEAD
986
+ [0.45.0]: https://github.com/nasser1941/lcode/compare/v0.44.0...v0.45.0
973
987
  [0.44.0]: https://github.com/nasser1941/lcode/compare/v0.43.0...v0.44.0
974
988
  [0.43.0]: https://github.com/nasser1941/lcode/compare/v0.42.0...v0.43.0
975
989
  [0.42.0]: https://github.com/nasser1941/lcode/compare/v0.41.0...v0.42.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lcode-cli
3
- Version: 0.44.0
3
+ Version: 0.45.0
4
4
  Summary: A local-first terminal coding agent powered by open-weight models on your own GPU or Mac, via Ollama.
5
5
  Project-URL: Homepage, https://nasser1941.github.io/lcode/
6
6
  Project-URL: Documentation, https://nasser1941.github.io/lcode/
@@ -167,6 +167,18 @@ video ❯ /play
167
167
  description and start image.
168
168
  - **Measured** on the RTX 4080 Laptop GPU (12 GB), a 5-second clip (121 frames): `fast` 100 s,
169
169
  `balanced` 150 s (the same from a start image), `best` 10.5 minutes.
170
+ - **First and last frame:** two pictures, `@start.png @end.png the flower opens`, make a clip that
171
+ goes from the first to the last. Wan 2.2 Fun InP 5B makes these (a 3 s clip in 62 s at `fast`;
172
+ `fast` can change abruptly halfway, `balanced` moves more smoothly).
173
+ - **Camera moves:** `--camera pan-left` (also `pan-right`, `pan-up`, `pan-down`, `tilt-up`,
174
+ `tilt-down`, `zoom-in`, `zoom-out`, `dolly-in`, `dolly-out`, `rotate-left`, `rotate-right`,
175
+ `static`), from a description or a start picture. Wan 2.1 Fun Camera 1.3B makes these, at 832×480
176
+ and 16 frames per second: a 3 s move takes about 2 minutes.
177
+ - **Extend a clip:** `/extend 4 --seconds 3 it flies away` continues clip #4 from its last frame
178
+ and joins the new part after it, as one longer clip (`…-extended.mp4`; 3 s more in about a
179
+ minute at `fast`).
180
+ - **Which model:** the one you chose (`/models`) makes ordinary clips. For first and last frames or a
181
+ camera move, lcode uses the model that can, installs it when you agree, and says so.
170
182
  - **Long renders:** progress shows the step and the time left. With `notify` on, lcode sends a
171
183
  desktop notification when a clip that took longer than `notify_after` seconds is done.
172
184
 
@@ -266,8 +278,8 @@ themselves:
266
278
 
267
279
  ## Coming next
268
280
 
269
- - **#154 More video:** first and last frames, several references, camera moves, longer clips,
270
- sound, and more video models in the catalog.
281
+ - **#154 More video:** several reference images, smoother and bigger clips (frame interpolation,
282
+ upscaling), restyling a video, and sound.
271
283
  - **#155 Movies:** storyboards with a consistent cast, shots stitched into one film.
272
284
  - **#156 Prompt assistant:** a small local model for prompts and follow-ups.
273
285
  - **#157 Cloud generation**, with costs shown (Grok Imagine first).
@@ -12,7 +12,7 @@ lcode
12
12
 
13
13
  ```text
14
14
  ╭──────────────────────────────────────────────╮
15
- │ lcode v0.44.0 — local coding agent │
15
+ │ lcode v0.45.0 — local coding agent │
16
16
  │ │
17
17
  │ model lcode-qwen3.6-35b │
18
18
  │ context 256K tokens │
@@ -3,7 +3,7 @@
3
3
  From a program: `from lcode import Session` (see lcode.api).
4
4
  """
5
5
 
6
- __version__ = "0.44.0"
6
+ __version__ = "0.45.0"
7
7
 
8
8
  API = ("Session", "Result", "Options", "SetupError")
9
9
 
@@ -35,6 +35,21 @@ STYLES = {
35
35
  "comic": "comic book art, bold ink outlines, halftone shading",
36
36
  "flat": "flat vector illustration, minimal, bold shapes",
37
37
  }
38
+ CAMERA = { # video: --camera name -> the camera model's pose
39
+ "static": "Static",
40
+ "pan-left": "Pan Left",
41
+ "pan-right": "Pan Right",
42
+ "pan-up": "Pan Up",
43
+ "pan-down": "Pan Down",
44
+ "tilt-up": "Pan Up",
45
+ "tilt-down": "Pan Down",
46
+ "zoom-in": "Zoom In",
47
+ "zoom-out": "Zoom Out",
48
+ "dolly-in": "Zoom In",
49
+ "dolly-out": "Zoom Out",
50
+ "rotate-left": "Anti Clockwise (ACW)",
51
+ "rotate-right": "ClockWise (CW)",
52
+ }
38
53
  ALIASES = {
39
54
  "n": "n",
40
55
  "count": "n",
@@ -49,6 +64,7 @@ ALIASES = {
49
64
  "style": "style",
50
65
  "seconds": "seconds",
51
66
  "duration": "seconds",
67
+ "camera": "camera",
52
68
  }
53
69
  FLAG = re.compile(r"(?:^|\s)--?(" + "|".join(sorted(ALIASES, key=len, reverse=True)) + r")\b(?=[\s=]|$)")
54
70
  DIMENSIONS = re.compile(r"^(\d{2,4})\s*[x×]\s*(\d{2,4})$")
@@ -68,12 +84,15 @@ class Options:
68
84
  negative: str = ""
69
85
  style: str = ""
70
86
  seconds: float = 5.0 # video only
87
+ camera: str = "" # video only: a move from CAMERA, made by a camera-control model
71
88
 
72
89
  def describe(self, video: bool = False) -> str:
73
90
  seed = "random" if self.seed is None else str(self.seed)
74
91
  parts = [f"n={self.n}", f"size={self.size}", f"seed={seed}", f"quality={self.quality}"]
75
92
  if video:
76
93
  parts.append(f"seconds={self.seconds:g}")
94
+ if self.camera:
95
+ parts.append(f"camera={self.camera}")
77
96
  if self.style:
78
97
  parts.append(f"style={self.style}")
79
98
  if self.negative:
@@ -98,7 +117,9 @@ def set_values(options: Options, values: dict[str, object]) -> Options:
98
117
  for raw_key, raw in values.items():
99
118
  key = ALIASES.get(str(raw_key).lower().lstrip("-"))
100
119
  if key is None:
101
- raise OptionError(f"unknown option {raw_key!r}: use n, ar (or size), seed, quality, style, no or seconds")
120
+ raise OptionError(
121
+ f"unknown option {raw_key!r}: use n, ar (or size), seed, quality, style, no, seconds or camera"
122
+ )
102
123
  value = str(raw).strip() if raw is not None else ""
103
124
  if key == "n":
104
125
  if not value.isdigit() or not 1 <= int(value) <= MAX_COUNT:
@@ -126,6 +147,13 @@ def set_values(options: Options, values: dict[str, object]) -> Options:
126
147
  raise OptionError(f"style is one of {', '.join(STYLES)} (or none)")
127
148
  elif key == "negative":
128
149
  out.negative = "" if value.lower() in ("none", "off") else value
150
+ elif key == "camera":
151
+ if value.lower() in ("none", "off", ""):
152
+ out.camera = ""
153
+ elif value.lower() in CAMERA:
154
+ out.camera = value.lower()
155
+ else:
156
+ raise OptionError(f"camera is one of {', '.join(CAMERA)} (or none)")
129
157
  elif key == "seconds":
130
158
  try:
131
159
  seconds = float(value.rstrip("s"))
@@ -292,7 +292,7 @@ def request(
292
292
  if agent.settings.mode == "video":
293
293
  if mask:
294
294
  raise MediaError("Video mode doesn't take a mask: @photo.png starts a video from a picture.")
295
- job = {"kind": "video", "prompt": prompt, "options": options, "sources": [str(p) for p in sources[:1]],
295
+ job = {"kind": "video", "prompt": prompt, "options": options, "sources": [str(p) for p in sources[:2]],
296
296
  "mask": None, "pads": None, "keep_shape": not SIZE_FLAG.search(text)} # fmt: skip
297
297
  return redo(agent, job, cancelled, ask)
298
298
  job = {"kind": "edit" if sources else "image", "prompt": prompt, "options": options,
@@ -488,10 +488,14 @@ def generate_video(
488
488
  image (the shapes video models know best; the image is cropped to it), unless the request gives one."""
489
489
  prompt = job["prompt"].strip()
490
490
  server = prepare(agent, ask, cancelled)
491
- model = current(agent, "video")
491
+ sources = [Path(p) for p in job.get("sources") or []]
492
+ start = sources[0] if sources else None
493
+ end = sources[1] if len(sources) > 1 else None
494
+ clip = Path(job["extend"]) if job.get("extend") else None
495
+ model = video_model(agent, "first-last-frame" if end else ("camera" if options.camera else
496
+ ("image-to-video" if clip else ""))) # fmt: skip
492
497
  files = ensure_installed(agent, model, server, ask)
493
498
  workflow = comfy.load_workflow(model.workflow)
494
- start = Path(job["sources"][0]) if job.get("sources") else None
495
499
  seed = options.seed if options.seed is not None else random.randrange(2**32)
496
500
  steps, megapixels = model.preset(options.quality)
497
501
  ratio = options.size
@@ -504,6 +508,8 @@ def generate_video(
504
508
  except (OSError, edits.EditError):
505
509
  pass
506
510
  width, height = imageopts.dimensions(imageopts.SIZES.get(ratio, ratio), megapixels, multiple=32)
511
+ if clip is not None: # the new part continues the clip, so it has its size
512
+ width, height = job["size"]
507
513
  fps = int(workflow.defaults.get("fps", 24))
508
514
  text = imageopts.styled(prompt, options.style)
509
515
  values = {**workflow.defaults, **files, "prompt": text, "seed": seed, "width": width, "height": height,
@@ -512,26 +518,70 @@ def generate_video(
512
518
  values["steps"] = steps
513
519
  if options.negative:
514
520
  values["negative"] = options.negative
521
+ values["camera"] = imageopts.CAMERA[options.camera or "static"]
515
522
  graph = comfy.fill(workflow.graph, values)
516
- if start is not None:
517
- try:
518
- name = server.upload(start)
519
- except (comfy.ComfyError, OSError) as e:
520
- raise MediaError(f"Couldn't hand the image to ComfyUI: {e}") from e
521
- latent = edits.node_of(graph, "Wan22ImageToVideoLatent")
522
- if latent is None:
523
- raise MediaError(f"{model.name} can't start from an image.")
524
- graph["8100"] = {"class_type": "LoadImage", "inputs": {"image": name}}
525
- graph[latent]["inputs"]["start_image"] = ["8100", 0]
523
+ latent = next((edits.node_of(graph, c) for c in VIDEO_LATENTS if edits.node_of(graph, c)), None)
524
+ try:
525
+ if start is not None:
526
+ graph["8100"] = {"class_type": "LoadImage", "inputs": {"image": server.upload(start)}}
527
+ graph[latent]["inputs"]["start_image"] = ["8100", 0]
528
+ loader = edits.node_of(graph, "CLIPVisionLoader")
529
+ if loader is not None: # the camera model also looks at the start image
530
+ look = {"clip_vision": [loader, 0], "image": ["8100", 0], "crop": "none"}
531
+ graph["8102"] = {"class_type": "CLIPVisionEncode", "inputs": look}
532
+ graph[latent]["inputs"]["clip_vision_output"] = ["8102", 0]
533
+ if end is not None:
534
+ graph["8101"] = {"class_type": "LoadImage", "inputs": {"image": server.upload(end)}}
535
+ graph[latent]["inputs"]["end_image"] = ["8101", 0]
536
+ if clip is not None:
537
+ continue_clip(graph, latent, server.upload(clip), values["length"])
538
+ except (comfy.ComfyError, OSError) as e:
539
+ raise MediaError(f"Couldn't hand the files to ComfyUI: {e}") from e
526
540
  add_thumbnail(graph, width, height)
527
541
  meta = describe_job(model, workflow, prompt, text, seed, values, options, files)
528
542
  meta.update({"seconds": round((values["length"] - 1) / fps, 2), "fps": fps, "frames": values["length"],
529
- "start_image": shown(agent, start) if start else ""}) # fmt: skip
530
- remembered = {k: job.get(k) for k in ("kind", "prompt", "sources", "mask", "pads", "keep_shape")}
531
- plan = Plan(model, workflow, graph, meta, prompt, seed, options, "video", job.get("sources") or [], remembered)
543
+ "start_image": shown(agent, start) if start else "", "end_image": shown(agent, end) if end else "",
544
+ "camera": options.camera, "extends": shown(agent, clip) if clip else ""}) # fmt: skip
545
+ remembered = {k: job.get(k) for k in ("kind", "prompt", "sources", "mask", "pads", "keep_shape", "extend", "size")}
546
+ stem = f"{clip.stem}-extended" if clip else ""
547
+ plan = Plan(model, workflow, graph, meta, prompt, seed, options, "video", job.get("sources") or [], remembered,
548
+ stem) # fmt: skip
532
549
  return run_plan(agent, server, plan, cancelled, label)
533
550
 
534
551
 
552
+ VIDEO_LATENTS = ("Wan22ImageToVideoLatent", "WanFunInpaintToVideo", "WanCameraImageToVideo")
553
+ WHY = {
554
+ "first-last-frame": "a clip between two frames",
555
+ "camera": "a camera move",
556
+ "image-to-video": "continuing a clip",
557
+ }
558
+
559
+
560
+ def video_model(agent: Agent, task: str) -> catalog.MediaModel:
561
+ """The video model for a job: the chosen one, or the first that does what the job needs."""
562
+ chosen = current(agent, "video")
563
+ if not task or task in chosen.tasks:
564
+ return chosen
565
+ model = catalog.load().for_task("video", task)
566
+ if model is None:
567
+ raise MediaError(f"None of lcode's video models does {WHY.get(task, task)} yet.")
568
+ agent.say(f"{model.name} makes this one ({WHY.get(task, task)}).")
569
+ return model
570
+
571
+
572
+ def continue_clip(graph: dict, latent: str, clip_name: str, length: int) -> None:
573
+ """Extending a clip: start from its last frame, and join the new frames (without that first one) after it."""
574
+ graph["8200"] = {"class_type": "LoadVideo", "inputs": {"file": clip_name}}
575
+ graph["8201"] = {"class_type": "GetVideoComponents", "inputs": {"video": ["8200", 0]}}
576
+ graph["8202"] = {"class_type": "ImageFromBatch", "inputs": {"image": ["8201", 0], "batch_index": -1, "length": 1}}
577
+ graph[latent]["inputs"]["start_image"] = ["8202", 0]
578
+ video = edits.node_of(graph, "CreateVideo")
579
+ decoded = graph[video]["inputs"]["images"]
580
+ graph["8203"] = {"class_type": "ImageFromBatch", "inputs": {"image": decoded, "batch_index": 1, "length": length}}
581
+ graph["8204"] = {"class_type": "ImageBatch", "inputs": {"image1": ["8201", 0], "image2": ["8203", 0]}}
582
+ graph[video]["inputs"]["images"] = ["8204", 0]
583
+
584
+
535
585
  def apply_tool(
536
586
  agent: Agent,
537
587
  task: str,
@@ -146,3 +146,53 @@ measured = "5 s clip: 100 s fast, 150 s balanced (864×480), 10.5 min best (1280
146
146
  notes = "5 s at 24 fps, up to 1280×704; text or a start image"
147
147
  quality = { fast = [12, 0.4], balanced = [20, 0.4], best = [30, 0.859] }
148
148
  negative = true
149
+
150
+ [files.wan22-fun-inp-5b]
151
+ folder = "diffusion_models"
152
+ name = "wan2.2_fun_inpaint_5B_bf16.safetensors"
153
+ url = "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_fun_inpaint_5B_bf16.safetensors"
154
+ gb = 10.00
155
+
156
+ [files.wan21-fun-camera-1-3b]
157
+ folder = "diffusion_models"
158
+ name = "wan2.1_fun_camera_v1.1_1.3B_bf16.safetensors"
159
+ url = "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_fun_camera_v1.1_1.3B_bf16.safetensors"
160
+ gb = 3.23
161
+
162
+ [files.clip-vision-h]
163
+ folder = "clip_vision"
164
+ name = "clip_vision_h.safetensors"
165
+ url = "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/clip_vision/clip_vision_h.safetensors"
166
+ gb = 1.26
167
+
168
+ [files.wan21-vae]
169
+ folder = "vae"
170
+ name = "wan_2.1_vae.safetensors"
171
+ url = "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/vae/wan_2.1_vae.safetensors"
172
+ gb = 0.25
173
+
174
+ [models.wan22-fun-inp-5b]
175
+ name = "Wan 2.2 Fun InP 5B"
176
+ mode = "video"
177
+ publisher = "Alibaba PAI"
178
+ license = "Apache 2.0"
179
+ tasks = ["first-last-frame"]
180
+ workflow = "wan22-fun-inp-5b"
181
+ files = { model = "wan22-fun-inp-5b", text_encoder = "umt5-xxl-fp8", vae = "wan22-vae" }
182
+ measured = "3 s clip, fast: 62 s (864×480) on an RTX 4080 Laptop, 12 GB"
183
+ notes = "a clip from a first frame to a last frame: @start.png @end.png"
184
+ quality = { fast = [12, 0.4], balanced = [20, 0.4], best = [30, 0.859] }
185
+ negative = true
186
+
187
+ [models.wan21-fun-camera-1-3b]
188
+ name = "Wan 2.1 Fun Camera 1.3B"
189
+ mode = "video"
190
+ publisher = "Alibaba PAI"
191
+ license = "Apache 2.0"
192
+ tasks = ["camera"]
193
+ workflow = "wan21-fun-camera"
194
+ files = { model = "wan21-fun-camera-1-3b", text_encoder = "umt5-xxl-fp8", vae = "wan21-vae", clip_vision = "clip-vision-h" }
195
+ measured = "3 s clip (49 frames, 832×480): 115 s on an RTX 4080 Laptop, 12 GB"
196
+ notes = "camera moves (--camera pan-left, zoom-in, …), small and fast, 16 fps"
197
+ quality = { fast = [12, 0.38], balanced = [20, 0.38], best = [30, 0.38] }
198
+ negative = true
@@ -112,6 +112,10 @@ class Catalog:
112
112
  def for_mode(self, mode: str) -> list[MediaModel]:
113
113
  return [m for m in self.models.values() if m.mode == mode]
114
114
 
115
+ def for_task(self, mode: str, task: str) -> MediaModel | None:
116
+ """The first model of a mode that does a task (first-last-frame, camera, image-to-video…)."""
117
+ return next((m for m in self.for_mode(mode) if task in m.tasks), None)
118
+
115
119
  def tool(self, task: str) -> MediaModel:
116
120
  """The tool for a task (upscale, remove-background)."""
117
121
  return next(m for m in self.models.values() if m.mode == "tool" and task in m.tasks)
@@ -86,7 +86,7 @@ COMMANDS = {
86
86
  "/open": "Open an image in your viewer, e.g. /open 3 (default: the last)",
87
87
  "/edit": "Image mode: change an image by instruction, e.g. /edit 3 make the sky purple (@photo.png: references)",
88
88
  "/use": "Image mode: images the next requests follow, e.g. /use 3 or /use photo.png (/use off)",
89
- "/extend": "Image mode: outpaint an image to a shape or by sides, e.g. /extend 3 --ar 16:9 (or --left 256)",
89
+ "/extend": "Outpaint an image (/extend 3 --ar 16:9), or continue a clip in video mode (/extend 4 --seconds 3)",
90
90
  "/upscale": "Image mode: 2 or 4 times the pixels, e.g. /upscale 3 4",
91
91
  "/nobg": "Image mode: remove the background (a transparent PNG), e.g. /nobg 3",
92
92
  "/animate": "Image mode: make a video starting from an image, e.g. /animate 3 the waves roll in (video mode)",
@@ -1406,7 +1406,7 @@ IMAGE_COMMANDS = (
1406
1406
  "/options", "/again", "/vary", "/gallery", "/open", "/play", "/edit", "/use", "/extend", "/upscale", "/nobg",
1407
1407
  "/animate",
1408
1408
  ) # fmt: skip
1409
- VIDEO_TOO = ("/options", "/again", "/vary", "/gallery", "/open", "/play", "/use") # what video mode shares
1409
+ VIDEO_TOO = ("/options", "/again", "/vary", "/gallery", "/open", "/play", "/use", "/extend") # video mode has these
1410
1410
  NUMBER = re.compile(r"^#?(\d+)$")
1411
1411
 
1412
1412
 
@@ -1495,6 +1495,17 @@ def image_command(agent: Agent, cmd: str, arg: str) -> None:
1495
1495
  "pads": None, "keep_shape": "--ar" not in rest and "--size" not in rest} # fmt: skip
1496
1496
  make_image(agent, run=lambda cancel, ask: media.redo(agent, job, cancel, ask))
1497
1497
  return
1498
+ if cmd == "/extend" and video:
1499
+ if edits.is_image(source):
1500
+ c.print(f"[yellow]#{entry['n']} is a picture: /animate it, or /extend a clip.[/]")
1501
+ return
1502
+ text, options = imageopts.parse(rest, imageopts.replace_seed(agent.image_options, n=1))
1503
+ job = {"kind": "video", "prompt": text or entry["prompt"], "options": options, "sources": [],
1504
+ "mask": None, "pads": None, "keep_shape": True, "extend": str(source),
1505
+ "size": [entry["width"], entry["height"]]} # fmt: skip
1506
+ c.print(f"[dim]Continuing #{entry['n']} from its last frame, {options.seconds:g} s more.[/]")
1507
+ make_image(agent, run=lambda cancel, ask: media.redo(agent, job, cancel, ask))
1508
+ return
1498
1509
  if cmd == "/extend":
1499
1510
  width, height = edits.image_size(source)
1500
1511
  pads, text = edits.parse_extend(rest, width, height)
@@ -0,0 +1,186 @@
1
+ {
2
+ "source": "ComfyUI's template video_wan2.1_fun_camera_v1.1_1.3B, in API format (lcode adds a start image when given)",
3
+ "defaults": {
4
+ "width": 832,
5
+ "height": 480,
6
+ "length": 81,
7
+ "fps": 16,
8
+ "steps": 20,
9
+ "negative": "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走",
10
+ "camera": "Static",
11
+ "speed": 1.0
12
+ },
13
+ "labels": {
14
+ "1": "loading the model",
15
+ "2": "loading the text encoder",
16
+ "3": "loading the VAE",
17
+ "5": "reading the prompt",
18
+ "6": "reading the prompt",
19
+ "7": "planning the camera move",
20
+ "8": "preparing the frames",
21
+ "9": "drawing the frames",
22
+ "10": "decoding the frames",
23
+ "11": "making the video",
24
+ "12": "saving"
25
+ },
26
+ "graph": {
27
+ "1": {
28
+ "class_type": "UNETLoader",
29
+ "inputs": {
30
+ "unet_name": "$model",
31
+ "weight_dtype": "default"
32
+ }
33
+ },
34
+ "2": {
35
+ "class_type": "CLIPLoader",
36
+ "inputs": {
37
+ "clip_name": "$text_encoder",
38
+ "type": "wan",
39
+ "device": "default"
40
+ }
41
+ },
42
+ "3": {
43
+ "class_type": "VAELoader",
44
+ "inputs": {
45
+ "vae_name": "$vae"
46
+ }
47
+ },
48
+ "4": {
49
+ "class_type": "ModelSamplingSD3",
50
+ "inputs": {
51
+ "model": [
52
+ "1",
53
+ 0
54
+ ],
55
+ "shift": 8
56
+ }
57
+ },
58
+ "5": {
59
+ "class_type": "CLIPTextEncode",
60
+ "inputs": {
61
+ "text": "$prompt",
62
+ "clip": [
63
+ "2",
64
+ 0
65
+ ]
66
+ }
67
+ },
68
+ "6": {
69
+ "class_type": "CLIPTextEncode",
70
+ "inputs": {
71
+ "text": "$negative",
72
+ "clip": [
73
+ "2",
74
+ 0
75
+ ]
76
+ }
77
+ },
78
+ "7": {
79
+ "class_type": "WanCameraEmbedding",
80
+ "inputs": {
81
+ "camera_pose": "$camera",
82
+ "width": "$width",
83
+ "height": "$height",
84
+ "length": "$length",
85
+ "speed": "$speed",
86
+ "fx": 0.5,
87
+ "fy": 0.5,
88
+ "cx": 0.5,
89
+ "cy": 0.5
90
+ }
91
+ },
92
+ "8": {
93
+ "class_type": "WanCameraImageToVideo",
94
+ "inputs": {
95
+ "positive": [
96
+ "5",
97
+ 0
98
+ ],
99
+ "negative": [
100
+ "6",
101
+ 0
102
+ ],
103
+ "vae": [
104
+ "3",
105
+ 0
106
+ ],
107
+ "width": "$width",
108
+ "height": "$height",
109
+ "length": "$length",
110
+ "batch_size": 1,
111
+ "camera_conditions": [
112
+ "7",
113
+ 0
114
+ ]
115
+ }
116
+ },
117
+ "9": {
118
+ "class_type": "KSampler",
119
+ "inputs": {
120
+ "model": [
121
+ "4",
122
+ 0
123
+ ],
124
+ "seed": "$seed",
125
+ "steps": "$steps",
126
+ "cfg": 6,
127
+ "sampler_name": "uni_pc",
128
+ "scheduler": "simple",
129
+ "positive": [
130
+ "8",
131
+ 0
132
+ ],
133
+ "negative": [
134
+ "8",
135
+ 1
136
+ ],
137
+ "latent_image": [
138
+ "8",
139
+ 2
140
+ ],
141
+ "denoise": 1
142
+ }
143
+ },
144
+ "10": {
145
+ "class_type": "VAEDecode",
146
+ "inputs": {
147
+ "samples": [
148
+ "9",
149
+ 0
150
+ ],
151
+ "vae": [
152
+ "3",
153
+ 0
154
+ ]
155
+ }
156
+ },
157
+ "11": {
158
+ "class_type": "CreateVideo",
159
+ "inputs": {
160
+ "images": [
161
+ "10",
162
+ 0
163
+ ],
164
+ "fps": "$fps"
165
+ }
166
+ },
167
+ "12": {
168
+ "class_type": "SaveVideo",
169
+ "inputs": {
170
+ "video": [
171
+ "11",
172
+ 0
173
+ ],
174
+ "filename_prefix": "lcode/video",
175
+ "format": "mp4",
176
+ "codec": "auto"
177
+ }
178
+ },
179
+ "13": {
180
+ "class_type": "CLIPVisionLoader",
181
+ "inputs": {
182
+ "clip_name": "$clip_vision"
183
+ }
184
+ }
185
+ }
186
+ }