From 8ce4d3a2e99a9b3a1a12a402c2f4c34c4514f03e Mon Sep 17 00:00:00 2001 From: xl-openai Date: Tue, 21 Apr 2026 00:19:44 -0700 Subject: [PATCH] Remove curated Sora skill package --- skills/.curated/sora/LICENSE.txt | 201 --- skills/.curated/sora/SKILL.md | 178 --- skills/.curated/sora/agents/openai.yaml | 6 - skills/.curated/sora/assets/sora-small.svg | 4 - skills/.curated/sora/assets/sora.png | Bin 11731 -> 0 bytes .../sora/references/cinematic-shots.md | 53 - skills/.curated/sora/references/cli.md | 235 --- .../.curated/sora/references/codex-network.md | 28 - skills/.curated/sora/references/prompting.md | 144 -- .../sora/references/sample-prompts.md | 128 -- skills/.curated/sora/references/social-ads.md | 42 - .../sora/references/troubleshooting.md | 69 - skills/.curated/sora/references/video-api.md | 86 -- skills/.curated/sora/scripts/sora.py | 1274 ----------------- 14 files changed, 2448 deletions(-) delete mode 100644 skills/.curated/sora/LICENSE.txt delete mode 100644 skills/.curated/sora/SKILL.md delete mode 100644 skills/.curated/sora/agents/openai.yaml delete mode 100644 skills/.curated/sora/assets/sora-small.svg delete mode 100644 skills/.curated/sora/assets/sora.png delete mode 100644 skills/.curated/sora/references/cinematic-shots.md delete mode 100644 skills/.curated/sora/references/cli.md delete mode 100644 skills/.curated/sora/references/codex-network.md delete mode 100644 skills/.curated/sora/references/prompting.md delete mode 100644 skills/.curated/sora/references/sample-prompts.md delete mode 100644 skills/.curated/sora/references/social-ads.md delete mode 100644 skills/.curated/sora/references/troubleshooting.md delete mode 100644 skills/.curated/sora/references/video-api.md delete mode 100644 skills/.curated/sora/scripts/sora.py diff --git a/skills/.curated/sora/LICENSE.txt b/skills/.curated/sora/LICENSE.txt deleted file mode 100644 index 13e25df8..00000000 --- a/skills/.curated/sora/LICENSE.txt +++ /dev/null @@ -1,201 +0,0 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf of - any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don\'t include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/skills/.curated/sora/SKILL.md b/skills/.curated/sora/SKILL.md deleted file mode 100644 index c20338f1..00000000 --- a/skills/.curated/sora/SKILL.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -name: "sora" -description: "Use when the user asks to generate, edit, extend, poll, list, download, or delete Sora videos, create reusable non-human Sora character references, or run local multi-video queues via the bundled CLI (`scripts/sora.py`); includes requests like: (i) generate AI video, (ii) edit this Sora clip, (iii) extend this video, (iv) create a character reference, (v) download video/thumbnail/spritesheet, and (vi) Sora batch planning; requires `OPENAI_API_KEY` and Sora API access." ---- - - -# Sora Video Generation Skill - -Creates or manages Sora video jobs for the current project (product demos, marketing spots, cinematic shots, social clips, UI mocks). Defaults to `sora-2` with structured prompt augmentation and prefers the bundled CLI for deterministic runs. Note: `$sora` is a skill tag in prompts, not a shell command. - -## When to use -- Generate a new video clip from a prompt -- Create a reusable character reference from a short non-human source clip -- Edit an existing generated video with a targeted prompt change -- Extend a completed video with a continuation prompt -- Poll status, list jobs, or download assets (video/thumbnail/spritesheet) -- Run a local multi-job queue now, or plan a true Batch API submission for offline rendering - -## Decision tree -- If the user has a short non-human reference clip they want to reuse across shots → `create-character` -- If the user has a completed video and wants the next beat/continuation → `extend` -- If the user has a completed video and wants a targeted change while preserving the shot → `edit` -- If the user has a video id and wants status or assets → `status`, `poll`, or `download` -- If the user needs many renders immediately inside Codex → `create-batch` (local fan-out, not the Batch API) -- If the user needs many renders for offline processing or a studio pipeline → use the official Batch API flow described in `references/video-api.md` -- Otherwise → `create` (or `create-and-poll` if they need a ready asset in one step) - -## Workflow -1. Decide intent: create vs create-character vs edit vs extend vs status/download vs local queue vs official Batch API. -2. Collect inputs: prompt, model, size, seconds, any image reference, and any character IDs. -3. Prefer CLI augmentation flags (`--use-case`, `--scene`, `--camera`, etc.) instead of hand-writing a long structured prompt. If you already have a structured prompt file, pass `--no-augment`. -4. Run the bundled CLI (`scripts/sora.py`) with sensible defaults. For long prompts, prefer `--prompt-file` to avoid shell-escaping issues. -5. For async jobs, poll until terminal status (or use `create-and-poll`). -6. Download assets (video/thumbnail/spritesheet) and save them locally before URLs expire. -7. If the user wants continuity across many shots, create character assets first, then reference them in later `create` calls. -8. If the user wants to iterate on a completed shot, prefer `edit`; if they want the shot to continue in time, prefer `extend`. -9. Use one targeted change per iteration. - -## Authentication -- `OPENAI_API_KEY` must be set for live API calls. - -If the key is missing, give the user these steps: -1. Create an API key in the OpenAI platform UI: https://platform.openai.com/api-keys -2. Set `OPENAI_API_KEY` as an environment variable in their system. -3. Offer to guide them through setting the environment variable for their OS/shell if needed. -- Never ask the user to paste the full key in chat. Ask them to set it locally and confirm when ready. - -## Defaults & rules -- Default model: `sora-2` (use `sora-2-pro` for higher fidelity). -- Default size: `1280x720`. -- Default seconds: `4` (allowed: `"4"`, `"8"`, `"12"`, `"16"`, `"20"`). -- Always set size and seconds via API params; prose will not change them. -- `sora-2-pro` is required for `1920x1080` and `1080x1920`. -- Use up to two characters per generation. -- Use the OpenAI Python SDK (`openai` package). If high-level SDK helpers lag the latest Sora guide, use low-level `client.post/get/delete` inside the official SDK rather than standalone HTTP code. -- Require `OPENAI_API_KEY` before any live API call. -- If uv cache permissions fail, set `UV_CACHE_DIR=/tmp/uv-cache`. -- Input reference images must be jpg/png/webp and should match target size. -- JSON `input_reference` objects use either `file_id` or `image_url`; uploaded file paths use multipart. -- Download URLs expire after about 1 hour; copy assets to your own storage. -- Batch-generated videos remain downloadable for up to 24 hours after the batch completes. -- `create-batch` in `scripts/sora.py` is a local concurrent queue, not the official Batch API. -- Prefer the bundled CLI and **never modify** `scripts/sora.py` unless the user asks. -- Sora can generate audio; if a user requests voiceover/audio, specify it explicitly in the `Audio:` and `Dialogue:` lines and keep it short. - -## API limitations -- Models are limited to `sora-2` and `sora-2-pro`. -- API access to Sora models requires an organization-verified account. -- Duration must be set via the `seconds` parameter and currently supports `4`, `8`, `12`, `16`, and `20`. -- Character uploads currently work best with short `2`-`4` second non-human MP4s in `16:9` or `9:16`, at `720p`-`1080p`. -- Extensions can add up to `20` seconds each, up to six times per source video, for a maximum total length of `120` seconds. -- Extensions currently do not support characters or image references. -- This skill supports editing existing generated videos by ID. -- The official Batch API currently supports `POST /v1/videos` only, with JSON bodies rather than multipart uploads. -- Output sizes are limited by model (see `references/video-api.md` for the supported sizes). -- Video creation is async; you must poll for completion before downloading. -- Rate limits apply by usage tier (do not list specific limits). -- Content restrictions are enforced by the API (see Guardrails below). - -## Guardrails (must enforce) -- Only content suitable for audiences under 18. -- No copyrighted characters or copyrighted music. -- No real people (including public figures). -- Input images with human faces are rejected. -- Character uploads in this skill are for non-human subjects only. - -## Prompt augmentation -Reformat prompts into a structured, production-oriented spec. Only make implicit details explicit; do not invent new creative requirements. - -Template (include only relevant lines): -``` -Use case: -Primary request: -Scene/background: -Subject:
-Action: -Camera: -Lighting/mood: -Color palette: <3-5 color anchors> -Style/format: -Timing/beats: -Audio: -Text (verbatim): "" -Dialogue: - -- Speaker: "Short line." - -Constraints: -Avoid: -``` - -Augmentation rules: -- Keep it short; add only details the user already implied or provided elsewhere. -- For edits, explicitly list invariants ("same shot, change only X"). -- For character-based shots, mention the character name verbatim in the prompt. -- If any critical detail is missing and blocks success, ask a question; otherwise proceed. -- If you pass a structured prompt file to the CLI, add `--no-augment` to avoid the tool re-wrapping it. - -## Examples - -### Generation example (single shot) -``` -Use case: product teaser -Primary request: a close-up of a matte black camera on a pedestal -Action: slow 30-degree orbit over 4 seconds -Camera: 85mm, shallow depth of field, gentle handheld drift -Lighting/mood: soft key light, subtle rim, premium studio feel -Constraints: no logos, no text -``` - -### Edit example (invariants) -``` -Primary request: same shot and framing, switch palette to teal/sand/rust with warmer backlight -Constraints: keep the subject and camera move unchanged -``` - -### Character consistency example -``` -Primary request: Mossy, a moss-covered teapot mascot, hurries through a lantern-lit market at dusk -Camera: cinematic tracking shot, 35mm, shoulder height -Lighting/mood: warm dusk practicals, soft haze -Constraints: keep Mossy’s silhouette and moss texture consistent across the shot -``` - -## Prompting best practices (short list) -- One main action + one camera move per shot. -- Use counts or beats for timing ("two steps, pause, turn"). -- Keep text short and the camera locked-off for UI or on-screen text. -- Add a brief avoid line when artifacts appear (flicker, jitter, fast motion). -- Shorter prompts are more creative; longer prompts are more controlled. -- Put dialogue in a dedicated block; keep lines short for 4-8s clips. -- Mention character names verbatim when using uploaded character IDs. -- State invariants explicitly for edits (same shot, same camera move). -- Prefer `edit` for targeted changes and `extend` for timeline continuation. -- Iterate with single-change follow-ups to preserve continuity. - -## Guidance by asset type -Use these modules when the request is for a specific artifact. They provide targeted templates and defaults. -- Cinematic shots: `references/cinematic-shots.md` -- Social ads: `references/social-ads.md` - -## CLI + environment notes -- CLI commands + examples: `references/cli.md` -- API parameter quick reference: `references/video-api.md` -- Prompting guidance: `references/prompting.md` -- Sample prompts: `references/sample-prompts.md` -- Troubleshooting: `references/troubleshooting.md` -- Network/sandbox tips: `references/codex-network.md` - -## Reference map -- **`references/cli.md`**: how to run create/edit/extend/create-character/poll/download/local-queue flows via `scripts/sora.py`. -- **`references/video-api.md`**: API-level knobs (models, sizes, duration, characters, edits, extensions, official Batch API). -- **`references/prompting.md`**: prompt structure, character continuity, editing, and extension guidance. -- **`references/sample-prompts.md`**: copy/paste prompt recipes (examples only; no extra theory). -- **`references/cinematic-shots.md`**: templates for filmic shots. -- **`references/social-ads.md`**: templates for short social ad beats. -- **`references/troubleshooting.md`**: common errors and fixes. -- **`references/codex-network.md`**: network/approval troubleshooting. diff --git a/skills/.curated/sora/agents/openai.yaml b/skills/.curated/sora/agents/openai.yaml deleted file mode 100644 index 05959da4..00000000 --- a/skills/.curated/sora/agents/openai.yaml +++ /dev/null @@ -1,6 +0,0 @@ -interface: - display_name: "Sora Video Generation Skill" - short_description: "Generate, edit, extend, and manage Sora videos" - icon_small: "./assets/sora-small.svg" - icon_large: "./assets/sora.png" - default_prompt: "Plan the right Sora workflow for this request, then generate, edit, extend, or manage the video with concrete prompt iterations." diff --git a/skills/.curated/sora/assets/sora-small.svg b/skills/.curated/sora/assets/sora-small.svg deleted file mode 100644 index baf09c02..00000000 --- a/skills/.curated/sora/assets/sora-small.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/skills/.curated/sora/assets/sora.png b/skills/.curated/sora/assets/sora.png deleted file mode 100644 index 5b17617ee19bdfb3256d4fdf40544847a9b7763a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11731 zcmV;^EiBTBP)h|~W2T$+o?%Vg) zt>stCz1@%bsOwr{CK93UH&gek{)lv^SwtHU<8=4KxfuzrO1>_2Up7Yfn;30MGqG#9 zO`<)nD;*eJn3yX{+5Xk;oeoJ2V(dhI-)DDP8XG^x@H@)PjRQn=-|U0MpfS(xY~MHQ zM1A~JKYol3zORJd}GIYhjI+xt)HH$?2PF-+7v-k zTbSMWw1mJkm4=`v6AhO(6^Z2hDHJv#hfpCK)Y#?>?z=7WEmw4|)X}8viy*)}cKwpv zcktfmh7zg#k{A;`ridWkl!!+iBS^sFF&r7+?hHHavha#U%&hyy2wq^ADycMIn=AA%qisN>gtCRe3uAKBV*=p@P~braC8Y|#;l3o8 zsjb0OKra`SNft_(%49fDR?hGiVn~`5T9t`6A8$6CnB-E7xlp1V3M?)_^U@M37&nAT z1f)eHK&FenRW`y`gdtUkk$jR=@%t#5D(nV42;{2ha=5|#IKu=Qz=S1-Bu0$j4l^%K z@p(lwrS6bgc<`J1XC_MUPT6eGNrE|a+|=qR2^ww83ZL**l`*=8eX4(KV2{aSDn5{; zK#&KnaugkqIxpZ1WnhNH-O(#gD{X2bm=Ftz##>q>_8PtfWg*O zqid$xohU6Bze-3mtoT7W4`Fi{S8_#_sNMYQaZ31AjD>NW-GFyXS)8Tu$fj3R!!a64 zpYG=zFvP4mV~`_Hz+mE{d)h<=x$da|lRr$j%`x$GkHwsJ?4e?1=ar=h=@^vE84y-D z(liZMPKn7CvG^ySQLWVur%-^gev;zGJOFixSK&j{A{wrSa0Lkkmg>kT@K|v`XW;_; zh*ZKzMTf*?mOQSTTcsSC3t)6DurUWK66r2;Bya+sVRR%|kYJtuCq|n9a41D_iJXD2 zbXPNkqI(d*o6SFx51elj-UvA+7`JMM{^3w+)OEw=1OCz{#5BaMa02LNu1MW{;ul%$=-+f z;E_Fg=gD+DskY7>aIHxbH3-)ZF|2p^H6}nn8kyBT#Gy~g)z(t}XHP^8+8E{*5xK*b zP~~{QfB8(Rxk|IHeMq2;*4 zGZ_Km`pjV?&P3)k0hWDcPv^x4&XYqI?#pv#&!AU#zFvNouiD7{Z}eAs;wY$#8(f+s zLyKK40LM_&q;pkUL&+)tNaYnGu!zyd`ViF6Di>2liG>%VC@-tm59k;=EM>CK%q;Nf{8JC7_g`=V{n`a5bi$a6$ycV{e_yw8E8X|Vv-~%= z|CH{3WVJgI;VNv)(2{P_GhaTyb%8ln|x8?Rk~pKie9)=4~Z^5c|Mau(r+sEk?-(8S*h(-R?cLhT^~8LlVIEbE|LFIA zl@DI9Z#j3?yALm8>@#adIo`f=H?7~im7aKZBR%xk8vgNPt7*rs-Bz(MCr6agR>H_L z|JlnXCF!;GEea1_N4-RZfQb-{ARTH>{bF0bCUE1aoYz1lyi*xSC!$75N=LQchDetl zxsV^a=`+DlgC!Z*7Cvq2rgHEmtn3iS{7ofX6?QAW{#t(D^225HXE)tNFKpSCX&*Bu z%r%f{s?Su~>zzezMS4(jxrx)h)FwL`Sg82_2!2e`oM$yYY=G!MLiR<^EjnNh-SHQn z?tbq}Lk8lLo%#@-hHc$~h~M_O^hvq;Gqq)XuM^Cl|IOW$Mr& ziSa`X4D18%?5kL9Yoa;iyss&L*qk4dQ9TE1q5@(11+;TB0UR^|$y&`SHn=9|coz&Vn-ZEF@%x7VCWuY*BMlV%xuJ+-$njyrrIU2^vE zy+Gu-4KIhAEnF~wcU{Dy9ICS%1xSNv=889ZZ;^%`7v6X0CYpoK`)caQLUYzU#a6tm zvs9XAhdtvtBbl?>7~weoChXqm{=e?BQ|RA+?BZbVKvRbdmMH`>{+;Xo?0jn`K5P#N zYg7BSjIHC&+G{#pe*TGt-k&UgPJHc1w@zioR+a9)oe4r94O4Ck{xwO5S)k0N{O)GP zez&TC5OHnl*yST;^@@@bf|*Lc48(Tl<<+oysjZ5}?wBqkYtcxt!_ z|Bxmo!~VJKD)D)w@uQgv>~}wOVSkOkb@M%v_?4T*Tp2J~B*W%oQVA^2*sOyh;T|~z z5{-qCUa4hx3Xa(YbV}uTV3|~f?kQ%V4yF#{Lpr}PSx>9dU}sRe-pasXx8wI>qJY5s zafS~ZeE*qDQA8zlqvqdUcOk7p42gbN{HJtX|lB+312eV8t$hb zKv}-GhZlu6z)f72bnj%>R;%a#^JlN3h4bf%@q;uzwz-k=$owVw zdGj}Y1a0>izZJSlS*h`3YT#^G8D{X-Z^>*_d`%O6fV4CBYIcrQpyWPC1x8XTu#G_a zXP$T%{iol#j4nOv*kE`Y%m9W@nmj$(9S# z>vm$o((riZjA?Yk$1mlqcv9xq^rj*N$69#TNcFvNy70s70Lt&@N1O9Lf|>2#YTXXAXPGYq@@`eWc}HL+T4oKbB5;*I~5m$+gBIA}o*zRe5B!RY*H2 zdTzuu=F1)6OZyR`9wVB42OOF2Aa~$CC;h*Hi#1}*4Iy0a1~_=ZKJtw(yr0ily4V;I zOzO3{?>zg7blF4J_)clAS~{V`)IxypSU;_LS<3lHn{J9|ORBMn5Fk($JoKLrTz+~d zkaY#w^>UNr4GK!Ra-PhTLSKvcpHlWQl~CeBJAa^eOG2aQ(W&E^R6Qn(0Bgu z+D<5Q3iF}Su|hb`RH2BHf{^TFqkWF9Gyup9hXn}jcx?~u?8+erc67K_K|nV5Hfc&& zQ*9YSG3ZIhEuz^oXGq_-sV~e14>Uflx<72l^Xgwge_GFX$Ra zz!RGFJ)OC95r5|^*U;=Pj`!rwnV(^Z$HG=HA%Y&REF^1hzAyGe>y6*~?)`G>JhCzvlX7LC;k0AZ8UKeLiwmyso zZ~ptAm9{4xyV#v}^o7JSIdi8u6r;bk>-9Z!|HID;9C?N#PLPDlb{tLsmQiwPQ{|4Y zBZ}p1rfh0OV2umU%%+5IecaUw_QN(X7CC6a-u##U@oJhiW4h%SJV?@{hJY*-35bv+ zrL3Lau4iDIA&M{q`pUQNpcT)(R0i#oLOP)k;Gm(2LczDc_h8ZT4_*) zaI!x-e9;1;YTpPuOeT7{;-`PO`l2DRW-N^$jJ3fW!m#VaC9jrJA}0@ni-p3?^JlI* zr-OriJT!%td03jm$~JNh4L>dH9OMrOe_;s`-m-yvdC>`x1GHlCz0{NIj|uq%dn8lk z50;3P{GnCo@_d8ryd`m^tqJHy53dfW=-M5zcs_mNnzQMXolp+w6s5%b{aoz1w{QIQ zy`dznc)vM`j06?1rDN{Zp+JHtcK?PXxM|Fq8d3c~#;&{r#Ht^$WRVJFH5T6X`ZFOZ zdQD!*LR)=!L=n3ej(uUhva zee-YcvXy{2hQQ+x(+s}4Y_uoxCnRJ)9ymx;xzBIb|H8T)+6 zqb1Xy27ZI_1eFC%nx7Em@J!8IdO~#zsJol)dl4X?cKji9^r7;tvPGs zX(6y<3Vr^+-7d?XeqJ@ryxNMjFVV_%FLf}qo<8>*7nWTpRQZ^|`He5_pk2RsqfF|^ zMf=l#{oEDdsI9+ey1sfLKtua!-1qRa^uBZ7WmR9P!dv`X2OrwsmREMBJ$np&x7bw6 zN=)H5#5dg3=h$jlczB@k2*Hd3C4n$4HLJ3m8_`vmTX*h?i;lNa1y>~zEqh3D@CpUJ zb?^WRh(7<{dtt1%kddt(*I1^{wCUxY^u_;iJ3aj53juo){b*l3L@P{u{Hn8P{=B`T z{G?`h;r#y2aE*r8oMaT~>H=Rl`c5E- z-;4F1bj-o@@hi`uPhNF~7LpqcIQp@ANc!#%A4^3TOrJ&v&fkX+4R%%~cRsj+ zZv0<&6``$O|5BOsRhOL9`Imh>+S%Fl`ONb#d4F|PI29=^xWL$yqA3eYiPG;d36Emk zLNFiWuYT8ldOK+Z879mLjE5HW<^=76|0pDNUyfe9KWo@3RRtA735_$Y&X&~uJi1~d zER=i4y`Xc(eP18zgmV8)pP_Gl`D!|{8-sg`?Sym750}f1&f$wgRzzO?t`bfb-&6m< zBWvaQKlnei`IXn`tdkC>-@5KR`mGP1$A~KX_K&aJp!Byguypb;e9|BoLb~vb|NWtcD2A@apD7i zsVK95BoP)}Ad5JN2XvIovM_x@LW;O?qGTazdyVXR?M;gM6Oa1|;%X9;$x8N`@eb>^ z%kuy9R{G41x61eaak(ax-TgP+`md8UJynO&9ky=NWG zshw=;AZ+u?yBty`6GWcgtx5u~6=ktxs+04OGs5* zB!yF_Koh;*7(d-d+e%?hFUT#Uhr5QnPSv=Eac?Q^jbiUs>8y8_xvY@m^$1HXtn`T? z>S9`Y^g;`xje*^lzrOup`ohvu>p~!P2=Pa$w!!4$Y7RhUH`P{rYv+39)4zkp6BKfBz{PHn=CbyvUO`vo_ z4q2E4h|H!&o=91nh7qcA?{K$w3pH*X$uUaH?n8$742Gajh-uH7@0SB7M*a^L7w=*!J7hsA&LY&W_QP@o^Yr$8@05^pES9i zte}dkx%5rWKJ{q$TZeev(-9jXf}hOseL^@zVhuE=u{?v(5hP47Eo7UGl|C##=-NNJ za?{vG1I^H-c8~)M?k9bQL5yip!AePq=g&KV{JTH-KHdE<&q{(-dg9s5Q7ul4>V4Gj zU4MQLhafoopDB{4<1LKaZ|>f5?rBG*P&ceNcCqB@LoSq}0^F^y9KW~>2M3?6Lko>i zX#7A3#IA@`7SK_7B#QvlbS&=xzDZaTQ3?b~+wa}|WOC>O#v8_yg^ul@QoC2@h?g9^ zpK!ue9?+!1!2~?(t;UJut%Hz#XVD*g;=O3AkeF$QgX}&Zx$+E_>>sifw=_1F7%L$` z*X{H!G2LtW^n@sGkx^nHBpS*2dwQX?Hlh7Zgu2+98W9%fl0coYcUkZY+qweb8#H7R z{DzFxhPcwi6OLMFYb?Is$$lmz=R62v`!;<*Fbou03jcM?;Rn!-U$}zz+jq9p&uI>Q ze)GAHT|y@xw>Vk1j73xV*r5y#J++xEa5yjX*@3dbYeaAZ{=!7E<@FmY;5e7?GhV4JThHba^6B>;s=-!{L5kMk* z8B_upu0>1bdS8}5^Q6O)B}HOqmPPx_-it0f_juZ`vzVUF{`j~f4y1EVJ(524!E4|4H(3Y2Ws4D zP^F9|Lg_g}eV)?G$6d3(yZ33j?uL;a#Yj9hek8c_EI!~CLiPDlqH$8)sUAy0+umA0X zl`|~xL8;shs{v$Dl(VnB^xZWaLX%2P(L@l@o!h^6#2te@XMlgARBu8SPZ4$lTEYi# z%EEKLl#;|YmWit!lz_KZ+}~{7_L})Yv-C2b3G;yyzI#zC9#rOq00tJ+a1OzU(+Wv! z4dqB)tV-EPGoju|jB)*cPpp2CzWx1WwIj@q(I)X$JB(n2j$N{VuK717dejlcKcq?! z)P#KaS5I|E?90S$5`v`Klf=4AE`LCXRjp09@*9;fQk0k>&Brf#KkyHx`35zt3MFPP zjq@83V9(zXw2-1{JoCr~7LJKP4W`)iZYyKf$L;s6r2RWbdil9WnQ5nG%WU#3C0x|s zSHJhf&g|dWtzE33m$&`GA)_p%b-(wT_D;I~*G}P+yA{gGw!T+#-=HnQnIybtLgW`8 zvHN6rlGp`k>%649@U_D>wcW6J8ym1H>Gp)L#92)O$*0D7b+SJmsU_g5o%eSHm`@1= zPcW-{f*Iq0c@$Mx9SFknImD#J*058@?>pz%F7i2~Tj5*7Pp^JaUf%Xv7b(r`ENDKx z_v~Z150k;n*gAw`pKACkFxlxL^8I4&pv|XIFb7}SBq9;aA&;lNa^se5^!lDR$@y7v zef;D8IK2U+?^?H#`o5v;J~_>T(TSUoj?*YUa7>_Yuz2CVCJ+-)|8FZ{SyVfGFbwDB z&Y4M9T)dR7xJUyANepSF-jAC#TAbbKMf~#uvAP2L@6$AyN=gDWr4$ zuLescK89Jb?b80^>m% zjw#YU1~DUtoN&7K5=WtkaFS3$k-|1h1ARMnKfso)uhD*U_6idR7)+^wwS&$6rP_7g z%0QE8JSs8-sLo66gZzFbXXsSd!-12FO$Je7fAr`R>*%@lFFUM&1`3|JQrl||wKHui zl=i>%|NHYHst_Dz8OfQgSB2JuSUAKJLZ;WcByp|)3TkVh+z)grP2Q(0p~*Sx&H{Kl zAA?VuZx{vvE&5~cER=@^jz7U&8tc^^zo2^`T0wZ~xa3Y+3eV6_(UZT_UrmKIV~?(- zcj`^tWhc$+D1J985i!@_->mOVgy7knyVFhUHg4yG=I>o;d~N#i+o3E^cx>`uaF*%- zqD&nW-iE>=wXmeQ!SLk6sE*2V|Iwf2H{JJ}Z~Y1B%hTAN#w9nChU@Y1oQrZy<*)aM zaj;mdg}v$@Ec8wJg>G-8c#&m1mOzm<#vtbIpI-ZNx0HGa`4kg##vmgxIVV}pNU8&Z zq0prIo+X*_KJ_ON5wa#(W-v}zBiJ8A)nLV(`_5{{rtG1N1Bk_?O-;HUN2507`<6m~2 zVr~vak~lXEu-ifBRe+GMr)xxmNkI zC>Iz}gd{hJ5oMtv%M|0fQCtdOcn(CI0&7zox&_%<&U}6Mo4j(}D|Fvg|+Alr_?Sqm)LLG>x@?rjLp>_WJ3$OHvpFF?u71Q2$ zl-#G!$rGEboO1>y=l%C**Kg%# z)@`N351AXxfW8GEDrVHyVpiq(OuC+!hCF1@C3y`^!kRb-7%ViF&m?;G?0JipuUJnj z*KDEn8@C$kec~Lxmcxvai6nPY2JW}x;-Ohi$4CVr&}M8Ui=&q)fm2yIQDuw0ME1j2 zr1w(1t!4k;JC>Iacrrhx)()3G2g|S`vCm2Ho4|el3#IdGR-t3_0)35&NV-&ikPP$da z2ld?%SS-@>68oAO5To*h*|ABPW2?>99ee2i{lm{GFYb)5RSOLt1*m`GakhAdW57sB zyIW+i_ATR#uhumro^hl^=d$I`o7TqTY4tXrystp#%&wg9$BmX=Ls*36a-k5G3$^II zt-|@%AG?FN@ilHqTA&8t3-po}tOtN>n>F1`N&Hh^Bl<6xZ5u zfsMV0Mrg3x(8e3wUs-Ol%5@vImFG~964cLMV(|=03ReIg8PCcS1H`_YCT(uWd*Fx> z7VGyBG`v4-CDu|D=Q#X2ursZ;kUJA~Smc}UQ6JO3@BWw7rK5|^STded$RIkZ@Vw4J ztfb}I9X??SA0pwGYM)h?ZgPNtKMr@S zC%r858Vh;YBZu<4NwaH^Fp}7dAk%sXCd3Nr#&}hIjyVvlM+p9e@s}Hn`QluL5tQOG zKl1cuS+n6)zVJPV(TT?#m^8O$iVu{SU1>VgSYkJoMb)*fR7e!`QHRj|D`J1V>^Uob zGmyer$pj%9$!v~Yi|33$2o}RG5oj?xA;u?9-WfsD^bg&W(k%;dBOFWQLi2qE3pJU+ zZ{Pajyg+|gG3?tdUO0zNJo-Rddh`L5Wak=>mAk1dq++m8&GbqL^tSi6HY41>=Z&}M zySF_|J9oW47$-jLiok##?>WYC2JtVu7t92<`>s>7`vDT{a~zS?Muc2s!JF zD9SGhvO5`T`)a=F)fATNDB=D6b7s$wMV)XK%$vyz=FOtLXH73Hr@eDTyE`WB?(%{4 zJ%n_vPd@&jw`VlfLU4;Ro*B(kKv*Y?H{YV~-}$&~-MW)=ey(>6*6DoiU^<5nN#~hz z;yF}Oak~-cy?#bmj)j3?v5E-m*>Gl9?+QWb1c@R^>?Ak+j{^*H*PgQxl7#7~N zjhMw-^n^Zb{$exr=V`Q0tB#9Cs$`f%?0iTN=E=A%fOQsf{e5k%wP0;5oHzrNL3W%? z(~4wflAK@-6LX;@t)&>OI#81n|7!-$-0rxypldH%O0#E8OPW8muc@ubEI8e>cKs`K z?}MwPvk>M1X~`|C!bW82bA^wJOzaha#%)|(VN)rKPJ*v#km)~ZBV%V5Fmss5}b{0%4uemZk zv}tiK5D7^p=flFa*%|Bzz3nG_MVNVL77a`^_HU!D_#zl5sFaA#()mmq_&9AelDTtd z(mT6=VA?wx-t*=p?b@|Rc6NU1jm~icUIJ3tiB)dY$S{S@44BUe7}ERvh{9Jelnb@M zL^}=F)ba9}Ad(%FZ0moktMdJET4o5SO5%_<`ejr@3&ru~QkQ_lpU>dPiYZAPYhK0~ z%lgH`#NY~;w&$%$-t^+mm}H2IEN9YrGL}Mh?C^!DtezH<@*FFkrw0f8pNKx=t|}ms z>XN$(OF&0a7vXUK<`bMrGT30^j+Z%1G#=a)hhyb$P9jslLEdk6JjVZ2>X!6ZJD97` zyiBA#3f=<8J+OdtL+G8MlxtX#G%;figK+}hR0?Z1a54qAr^=>0@q^p6CXp+o$@iIh zyjmlvyK~(#O5-y7J7g{)w5PvlnC-K}t3zlPWXm`a!KPcFV6D}MU z1AYfB>{DiVL?2Hfm{6c*fyRve6A+Kf?z3eF0yp0LM#@b3LEbBtTBjC zi$P7Psd5g+K4i>NwO^S6?MbLkZcYF-4oFg?Gc%=}L=oA_wL!Z-%SKdt_O#sY_^!fi zBI+`<7?FoM4dt|+sxFv3zH_U0FNSa^_HGTvFW2qQrMhL=B*fv{oUe&F1%qyzpaaAc z;));$53OR*GEO<&uqS%tS}GwJ@&}Isk}aaKq4js;l0w87*P?T&>s$7iEJKOwnqJD= z)Iuml6qvn8m_E{ey%2G{^Ia%aiA_VaaKx@jo|goXfIw3Sp&ZWXwA>h5_$CPlqU#;W z%{3`RHLHSbf-a}Qb6!c$$0$UCG%_XeP90l3T}#)D-N|zRVS@3HG!z`;flniUy8)^n z{$yX@AOy0}Hth-ORR$99!KzaGJPs6xtC?6L)=XMDCA_Rn>z+u5pew)L=OU16B6LnV{;7b2%Am%EH~^gzQzg z%JuSp%dRDfvK*l3ib<^Q)e1-yb+^X9M@6L(9!n@GAp(22rIBW-uoq#Dd}WL%?H!8w zXDDJHkZrdYyDMPWu!pQbC@VpmIY(5JnCs@ReP=o4QW_gX zmWPRuy`n&jP*mJc6Q`V%S={pQy7d@_lOQa!6h{~6w?|!(_LNNy9-f)0MvY!I!&>7F zKnQN}H_f>~iA6dgF!vDwKmtuOYYV0&%@-R}XULGG7vl&e4)<^5+RiC-=long`j^Nq z0Y?q;6lR|q*?d}*S4@C9ZflCif_2MqX0m@;yDf7?lQxP;`vi z84~oY#%x%hXyM?e&=6L|g%4kE8cO(ctEPzLuq0HwON&{eZ|f-WQlqRU!HsY{x}iiE zr7X#71>re?45Q$bCTMb)KqPCA@roD^yRe0}=o4|hX(%3^OqCm3HjxMpKoTd|N}5Wh zF|e%aBQ<#hn8X}_ElgGDPF`(byN6=iv{aE!x6q~&N8G<`!fjC-TUm44WTzvlqfhyQ-H|C;cEW&4N4Y=t$SsUhjq{< zjmzZ4$82rGk%Df?AyWh^%(iw!>=%hN>_s>l=P|7?S%x1nk47nZN)vQZr$wApu0S7@ z@E}k`!_eda5HV7UoH+DKHNb=WGPM02K#!`Tt$|cTXXJSz4@xNc1!K6MLyaRDVh48X psj@Hj@y!Dz2BWkVoyQBaB diff --git a/skills/.curated/sora/references/cinematic-shots.md b/skills/.curated/sora/references/cinematic-shots.md deleted file mode 100644 index 6585beeb..00000000 --- a/skills/.curated/sora/references/cinematic-shots.md +++ /dev/null @@ -1,53 +0,0 @@ -# Cinematic shot templates - -Use these for filmic, mood-forward clips. Keep one subject, one action, one camera move. - -## Shot grammar (pick one) -- Static wide: locked-off, slow atmosphere changes -- Dolly-in: slow push toward subject -- Dolly-out: reveal more context -- Orbit: 15-45 degree arc around subject -- Lateral move: smooth left-right slide -- Crane: subtle vertical rise -- Handheld drift: gentle, controlled sway - -## Default template -``` -Use case: cinematic shot -Primary request: -Scene/background: -Subject:
-Action: -Camera: -Lighting/mood: -Color palette: <3-5 anchors> -Style/format: filmic, natural grain -Constraints: no logos, no text, no people -Avoid: jitter; flicker; oversharpening -``` - -## Example: moody exterior -``` -Use case: cinematic shot -Primary request: a lone cabin on a cliff above the sea -Scene/background: foggy coastline at dawn, drifting mist -Subject: small wooden cabin with warm window glow -Action: light fog rolls past the cabin -Camera: slow dolly-in, 35mm, steady -Lighting/mood: moody, soft dawn light, subtle contrast -Color palette: deep blue, slate, warm amber -Constraints: no logos, no text, no people -``` - -## Example: intimate detail -``` -Use case: cinematic detail -Primary request: close-up of a vinyl record spinning -Scene/background: dim room, soft lamp glow -Subject: record grooves and stylus -Action: slow rotation, subtle dust motes -Camera: macro, locked-off -Lighting/mood: warm, low-key, soft highlights -Color palette: warm amber, deep brown, charcoal -Constraints: no logos, no text -``` diff --git a/skills/.curated/sora/references/cli.md b/skills/.curated/sora/references/cli.md deleted file mode 100644 index a84d308c..00000000 --- a/skills/.curated/sora/references/cli.md +++ /dev/null @@ -1,235 +0,0 @@ -# CLI reference (`scripts/sora.py`) - -This file contains the command catalog for the bundled Sora CLI. Keep `SKILL.md` overview-first; put verbose CLI details here. - -## What this CLI does -- `create`: create a new video job -- `create-and-poll`: create a job, poll until complete, optionally download -- `create-character`: upload a reusable non-human character reference clip -- `edit`: edit an existing generated video by ID -- `extend`: continue a completed video -- `poll`: wait for an existing job to finish -- `status`: retrieve job status/details -- `download`: download video/thumbnail/spritesheet -- `list`: list recent jobs -- `delete`: delete a job -- `remix`: legacy remix endpoint -- `create-batch`: create multiple video jobs locally from JSONL input - -Real API calls require network access and `OPENAI_API_KEY`. `--dry-run` does not. - -## Important distinction -- `create-batch` is a local concurrent fan-out helper. -- It is not the official Batch API. -- For the official Batch API, prepare a JSONL file for `POST /v1/videos`, upload it with `purpose=batch`, then create a batch via the Files and Batches APIs. - -## Quick start -Set a stable path to the skill CLI (default `CODEX_HOME` is `~/.codex`): - -```bash -export CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" -export SORA_CLI="$CODEX_HOME/skills/sora/scripts/sora.py" -``` - -If you're in this repo, set the path directly: - -```bash -export SORA_CLI="$(git rev-parse --show-toplevel)//scripts/sora.py" -``` - -If uv cache fails with permission errors: - -```bash -export UV_CACHE_DIR="/tmp/uv-cache" -``` - -Dry-run without calling the API: - -```bash -python "$SORA_CLI" create --prompt "Test" --dry-run -``` - -## Defaults -- Model: `sora-2` -- Size: `1280x720` -- Seconds: `4` -- Variant: `video` -- Poll interval: `10` seconds - -Allowed seconds: `4`, `8`, `12`, `16`, `20` - -Allowed sizes: -- `sora-2`: `1280x720`, `720x1280` -- `sora-2-pro`: `1280x720`, `720x1280`, `1024x1792`, `1792x1024`, `1920x1080`, `1080x1920` - -## Create -Create a job: - -```bash -uv run --with openai python "$SORA_CLI" create \ - --model sora-2 \ - --prompt "Wide tracking shot of a teal coupe on a desert highway" \ - --size 1280x720 \ - --seconds 8 -``` - -Create with a file-based first-frame reference: - -```bash -uv run --with openai python "$SORA_CLI" create \ - --model sora-2-pro \ - --prompt "She turns around and smiles, then slowly walks out of frame." \ - --size 1280x720 \ - --seconds 8 \ - --input-reference sample_720p.jpeg -``` - -Create with a stored/remote JSON reference object: - -```bash -uv run --with openai python "$SORA_CLI" create \ - --prompt "Slow reveal of a mossy mascot in a lantern-lit market" \ - --input-reference-file-id file_abc123 -``` - -Create with characters: - -```bash -uv run --with openai python "$SORA_CLI" create \ - --model sora-2 \ - --prompt "Mossy, a moss-covered teapot mascot, rushes through a lantern-lit market at dusk." \ - --character-id char_123 \ - --seconds 8 -``` - -If the prompt is already structured, disable augmentation: - -```bash -uv run --with openai python "$SORA_CLI" create \ - --prompt-file prompt.txt \ - --no-augment \ - --seconds 16 -``` - -## Create and poll - -```bash -uv run --with openai python "$SORA_CLI" create-and-poll \ - --model sora-2-pro \ - --prompt "Close-up of a steaming coffee cup on a wooden table" \ - --size 1920x1080 \ - --seconds 16 \ - --download \ - --variant video \ - --out coffee.mp4 -``` - -## Create a character - -```bash -uv run --with openai python "$SORA_CLI" create-character \ - --name Mossy \ - --video-file character.mp4 -``` - -Use short non-human MP4 source clips and mention the character name verbatim in later prompts. - -## Edit -Edit an existing generated video by ID: - -```bash -uv run --with openai python "$SORA_CLI" edit \ - --id video_abc123 \ - --prompt "Same shot and camera move; shift the palette to teal, sand, and rust." -``` - -## Extend - -```bash -uv run --with openai python "$SORA_CLI" extend \ - --id video_abc123 \ - --seconds 8 \ - --prompt "Continue the scene as the camera rises above the rooftops and reveals sunrise." -``` - -## Poll / status / download - -```bash -uv run --with openai python "$SORA_CLI" poll --id video_abc123 --download --out out.mp4 -uv run --with openai python "$SORA_CLI" status --id video_abc123 -uv run --with openai python "$SORA_CLI" download --id video_abc123 --variant thumbnail --out thumb.webp -uv run --with openai python "$SORA_CLI" download --id video_abc123 --variant spritesheet --out sheet.jpg -``` - -## List / delete - -```bash -uv run --with openai python "$SORA_CLI" list --limit 20 --after video_123 --order asc -uv run --with openai python "$SORA_CLI" delete --id video_abc123 -``` - -## Legacy remix - -```bash -uv run --with openai python "$SORA_CLI" remix \ - --id video_abc123 \ - --prompt "Same shot and framing; change only the palette to teal and sand." -``` - -Use `edit` for new workflows. `remix` is retained only for legacy compatibility. - -## JSON output (`--json-out`) -- `create`, `status`, `list`, `delete`, `poll`, `remix`, `edit`, `extend`, and `create-character` write the response to a file. -- `create-and-poll` writes `{ "create": ..., "final": ... }`. -- In `--dry-run`, `--json-out` writes the request preview. -- If the path has no extension, `.json` is added automatically. - -## Local batch JSONL schema (`create-batch`) -Each line is a JSON object (or a raw prompt string). Required key: `prompt`. - -Common top-level keys: -- `model`, `size`, `seconds` -- `characters`: list like `[{"id":"char_123"}]` or `["char_123"]` -- `character_ids`: alternate list form such as `["char_123"]` -- `input_reference`: either a file path string or a JSON object with `file_id` or `image_url` -- `input_reference_path` / `input_reference_file`: file path aliases -- `input_reference_file_id` -- `input_reference_url` -- `out`: optional output filename for the job JSON - -Prompt augmentation keys: -- `use_case`, `scene`, `subject`, `action`, `camera`, `style`, `lighting`, `palette`, `audio`, `dialogue`, `text`, `timing`, `constraints`, `negative` - -Example: - -```bash -mkdir -p tmp/sora -cat > tmp/sora/prompts.jsonl << 'EOB' -{"prompt":"A neon-lit rainy alley, slow dolly-in","seconds":"8"} -{"prompt":"Mossy, a moss-covered teapot mascot, jogs through a lantern-lit alley","seconds":"16","character_ids":["char_123"]} -{"prompt":"A warm sunrise over a misty lake, gentle pan","input_reference":{"file_id":"file_abc123"}} -EOB - -uv run --with openai python "$SORA_CLI" create-batch \ - --input tmp/sora/prompts.jsonl \ - --out-dir out \ - --concurrency 3 -``` - -Notes: -- `create-batch` writes one JSON response per job under `--out-dir`. -- Output names default to `NNN-.json`. -- Higher concurrency can hit rate limits. -- Treat the JSONL file as temporary and clean it up after use. - -## Guardrails -- Use `python "$SORA_CLI" ...` or `uv run --with openai python "$SORA_CLI" ...`. -- For live API calls, prefer `uv run --with openai ...`. -- Do not create one-off runners unless the user explicitly asks. -- `edit` replaces `remix` for new integrations. - -## See also -- API parameter quick reference: `references/video-api.md` -- Prompt structure and iteration: `references/prompting.md` -- Sample prompts: `references/sample-prompts.md` -- Troubleshooting: `references/troubleshooting.md` diff --git a/skills/.curated/sora/references/codex-network.md b/skills/.curated/sora/references/codex-network.md deleted file mode 100644 index e702b8bf..00000000 --- a/skills/.curated/sora/references/codex-network.md +++ /dev/null @@ -1,28 +0,0 @@ -# Codex network approvals / sandbox notes - -This guidance is intentionally isolated from `SKILL.md` because it can vary by environment and may become stale. Prefer the defaults in your environment when in doubt. - -## Why am I asked to approve every video generation call? -Video generation uses the OpenAI Video API, so the CLI needs outbound network access. In many Codex setups, network access is disabled by default (especially under stricter sandbox modes), and/or the approval policy may require confirmation before networked commands run. - -## How do I reduce repeated approval prompts (network)? -If you trust the repo and want fewer prompts, enable network access for the relevant sandbox mode and relax the approval policy. - -Example `~/.codex/config.toml` pattern: - -``` -approval_policy = "never" -sandbox_mode = "workspace-write" - -[sandbox_workspace_write] -network_access = true -``` - -Or for a single session: - -``` -codex --sandbox workspace-write --ask-for-approval never -``` - -## Safety note -Use caution: enabling network and disabling approvals reduces friction but increases risk if you run untrusted code or work in an untrusted repository. diff --git a/skills/.curated/sora/references/prompting.md b/skills/.curated/sora/references/prompting.md deleted file mode 100644 index 6f35745b..00000000 --- a/skills/.curated/sora/references/prompting.md +++ /dev/null @@ -1,144 +0,0 @@ -# Prompting best practices (Sora) - -## Contents -- [Mindset & tradeoffs](#mindset--tradeoffs) -- [API-controlled params](#api-controlled-params) -- [Structure](#structure) -- [Specificity](#specificity) -- [Style & visual cues](#style--visual-cues) -- [Camera & composition](#camera--composition) -- [Motion & timing](#motion--timing) -- [Lighting & palette](#lighting--palette) -- [Character continuity](#character-continuity) -- [Multi-shot prompts](#multi-shot-prompts) -- [Ultra-detailed briefs](#ultra-detailed-briefs) -- [Image input](#image-input) -- [Constraints & invariants](#constraints--invariants) -- [Text, dialogue & audio](#text-dialogue--audio) -- [Avoiding artifacts](#avoiding-artifacts) -- [Editing & extensions](#editing--extensions) -- [Iterate deliberately](#iterate-deliberately) - -## Mindset & tradeoffs -- Treat the prompt like a cinematography brief, not a contract. -- The same prompt can yield different results; rerun for variants. -- Short prompts give more creative freedom; longer prompts give more control. -- Shorter clips tend to follow instructions better; even though `16`s and `20`s are available, start shorter when precision matters. - -## API-controlled params -- Model, size, seconds, and character IDs are controlled by API params, not prose. -- Put desired duration in the `seconds` param; the prompt cannot make a clip longer. -- `1920x1080` and `1080x1920` require `sora-2-pro`. - -## Structure -- Use short labeled lines; omit sections that do not matter. -- Keep one main subject and one main action. -- Put timing in beats or counts if it matters. -- If you prefer a prose-first template, use: -``` - - -Cinematography: -Camera shot: -Mood: - -Actions: -- -- - -Dialogue: - -``` - -## Specificity -- Name the subject and materials (metal, fabric, glass). -- Use camera language (lens, angle, shot type) for stability. -- Describe the environment with time of day and atmosphere. - -## Style & visual cues -- Set style early (e.g., "1970s film", "IMAX-scale", "16mm black-and-white"). -- Use visible nouns and verbs, not vague adjectives. -- Weak: "A beautiful street at night." -- Strong: "Wet asphalt, zebra crosswalk, neon signs reflecting in puddles." - -## Camera & composition -- Prefer one camera move: dolly, orbit, lateral slide, or locked-off. -- Straight-on framing is best for UI and text. -- For close-ups, use longer lenses (85mm+); for wide scenes, 24-35mm. -- Depth of field is a strong lever: shallow for subject isolation, deep for context. -- Example framings: wide establishing, medium close-up, aerial wide, low angle. -- Example camera motions: slow tilt, gentle handheld drift, smooth lateral slide. - -## Motion & timing -- Use short beats: "0-2s", "2-4s", "4-6s". -- Keep actions sequential, not simultaneous. -- For 4s clips, limit to 1-2 beats. -- Describe actions as counts or steps when possible (e.g., "takes four steps, pauses, turns in the final second"). - -## Lighting & palette -- Describe light quality and direction (soft window light, hard rim, backlight). -- Name 3-5 palette anchors to stabilize color across shots. -- If continuity matters, keep lighting logic consistent across clips. - -## Character continuity -- Keep character descriptors consistent across shots; reuse phrasing. -- Avoid mixing competing traits that can shift identity or pose. -- When using uploaded character assets, mention the character name verbatim in the prompt. -- Use no more than two characters per generation. -- Character uploads work best from short non-human MP4 reference clips. - -## Multi-shot prompts -- You can describe multiple shots in one prompt, but keep each shot block distinct. -- For each shot, specify one camera setup, one action, one lighting recipe. -- Treat each shot as a creative unit you can later edit or stitch. - -## Ultra-detailed briefs -- Use when you need a specific, filmic look or strict continuity. -- Call out format/look, lensing/filters, grade/palette, lighting direction, texture, and sound. -- If needed, include a short shot list with timing beats. - -## Image input -- Use an input image to lock composition, character design, or set dressing. -- The input image should match the target size and be jpg/png/webp. -- The image anchors the first frame; the prompt describes what happens next. -- If you lack a reference, generate one first and pass it as `input_reference`. - -## Constraints & invariants -- State what must not change: "same shot", "same framing", "keep background". -- Repeat invariants in every edit to reduce drift. -- Use invariants sparingly in extensions; tell the model what should continue, not just what should stay frozen. - -## Text, dialogue & audio -- Keep text short and specific; quote exact strings. -- Specify placement and avoid motion blur. -- For dialogue, use a dedicated block and keep lines short. -- Label speakers consistently for multi-character scenes. -- If silent, you can still add a small ambient sound cue to set rhythm. -- Sora can generate audio; include an `Audio:` line and a short dialogue block when needed. -- As a rule of thumb, 4s clips fit 1-2 short lines; 8s clips can handle a few more. - -Example: -``` -Audio: soft ambient café noise, clear warm voiceover -Dialogue: - -- Speaker: "Let's get started." - -``` - -## Avoiding artifacts -- Avoid multiple actions in 4-8 seconds. -- Keep camera motion smooth and limited. -- Add explicit negatives when needed: "avoid flicker", "avoid jitter", "no fast motion". - -## Editing & extensions -- Prefer edits when the shot is mostly right and you want one targeted change. -- Prefer extensions when the existing clip should continue forward in time. -- For edits, change one thing at a time: palette, lighting, or action. -- For extensions, describe the next beat clearly and preserve motion continuity. -- If a shot misfires, simplify: freeze the camera, reduce action, clear background, then add complexity back in. - -## Iterate deliberately -- Start simple, then add one constraint per iteration. -- If results look chaotic, reduce motion and simplify the scene. -- When a result is close, pin it as a reference and describe only the tweak. diff --git a/skills/.curated/sora/references/sample-prompts.md b/skills/.curated/sora/references/sample-prompts.md deleted file mode 100644 index 02f09bdf..00000000 --- a/skills/.curated/sora/references/sample-prompts.md +++ /dev/null @@ -1,128 +0,0 @@ -# Sample prompts (copy/paste) - -Use these as starting points. Keep user-provided requirements and constraints; do not invent new creative elements. - -For prompting principles (structure, invariants, iteration), see `references/prompting.md`. - -## Contents -- [Product teaser (single shot)](#product-teaser-single-shot) -- [UI demo (screen recording style)](#ui-demo-screen-recording-style) -- [Cinematic detail shot](#cinematic-detail-shot) -- [Social ad (6s with beats)](#social-ad-6s-with-beats) -- [Character continuity shot](#character-continuity-shot) -- [Edit follow-up](#edit-follow-up) -- [Extension follow-up](#extension-follow-up) -- [Motion graphics explainer](#motion-graphics-explainer) -- [Ambient loop (atmosphere)](#ambient-loop-atmosphere) - -## Product teaser (single shot) -``` -Use case: product teaser -Primary request: close-up of a matte black wireless speaker on a stone pedestal -Scene/background: dark studio cyclorama, subtle haze -Subject: compact speaker with soft fabric texture -Action: slow 20-degree orbit over 4 seconds -Camera: 85mm, shallow depth of field, steady dolly -Lighting/mood: soft key, gentle rim, premium studio feel -Color palette: charcoal, slate, warm amber accents -Constraints: no logos, no text -Avoid: harsh bloom; oversharpening; clutter -``` - -## UI demo (screen recording style) -``` -Use case: UI product demo -Primary request: a clean mobile budgeting app demo showing a weekly spend chart -Scene/background: neutral gradient backdrop -Subject: smartphone UI, centered, screen content crisp and legible -Action: tap the "Add expense" button, modal opens, amount typed, save -Camera: locked-off, straight-on, no tilt -Lighting/mood: soft studio light, minimal reflections -Color palette: off-white, slate, mint accent -Text (verbatim): "Add expense", "$24.50", "Groceries" -Constraints: no brand logos; keep UI text readable; avoid motion blur -``` - -## Cinematic detail shot -``` -Use case: cinematic product detail -Primary request: macro shot of raindrops sliding across a car hood -Scene/background: night city bokeh, soft rain mist -Subject: glossy hood surface with water beads -Action: slow push-in over 4 seconds -Camera: 100mm macro, shallow depth of field -Lighting/mood: moody, high-contrast reflections, soft speculars -Color palette: deep navy, teal, silver highlights -Constraints: no logos, no text -Avoid: flicker; unstable reflections; excessive noise -``` - -## Social ad (6s with beats) -``` -Use case: social ad -Primary request: minimal coffee subscription ad with three quick beats -Scene/background: warm kitchen counter, morning light -Subject: ceramic mug, coffee bag, steam -Action: beat 1 (0-2s) pour coffee; beat 2 (2-4s) steam rises; beat 3 (4-6s) mug slides to center -Camera: 50mm, gentle handheld drift -Lighting/mood: warm, cozy, natural light -Text (verbatim): "Fresh roast" (top-left), "Weekly delivery" (bottom-right) -Constraints: no logos; text must be legible; avoid fast motion -``` - -## Character continuity shot -``` -Use case: mascot continuity -Primary request: Mossy, a moss-covered teapot mascot, rushes through a lantern-lit market at dusk -Scene/background: narrow alley, hanging lanterns, light haze -Subject: Mossy the moss-covered teapot mascot -Action: quick jog through the alley, glances toward camera near the end -Camera: 35mm, shoulder-height tracking shot, smooth lateral move -Lighting/mood: warm dusk practicals, cinematic glow -Color palette: moss green, warm amber, charcoal -Constraints: keep Mossy's silhouette, moss texture, and teapot proportions consistent -Avoid: flicker; warped limbs; identity drift -``` - -## Edit follow-up -``` -Primary request: same shot and camera move; change only the palette to teal, sand, and rust with a warmer backlight -Constraints: keep the subject, framing, and motion unchanged -Avoid: new objects; reframing; speed changes -``` - -## Extension follow-up -``` -Primary request: continue the same shot as the camera rises above the rooftops and reveals sunrise over the city -Action: maintain the existing motion, then gently tilt upward into the skyline reveal -Lighting/mood: dawn light growing warmer through the extension -Constraints: preserve scene continuity, camera direction, and overall pacing -Avoid: abrupt cuts; jumpy motion; sudden subject changes -``` - -## Motion graphics explainer -``` -Use case: explainer clip -Primary request: clean motion-graphics animation showing data flowing into a dashboard -Scene/background: soft gradient background -Subject: abstract nodes and lines, simple dashboard cards -Action: nodes connect, data pulses, cards fill with charts -Camera: locked-off, no depth, flat design -Lighting/mood: minimal, modern -Color palette: off-white, graphite, teal, coral accents -Constraints: no logos; keep shapes simple; avoid heavy texture -``` - -## Ambient loop (atmosphere) -``` -Use case: ambient background loop -Primary request: fog drifting through a pine forest at dawn -Scene/background: tall pines, soft fog layers, distant hills -Subject: drifting fog and light rays -Action: slow lateral drift, subtle light change -Camera: wide, locked-off, no tilt -Lighting/mood: calm, soft dawn light -Color palette: muted greens, cool gray, pale gold -Constraints: no text, no logos, no people -Avoid: fast motion; flicker; abrupt lighting shifts -``` diff --git a/skills/.curated/sora/references/social-ads.md b/skills/.curated/sora/references/social-ads.md deleted file mode 100644 index 17547930..00000000 --- a/skills/.curated/sora/references/social-ads.md +++ /dev/null @@ -1,42 +0,0 @@ -# Social ad templates (4-8s) - -Short clips work best with clear beats. Use 2-3 beats and keep text minimal. - -## Default template -``` -Use case: social ad -Primary request: -Scene/background: -Subject: -Action: beat 1 (0-2s) ; beat 2 (2-4s) ; beat 3 (4-6s) -Camera: -Lighting/mood: -Text (verbatim): "", "" -Constraints: no logos; keep text legible; avoid fast motion -``` - -## Example: product benefit -``` -Use case: social ad -Primary request: a compact humidifier emphasizing quiet operation -Scene/background: minimal bedroom nightstand -Subject: matte white humidifier with soft vapor -Action: beat 1 (0-2s) vapor begins; beat 2 (2-4s) soft glow turns on; beat 3 (4-6s) device slides to center -Camera: 50mm, gentle push-in -Lighting/mood: calm, warm night light -Text (verbatim): "Quiet mist", "Sleep better" -Constraints: no logos; text must be legible; avoid harsh highlights -``` - -## Example: before/after -``` -Use case: social ad -Primary request: before/after of a cluttered desk becoming tidy -Scene/background: home office desk, neutral wall -Subject: desk surface, organizer tray -Action: beat 1 (0-2s) cluttered desk; beat 2 (2-4s) quick tidy motion; beat 3 (4-6s) clean desk with organizer -Camera: top-down, locked-off -Lighting/mood: soft daylight -Text (verbatim): "Before", "After" -Constraints: no logos; keep motion minimal; avoid blur -``` diff --git a/skills/.curated/sora/references/troubleshooting.md b/skills/.curated/sora/references/troubleshooting.md deleted file mode 100644 index ee74f560..00000000 --- a/skills/.curated/sora/references/troubleshooting.md +++ /dev/null @@ -1,69 +0,0 @@ -# Troubleshooting - -## Job fails with size or seconds errors -- Cause: size is not supported by the chosen model, or seconds is outside `4`, `8`, `12`, `16`, `20`. -- Fix: match size to model; use `sora-2-pro` for `1920x1080` or `1080x1920`. - -## Docs and SDK disagree on the latest limits or helpers -- Cause: the March 2026 Sora guide/changelog is ahead of some typed SDK/API-reference surfaces. -- Fix: follow the latest guide/changelog and use the bundled CLI, which bridges new flows through the official client’s low-level methods. - -## `edit`, `extend`, or `create-character` isn't available in your installed Python SDK -- Cause: the published SDK may not expose new Sora helpers yet. -- Fix: use `scripts/sora.py`; it uses the official OpenAI client directly for those endpoints. - -## openai SDK not installed -- Cause: running `python "$SORA_CLI" ...` without the OpenAI SDK available. -- Fix: run with `uv run --with openai python "$SORA_CLI" ...`. - -## uv cache permission error -- Cause: uv cache directory is not writable in CI or sandboxed environments. -- Fix: set `UV_CACHE_DIR=/tmp/uv-cache` (or another writable path) before running `uv`. - -## Prompt shell escaping issues -- Cause: multi-line prompts or quotes break the shell. -- Fix: use `--prompt-file prompt.txt`. - -## Prompt looks double-wrapped ("Primary request: Use case: ...") -- Cause: you structured the prompt manually but left CLI augmentation on. -- Fix: add `--no-augment`, or use the CLI fields (`--use-case`, `--scene`, etc.) instead of pre-formatting. - -## Input reference rejected -- Cause: the file is not jpg/png/webp, includes a human face, or does not match the target size. -- Fix: convert to jpg/png/webp, remove faces, and resize to match `--size`. - -## Character continuity is weak -- Cause: the character clip is too long, mismatched in aspect ratio, outside the skill's non-human character workflow, or the prompt never names the character. -- Fix: use a short non-human MP4, match aspect ratio to the target shot, and mention the character name verbatim in the prompt. - -## Extension looks jumpy or drifts -- Cause: the continuation prompt changes too many things at once, or asks for a hard scene break. -- Fix: describe the next beat only, preserve motion direction, and avoid introducing unrelated subjects or abrupt camera changes. - -## Remix drifts from the original -- Cause: remix is a legacy endpoint and too many changes were requested at once. -- Fix: prefer `edit`, state invariants explicitly, and change one element at a time. - -## Download fails or returns expired URL -- Cause: normal download URLs expire after about 1 hour. -- Fix: re-download while the link is fresh and copy the asset to your own storage promptly. - -## Video completes but looks unstable or flickers -- Cause: multiple actions, aggressive camera motion, or overly long prompt timing for the clip length. -- Fix: reduce to one main action and one camera move; keep beats simple; add constraints like `avoid flicker` or `stable motion`. - -## Text is unreadable -- Cause: text is too long, too small, or moving. -- Fix: shorten text, keep the camera locked-off, and avoid fast motion. - -## Job stuck in `queued` or `in_progress` -- Cause: temporary queue delays or slower higher-resolution renders. -- Fix: increase timeout, poll less aggressively, and expect longer waits for `16`/`20` second or 1080p jobs. - -## `create-batch` is not behaving like the Batch API -- Cause: `create-batch` is a local concurrent helper, not the official Batch API. -- Fix: use the Files + Batches APIs for true offline batching; use `create-batch` only for immediate local fan-out. - -## Cleanup blocked by sandbox policy -- Cause: some environments block `rm`. -- Fix: skip cleanup, or truncate temporary files instead of deleting them. diff --git a/skills/.curated/sora/references/video-api.md b/skills/.curated/sora/references/video-api.md deleted file mode 100644 index 5d2481f4..00000000 --- a/skills/.curated/sora/references/video-api.md +++ /dev/null @@ -1,86 +0,0 @@ -# Sora Video API quick reference - -Keep this file short; the full source of truth is the latest OpenAI Sora guide plus the API changelog. - -## Source-of-truth note -- The March 2026 changelog and Sora guide added characters, 16s/20s clips, `1920x1080` / `1080x1920` on `sora-2-pro`, extensions, and edits. -- Some typed SDK and API-reference pages may still show the older `4`/`8`/`12` and pre-1080p enums. -- If they disagree, follow the latest guide/changelog and use the bundled CLI, which bridges the SDK lag with low-level official-client calls. - -## Models -- `sora-2`: faster, flexible iteration -- `sora-2-pro`: higher fidelity, slower, more expensive - -## Sizes (by model) -- `sora-2`: `1280x720`, `720x1280` -- `sora-2-pro`: `1280x720`, `720x1280`, `1024x1792`, `1792x1024`, `1920x1080`, `1080x1920` -- Use `sora-2-pro` for 1080p exports. - -## Duration -- `seconds`: `"4"`, `"8"`, `"12"`, `"16"`, `"20"` -- Use shorter clips first when iterating on motion, timing, or composition. - -## Input references -- `input_reference` guides the first frame of a generation. -- Multipart requests use an uploaded image file. -- JSON requests use an object with exactly one of `file_id` or `image_url`. -- Supported image formats: jpg/jpeg, png, webp. -- Input references should match the target `size`. - -## Characters -- Create reusable non-human characters via `POST /v1/videos/characters`. -- Character source clips work best as short MP4s (`2`-`4`s) in `16:9` or `9:16`, at `720p`-`1080p`. -- Reference up to two characters per generation with `characters: [{"id": "..."}]`. -- Mention the character name verbatim in the prompt; the ID alone is not enough. -- Characters can be combined with `input_reference`. -- In this skill, character workflows are limited to non-human subjects. - -## Edits vs remix -- Preferred: `POST /v1/videos/edits` -- Legacy/deprecated: `POST /v1/videos/{video_id}/remix` -- Use edits for new integrations. -- In this skill, use edits for existing generated video IDs only. - -## Extensions -- Use `POST /v1/videos/extensions` to continue a completed video. -- Each extension can add up to `20` seconds. -- A single video can be extended up to six times, for a maximum total length of `120` seconds. -- Extensions do not support characters or image references. - -## Jobs and status -- Creation, edit, and extension jobs are async. -- Common statuses: `queued`, `in_progress`, `completed`, `failed` -- Poll every `10`-`20`s or use webhooks. -- Webhook events: `video.completed`, `video.failed` - -## Core endpoints -- `POST /videos`: create -- `POST /videos/characters`: create a reusable character -- `POST /videos/edits`: edit an existing generated video by ID -- `POST /videos/extensions`: extend a completed video -- `GET /videos/{id}`: retrieve status/details -- `GET /videos/{id}/content`: download content -- `GET /videos`: list -- `DELETE /videos/{id}`: delete -- `POST /videos/{id}/remix`: legacy/deprecated - -## Download variants -- `video` -> mp4 -- `thumbnail` -> webp -- `spritesheet` -> jpg - -Download URLs expire after about 1 hour; save assets to your own storage promptly. - -## Batch API -- The official Batch API supports `POST /v1/videos` only. -- Batch requests must use JSON, not multipart. -- Upload assets ahead of time and reference them in the JSON body. -- For image-guided Batch jobs, use JSON `input_reference` with `file_id` or `image_url`. -- Batch-generated videos remain downloadable for up to 24 hours after the batch completes. -- The bundled `scripts/sora.py create-batch` command is a local fan-out helper, not the official Batch API. - -## Guardrails -- Only content suitable for audiences under 18 -- No copyrighted characters or copyrighted music -- No real people (including public figures) -- Input images with human faces are currently rejected diff --git a/skills/.curated/sora/scripts/sora.py b/skills/.curated/sora/scripts/sora.py deleted file mode 100644 index 9231a5b7..00000000 --- a/skills/.curated/sora/scripts/sora.py +++ /dev/null @@ -1,1274 +0,0 @@ -#!/usr/bin/env python3 -"""Create and manage Sora videos with the OpenAI Video API. - -Defaults to sora-2 and a structured prompt augmentation workflow. -""" - -from __future__ import annotations - -import argparse -import asyncio -import json -import os -from pathlib import Path -import re -import sys -import time -from typing import Any, Dict, Iterable, List, Optional, Tuple, Union - -DEFAULT_MODEL = "sora-2" -DEFAULT_SIZE = "1280x720" -DEFAULT_SECONDS = "4" -DEFAULT_POLL_INTERVAL = 10.0 -DEFAULT_VARIANT = "video" -DEFAULT_CONCURRENCY = 3 -DEFAULT_MAX_ATTEMPTS = 3 - -ALLOWED_MODELS = {"sora-2", "sora-2-pro"} -ALLOWED_SIZES_SORA2 = {"1280x720", "720x1280"} -ALLOWED_SIZES_SORA2_PRO = { - "1280x720", - "720x1280", - "1024x1792", - "1792x1024", - "1080x1920", - "1920x1080", -} -ALLOWED_SECONDS = {"4", "8", "12", "16", "20"} -ALLOWED_VARIANTS = {"video", "thumbnail", "spritesheet"} -ALLOWED_ORDERS = {"asc", "desc"} -ALLOWED_INPUT_EXTS = {".jpg", ".jpeg", ".png", ".webp"} -ALLOWED_VIDEO_EXTS = {".mp4"} -TERMINAL_STATUSES = {"completed", "failed", "canceled", "expired"} - -VARIANT_EXTENSIONS = {"video": ".mp4", "thumbnail": ".webp", "spritesheet": ".jpg"} - -MAX_BATCH_JOBS = 200 - - -def _die(message: str, code: int = 1) -> None: - print(f"Error: {message}", file=sys.stderr) - raise SystemExit(code) - - -def _warn(message: str) -> None: - print(f"Warning: {message}", file=sys.stderr) - - -def _ensure_api_key(dry_run: bool) -> None: - if os.getenv("OPENAI_API_KEY"): - print("OPENAI_API_KEY is set.", file=sys.stderr) - return - if dry_run: - _warn("OPENAI_API_KEY is not set; dry-run only.") - return - _die("OPENAI_API_KEY is not set. Export it before running.") - - -def _read_prompt(prompt: Optional[str], prompt_file: Optional[str]) -> str: - if prompt and prompt_file: - _die("Use --prompt or --prompt-file, not both.") - if prompt_file: - path = Path(prompt_file) - if not path.exists(): - _die(f"Prompt file not found: {path}") - return path.read_text(encoding="utf-8").strip() - if prompt: - return prompt.strip() - _die("Missing prompt. Use --prompt or --prompt-file.") - return "" # unreachable - - -def _normalize_model(model: Optional[str]) -> str: - value = (model or DEFAULT_MODEL).strip().lower() - if value not in ALLOWED_MODELS: - _die("model must be one of: sora-2, sora-2-pro") - return value - - -def _normalize_size(size: Optional[str], model: str) -> str: - value = (size or DEFAULT_SIZE).strip().lower() - allowed = ALLOWED_SIZES_SORA2 if model == "sora-2" else ALLOWED_SIZES_SORA2_PRO - if value not in allowed: - allowed_list = ", ".join(sorted(allowed)) - _die(f"size must be one of: {allowed_list} for model {model}") - return value - - -def _normalize_seconds(seconds: Optional[Union[int, str]]) -> str: - if seconds is None: - value = DEFAULT_SECONDS - elif isinstance(seconds, int): - value = str(seconds) - else: - value = str(seconds).strip() - if value not in ALLOWED_SECONDS: - _die("seconds must be one of: 4, 8, 12, 16, 20") - return value - - -def _normalize_variant(variant: Optional[str]) -> str: - value = (variant or DEFAULT_VARIANT).strip().lower() - if value not in ALLOWED_VARIANTS: - _die("variant must be one of: video, thumbnail, spritesheet") - return value - - -def _normalize_order(order: Optional[str]) -> Optional[str]: - if order is None: - return None - value = order.strip().lower() - if value not in ALLOWED_ORDERS: - _die("order must be one of: asc, desc") - return value - - -def _normalize_poll_interval(interval: Optional[float]) -> float: - value = float(interval if interval is not None else DEFAULT_POLL_INTERVAL) - if value <= 0: - _die("poll-interval must be > 0") - return value - - -def _normalize_timeout(timeout: Optional[float]) -> Optional[float]: - if timeout is None: - return None - value = float(timeout) - if value <= 0: - _die("timeout must be > 0") - return value - - -def _default_out_path(variant: str) -> Path: - if variant == "video": - return Path("video.mp4") - if variant == "thumbnail": - return Path("thumbnail.webp") - return Path("spritesheet.jpg") - - -def _normalize_out_path(out: Optional[str], variant: str) -> Path: - expected_ext = VARIANT_EXTENSIONS[variant] - if not out: - return _default_out_path(variant) - path = Path(out) - if path.suffix == "": - return path.with_suffix(expected_ext) - if path.suffix.lower() != expected_ext: - _warn(f"Output extension {path.suffix} does not match {expected_ext} for {variant}.") - return path - - -def _normalize_json_out(out: Optional[str], default_name: str) -> Optional[Path]: - if not out: - return None - raw = str(out) - if raw.endswith("/") or raw.endswith(os.sep): - return Path(raw) / default_name - path = Path(out) - if path.exists() and path.is_dir(): - return path / default_name - if path.suffix == "": - path = path.with_suffix(".json") - return path - - -def _normalize_input_reference_object(value: Any) -> Dict[str, str]: - if not isinstance(value, dict): - _die("input_reference object must be a JSON object with file_id or image_url.") - - file_id = str(value.get("file_id", "")).strip() - image_url = str(value.get("image_url", "")).strip() - - if bool(file_id) == bool(image_url): - _die("input_reference object must include exactly one of file_id or image_url.") - - if file_id: - return {"file_id": file_id} - return {"image_url": image_url} - - -def _normalize_input_reference( - *, - value: Any = None, - path: Optional[str] = None, - file_id: Optional[str] = None, - image_url: Optional[str] = None, -) -> Tuple[Optional[str], Optional[Dict[str, str]]]: - if value is not None: - if any(item is not None for item in (path, file_id, image_url)): - _die( - "Use either input_reference or explicit input-reference path/file-id/url fields, not both." - ) - if isinstance(value, str): - path = value - elif isinstance(value, dict): - return None, _normalize_input_reference_object(value) - else: - _die("input_reference must be a file path string or a JSON object.") - - provided = [bool(path), bool(file_id), bool(image_url)] - if sum(provided) > 1: - _die("Use only one of --input-reference, --input-reference-file-id, or --input-reference-url.") - - if path: - return str(path), None - if file_id: - return None, {"file_id": str(file_id).strip()} - if image_url: - return None, {"image_url": str(image_url).strip()} - return None, None - - -def _normalize_characters(raw: Any) -> Optional[List[Dict[str, str]]]: - if raw is None: - return None - - items: List[Any] - if isinstance(raw, str): - items = [part.strip() for part in raw.split(",") if part.strip()] - elif isinstance(raw, (list, tuple)): - items = list(raw) - else: - _die("characters must be a list of IDs, a comma-separated string, or objects with an id field.") - return None - - if not items: - return None - - normalized: List[Dict[str, str]] = [] - for item in items: - if isinstance(item, str): - char_id = item.strip() - elif isinstance(item, dict): - char_id = str(item.get("id", "")).strip() - else: - _die("Each character must be a string ID or an object with an id field.") - return None - - if not char_id: - _die("Character IDs must be non-empty.") - normalized.append({"id": char_id}) - - if len(normalized) > 2: - _die("A single video can include at most 2 characters.") - - return normalized - - -def _open_input_reference(path: Optional[str]): - if not path: - return _NullContext() - p = Path(path) - if not p.exists(): - _die(f"Input reference not found: {p}") - if p.suffix.lower() not in ALLOWED_INPUT_EXTS: - _warn("Input reference should be jpeg, png, or webp.") - return _SingleFile(p) - - -def _open_video_upload(path: Optional[str], *, label: str) -> Any: - if not path: - return _NullContext() - p = Path(path) - if not p.exists(): - _die(f"{label} not found: {p}") - if p.suffix.lower() not in ALLOWED_VIDEO_EXTS: - _warn(f"{label} should usually be an MP4 file.") - return _SingleFile(p) - - -def _create_client(): - try: - from openai import OpenAI - except ImportError: - _die("openai SDK not installed. Run with `uv run --with openai` or install with `uv pip install openai`.") - return OpenAI() - - -def _create_async_client(): - try: - from openai import AsyncOpenAI - except ImportError: - try: - import openai as _openai # noqa: F401 - except ImportError: - _die("openai SDK not installed. Run with `uv run --with openai` or install with `uv pip install openai`.") - _die( - "AsyncOpenAI not available in this openai SDK version. Upgrade with `uv pip install -U openai`." - ) - return AsyncOpenAI() - - -def _make_request_options(*, multipart: bool) -> Dict[str, Any]: - from openai.resources.videos import make_request_options - - headers = {"Content-Type": "multipart/form-data"} if multipart else None - return make_request_options(extra_headers=headers) - - -def _video_post( - client: Any, - path: str, - payload: Dict[str, Any], - *, - files: Optional[List[Tuple[str, Any]]] = None, -) -> Any: - return client.post( - path, - cast_to=dict, - body=payload, - files=files, - options=_make_request_options(multipart=bool(files)), - ) - - -async def _async_video_post( - client: Any, - path: str, - payload: Dict[str, Any], - *, - files: Optional[List[Tuple[str, Any]]] = None, -) -> Any: - return await client.post( - path, - cast_to=dict, - body=payload, - files=files, - options=_make_request_options(multipart=bool(files)), - ) - - -def _to_dict(obj: Any) -> Any: - if isinstance(obj, dict): - return obj - if hasattr(obj, "model_dump"): - return obj.model_dump() - if hasattr(obj, "dict"): - return obj.dict() - if hasattr(obj, "__dict__"): - return obj.__dict__ - return obj - - -def _print_json(obj: Any) -> None: - print(json.dumps(_to_dict(obj), indent=2, sort_keys=True)) - - -def _print_request(payload: Dict[str, Any]) -> None: - print(json.dumps(payload, indent=2, sort_keys=True)) - - -def _slugify(value: str) -> str: - value = value.strip().lower() - value = re.sub(r"[^a-z0-9]+", "-", value) - value = re.sub(r"-{2,}", "-", value).strip("-") - return value[:60] if value else "job" - - -def _normalize_job(job: Any, idx: int) -> Dict[str, Any]: - if isinstance(job, str): - prompt = job.strip() - if not prompt: - _die(f"Empty prompt at job {idx}") - return {"prompt": prompt} - if isinstance(job, dict): - if "prompt" not in job or not str(job["prompt"]).strip(): - _die(f"Missing prompt for job {idx}") - return job - _die(f"Invalid job at index {idx}: expected string or object.") - return {} # unreachable - - -def _read_jobs_jsonl(path: str) -> List[Dict[str, Any]]: - p = Path(path) - if not p.exists(): - _die(f"Input file not found: {p}") - jobs: List[Dict[str, Any]] = [] - for line_no, raw in enumerate(p.read_text(encoding="utf-8").splitlines(), start=1): - line = raw.strip() - if not line or line.startswith("#"): - continue - try: - item: Any - if line.startswith("{"): - item = json.loads(line) - else: - item = line - jobs.append(_normalize_job(item, idx=line_no)) - except json.JSONDecodeError as exc: - _die(f"Invalid JSON on line {line_no}: {exc}") - if not jobs: - _die("No jobs found in input file.") - if len(jobs) > MAX_BATCH_JOBS: - _die(f"Too many jobs ({len(jobs)}). Max is {MAX_BATCH_JOBS}.") - return jobs - - -def _merge_non_null(dst: Dict[str, Any], src: Dict[str, Any]) -> Dict[str, Any]: - merged = dict(dst) - for k, v in src.items(): - if v is not None: - merged[k] = v - return merged - - -def _job_output_path(out_dir: Path, idx: int, prompt: str, explicit_out: Optional[str]) -> Path: - out_dir.mkdir(parents=True, exist_ok=True) - if explicit_out: - path = Path(explicit_out) - if path.suffix == "": - path = path.with_suffix(".json") - return out_dir / path.name - slug = _slugify(prompt[:80]) - return out_dir / f"{idx:03d}-{slug}.json" - - -def _extract_retry_after_seconds(exc: Exception) -> Optional[float]: - for attr in ("retry_after", "retry_after_seconds"): - val = getattr(exc, attr, None) - if isinstance(val, (int, float)) and val >= 0: - return float(val) - msg = str(exc) - m = re.search(r"retry[- ]after[:= ]+([0-9]+(?:\\.[0-9]+)?)", msg, re.IGNORECASE) - if m: - try: - return float(m.group(1)) - except Exception: - return None - return None - - -def _is_rate_limit_error(exc: Exception) -> bool: - name = exc.__class__.__name__.lower() - if "ratelimit" in name or "rate_limit" in name: - return True - msg = str(exc).lower() - return "429" in msg or "rate limit" in msg or "too many requests" in msg - - -def _is_transient_error(exc: Exception) -> bool: - if _is_rate_limit_error(exc): - return True - name = exc.__class__.__name__.lower() - if "timeout" in name or "timedout" in name or "tempor" in name: - return True - msg = str(exc).lower() - return "timeout" in msg or "timed out" in msg or "connection reset" in msg - - -def _fields_from_args(args: argparse.Namespace) -> Dict[str, Optional[str]]: - return { - "use_case": getattr(args, "use_case", None), - "scene": getattr(args, "scene", None), - "subject": getattr(args, "subject", None), - "action": getattr(args, "action", None), - "camera": getattr(args, "camera", None), - "style": getattr(args, "style", None), - "lighting": getattr(args, "lighting", None), - "palette": getattr(args, "palette", None), - "audio": getattr(args, "audio", None), - "dialogue": getattr(args, "dialogue", None), - "text": getattr(args, "text", None), - "timing": getattr(args, "timing", None), - "constraints": getattr(args, "constraints", None), - "negative": getattr(args, "negative", None), - } - - -def _augment_prompt_fields(augment: bool, prompt: str, fields: Dict[str, Optional[str]]) -> str: - if not augment: - return prompt - - sections: List[str] = [] - if fields.get("use_case"): - sections.append(f"Use case: {fields['use_case']}") - sections.append(f"Primary request: {prompt}") - if fields.get("scene"): - sections.append(f"Scene/background: {fields['scene']}") - if fields.get("subject"): - sections.append(f"Subject: {fields['subject']}") - if fields.get("action"): - sections.append(f"Action: {fields['action']}") - if fields.get("camera"): - sections.append(f"Camera: {fields['camera']}") - if fields.get("lighting"): - sections.append(f"Lighting/mood: {fields['lighting']}") - if fields.get("palette"): - sections.append(f"Color palette: {fields['palette']}") - if fields.get("style"): - sections.append(f"Style/format: {fields['style']}") - if fields.get("timing"): - sections.append(f"Timing/beats: {fields['timing']}") - if fields.get("audio"): - sections.append(f"Audio: {fields['audio']}") - if fields.get("text"): - sections.append(f"Text (verbatim): \"{fields['text']}\"") - if fields.get("dialogue"): - dialogue = fields["dialogue"].strip() - sections.append("Dialogue:\n\n" + dialogue + "\n") - if fields.get("constraints"): - sections.append(f"Constraints: {fields['constraints']}") - if fields.get("negative"): - sections.append(f"Avoid: {fields['negative']}") - - return "\n".join(sections) - - -def _augment_prompt(args: argparse.Namespace, prompt: str) -> str: - fields = _fields_from_args(args) - return _augment_prompt_fields(args.augment, prompt, fields) - - -def _get_status(video: Any) -> Optional[str]: - if isinstance(video, dict): - for key in ("status", "state"): - if key in video and isinstance(video[key], str): - return video[key] - data = video.get("data") if isinstance(video.get("data"), dict) else None - if data: - for key in ("status", "state"): - if key in data and isinstance(data[key], str): - return data[key] - return None - for key in ("status", "state"): - val = getattr(video, key, None) - if isinstance(val, str): - return val - return None - - -def _get_video_id(video: Any) -> Optional[str]: - if isinstance(video, dict): - if isinstance(video.get("id"), str): - return video["id"] - data = video.get("data") if isinstance(video.get("data"), dict) else None - if data and isinstance(data.get("id"), str): - return data["id"] - return None - vid = getattr(video, "id", None) - return vid if isinstance(vid, str) else None - - -def _poll_video( - client: Any, - video_id: str, - *, - poll_interval: float, - timeout: Optional[float], -) -> Any: - start = time.time() - last_status: Optional[str] = None - - while True: - video = client.videos.retrieve(video_id) - status = _get_status(video) or "unknown" - if status != last_status: - print(f"Status: {status}", file=sys.stderr) - last_status = status - if status in TERMINAL_STATUSES: - return video - if timeout is not None and (time.time() - start) > timeout: - _die(f"Timed out after {timeout:.1f}s waiting for {video_id}") - time.sleep(poll_interval) - - -def _download_content(client: Any, video_id: str, variant: str) -> Any: - content = client.videos.download_content(video_id, variant=variant) - if hasattr(content, "write_to_file"): - return content - if hasattr(content, "read"): - return content.read() - if isinstance(content, (bytes, bytearray)): - return bytes(content) - if hasattr(content, "content"): - return content.content - return content - - -def _write_download(data: Any, out_path: Path, *, force: bool) -> None: - if out_path.exists() and not force: - _die(f"Output exists: {out_path} (use --force to overwrite)") - if hasattr(data, "write_to_file"): - data.write_to_file(out_path) - print(f"Wrote {out_path}") - return - if hasattr(data, "read"): - out_path.write_bytes(data.read()) - print(f"Wrote {out_path}") - return - out_path.write_bytes(data) - print(f"Wrote {out_path}") - - -def _build_create_payload(args: argparse.Namespace, prompt: str) -> Dict[str, Any]: - model = _normalize_model(args.model) - size = _normalize_size(args.size, model) - seconds = _normalize_seconds(args.seconds) - payload: Dict[str, Any] = { - "model": model, - "prompt": prompt, - "size": size, - "seconds": seconds, - } - characters = _normalize_characters(getattr(args, "character_id", None)) - if characters: - payload["characters"] = characters - - _, input_reference_json = _normalize_input_reference( - path=getattr(args, "input_reference", None), - file_id=getattr(args, "input_reference_file_id", None), - image_url=getattr(args, "input_reference_url", None), - ) - if input_reference_json is not None: - payload["input_reference"] = input_reference_json - - return payload - - -def _prepare_job_payload( - args: argparse.Namespace, - job: Dict[str, Any], - base_fields: Dict[str, Optional[str]], - base_payload: Dict[str, Any], -) -> Tuple[Dict[str, Any], Optional[str], str]: - prompt = str(job["prompt"]).strip() - fields = _merge_non_null(base_fields, job.get("fields", {})) - fields = _merge_non_null(fields, {k: job.get(k) for k in base_fields.keys()}) - augmented = _augment_prompt_fields(args.augment, prompt, fields) - - payload = dict(base_payload) - payload["prompt"] = augmented - payload = _merge_non_null(payload, {k: job.get(k) for k in base_payload.keys()}) - payload = {k: v for k, v in payload.items() if v is not None} - - model = _normalize_model(payload.get("model")) - size = _normalize_size(payload.get("size"), model) - seconds = _normalize_seconds(payload.get("seconds")) - - payload["model"] = model - payload["size"] = size - payload["seconds"] = seconds - - raw_characters: Any = payload.get("characters") - if "characters" in job: - raw_characters = job.get("characters") - elif "character_ids" in job: - raw_characters = job.get("character_ids") - - characters = _normalize_characters(raw_characters) - if characters: - payload["characters"] = characters - else: - payload.pop("characters", None) - - default_input_ref_path, default_input_ref_json = _normalize_input_reference( - path=getattr(args, "input_reference", None), - file_id=getattr(args, "input_reference_file_id", None), - image_url=getattr(args, "input_reference_url", None), - ) - input_ref_path = default_input_ref_path - input_ref_json = dict(default_input_ref_json) if default_input_ref_json else None - - if any( - key in job - for key in ( - "input_reference", - "input_reference_path", - "input_reference_file", - "input_reference_file_id", - "input_reference_url", - ) - ): - input_ref_path, input_ref_json = _normalize_input_reference( - value=job.get("input_reference"), - path=job.get("input_reference_path") or job.get("input_reference_file"), - file_id=job.get("input_reference_file_id"), - image_url=job.get("input_reference_url"), - ) - - if input_ref_json is not None: - payload["input_reference"] = input_ref_json - else: - payload.pop("input_reference", None) - - return payload, input_ref_path, prompt - - -def _write_json(path: Path, obj: Any) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(_to_dict(obj), indent=2, sort_keys=True), encoding="utf-8") - print(f"Wrote {path}") - - -def _write_json_out(out_path: Optional[Path], obj: Any) -> None: - if out_path is None: - return - _write_json(out_path, obj) - - -async def _create_one_with_retries( - client: Any, - payload: Dict[str, Any], - *, - files: Optional[List[Tuple[str, Any]]] = None, - attempts: int, - job_label: str, -) -> Any: - last_exc: Optional[Exception] = None - for attempt in range(1, attempts + 1): - try: - return await _async_video_post(client, "/videos", payload, files=files) - except Exception as exc: - last_exc = exc - if not _is_transient_error(exc): - raise - if attempt == attempts: - raise - sleep_s = _extract_retry_after_seconds(exc) - if sleep_s is None: - sleep_s = min(60.0, 2.0**attempt) - print( - f"{job_label} attempt {attempt}/{attempts} failed ({exc.__class__.__name__}); retrying in {sleep_s:.1f}s", - file=sys.stderr, - ) - await asyncio.sleep(sleep_s) - raise last_exc or RuntimeError("unknown error") - - -async def _run_create_batch(args: argparse.Namespace) -> int: - jobs = _read_jobs_jsonl(args.input) - out_dir = Path(args.out_dir) - - base_fields = _fields_from_args(args) - base_payload = { - "model": args.model, - "size": args.size, - "seconds": args.seconds, - "characters": _normalize_characters(getattr(args, "character_id", None)), - } - - if args.dry_run: - for i, job in enumerate(jobs, start=1): - payload, input_ref, prompt = _prepare_job_payload(args, job, base_fields, base_payload) - out_path = _job_output_path(out_dir, i, prompt, job.get("out")) - preview = dict(payload) - if input_ref: - preview["input_reference"] = input_ref - _print_request( - { - "endpoint": "/v1/videos", - "job": i, - "output": str(out_path), - **preview, - } - ) - return 0 - - client = _create_async_client() - sem = asyncio.Semaphore(args.concurrency) - any_failed = False - - async def run_job(i: int, job: Dict[str, Any]) -> Tuple[int, Optional[str]]: - nonlocal any_failed - payload, input_ref, prompt = _prepare_job_payload(args, job, base_fields, base_payload) - job_label = f"[job {i}/{len(jobs)}]" - out_path = _job_output_path(out_dir, i, prompt, job.get("out")) - - try: - async with sem: - print(f"{job_label} starting", file=sys.stderr) - started = time.time() - with _open_input_reference(input_ref) as ref: - files = [("input_reference", ref)] if ref is not None else None - result = await _create_one_with_retries( - client, - payload, - files=files, - attempts=args.max_attempts, - job_label=job_label, - ) - elapsed = time.time() - started - print(f"{job_label} completed in {elapsed:.1f}s", file=sys.stderr) - _write_json(out_path, result) - return i, None - except Exception as exc: - any_failed = True - print(f"{job_label} failed: {exc}", file=sys.stderr) - if args.fail_fast: - raise - return i, str(exc) - - tasks = [asyncio.create_task(run_job(i, job)) for i, job in enumerate(jobs, start=1)] - - try: - await asyncio.gather(*tasks) - except Exception: - for t in tasks: - if not t.done(): - t.cancel() - raise - - return 1 if any_failed else 0 - - -def _create_batch(args: argparse.Namespace) -> None: - exit_code = asyncio.run(_run_create_batch(args)) - if exit_code: - raise SystemExit(exit_code) - - -def _cmd_create(args: argparse.Namespace) -> int: - prompt = _read_prompt(args.prompt, args.prompt_file) - prompt = _augment_prompt(args, prompt) - - payload = _build_create_payload(args, prompt) - input_reference_path, _ = _normalize_input_reference( - path=args.input_reference, - file_id=args.input_reference_file_id, - image_url=args.input_reference_url, - ) - json_out = _normalize_json_out(args.json_out, "create.json") - - if args.dry_run: - preview = dict(payload) - if input_reference_path: - preview["input_reference"] = input_reference_path - _print_request({"endpoint": "/v1/videos", **preview}) - _write_json_out(json_out, {"dry_run": True, "request": {"endpoint": "/v1/videos", **preview}}) - return 0 - - client = _create_client() - with _open_input_reference(input_reference_path) as input_ref: - files = [("input_reference", input_ref)] if input_ref is not None else None - video = _video_post(client, "/videos", payload, files=files) - _print_json(video) - _write_json_out(json_out, video) - return 0 - - -def _cmd_create_and_poll(args: argparse.Namespace) -> int: - prompt = _read_prompt(args.prompt, args.prompt_file) - prompt = _augment_prompt(args, prompt) - - payload = _build_create_payload(args, prompt) - input_reference_path, _ = _normalize_input_reference( - path=args.input_reference, - file_id=args.input_reference_file_id, - image_url=args.input_reference_url, - ) - json_out = _normalize_json_out(args.json_out, "create-and-poll.json") - - if args.dry_run: - preview = dict(payload) - if input_reference_path: - preview["input_reference"] = input_reference_path - _print_request({"endpoint": "/v1/videos", **preview}) - print("Would poll for completion.") - if args.download: - variant = _normalize_variant(args.variant) - out_path = _normalize_out_path(args.out, variant) - print(f"Would download variant={variant} to {out_path}") - if json_out: - dry_bundle: Dict[str, Any] = { - "dry_run": True, - "request": {"endpoint": "/v1/videos", **preview}, - "poll": True, - } - if args.download: - dry_bundle["download"] = { - "variant": variant, - "out": str(out_path), - } - _write_json_out(json_out, dry_bundle) - return 0 - - client = _create_client() - with _open_input_reference(input_reference_path) as input_ref: - files = [("input_reference", input_ref)] if input_ref is not None else None - video = _video_post(client, "/videos", payload, files=files) - _print_json(video) - - video_id = _get_video_id(video) - if not video_id: - _die("Could not determine video id from create response.") - - poll_interval = _normalize_poll_interval(args.poll_interval) - timeout = _normalize_timeout(args.timeout) - final_video = _poll_video( - client, - video_id, - poll_interval=poll_interval, - timeout=timeout, - ) - _print_json(final_video) - - if args.download: - status = _get_status(final_video) or "unknown" - if status != "completed": - _die(f"Video status is {status}; download is available only after completion.") - variant = _normalize_variant(args.variant) - out_path = _normalize_out_path(args.out, variant) - data = _download_content(client, video_id, variant) - _write_download(data, out_path, force=args.force) - - if json_out: - _write_json_out( - json_out, - {"create": _to_dict(video), "final": _to_dict(final_video)}, - ) - - return 0 - - -def _cmd_poll(args: argparse.Namespace) -> int: - poll_interval = _normalize_poll_interval(args.poll_interval) - timeout = _normalize_timeout(args.timeout) - json_out = _normalize_json_out(args.json_out, "poll.json") - - client = _create_client() - final_video = _poll_video( - client, - args.id, - poll_interval=poll_interval, - timeout=timeout, - ) - _print_json(final_video) - _write_json_out(json_out, final_video) - - if args.download: - status = _get_status(final_video) or "unknown" - if status != "completed": - _die(f"Video status is {status}; download is available only after completion.") - variant = _normalize_variant(args.variant) - out_path = _normalize_out_path(args.out, variant) - data = _download_content(client, args.id, variant) - _write_download(data, out_path, force=args.force) - - return 0 - - -def _cmd_status(args: argparse.Namespace) -> int: - json_out = _normalize_json_out(args.json_out, "status.json") - client = _create_client() - video = client.videos.retrieve(args.id) - _print_json(video) - _write_json_out(json_out, video) - return 0 - - -def _cmd_list(args: argparse.Namespace) -> int: - if getattr(args, "before", None): - _die("--before is no longer supported by the Videos API docs. Use --after for pagination.") - - params: Dict[str, Any] = { - "limit": args.limit, - "order": _normalize_order(args.order), - "after": args.after, - } - params = {k: v for k, v in params.items() if v is not None} - json_out = _normalize_json_out(args.json_out, "list.json") - client = _create_client() - videos = client.videos.list(**params) - _print_json(videos) - _write_json_out(json_out, videos) - return 0 - - -def _cmd_delete(args: argparse.Namespace) -> int: - json_out = _normalize_json_out(args.json_out, "delete.json") - client = _create_client() - result = client.videos.delete(args.id) - _print_json(result) - _write_json_out(json_out, result) - return 0 - - -def _cmd_remix(args: argparse.Namespace) -> int: - prompt = _read_prompt(args.prompt, args.prompt_file) - prompt = _augment_prompt(args, prompt) - json_out = _normalize_json_out(args.json_out, "remix.json") - _warn("The remix endpoint is deprecated in the latest Sora docs. Prefer the `edit` command for new workflows.") - - if args.dry_run: - preview = {"endpoint": f"/v1/videos/{args.id}/remix", "prompt": prompt} - _print_request(preview) - _write_json_out(json_out, {"dry_run": True, "request": preview}) - return 0 - - client = _create_client() - result = client.videos.remix(video_id=args.id, prompt=prompt) - _print_json(result) - _write_json_out(json_out, result) - return 0 - - -def _cmd_download(args: argparse.Namespace) -> int: - variant = _normalize_variant(args.variant) - out_path = _normalize_out_path(args.out, variant) - - client = _create_client() - data = _download_content(client, args.id, variant) - _write_download(data, out_path, force=args.force) - return 0 - - -def _cmd_create_character(args: argparse.Namespace) -> int: - json_out = _normalize_json_out(args.json_out, "create-character.json") - - if args.dry_run: - preview = { - "endpoint": "/v1/videos/characters", - "name": args.name, - "video": args.video_file, - } - _print_request(preview) - _write_json_out(json_out, {"dry_run": True, "request": preview}) - return 0 - - client = _create_client() - with _open_video_upload(args.video_file, label="Character video") as video_file: - result = _video_post( - client, - "/videos/characters", - {"name": args.name}, - files=[("video", video_file)], - ) - _print_json(result) - _write_json_out(json_out, result) - return 0 - - -def _cmd_extend(args: argparse.Namespace) -> int: - prompt = _read_prompt(args.prompt, args.prompt_file) - prompt = _augment_prompt(args, prompt) - seconds = _normalize_seconds(args.seconds) - json_out = _normalize_json_out(args.json_out, "extend.json") - - payload = { - "video": {"id": args.id}, - "prompt": prompt, - "seconds": seconds, - } - - if args.dry_run: - _print_request({"endpoint": "/v1/videos/extensions", **payload}) - _write_json_out( - json_out, - {"dry_run": True, "request": {"endpoint": "/v1/videos/extensions", **payload}}, - ) - return 0 - - client = _create_client() - result = _video_post(client, "/videos/extensions", payload) - _print_json(result) - _write_json_out(json_out, result) - return 0 - - -def _cmd_edit(args: argparse.Namespace) -> int: - prompt = _read_prompt(args.prompt, args.prompt_file) - prompt = _augment_prompt(args, prompt) - json_out = _normalize_json_out(args.json_out, "edit.json") - - payload: Dict[str, Any] = {"prompt": prompt, "video": {"id": args.id}} - - if args.dry_run: - _print_request({"endpoint": "/v1/videos/edits", **payload}) - _write_json_out( - json_out, - {"dry_run": True, "request": {"endpoint": "/v1/videos/edits", **payload}}, - ) - return 0 - - client = _create_client() - result = _video_post(client, "/videos/edits", payload) - _print_json(result) - _write_json_out(json_out, result) - return 0 - - -class _NullContext: - def __enter__(self): - return None - - def __exit__(self, exc_type, exc, tb): - return False - - -class _SingleFile: - def __init__(self, path: Path): - self._path = path - self._handle = None - - def __enter__(self): - self._handle = self._path.open("rb") - return self._handle - - def __exit__(self, exc_type, exc, tb): - if self._handle: - try: - self._handle.close() - except Exception: - pass - return False - - -def _add_prompt_args(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--prompt") - parser.add_argument("--prompt-file") - parser.add_argument("--augment", dest="augment", action="store_true") - parser.add_argument("--no-augment", dest="augment", action="store_false") - parser.set_defaults(augment=True) - - parser.add_argument("--use-case") - parser.add_argument("--scene") - parser.add_argument("--subject") - parser.add_argument("--action") - parser.add_argument("--camera") - parser.add_argument("--style") - parser.add_argument("--lighting") - parser.add_argument("--palette") - parser.add_argument("--audio") - parser.add_argument("--dialogue") - parser.add_argument("--text") - parser.add_argument("--timing") - parser.add_argument("--constraints") - parser.add_argument("--negative") - - -def _add_create_args(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--model", default=DEFAULT_MODEL) - parser.add_argument("--size", default=DEFAULT_SIZE) - parser.add_argument("--seconds", default=DEFAULT_SECONDS) - parser.add_argument("--input-reference") - parser.add_argument("--input-reference-file-id") - parser.add_argument("--input-reference-url") - parser.add_argument("--character-id", action="append", default=[]) - parser.add_argument("--dry-run", action="store_true") - _add_prompt_args(parser) - - -def _add_poll_args(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--poll-interval", type=float, default=DEFAULT_POLL_INTERVAL) - parser.add_argument("--timeout", type=float) - - -def _add_download_args(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--download", action="store_true") - parser.add_argument("--variant", default=DEFAULT_VARIANT) - parser.add_argument("--out") - parser.add_argument("--force", action="store_true") - - -def _add_json_out(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--json-out") - - -def main() -> int: - parser = argparse.ArgumentParser(description="Create and manage videos via the Sora Video API") - subparsers = parser.add_subparsers(dest="command", required=True) - - create_parser = subparsers.add_parser("create", help="Create a new video job") - _add_create_args(create_parser) - _add_json_out(create_parser) - create_parser.set_defaults(func=_cmd_create) - - create_poll_parser = subparsers.add_parser( - "create-and-poll", - help="Create a job, poll until complete, optionally download", - ) - _add_create_args(create_poll_parser) - _add_poll_args(create_poll_parser) - _add_download_args(create_poll_parser) - _add_json_out(create_poll_parser) - create_poll_parser.set_defaults(func=_cmd_create_and_poll) - - poll_parser = subparsers.add_parser("poll", help="Poll a job until it completes") - poll_parser.add_argument("--id", required=True) - _add_poll_args(poll_parser) - _add_download_args(poll_parser) - _add_json_out(poll_parser) - poll_parser.set_defaults(func=_cmd_poll) - - status_parser = subparsers.add_parser("status", help="Retrieve a job status") - status_parser.add_argument("--id", required=True) - _add_json_out(status_parser) - status_parser.set_defaults(func=_cmd_status) - - list_parser = subparsers.add_parser("list", help="List recent video jobs") - list_parser.add_argument("--limit", type=int) - list_parser.add_argument("--order") - list_parser.add_argument("--after") - _add_json_out(list_parser) - list_parser.set_defaults(func=_cmd_list) - - delete_parser = subparsers.add_parser("delete", help="Delete a video job") - delete_parser.add_argument("--id", required=True) - _add_json_out(delete_parser) - delete_parser.set_defaults(func=_cmd_delete) - - remix_parser = subparsers.add_parser("remix", help="Legacy remix of a completed video job") - remix_parser.add_argument("--id", required=True) - remix_parser.add_argument("--dry-run", action="store_true") - _add_prompt_args(remix_parser) - _add_json_out(remix_parser) - remix_parser.set_defaults(func=_cmd_remix) - - download_parser = subparsers.add_parser("download", help="Download video/thumbnail/spritesheet") - download_parser.add_argument("--id", required=True) - download_parser.add_argument("--variant", default=DEFAULT_VARIANT) - download_parser.add_argument("--out") - download_parser.add_argument("--force", action="store_true") - download_parser.set_defaults(func=_cmd_download) - - batch_parser = subparsers.add_parser( - "create-batch", - help="Create multiple video jobs locally from JSONL input (not the Batch API)", - ) - _add_create_args(batch_parser) - batch_parser.add_argument("--input", required=True, help="Path to JSONL file (one job per line)") - batch_parser.add_argument("--out-dir", required=True) - batch_parser.add_argument("--concurrency", type=int, default=DEFAULT_CONCURRENCY) - batch_parser.add_argument("--max-attempts", type=int, default=DEFAULT_MAX_ATTEMPTS) - batch_parser.add_argument("--fail-fast", action="store_true") - batch_parser.set_defaults(func=_create_batch) - - character_parser = subparsers.add_parser("create-character", help="Create a reusable non-human character from a video") - character_parser.add_argument("--name", required=True) - character_parser.add_argument("--video-file", required=True) - character_parser.add_argument("--dry-run", action="store_true") - _add_json_out(character_parser) - character_parser.set_defaults(func=_cmd_create_character) - - extend_parser = subparsers.add_parser("extend", help="Extend a completed video") - extend_parser.add_argument("--id", required=True) - extend_parser.add_argument("--seconds", default=DEFAULT_SECONDS) - extend_parser.add_argument("--dry-run", action="store_true") - _add_prompt_args(extend_parser) - _add_json_out(extend_parser) - extend_parser.set_defaults(func=_cmd_extend) - - edit_parser = subparsers.add_parser("edit", help="Edit an existing generated video by ID") - edit_parser.add_argument("--id", required=True, help="Existing generated video ID to edit") - edit_parser.add_argument("--dry-run", action="store_true") - _add_prompt_args(edit_parser) - _add_json_out(edit_parser) - edit_parser.set_defaults(func=_cmd_edit) - - args = parser.parse_args() - - if getattr(args, "concurrency", 1) < 1 or getattr(args, "concurrency", 1) > 10: - _die("--concurrency must be between 1 and 10") - if getattr(args, "max_attempts", DEFAULT_MAX_ATTEMPTS) < 1 or getattr(args, "max_attempts", DEFAULT_MAX_ATTEMPTS) > 10: - _die("--max-attempts must be between 1 and 10") - - dry_run = bool(getattr(args, "dry_run", False)) - _ensure_api_key(dry_run) - - args.func(args) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main())