idiotproof 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,74 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ build:
13
+ name: Build and verify distributions
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - name: Check out repository
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Set up Python
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version: "3.13"
24
+
25
+ - name: Install build and test dependencies
26
+ run: |
27
+ python -m pip install --upgrade pip
28
+ python -m pip install -e .
29
+ python -m pip install build twine pytest pytest-asyncio ruff mypy
30
+
31
+ - name: Verify tag matches package version
32
+ run: |
33
+ python -c 'import os, tomllib; version = tomllib.load(open("pyproject.toml", "rb"))["project"]["version"]; assert os.environ["GITHUB_REF_NAME"] == f"v{version}", "Release tag must match package version"'
34
+
35
+ - name: Lint
36
+ run: ruff check .
37
+
38
+ - name: Type-check
39
+ run: mypy src/idiotproof
40
+
41
+ - name: Test
42
+ run: pytest
43
+
44
+ - name: Build distributions
45
+ run: python -m build
46
+
47
+ - name: Validate distributions
48
+ run: twine check dist/*
49
+
50
+ - name: Upload distributions
51
+ uses: actions/upload-artifact@v4
52
+ with:
53
+ name: python-package-distributions
54
+ path: dist/
55
+
56
+ publish:
57
+ name: Publish distributions to PyPI
58
+ needs: build
59
+ runs-on: ubuntu-latest
60
+ environment:
61
+ name: pypi
62
+ url: https://pypi.org/p/idiotproof
63
+ permissions:
64
+ id-token: write
65
+
66
+ steps:
67
+ - name: Download distributions
68
+ uses: actions/download-artifact@v4
69
+ with:
70
+ name: python-package-distributions
71
+ path: dist/
72
+
73
+ - name: Publish distributions to PyPI
74
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .mypy_cache/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ site/
9
+ .env
10
+ .env.*
11
+ !.env.example
@@ -0,0 +1,88 @@
1
+ # Changelog
2
+
3
+ ## 0.5.0 — 2026-09-03
4
+
5
+ - Renamed the public PyPI distribution to `idiotproof` before its first
6
+ publication. The Python import is `idiotproof`; the CLI remains `idea-adk`.
7
+ - Replaced the provisional reservation endpoint with the deployed metadata-only
8
+ TUS batch contract.
9
+ - Added automatic client-side FFmpeg segmentation in `upload_batch()` for
10
+ sources longer than 600 decoded frames.
11
+ - Made `upload_file()` detect long video and transparently return an ordered
12
+ aggregate of reserved parts instead of allowing server-side truncation.
13
+ - Verify every generated part is independently decodable, contains at most 600
14
+ frames, and preserves the source's total decoded frame count.
15
+ - Add `uploadBatchId`, `uploadBatchIndex`, and `uploadBatchSize` directly to
16
+ every authenticated TUS creation without changing the public upload API.
17
+ - Validate batch placement and normalized output basenames before returning
18
+ ordered results.
19
+ - Preserve every concatenated video frame when remuxing `audio_source`, even
20
+ when the source audio ends slightly before the video, while stream-copying
21
+ the complete encoded audio track.
22
+ - Reuse `upload_file()`'s decoded source-frame count during segmentation
23
+ instead of decoding every long source twice.
24
+ - Run independent generated-part and downloaded-part verification probes with
25
+ bounded local concurrency while preserving ordered validation results.
26
+
27
+ ## 0.4.0 — 2026-09-02
28
+
29
+ - Added immutable, locally validated `UploadSequence` placement metadata for
30
+ ordered multi-asset uploads.
31
+ - Added optional `sequence=` support to `upload_file()` while continuing to
32
+ send only server-authorized metadata to TUS.
33
+ - Added `upload_batch()` for complete preflight validation, generated opaque
34
+ batch/asset IDs, bounded concurrency, and nested input-order results across
35
+ segmented videos and singleton images.
36
+ - Added strict checks that authorization and normalized results preserve the
37
+ requested sequence, plus CLI and MCP schema support for explicit placement.
38
+
39
+ ## 0.3.0 — 2026-09-02
40
+
41
+ - Added `Idea.download_media()` with safe streaming fallback, verified bounded
42
+ HTTP ranges, strong-ETag identity checks, resumable range state, SHA-256, and
43
+ atomic destination replacement.
44
+ - Added `download_and_concat()` for ordered workspace videos, including stable
45
+ indexed part names, complete ffprobe reports, strict dimension invariants,
46
+ copy compatibility validation, explicit encoding, optional source-audio
47
+ remuxing, resumable manifests, and verified atomic output.
48
+ - Kept signed media URLs out of manifests, subprocesses, diagnostics, and
49
+ durable state.
50
+ - Documented preview-first iteration as distinct from durable download and
51
+ concatenation.
52
+
53
+ ## 0.2.1 — 2026-09-02
54
+
55
+ - Added `numbered_media()` for predictable, unique, ordered workspace and
56
+ output path generation.
57
+
58
+ ## 0.2.0 — 2026-09-02
59
+
60
+ - Replaced generated async functions with cached, callable `RuntimeTool`
61
+ objects that retain live names, schemas, documentation, and signatures.
62
+ - Added reusable common-argument binding with `partial()`.
63
+ - Added ordered asynchronous `map()` with bounded concurrency, complete local
64
+ preflight validation, and no automatic remote-tool retries.
65
+ - Added structured `MapItem` outcomes and fail-after-settlement
66
+ `IdeaBatchError` handling.
67
+ - Added the CLI `map` command and documented cancellation and the boundary
68
+ between media segmentation and sequential resumable TUS chunks.
69
+ ## 0.1.0 — 2026-09-02
70
+
71
+ - Established the working IDEA ADK package.
72
+ - Defined an ADK as unstable software for agents and one-off scripts, not an
73
+ application framework for persistent software.
74
+ - Kept the primary Python surface centered on `Idea`, `ChatId`, and structured
75
+ `IdeaError` failures.
76
+ - Added local chat ID creation, public TUS uploads, stable media publishing,
77
+ and live MCP tool discovery.
78
+ - Added callable `RuntimeTool` objects generated from MCP descriptions and JSON
79
+ Schemas, with reusable partial binding and ordered, bounded asynchronous
80
+ `map()`.
81
+ - Added `MapItem` outcomes and `IdeaBatchError` so mapped successes remain
82
+ observable when other items fail.
83
+ - Added a JSON CLI and stdio MCP adapter for coding agents.
84
+ - Added the JSON CLI `map` command with fail-after-settlement and collected-error
85
+ modes.
86
+ - Added explicit localhost visual-review instructions for transient signed
87
+ media URLs.
88
+ - Consolidated the complete public documentation into the PyPI README.
@@ -0,0 +1,364 @@
1
+ Metadata-Version: 2.5
2
+ Name: idiotproof
3
+ Version: 0.5.0
4
+ Summary: Software for agents that create and edit video.
5
+ Project-URL: Repository, https://github.com/4014-Labs/idiotproof
6
+ Project-URL: Issues, https://github.com/4014-Labs/idiotproof/issues
7
+ Author: 4014 Labs
8
+ License: Proprietary
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: httpx<1,>=0.27
19
+ Description-Content-Type: text/markdown
20
+
21
+ # idiotproof
22
+
23
+ Software for agents that create and edit video.
24
+
25
+ `idiotproof` is the IDEA ADK. ADK means **Agent Development Kit**: software
26
+ intended to be written and run by agents. It is designed for one-off Python scripts, CLI calls, and
27
+ headless agent workflows alongside FFmpeg. It is not an application framework
28
+ or a stable dependency for persistent software. Do not build a long-lived
29
+ service around its current interfaces; the package and dynamic tool schemas
30
+ will change as the product develops.
31
+
32
+ Bobbie plus FFmpeg is intended to be a complete headless media toolchain. Use
33
+ FFmpeg for cutting, joining, transcoding, and audio. Use Bobbie for GPU-native
34
+ vision and rendering.
35
+
36
+ ## Install and authenticate
37
+
38
+ ```bash
39
+ pip install idiotproof
40
+ export IDEA_SECRET_KEY=idea_sk_test_...
41
+ ```
42
+
43
+ The alpha client defaults to `https://dev.67idea.com/v1`. Override it with
44
+ `IDEA_API_BASE_URL` or `Idea(base_url=...)`. A secret key belongs to one IDEA
45
+ project and environment. The API mints short-lived, workspace-scoped JWTs
46
+ internally; your code never creates or forwards those JWTs.
47
+
48
+ ## Required visual-review workflow
49
+
50
+ `media_publish()` returns signed URLs specifically so an agent can inspect its
51
+ work without downloading it. Signed URLs are transient credentials, not values
52
+ that must be ignored.
53
+
54
+ After publishing media, an agent should:
55
+
56
+ 1. Keep the signed URL only in the local viewer process's memory.
57
+ 2. Generate the viewer response dynamically. A page may contain several URLs
58
+ to compare multiple edits, but no signed URL belongs in an on-disk HTML file
59
+ or manifest.
60
+ 3. Serve only that directory with a server bound to `127.0.0.1`, never
61
+ `0.0.0.0`.
62
+ 4. Open the localhost URL in the available browser. If browser control is
63
+ unavailable, still create the viewer and provide its localhost URL.
64
+ 5. View the remote media through the page; do not download it merely to
65
+ preview it.
66
+ 6. Do not print, paste into chat, persist, or commit the signed URL. Stop the
67
+ viewer process after review or expiration.
68
+
69
+ Downloading is appropriate when saving an edit for continued local work or
70
+ durable storage. It is not required for visual review.
71
+
72
+ ## Python
73
+
74
+ The public vocabulary is deliberately small: one `Idea` client, opaque
75
+ `ChatId` values, dynamic runtime tools, and structured `IdeaError` failures.
76
+
77
+ ```python
78
+ import asyncio
79
+ import json
80
+ from pathlib import Path
81
+
82
+ from idiotproof import Idea
83
+
84
+
85
+ async def main() -> None:
86
+ async with Idea() as idea:
87
+ chat_id = await idea.create_chat()
88
+ upload = await idea.upload_file(chat_id, "clip.mp4")
89
+
90
+ tools = await idea.get_tools()
91
+ print([definition["id"] for definition in tools])
92
+
93
+ # Functions and documentation come from the live MCP catalog.
94
+ print(tools.submit_bobbie_job.__doc__)
95
+ render_input = json.loads(Path("render.json").read_text())
96
+ render_input.setdefault("input_media", upload["workspace_uri"])
97
+ result = await tools.submit_bobbie_job(chat_id, **render_input)
98
+
99
+ published = await idea.media_publish(chat_id, result["output_media"])
100
+ # Give display_url directly to an in-memory viewer bound to 127.0.0.1.
101
+ # That viewer is agent workflow code, not an SDK side effect. Do not
102
+ # print the URL or write it into HTML, JSON, logs, or a manifest.
103
+
104
+
105
+ asyncio.run(main())
106
+ ```
107
+
108
+ `create_chat()` returns a random opaque `ChatId` locally. The API lazily
109
+ materializes its project-scoped server workspace on the first upload or tool
110
+ call. Save the ID if another process must continue in the same workspace. Do
111
+ not put email addresses or other personal data in it.
112
+
113
+ `upload_file(chat_id, path)` probes video before network activity. An image or
114
+ video of at most 600 decoded frames follows the ordinary authorized TUS path
115
+ and returns one upload JSON object with `workspace_uri`. A longer video
116
+ automatically follows the ordered TUS metadata workflow described below and
117
+ returns an `upload_asset` JSON object with ordered `parts` and
118
+ `workspace_uris`; it never silently uploads a truncated single result.
119
+
120
+ For long videos or concurrent ordered uploads, use `upload_batch()` and
121
+ describe logical assets rather than a global upload queue:
122
+
123
+ ```python
124
+ uploads = await idea.upload_batch(
125
+ chat_id,
126
+ [
127
+ "long-video-a.mp4",
128
+ ["video-b-001.mp4", "video-b-002.mp4"],
129
+ "reference.png",
130
+ ],
131
+ concurrency=4,
132
+ )
133
+ ```
134
+
135
+ Each top-level item is one logical asset. A bare path is one source asset; an
136
+ ordered path sequence supplies already separated pieces of one asset. Before
137
+ any request, the helper probes videos with `ffprobe` and uses FFmpeg to
138
+ re-encode every source longer than 600 frames into independently decodable
139
+ parts of at most 600 frames. It verifies that the parts preserve the complete
140
+ decoded frame count.
141
+
142
+ The helper flattens those parts in caller order and adds `uploadBatchId`,
143
+ `uploadBatchIndex`, and `uploadBatchSize` to each authenticated TUS creation.
144
+ The first creation reserves the complete contiguous block of `upload_NNN`
145
+ names; transfers may then proceed concurrently in any order. No reservation
146
+ endpoint or client-side completion throttle is involved. Results retain the
147
+ same nested asset/part order, and the helper verifies that returned basenames
148
+ form one contiguous block in that order. If an upload fails, it lets every
149
+ started operation settle and raises `IdeaBatchError`; its flat `items` retain
150
+ successes and failures in original asset/part traversal order.
151
+
152
+ `media_publish(chat_id, media_path)` calls the stable API-key-authenticated
153
+ media endpoint and returns short-lived viewing and download URLs. It is outside
154
+ the dynamic MCP catalog and does not depend on tool discovery.
155
+
156
+ `download_media(chat_id, media_path, destination)` is the durable counterpart.
157
+ It publishes internally, keeps the signed download URL in memory, verifies the
158
+ download, and atomically replaces the destination only after success. Small
159
+ objects and origins without safe range support use one streamed request. Large
160
+ objects use bounded parallel byte ranges only when the origin supplies a
161
+ content length, byte-range support, and a strong ETag. Range responses must
162
+ match their requested offsets and object identity; otherwise the helper safely
163
+ falls back or fails. `resume=True` verifies completed temporary ranges after
164
+ republishing the stable workspace path. Resume files never contain signed
165
+ URLs. In `download_and_concat()`, one shared transfer limit bounds publishing,
166
+ whole-object requests, and nested range requests so concurrency does not
167
+ multiply by the number of files.
168
+
169
+ `get_tools()` fetches each dynamic tool's name, description, and JSON Schema at
170
+ runtime. It returns an iterable catalog and generates documented async methods
171
+ such as `tools.submit_bobbie_job(...)`. Punctuation becomes `_`; use
172
+ `await tools.call(exact_name, chat_id, input_dict)` for unusual or colliding
173
+ names. Tool methods return the tool output rather than a transport wrapper.
174
+
175
+ Every discovered tool is a callable `RuntimeTool`. Call one normally, bind
176
+ shared arguments once with `partial()`, or map it over varying inputs with
177
+ bounded concurrency:
178
+
179
+ ```python
180
+ render = tools.submit_bobbie_job.partial(
181
+ chat_id,
182
+ passes=effect,
183
+ timeout_seconds=300,
184
+ )
185
+ jobs = await render.map(
186
+ [
187
+ {"input_media": "workspace:segment-001.mp4", "output_media": "one.mp4"},
188
+ {"input_media": "workspace:segment-002.mp4", "output_media": "two.mp4"},
189
+ ],
190
+ concurrency=4,
191
+ )
192
+ ```
193
+
194
+ Results retain input order even when calls finish out of order. The entire
195
+ input iterable is validated before any request starts, and an input key may
196
+ not duplicate a bound/common key. `tools.map(exact_name, chat_id, inputs)` is
197
+ the escape hatch for exact MCP names. `errors="collect"` returns ordered
198
+ `MapItem` values; the default waits for every item to settle and then raises
199
+ `IdeaBatchError`, whose `items` preserve both successes and failures. Mapped
200
+ tool calls are not automatically retried because they may have side effects.
201
+
202
+ Cancelling `map()` prevents queued calls from starting and cancels local waits
203
+ for calls already in flight. It cannot retract a remote operation that the
204
+ service already accepted.
205
+
206
+ Use `numbered_media()` when a pipeline needs predictable ordered workspace or
207
+ output names:
208
+
209
+ ```python
210
+ from idiotproof import numbered_media
211
+
212
+ media = numbered_media(
213
+ "workspace:video_a_rendered_{index:03d}.mp4",
214
+ count=3,
215
+ )
216
+ # workspace:video_a_rendered_001.mp4, ...002.mp4, ...003.mp4
217
+ ```
218
+
219
+ `count` must be non-negative, and the formatted values must be unique. Use a
220
+ stable sequence prefix when several source videos share a workspace. Generate
221
+ names before launching concurrent work and retain input order when collecting
222
+ results; completion time must never determine segment order. A workflow
223
+ manifest remains authoritative—zero-padding is convenient, not an ordering
224
+ guarantee. This helper is for predictable workspace and output names, never
225
+ `media_publish()` URLs: published URLs are signed and cannot be reconstructed
226
+ from a pattern.
227
+
228
+ Use `download_and_concat()` when ordered workspace videos are final and must
229
+ become one durable local video:
230
+
231
+ ```python
232
+ from idiotproof import download_and_concat, numbered_media
233
+
234
+ combined = await download_and_concat(
235
+ idea,
236
+ chat_id,
237
+ numbered_media(
238
+ "workspace:video_a_rendered_{index:03d}.mp4",
239
+ count=11,
240
+ ),
241
+ output="output/final.mp4",
242
+ concurrency=4,
243
+ resume=True,
244
+ )
245
+ ```
246
+
247
+ The input must be an ordered sequence; sets, mappings, and bare strings are
248
+ rejected. Downloads may complete in any order, but index-derived local names
249
+ and caller order control concatenation. Every part is probed with `ffprobe`.
250
+ All selected video streams must have exactly equal width and height—neither
251
+ mode scales, crops, pads, or rotates a mismatch. The default
252
+ `concat_mode="copy"` requires compatible streams and never re-encodes.
253
+ Explicit `"encode"` mode may normalize other stream properties while
254
+ preserving the dimension invariant. Supplying `audio_source` ignores segment
255
+ audio and remuxes that local audio once with stream copying. It preserves the
256
+ complete video sequence even when that audio ends slightly earlier. FFmpeg
257
+ and ffprobe must be installed. The result is a `CombinedMedia` containing the
258
+ final probe summary and SHA-256.
259
+
260
+ This is a durable-output helper, not the visual-review path. Iterate by
261
+ publishing a short representative clip and comparing remote `display_url`
262
+ variants through the localhost viewer. Download and concatenate only after an
263
+ effect is selected.
264
+
265
+ All individual failures are `IdeaError`; inspect `code`, `status_code`,
266
+ `details`, and `request_id`. Never log the secret key or upload token. Signed
267
+ media URLs may be retained transiently for the localhost viewer, but should
268
+ not appear in terminal history, chat messages, durable logs, or version
269
+ control.
270
+
271
+ ## CLI and MCP
272
+
273
+ Commands emit JSON so agents can compose them with scripts:
274
+
275
+ ```bash
276
+ idea-adk create-chat
277
+ idea-adk upload-file "$CHAT_ID" clip.mp4
278
+ idea-adk get-tools
279
+ idea-adk call "$CHAT_ID" submit_bobbie_job --input @render.json
280
+ idea-adk map "$CHAT_ID" submit_bobbie_job --input @batch.json --concurrency 4
281
+ idea-adk media-publish "$CHAT_ID" workspace:result.mp4
282
+ ```
283
+
284
+ `batch.json` contains an `inputs` array and an optional `common` object:
285
+
286
+ ```json
287
+ {
288
+ "common": {"passes": [{"kind": "fragment_shader", "shader_text": "..."}]},
289
+ "inputs": [
290
+ {"input_media": "workspace:one.mp4", "output_media": "one-out.mp4"},
291
+ {"input_media": "workspace:two.mp4", "output_media": "two-out.mp4"}
292
+ ]
293
+ }
294
+ ```
295
+
296
+ The CLI returns `{"results": [...]}` by default. With `--errors collect`, it
297
+ returns ordered `items`, each containing its `index` and exactly one of
298
+ `result` or structured `error`.
299
+
300
+ Use Python `upload_batch()` when a source may exceed 600 frames or the ADK must
301
+ reserve and upload a whole multi-asset manifest.
302
+
303
+ `media-publish` returns its requested JSON to stdout, including signed URLs;
304
+ do not use that command in a logged shell. A localhost review process should
305
+ call `media_publish()` internally and retain the response only in memory.
306
+
307
+ Run `idea-adk mcp` as a stdio MCP adapter and let it inherit
308
+ `IDEA_SECRET_KEY`. It exposes `create_chat`, `upload_file`, and `get_tools`,
309
+ followed by functions generated from the live server catalog. Media publishing
310
+ remains a separate stable Python/CLI operation and is intentionally not added
311
+ to the dynamic MCP catalog.
312
+
313
+ This README is the default advice agents should receive. Project-specific
314
+ instructions belong in `AGENTS.md` for Codex or `CLAUDE.md` for Claude Code.
315
+
316
+ ## Rules of thumb for agents
317
+
318
+ Uploads are currently normalized to exactly 600 frames at 3 megapixels,
319
+ slightly above 1080p. `upload_file()` detects a source beyond the input limit
320
+ and automatically creates independently decodable parts without dropping
321
+ source frames. Use `upload_batch()` directly for several logical assets or
322
+ when you want an explicitly nested result.
323
+
324
+ For a novel effect, first probe frame rate and time base and render one
325
+ representative preview clip targeting about 20 seconds while staying safely
326
+ below the service limit—590 frames is a useful cap when the maximum is 600.
327
+ Use an explicit range when supplied; otherwise the source midpoint is a
328
+ deterministic fallback, while a vision-capable agent may deliberately select a
329
+ high-motion or representative region. Upload that preview once and map several
330
+ effect variants over it. After choosing an effect, render the complete ordered
331
+ segments once. Direct full-source rendering remains reasonable for a known or
332
+ trivial effect.
333
+
334
+ Each TUS resource uses sequential, resumable chunks; core TUS requires ordered
335
+ offsets within that resource. For a long source, `upload_file()` delegates to
336
+ `upload_batch()`, which performs media-aware splitting before network activity,
337
+ reserves every output name in one transaction, and uploads those separate
338
+ resources concurrently. TUS chunks are transport details; they are not
339
+ independently decodable media segments. Upload completion order is never media
340
+ order.
341
+
342
+ Bobbie is a custom rendering engine that runs arbitrary GLSL. You describe the
343
+ pipeline but do not control bindings; Bobbie assigns them programmatically. A
344
+ pipeline can combine GLSL, smaller ML models, optimized CUDA kernels for
345
+ classical computer vision, and optional Bayesian priors over color and shape.
346
+ CUDA-GLSL interop keeps video on the GPU. Because Bobbie controls every tensor
347
+ dimension, supported pipelines should not run out of GPU memory.
348
+
349
+ Invalid requests should fail before rendering. The expected render-time
350
+ failures are a video timeout and `file too large`. Incompressible output such as
351
+ raw static can exceed 100 MB; the service deletes it. Elaborate pipelines can
352
+ serve either image/video editing or advanced computer-vision work.
353
+
354
+ A separate tool extracts frames for a VLM. The current model is Qwen-VL 27B;
355
+ custom VLMs are not supported. Bobbie requires grounded objects and coordinates
356
+ normalized to the half-open interval `[0, 1000)`.
357
+
358
+ Frame/VLM inspection plus Bobbie lets an agent move through any video. Use code
359
+ to crop, zoom, and rotate. Use vision tools to measure camera and object motion
360
+ and perform tracking, detection, and segmentation. Other catalog tools expose
361
+ metadata about uploaded files, processes, and jobs.
362
+
363
+ IDEA deletes media aggressively. If an edit matters, download it for continued
364
+ local editing or copy it immediately into durable storage such as S3.