npcsh 1.1.21__py3-none-any.whl → 1.1.23__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 (188) hide show
  1. npcsh/_state.py +282 -125
  2. npcsh/benchmark/npcsh_agent.py +77 -232
  3. npcsh/benchmark/templates/install-npcsh.sh.j2 +12 -4
  4. npcsh/config.py +5 -2
  5. npcsh/mcp_server.py +9 -1
  6. npcsh/npc_team/alicanto.npc +8 -6
  7. npcsh/npc_team/corca.npc +5 -12
  8. npcsh/npc_team/frederic.npc +6 -9
  9. npcsh/npc_team/guac.npc +4 -4
  10. npcsh/npc_team/jinxs/lib/core/delegate.jinx +1 -1
  11. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +84 -62
  12. npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
  13. npcsh/npc_team/jinxs/lib/core/skill.jinx +59 -0
  14. npcsh/npc_team/jinxs/lib/utils/help.jinx +194 -10
  15. npcsh/npc_team/jinxs/lib/utils/init.jinx +528 -37
  16. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -1
  17. npcsh/npc_team/jinxs/lib/utils/serve.jinx +938 -21
  18. npcsh/npc_team/jinxs/modes/alicanto.jinx +102 -41
  19. npcsh/npc_team/jinxs/modes/build.jinx +378 -0
  20. npcsh-1.1.21.data/data/npcsh/npc_team/config_tui.jinx → npcsh/npc_team/jinxs/modes/config.jinx +1 -1
  21. npcsh/npc_team/jinxs/modes/convene.jinx +670 -0
  22. npcsh/npc_team/jinxs/modes/corca.jinx +777 -387
  23. npcsh/npc_team/jinxs/modes/crond.jinx +818 -0
  24. npcsh/npc_team/jinxs/modes/kg.jinx +69 -2
  25. npcsh/npc_team/jinxs/modes/plonk.jinx +86 -15
  26. npcsh/npc_team/jinxs/modes/roll.jinx +368 -55
  27. npcsh/npc_team/jinxs/modes/skills.jinx +621 -0
  28. npcsh/npc_team/jinxs/modes/yap.jinx +1092 -177
  29. npcsh/npc_team/jinxs/skills/code-review/SKILL.md +45 -0
  30. npcsh/npc_team/jinxs/skills/debugging/SKILL.md +44 -0
  31. npcsh/npc_team/jinxs/skills/git-workflow.jinx +44 -0
  32. npcsh/npc_team/kadiefa.npc +6 -6
  33. npcsh/npc_team/npcsh.ctx +16 -0
  34. npcsh/npc_team/plonk.npc +5 -9
  35. npcsh/npc_team/sibiji.npc +15 -7
  36. npcsh/npcsh.py +1 -0
  37. npcsh/routes.py +0 -4
  38. npcsh/yap.py +22 -4
  39. npcsh-1.1.23.data/data/npcsh/npc_team/SKILL.md +44 -0
  40. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.jinx +102 -41
  41. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.npc +8 -6
  42. npcsh-1.1.23.data/data/npcsh/npc_team/build.jinx +378 -0
  43. npcsh/npc_team/jinxs/modes/config_tui.jinx → npcsh-1.1.23.data/data/npcsh/npc_team/config.jinx +1 -1
  44. npcsh-1.1.23.data/data/npcsh/npc_team/convene.jinx +670 -0
  45. npcsh-1.1.23.data/data/npcsh/npc_team/corca.jinx +820 -0
  46. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.npc +5 -12
  47. npcsh-1.1.23.data/data/npcsh/npc_team/crond.jinx +818 -0
  48. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/delegate.jinx +1 -1
  49. npcsh-1.1.23.data/data/npcsh/npc_team/edit_file.jinx +119 -0
  50. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic.npc +6 -9
  51. npcsh-1.1.23.data/data/npcsh/npc_team/git-workflow.jinx +44 -0
  52. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.npc +4 -4
  53. npcsh-1.1.23.data/data/npcsh/npc_team/help.jinx +236 -0
  54. npcsh-1.1.23.data/data/npcsh/npc_team/init.jinx +532 -0
  55. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/jinxs.jinx +0 -1
  56. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.npc +6 -6
  57. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kg.jinx +69 -2
  58. npcsh-1.1.23.data/data/npcsh/npc_team/npcsh.ctx +34 -0
  59. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.jinx +86 -15
  60. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.npc +5 -9
  61. npcsh-1.1.23.data/data/npcsh/npc_team/roll.jinx +378 -0
  62. npcsh-1.1.23.data/data/npcsh/npc_team/serve.jinx +943 -0
  63. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sh.jinx +1 -1
  64. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.npc +15 -7
  65. npcsh-1.1.23.data/data/npcsh/npc_team/skill.jinx +59 -0
  66. npcsh-1.1.23.data/data/npcsh/npc_team/skills.jinx +621 -0
  67. npcsh-1.1.23.data/data/npcsh/npc_team/yap.jinx +1190 -0
  68. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/METADATA +404 -278
  69. npcsh-1.1.23.dist-info/RECORD +216 -0
  70. npcsh/npc_team/jinxs/incognide/add_tab.jinx +0 -11
  71. npcsh/npc_team/jinxs/incognide/close_pane.jinx +0 -9
  72. npcsh/npc_team/jinxs/incognide/close_tab.jinx +0 -10
  73. npcsh/npc_team/jinxs/incognide/confirm.jinx +0 -10
  74. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +0 -9
  75. npcsh/npc_team/jinxs/incognide/list_panes.jinx +0 -8
  76. npcsh/npc_team/jinxs/incognide/navigate.jinx +0 -10
  77. npcsh/npc_team/jinxs/incognide/notify.jinx +0 -10
  78. npcsh/npc_team/jinxs/incognide/open_pane.jinx +0 -13
  79. npcsh/npc_team/jinxs/incognide/read_pane.jinx +0 -9
  80. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +0 -10
  81. npcsh/npc_team/jinxs/incognide/send_message.jinx +0 -10
  82. npcsh/npc_team/jinxs/incognide/split_pane.jinx +0 -12
  83. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +0 -10
  84. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +0 -10
  85. npcsh/npc_team/jinxs/incognide/write_file.jinx +0 -11
  86. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +0 -9
  87. npcsh/npc_team/jinxs/lib/core/convene.jinx +0 -232
  88. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +0 -429
  89. npcsh/npc_team/jinxs/lib/core/search.jinx +0 -54
  90. npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -65
  91. npcsh-1.1.21.data/data/npcsh/npc_team/add_tab.jinx +0 -11
  92. npcsh-1.1.21.data/data/npcsh/npc_team/build.jinx +0 -65
  93. npcsh-1.1.21.data/data/npcsh/npc_team/close_pane.jinx +0 -9
  94. npcsh-1.1.21.data/data/npcsh/npc_team/close_tab.jinx +0 -10
  95. npcsh-1.1.21.data/data/npcsh/npc_team/confirm.jinx +0 -10
  96. npcsh-1.1.21.data/data/npcsh/npc_team/convene.jinx +0 -232
  97. npcsh-1.1.21.data/data/npcsh/npc_team/corca.jinx +0 -430
  98. npcsh-1.1.21.data/data/npcsh/npc_team/edit_file.jinx +0 -97
  99. npcsh-1.1.21.data/data/npcsh/npc_team/focus_pane.jinx +0 -9
  100. npcsh-1.1.21.data/data/npcsh/npc_team/help.jinx +0 -52
  101. npcsh-1.1.21.data/data/npcsh/npc_team/init.jinx +0 -41
  102. npcsh-1.1.21.data/data/npcsh/npc_team/kg_search.jinx +0 -429
  103. npcsh-1.1.21.data/data/npcsh/npc_team/list_panes.jinx +0 -8
  104. npcsh-1.1.21.data/data/npcsh/npc_team/navigate.jinx +0 -10
  105. npcsh-1.1.21.data/data/npcsh/npc_team/notify.jinx +0 -10
  106. npcsh-1.1.21.data/data/npcsh/npc_team/npcsh.ctx +0 -18
  107. npcsh-1.1.21.data/data/npcsh/npc_team/open_pane.jinx +0 -13
  108. npcsh-1.1.21.data/data/npcsh/npc_team/read_pane.jinx +0 -9
  109. npcsh-1.1.21.data/data/npcsh/npc_team/roll.jinx +0 -65
  110. npcsh-1.1.21.data/data/npcsh/npc_team/run_terminal.jinx +0 -10
  111. npcsh-1.1.21.data/data/npcsh/npc_team/search.jinx +0 -54
  112. npcsh-1.1.21.data/data/npcsh/npc_team/send_message.jinx +0 -10
  113. npcsh-1.1.21.data/data/npcsh/npc_team/serve.jinx +0 -26
  114. npcsh-1.1.21.data/data/npcsh/npc_team/split_pane.jinx +0 -12
  115. npcsh-1.1.21.data/data/npcsh/npc_team/switch_npc.jinx +0 -10
  116. npcsh-1.1.21.data/data/npcsh/npc_team/switch_tab.jinx +0 -10
  117. npcsh-1.1.21.data/data/npcsh/npc_team/write_file.jinx +0 -11
  118. npcsh-1.1.21.data/data/npcsh/npc_team/yap.jinx +0 -275
  119. npcsh-1.1.21.data/data/npcsh/npc_team/zen_mode.jinx +0 -9
  120. npcsh-1.1.21.dist-info/RECORD +0 -243
  121. /npcsh/npc_team/jinxs/lib/{core → utils}/chat.jinx +0 -0
  122. /npcsh/npc_team/jinxs/lib/{core → utils}/cmd.jinx +0 -0
  123. /npcsh/npc_team/jinxs/{incognide → lib/utils}/incognide.jinx +0 -0
  124. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.png +0 -0
  125. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
  126. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
  127. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  128. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  129. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/chat.jinx +0 -0
  130. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/click.jinx +0 -0
  131. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  132. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  133. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compile.jinx +0 -0
  134. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compress.jinx +0 -0
  135. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.png +0 -0
  136. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca_example.png +0 -0
  137. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/db_search.jinx +0 -0
  138. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/file_search.jinx +0 -0
  139. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic4.png +0 -0
  140. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/git.jinx +0 -0
  141. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.jinx +0 -0
  142. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.png +0 -0
  143. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  144. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  145. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  146. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  147. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  148. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/memories.jinx +0 -0
  149. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/models.jinx +0 -0
  150. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  151. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/nql.jinx +0 -0
  152. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  153. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/ots.jinx +0 -0
  154. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/papers.jinx +0 -0
  155. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/paste.jinx +0 -0
  156. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.png +0 -0
  157. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  158. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/pti.jinx +0 -0
  159. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/python.jinx +0 -0
  160. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/reattach.jinx +0 -0
  161. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sample.jinx +0 -0
  162. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  163. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/set.jinx +0 -0
  164. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/setup.jinx +0 -0
  165. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/shh.jinx +0 -0
  166. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.png +0 -0
  167. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  168. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.jinx +0 -0
  169. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.png +0 -0
  170. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sql.jinx +0 -0
  171. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switch.jinx +0 -0
  172. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switches.jinx +0 -0
  173. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sync.jinx +0 -0
  174. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/team.jinx +0 -0
  175. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  176. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  177. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  178. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/usage.jinx +0 -0
  179. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  180. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  181. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wait.jinx +0 -0
  182. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wander.jinx +0 -0
  183. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/web_search.jinx +0 -0
  184. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.png +0 -0
  185. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/WHEEL +0 -0
  186. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/entry_points.txt +0 -0
  187. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/licenses/LICENSE +0 -0
  188. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/top_level.txt +0 -0
