Repository navigation
Clip deletion, and M8 — AI generation via fal.ai - #34
Merged
Merged
Conversation
The contract the backend and frontend are built against in parallel: queue API with persisted request ids for restart safety, a separate generation worker queue, data-URI bases, a catalogue that describes each endpoint's parameter dialect, admins named in config, provenance columns on the asset, and exact cost from fal's billing header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPmZjuM4q96DFRfU9cHL33
…t with its clips
A clip copies its parent's thumb_key, a URL import's chapter clips all copy
the site thumbnail, and a promote whose own thumbnail failed keeps the copy.
delete_asset unlinked whatever keys the deleted row held, so deleting one
clip deleted the poster of the parent and every sibling clip, and deleting
a parent took the poster of a promoted child it had just let go. A key is
now unlinked only once no surviving row of that user references it.
DELETE /api/assets/{id}?with_clips=true deletes the parent's live clips and
then the parent in one transaction; without the flag the 409 still stands,
so an older client never cascades by accident. A delete is refused with
409 extraction_in_progress while any row it would remove has a queued or
running extract_subvideo job, which would otherwise finish into a missing
row after ffmpeg had written an unreferenced file. The 409 message now says
"1 clip depends".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPmZjuM4q96DFRfU9cHL33
…ete guard
The Clip tab listed a video's clips with no way to remove one, and could
not even open a live clip; a freshly saved clip only reached the grid on
the next reload; and the parent's guard offered only to promote its clips.
- ClipEditor: every row's name links to /a/{id}; a live-clip row gets a
trash button with an inline "Delete clip / Cancel" confirm (sub-video
rows own real bytes and get none); a saved clip is prepended to the grid.
- AssetDetail: "Delete clips too" beside "Promote and delete", through a
new non-optimistic removeWithClips so a failure leaves the guard mounted
to say why (the remount bug M7 recorded); clip-specific wording in the
Info-tab confirm, and no pointless clip lookup when deleting a clip.
- library store: removeWithClips, prepend, and remove restoring the
server's total on failure (it restored the loaded-row count) and only
counting down a row it actually had.
- docs: the poster bug and its fix in m7-clips-and-subvideos.md; the
chapter-clip limitation in url-import.md now that the follow-up exists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPmZjuM4q96DFRfU9cHL33
Text → image, image → image from up to ten bases, and image → video with an optional end frame, as specified in docs/m8-ai-generation.md. Results are ordinary assets with source "ai_generated", carry their provenance, and are costed from fal's billing header. - One Alembic revision: seven ai_* columns on asset, usageevent.external_ref, the generationmodel catalogue (unique endpoint id) and nine seed rows whose field names and allowed values were checked against fal's generated endpoint types. - ADMIN_USERS and an AdminUser dependency; /api/me resolves is_admin. - Catalogue CRUD at /api/generate/models (admin writes, PATCH by fields present so null clears); the fal key at /api/settings/generation. - A raw-httpx fal queue client (submit/status/result/cancel) with readable errors; bases normalised to data URIs; a streaming, size-capped, SSRF-checked download that re-checks every redirect hop. - A second JobQueue for generation over the same table, with a kind filter in recover_pending and sweep_stale, routing and cancel by kind. - The job persists fal's request ids before polling, resumes on restart, records created assets per output and usage once, and cancels at fal on a user cancel or the deadline. - Exact cost from x-fal-billable-units × fal's pricing API, falling back to the catalogue list price and to units derived from the files; a mixed usage total now reports itself as an estimate. - POST /api/generate and /regenerate; AssetRead.generation; generated video is not auto-transcribed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPmZjuM4q96DFRfU9cHL33
One GenerateForm behind three entry points, built to the API section of docs/m8-ai-generation.md: - api/generate.ts: the catalogue and generation types beside generateApi (models, createModel, updateModel, deleteModel, start, regenerate). Option values stay string | number end to end, because fal's endpoints disagree on the type of a duration and the server checks verbatim. - api/settings.ts gains generationSettingsApi for the fal key; Asset gains `generation`, User `is_admin`, ActivityJob `result_asset_ids`. - stores/generation.ts: the active catalogue, loaded once, staleness-guarded, reset on sign-out. - GenerateForm: kind follows the bases (none → text → image; one or two → image → image or image → video with the second as the end frame), models filtered to what can take these bases, every option control shown only when the row declares it, fal_key_missing as a Settings signpost. - Entry points: a third section in the add panel; "Generate from these" in the selection bar when every pick is an image with a file (bases in pick order); a Generate tab leading with provenance, Regenerate, Regenerate with the same seed, and Edit and generate (the form, pre-filled). - LibraryView adds every output of a finished generation to the grid. - Settings: the fal key, and the catalogue — read-only, or full CRUD for admins with lists that round-trip their values' types. - "AI cost (est.)" and the usage panel's "not a bill" now follow `estimated`, since fal's billing header makes an exact cost possible. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPmZjuM4q96DFRfU9cHL33
The backend and frontend were built against docs/m8-ai-generation.md in parallel; reading one against the other found three places they disagreed. - "Regenerate with the same seed" needed fal's echoed seed, but the server's reuse_seed also falls back to the seed the request asked for, which is all there is for an endpoint like nano-banana-2 that echoes none. The button now shows whenever there is a seed the server would send. - AssetGenerationRead.generated_at is nullable (the ai_* columns are read leniently); the frontend typed it as a string and would have dated a hand-edited row 1 January 1970. - The catalogue form claims to mirror the server's invalid_model_entry rules and missed one: max_images above 1 needs image_field_is_list. Comments and test data that said Hailuo takes a numeric duration now say Wan: the seeded Hailuo row, checked against fal's types, takes "6" and "10". The spec now records what the build settled or did differently: the stricter catalogue validation, PATCH nulls, regenerate's full error order and seed handling, num_images, the usage `estimated` change, and the limitations both builders left open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPmZjuM4q96DFRfU9cHL33
Generation, once fal has billed: - record the UsageEvent even when saving the outputs fails or is cancelled part-way; an errored job is never run again to record it later - retry a download that fails with a 429, a 5xx or a dropped connection - fail at once on a result-fetch 5xx that carries fal's request id instead of polling it until the deadline and reporting "did not finish" - a restart inside ingest_file no longer leaves a provenance-less copy beside the finished asset: the index being saved is persisted first, and a resumed run discards the half-saved one by its bytes - cancelling a queued generation recovered after a restart cancels it at fal Security: - ADMIN_USERS matches usernames exactly; Notes allows usernames differing only in case and lets anyone rename, so case-folding made a renamed user an admin - the download asks for and accepts only an uncompressed body (httpx inflates brotli with no limit) and has a whole-file deadline - test the guard that keeps the fal key on fal's own queue host The migration's downgrade failed on any populated library: SQLite ignores PRAGMA foreign_keys inside a transaction, and the usageevent rebuild opens one. The pragmas now run in an autocommit block. The migration tests could not see this — their engine never enforced foreign keys and reused a connection an earlier revision left with them off — so they now do both as production does. Frontend: - a refused delete says why on the page that asked; the store's delete waits for the server instead of unmounting the panel first - the library grid adds the outputs a generation saved before it failed - "Generate audio" sends what the box shows Docs: plan-of-attack brought up to date for clip deletion and M8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPmZjuM4q96DFRfU9cHL33
davior
marked this pull request as ready for review
October 9, 2026 14:38
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Clip deletion
Clips couldn't be deleted from anywhere you'd look for them. Fixed:
DELETE /api/assets/{id}?with_clips=true, which removes the clips and the parent in one transaction. A client that leaves out the flag still gets the 409.thumb_key, so deleting a clip deleted the parent's poster image, and the poster of every other clip cut from it. Files are now removed only when no surviving row still uses them.extraction_in_progresswhile an extraction is running, and the reason now shows on the page that asked. Previously the optimistic store delete unmounted the panel before the refusal arrived, so the message was lost.M8 — AI generation via fal.ai
Spec and as-built record:
docs/m8-ai-generation.md.ADMIN_USERS, matched exactly. A Notes user id is recommended.x-fal-billable-units× the unit price from fal's pricing API, falling back to an estimate that is labelled as one.Review
An adversarial review (four lenses, each finding independently verified) confirmed 12 findings. A hand-run live smoke test then found a 13th. All are fixed in
59f760c, each with a test that fails without the fix:ADMIN_USERSlet a renamed Notes user become admin;plan-of-attack.mdwas stale;PRAGMA foreign_keysinside a transaction. The migration tests couldn't see this: their engine never enforced foreign keys, and they reused a connection an earlier revision had left with them off. They now enforce foreign keys and use a fresh connection, as production does.Live run on this sandbox:
Not yet verified against fal
This sandbox cannot reach fal.ai. The first real generation will answer three questions:
The first two fall back to an estimated cost if not.
Checks (local, at
59f760c)pytest -q: 1204 passed🤖 Generated with Claude Code
https://claude.ai/code/session_01TPmZjuM4q96DFRfU9cHL33