Downloads media URLs discovered from:
https://sora.chatgpt.com/login(opened first by default so you can sign in)https://sora.chatgpt.com/profilehttps://sora.chatgpt.com/drafts
Use this only for content you own or are authorized to download.
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
python -m playwright install chromiumcapture defaults to your installed Google Chrome (--browser-channel chrome). If you do not have Chrome, install it or run with --browser-channel msedge or --browser-channel chromium.
Generated from:
python sora_downloader.py --help
python sora_downloader.py capture --help
python sora_downloader.py download --helpTop-level:
usage: sora_downloader.py [-h] [--output-dir OUTPUT_DIR] {capture,download} ...
Capture:
usage: sora_downloader.py capture [-h] [--targets [TARGETS ...]]
[--seconds-per-target SECONDS_PER_TARGET]
[--profile-dir PROFILE_DIR]
[--login-timeout-ms LOGIN_TIMEOUT_MS]
[--nav-max-attempts NAV_MAX_ATTEMPTS]
[--nav-retry-delay-ms NAV_RETRY_DELAY_MS]
[--goto-wait-until {commit,domcontentloaded,load,networkidle}]
[--browser-channel {chromium,chrome,msedge}]
[--bootstrap-url BOOTSTRAP_URL]
[--no-bootstrap] [--headless]
[--verification-prompt]
[--no-auto-explore]
[--auto-scroll-steps AUTO_SCROLL_STEPS]
[--auto-step-wait-ms AUTO_STEP_WAIT_MS]
[--auto-click-interval AUTO_CLICK_INTERVAL]
[--auto-max-seconds AUTO_MAX_SECONDS]
[--no-detail-download-links]
[--passive-media-capture]
[--detail-dom-fallback]
[--max-detail-pages MAX_DETAIL_PAGES]
[--detail-gather-max-rounds DETAIL_GATHER_MAX_ROUNDS]
[--detail-gather-idle-rounds DETAIL_GATHER_IDLE_ROUNDS]
[--detail-gather-pause-ms DETAIL_GATHER_PAUSE_MS]
[--detail-settle-ms DETAIL_SETTLE_MS]
[--detail-download-timeout-ms DETAIL_DOWNLOAD_TIMEOUT_MS]
Download:
usage: sora_downloader.py download [-h] [--manifest MANIFEST]
[--storage-state STORAGE_STATE]
[--overwrite] [--upload-dir DIR]
[--timeout-ms TIMEOUT_MS]
Capture opens a visible browser (default: installed Chrome) with a persistent profile. It loads https://sora.chatgpt.com/login once first so you can sign in, then opens each target. After navigation it prints [nav] url=... title=... so you can confirm you reached the real site.
Unattended by default: capture does not wait for Enter and does not add idle time after each target (--seconds-per-target defaults to 0). Use a --profile-dir that is already logged in to Sora (log in once with --verification-prompt, or sign in manually in that profile outside the script). For Cloudflare/login struggles, run once with --verification-prompt and optional --seconds-per-target 30.
Detail-page downloads (default on): after each list page (/profile, /drafts) finishes auto-scroll (and any --seconds-per-target wait), the script scrolls the list again until almost no new links appear, collects every unique detail URL it finds (any element with [href] on sora.chatgpt.com), sorts /p/… posts first, then opens each detail page and resolves URLs only through the site’s Download flow (⋯ → Download, /backend/project_y/download/…, visible Download controls, MP4/download events)—not from <video> preview streams unless you pass --detail-dom-fallback.
No overview/grid streams (default): --passive-media-capture is off by default, so scrolling the profile/drafts grid does not save preview video URLs—only URLs produced after opening each detail and using Download. Use --passive-media-capture if you want the old behavior (record all network media). By default --max-detail-pages 0 = no cap. Set e.g. --max-detail-pages 50 to limit.
On Sora post pages (https://sora.chatgpt.com/p/…), Download is only in the main post ⋮ menu. With comments, the script first restricts ⋮ buttons to those above the “add comment” field (when it can find it), then tries top-to-bottom by position; it scrolls the <video> into view first. Any trigger that doesn’t open Download is closed with Escape before the next. Remaining triggers are tried in a final fallback pass. If you see Download API returned 401, sign in (same --profile-dir) and run capture again.
If Cloudflare appears, run with --verification-prompt, solve it in the browser, then press Enter in the terminal (or fix the session in that profile first).
python sora_downloader.py captureFirst-time login (interactive), then reuse profile for unattended runs:
python sora_downloader.py capture --verification-prompt --seconds-per-target 45
python sora_downloader.py captureIf the login UI still does not appear, try Edge or a longer load wait:
python sora_downloader.py capture --browser-channel msedge --goto-wait-until load --login-timeout-ms 120000 --verification-prompt --seconds-per-target 120Useful options:
python sora_downloader.py capture --auto-max-seconds 20
python sora_downloader.py capture --auto-scroll-steps 80 --auto-step-wait-ms 800
python sora_downloader.py capture --no-auto-explore --seconds-per-target 30Capture options:
--targets <url1> <url2> ...
Pages to visit. Default isprofileanddrafts.--seconds-per-target <int>
Extra idle seconds after auto-explore per list target. Default:0(none).--profile-dir <path>
Persistent Chromium profile directory for login/session reuse. Default:.playwright-profile.--login-timeout-ms <int>
Timeout for opening each target/login page. Default:60000(60s). If timeout is reached, capture keeps the page open so you can still complete verification manually.--nav-max-attempts <int>
How many times to retrygotoper URL (bootstrap, each list target, each detail) before skipping. Default:4.--nav-retry-delay-ms <int>
Pause between those retries. Default:1000.--goto-wait-until <commit|domcontentloaded|load|networkidle>
How longgotowaits before continuing. Default:domcontentloaded. Useloadif the login UI appears late.--browser-channel <chromium|chrome|msedge>
Engine: bundled Chromium or installed Chrome / Edge. Default:chrome(best for ChatGPT SSO flows).--bootstrap-url <url>
Opened once before targets; defaulthttps://sora.chatgpt.com/login.--no-bootstrap
Skip the bootstrap step.--headless
Run without visible browser window.--verification-prompt
Pause for Enter after bootstrap and each target (for login/Cloudflare). Default: off (unattended).--no-auto-explore
Disable automatic scrolling/pagination clicking.--auto-scroll-steps <int>
Max auto-explore scroll steps per page. Default:40.--auto-step-wait-ms <int>
Delay after each auto step. Default:600.--auto-click-interval <int>
Try clicking pagination controls every N steps. Default:4.--auto-max-seconds <int>
Hard time limit for auto-explore per page. Default:30.--no-detail-download-links
Do not visit detail pages (manifest will only get media if--passive-media-captureis on).--passive-media-capture
Record video URLs from all network traffic (grid + detail). Default: off (detail Download only).--detail-dom-fallback
On detail pages, if Download fails, also scrape<video>/ links (may match preview streams like the grid).--max-detail-pages <int>
Max detail pages per list URL;0= all links found after gathering (default:0).--detail-gather-max-rounds <int>
Scroll rounds while discovering links on the list (default:60).--detail-gather-idle-rounds <int>
Stop gathering after this many rounds with no new links (default:4).--detail-gather-pause-ms <int>
Delay between gather scroll steps (default:700).--detail-settle-ms <int>
Wait after loading a detail page before scraping. Default:2500.--detail-download-timeout-ms <int>
How long to wait for a browser download after clicking Download. Default:20000.- Global:
--output-dir <path>
Base folder for manifests/downloads. Default:output.
Outputs:
output/manifests/capture-<timestamp>.json(captured URLs; entries from detail pages includedetail_pagewhen applicable)output/manifests/storage-state-<timestamp>.json(auth/session state)
python sora_downloader.py downloadOptional:
python sora_downloader.py download --manifest output/manifests/capture-YYYYMMDD-HHMMSS.json
python sora_downloader.py download --overwrite
python sora_downloader.py download --upload-dir uploadDownload options:
--manifest <path>
Use a specific capture manifest. Default: latest manifest inoutput/manifests.--storage-state <path>
Override auth/session state file used for requests.--overwrite
Replace existing files in the download folder.--upload-dir <path>
Write media files here. Default:<output-dir>/downloads(e.g.output/downloads). Use e.g.uploadorC:\Videos\SoraUploadfor your upload folder.--timeout-ms <int>
Per-request timeout. Default:120000.- Global:
--output-dir <path>
Base folder for manifests and default download location. Default:output.
Press Ctrl+C to stop either command early; the script exits gracefully and keeps partial progress.
Downloaded files go to --upload-dir if set, otherwise output/downloads.
- If a URL expires, run
captureagain to refresh signed links. - Duplicate downloads are automatically skipped using a stable media key (host + path), so refreshed signed URLs with different query params do not redownload.
- Some captures may include
.m3u8playlists. This tool saves them as-is. - You can customize target pages:
python sora_downloader.py capture --targets https://sora.chatgpt.com/profile https://sora.chatgpt.com/drafts