@@ -1,65 +1,378 @@
1
1
  jinx_name: "roll"
2
- description: "Generate a video from a text prompt."
2
+ description: "Video creation studio TUI - generate and manage videos with parameter controls"
3
+ interactive: true
3
4
  inputs:
4
- - prompt: ""
5
- - vgmodel: ""
6
- - vgprovider: ""
7
- - num_frames: 125
8
- - width: 256
9
- - height: 256
10
- - output_path: "output.mp4"
5
+ - prompt: null
6
+ - model: null
7
+ - provider: null
8
+ - output_path: null
9
+ - num_frames: null
10
+ - width: null
11
+ - height: null
11
12
  steps:
12
- - name: "generate_video"
13
+ - name: "roll_tui"
13
14
  engine: "python"
14
15
  code: |
15
- import traceback
16
+ import os
17
+ import sys
18
+ import tty
19
+ import termios
20
+ import select
21
+ from datetime import datetime
22
+
16
23
  from npcpy.llm_funcs import gen_video
17
24
 
18
- prompt = context.get('prompt')
19
- num_frames = int(context.get('num_frames', 125))
20
- width = int(context.get('width', 256))
21
- height = int(context.get('height', 256))
22
- output_path = context.get('output_path')
23
- video_gen_model = context.get('vgmodel')
24
- video_gen_provider = context.get('vgprovider')
25
- output_messages = context.get('messages', [])
26
- current_npc = context.get('npc')
27
-
28
- if not prompt or not prompt.strip():
29
- context['output'] = "Usage: /roll <your prompt>"
30
- context['messages'] = output_messages
31
- exit()
32
-
33
- if not video_gen_model and current_npc and current_npc.model:
34
- video_gen_model = current_npc.model
35
- if not video_gen_provider and current_npc and current_npc.provider:
36
- video_gen_provider = current_npc.provider
37
-
38
- if not video_gen_model:
39
- video_gen_model = "stable-video-diffusion"
40
- if not video_gen_provider:
41
- video_gen_provider = "diffusers"
25
+ npc = context.get('npc')
26
+ if isinstance(npc, str):
27
+ npc = None
28
+
29
+ # ========== State ==========
30
+ class RollState:
31
+ def __init__(self):
32
+ self.prompt = ""
33
+ self.model = ""
34
+ self.provider = ""
35
+ self.width_val = 256
36
+ self.height_val = 256
37
+ self.num_frames = 125
38
+ self.output_path = ""
39
+ # UI state
40
+ self.params = ['prompt', 'model', 'provider', 'width', 'height', 'num_frames', 'output']
41
+ self.sel = 0
42
+ self.scroll = 0
43
+ self.mode = 'params' # params, editing, gallery, generating
44
+ self.edit_buf = ""
45
+ self.edit_cursor = 0
46
+ self.status = "Ready"
47
+ self.gallery = [] # [{"path": str, "prompt": str, "timestamp": str}]
48
+ self.gallery_sel = 0
49
+ self.gallery_scroll = 0
50
+
51
+ ui = RollState()
42
52
 
53
+ # Load from context
54
+ ui.prompt = str(context.get('prompt') or '')
55
+ ui.model = str(context.get('model') or os.getenv('NPCSH_VIDEO_GEN_MODEL', ''))
56
+ ui.provider = str(context.get('provider') or os.getenv('NPCSH_VIDEO_GEN_PROVIDER', ''))
57
+ if not ui.model and npc and hasattr(npc, 'model') and npc.model:
58
+ ui.model = npc.model
59
+ if not ui.provider and npc and hasattr(npc, 'provider') and npc.provider:
60
+ ui.provider = npc.provider
61
+ if not ui.model:
62
+ ui.model = "stable-video-diffusion"
63
+ if not ui.provider:
64
+ ui.provider = "diffusers"
43
65
  try:
44
- result = gen_video(
45
- prompt=prompt,
46
- model=video_gen_model,
47
- provider=video_gen_provider,
48
- npc=current_npc,
49
- num_frames=num_frames,
50
- width=width,
51
- height=height,
52
- output_path=output_path,
53
- **context.get('api_kwargs', {})
54
- )
55
-
56
- if isinstance(result, dict):
57
- context['output'] = result.get('output', 'Video generated.')
58
- context['messages'] = result.get('messages', output_messages)
66
+ ui.width_val = int(context.get('width') or 256)
67
+ except:
68
+ pass
69
+ try:
70
+ ui.height_val = int(context.get('height') or 256)
71
+ except:
72
+ pass
73
+ try:
74
+ ui.num_frames = int(context.get('num_frames') or 125)
75
+ except:
76
+ pass
77
+ ui.output_path = str(context.get('output_path') or '')
78
+
79
+ # Load existing gallery
80
+ vid_dir = os.path.expanduser("~/.npcsh/videos/")
81
+ if os.path.isdir(vid_dir):
82
+ for f in sorted(os.listdir(vid_dir), reverse=True)[:50]:
83
+ if f.lower().endswith(('.mp4', '.webm', '.avi', '.mov', '.mkv')):
84
+ ui.gallery.append({"path": os.path.join(vid_dir, f), "prompt": "", "timestamp": f})
85
+
86
+ def get_size():
87
+ try:
88
+ s = os.get_terminal_size()
89
+ return s.columns, s.lines
90
+ except:
91
+ return 80, 24
92
+
93
+ def get_param_value(idx):
94
+ p = ui.params[idx]
95
+ if p == 'prompt': return ui.prompt
96
+ if p == 'model': return ui.model or '(default)'
97
+ if p == 'provider': return ui.provider or '(default)'
98
+ if p == 'width': return str(ui.width_val)
99
+ if p == 'height': return str(ui.height_val)
100
+ if p == 'num_frames': return str(ui.num_frames)
101
+ if p == 'output': return ui.output_path or '(auto)'
102
+ return ''
103
+
104
+ def set_param_value(idx, val):
105
+ p = ui.params[idx]
106
+ if p == 'prompt': ui.prompt = val
107
+ elif p == 'model': ui.model = val
108
+ elif p == 'provider': ui.provider = val
109
+ elif p == 'width':
110
+ try: ui.width_val = int(val)
111
+ except: pass
112
+ elif p == 'height':
113
+ try: ui.height_val = int(val)
114
+ except: pass
115
+ elif p == 'num_frames':
116
+ try: ui.num_frames = max(1, int(val))
117
+ except: pass
118
+ elif p == 'output': ui.output_path = val
119
+
120
+ def generate_videos():
121
+ ui.mode = 'generating'
122
+ ui.status = "Generating..."
123
+ render_screen()
124
+
125
+ if not ui.prompt:
126
+ ui.status = "Error: No prompt provided"
127
+ ui.mode = 'params'
128
+ return
129
+
130
+ try:
131
+ if ui.output_path and ui.output_path.strip():
132
+ out_file = os.path.expanduser(ui.output_path)
133
+ else:
134
+ os.makedirs(vid_dir, exist_ok=True)
135
+ out_file = os.path.join(vid_dir, "roll_" + datetime.now().strftime('%Y%m%d_%H%M%S') + ".mp4")
136
+
137
+ result = gen_video(
138
+ prompt=ui.prompt,
139
+ model=ui.model or None,
140
+ provider=ui.provider or None,
141
+ npc=npc,
142
+ num_frames=ui.num_frames,
143
+ width=ui.width_val,
144
+ height=ui.height_val,
145
+ output_path=out_file,
146
+ **context.get('api_kwargs', {})
147
+ )
148
+
149
+ if isinstance(result, dict):
150
+ msg = result.get('output', 'Video generated.')
151
+ else:
152
+ msg = str(result)
153
+
154
+ ui.gallery.insert(0, {"path": out_file, "prompt": ui.prompt, "timestamp": os.path.basename(out_file)})
155
+ ui.status = "Generated: " + os.path.basename(out_file)
156
+ except Exception as e:
157
+ ui.status = "Error: " + str(e)[:60]
158
+
159
+ ui.mode = 'params'
160
+
161
+ # ========== Rendering ==========
162
+ def render_screen():
163
+ width, height = get_size()
164
+ out = []
165
+ out.append("\033[H")
166
+
167
+ header = " ROLL - Video Creation Studio "
168
+ out.append("\033[1;1H\033[7;1m" + header.ljust(width) + "\033[0m")
169
+
170
+ if ui.mode in ('params', 'editing', 'generating'):
171
+ # Parameter panel
172
+ out.append("\033[3;1H\033[36;1m Parameters \033[90m" + ("-" * (width - 13)) + "\033[0m")
173
+
174
+ for i, p in enumerate(ui.params):
175
+ row = 4 + i
176
+ out.append("\033[" + str(row) + ";1H\033[K")
177
+ label = p.capitalize() + ":"
178
+ val = get_param_value(i)
179
+
180
+ if ui.mode == 'editing' and i == ui.sel:
181
+ line = " " + label.ljust(14) + "\033[7m " + ui.edit_buf + " \033[0m"
182
+ else:
183
+ line = " " + label.ljust(14) + val[:width - 18]
184
+
185
+ if i == ui.sel and ui.mode != 'editing':
186
+ out.append("\033[7m>" + line + "\033[0m")
187
+ else:
188
+ out.append(" " + line)
189
+
190
+ # Gallery preview
191
+ gallery_row = 4 + len(ui.params) + 1
192
+ out.append("\033[" + str(gallery_row) + ";1H\033[33;1m Gallery (" + str(len(ui.gallery)) + ") \033[90m" + ("-" * (width - 20)) + "\033[0m")
193
+
194
+ gallery_h = height - gallery_row - 4
195
+ for i in range(max(0, gallery_h)):
196
+ idx = ui.gallery_scroll + i
197
+ row = gallery_row + 1 + i
198
+ out.append("\033[" + str(row) + ";1H\033[K")
199
+ if idx >= len(ui.gallery):
200
+ continue
201
+ g = ui.gallery[idx]
202
+ fname = os.path.basename(g['path'])[:width - 6]
203
+ out.append(" " + fname)
204
+
205
+ elif ui.mode == 'gallery':
206
+ out.append("\033[3;1H\033[33;1m Gallery (" + str(len(ui.gallery)) + ") \033[90m" + ("-" * (width - 20)) + "\033[0m")
207
+
208
+ gallery_h = height - 6
209
+ for i in range(gallery_h):
210
+ idx = ui.gallery_scroll + i
211
+ row = 4 + i
212
+ out.append("\033[" + str(row) + ";1H\033[K")
213
+ if idx >= len(ui.gallery):
214
+ continue
215
+ g = ui.gallery[idx]
216
+ fname = os.path.basename(g['path'])
217
+ if idx == ui.gallery_sel:
218
+ out.append("\033[7m> " + fname[:width-4] + "\033[0m")
219
+ else:
220
+ out.append(" " + fname[:width-4])
221
+
222
+ # Status + footer
223
+ out.append("\033[" + str(height-2) + ";1H\033[K\033[90m" + ("-" * width) + "\033[0m")
224
+ out.append("\033[" + str(height-1) + ";1H\033[K " + ui.status[:width-2])
225
+
226
+ if ui.mode == 'editing':
227
+ footer = " Type value, Enter:Confirm Esc:Cancel "
228
+ elif ui.mode == 'gallery':
229
+ footer = " j/k:Nav o:Open b:Back q:Quit "
230
+ elif ui.mode == 'generating':
231
+ footer = " Generating... "
59
232
  else:
60
- context['output'] = str(result)
61
- context['messages'] = output_messages
62
- except Exception as e:
63
- traceback.print_exc()
64
- context['output'] = f"Error generating video: {e}"
65
- context['messages'] = output_messages
233
+ footer = " j/k:Nav e:Edit Enter:Generate g:Gallery q:Quit "
234
+ out.append("\033[" + str(height) + ";1H\033[K\033[7m" + footer.ljust(width) + "\033[0m")
235
+
236
+ sys.stdout.write(''.join(out))
237
+ sys.stdout.flush()
238
+
239
+ # ========== Input Handling ==========
240
+ def handle_input(c, fd):
241
+ if ui.mode == 'editing':
242
+ return handle_edit(c, fd)
243
+ if ui.mode == 'gallery':
244
+ return handle_gallery(c, fd)
245
+
246
+ if c == '\x1b':
247
+ if select.select([fd], [], [], 0.05)[0]:
248
+ c2 = os.read(fd, 1).decode('latin-1')
249
+ if c2 == '[':
250
+ c3 = os.read(fd, 1).decode('latin-1')
251
+ if c3 == 'A':
252
+ ui.sel = max(0, ui.sel - 1)
253
+ elif c3 == 'B':
254
+ ui.sel = min(len(ui.params) - 1, ui.sel + 1)
255
+ return True
256
+
257
+ if c == 'q':
258
+ return False
259
+ elif c == 'j':
260
+ ui.sel = min(len(ui.params) - 1, ui.sel + 1)
261
+ elif c == 'k':
262
+ ui.sel = max(0, ui.sel - 1)
263
+ elif c == 'e' or c in ('\r', '\n'):
264
+ if c in ('\r', '\n') and ui.sel == 0 and ui.prompt:
265
+ # Enter on prompt with existing prompt = generate
266
+ generate_videos()
267
+ else:
268
+ ui.mode = 'editing'
269
+ ui.edit_buf = get_param_value(ui.sel)
270
+ if ui.edit_buf in ('(default)', '(auto)'):
271
+ ui.edit_buf = ""
272
+ ui.edit_cursor = len(ui.edit_buf)
273
+ elif c == 'g':
274
+ ui.mode = 'gallery'
275
+ ui.gallery_sel = 0
276
+ ui.gallery_scroll = 0
277
+ elif c == 'G':
278
+ generate_videos()
279
+
280
+ return True
281
+
282
+ def handle_edit(c, fd):
283
+ if c == '\x1b':
284
+ if select.select([fd], [], [], 0.05)[0]:
285
+ os.read(fd, 2)
286
+ ui.mode = 'params'
287
+ return True
288
+
289
+ if c in ('\r', '\n'):
290
+ set_param_value(ui.sel, ui.edit_buf)
291
+ ui.mode = 'params'
292
+ return True
293
+
294
+ if c == '\x7f' or c == '\x08':
295
+ if ui.edit_cursor > 0:
296
+ ui.edit_buf = ui.edit_buf[:ui.edit_cursor-1] + ui.edit_buf[ui.edit_cursor:]
297
+ ui.edit_cursor -= 1
298
+ elif c >= ' ' and c <= '~':
299
+ ui.edit_buf = ui.edit_buf[:ui.edit_cursor] + c + ui.edit_buf[ui.edit_cursor:]
300
+ ui.edit_cursor += 1
301
+
302
+ return True
303
+
304
+ def handle_gallery(c, fd):
305
+ if c == '\x1b':
306
+ if select.select([fd], [], [], 0.05)[0]:
307
+ c2 = os.read(fd, 1).decode('latin-1')
308
+ if c2 == '[':
309
+ c3 = os.read(fd, 1).decode('latin-1')
310
+ if c3 == 'A':
311
+ ui.gallery_sel = max(0, ui.gallery_sel - 1)
312
+ elif c3 == 'B':
313
+ ui.gallery_sel = min(max(0, len(ui.gallery) - 1), ui.gallery_sel + 1)
314
+ else:
315
+ ui.mode = 'params'
316
+ return True
317
+
318
+ if c == 'q':
319
+ return False
320
+ elif c == 'b':
321
+ ui.mode = 'params'
322
+ elif c == 'j':
323
+ ui.gallery_sel = min(max(0, len(ui.gallery) - 1), ui.gallery_sel + 1)
324
+ elif c == 'k':
325
+ ui.gallery_sel = max(0, ui.gallery_sel - 1)
326
+ elif c == 'o' and ui.gallery:
327
+ import subprocess
328
+ path = ui.gallery[ui.gallery_sel]['path']
329
+ try:
330
+ subprocess.Popen(['xdg-open', path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
331
+ ui.status = "Opened: " + os.path.basename(path)
332
+ except:
333
+ ui.status = "Could not open video"
334
+
335
+ # Keep gallery_scroll in range
336
+ _, height = get_size()
337
+ gallery_h = height - 6
338
+ if ui.gallery_sel < ui.gallery_scroll:
339
+ ui.gallery_scroll = ui.gallery_sel
340
+ elif ui.gallery_sel >= ui.gallery_scroll + gallery_h:
341
+ ui.gallery_scroll = ui.gallery_sel - gallery_h + 1
342
+
343
+ return True
344
+
345
+ # ========== One-shot mode ==========
346
+ if ui.prompt and context.get('prompt'):
347
+ # If prompt provided via CLI args, generate immediately
348
+ generate_videos()
349
+ context['output'] = ui.status
350
+ context['messages'] = context.get('messages', [])
351
+
352
+ # ========== Main TUI Loop ==========
353
+ elif not sys.stdin.isatty():
354
+ context['output'] = "Roll requires an interactive terminal."
355
+ else:
356
+ fd = sys.stdin.fileno()
357
+ old_settings = termios.tcgetattr(fd)
358
+
359
+ try:
360
+ tty.setcbreak(fd)
361
+ sys.stdout.write('\033[?25l')
362
+ sys.stdout.write('\033[2J')
363
+ render_screen()
364
+
365
+ running = True
366
+ while running:
367
+ c = os.read(fd, 1).decode('latin-1')
368
+ running = handle_input(c, fd)
369
+ render_screen()
370
+
371
+ finally:
372
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
373
+ sys.stdout.write('\033[?25h')
374
+ sys.stdout.write('\033[2J\033[H')
375
+ sys.stdout.flush()
376
+
377
+ context['output'] = ui.status
378
+ context['messages'] = context.get('messages', [])