Skip to content

fix: sync _version.py to 0.15.4, and guard the pair - #39

Merged
spencer-zqian merged 1 commit into
mainfrom
fix/version-sync-0.15.4
Sep 4, 2026
Merged

spencer-zqian merged 1 commit into
mainfrom
fix/version-sync-0.15.4

Conversation

@spencer-zqian

Copy link
Copy Markdown
Contributor

#38 bumped pyproject.toml to 0.15.4 but left src/sonilo/_version.py on 0.15.3 — my miss, caught while verifying main after the merge.

__version__ ships as the x-sonilo-client-version header, so releasing 0.15.4 as-is would have had every client on that version identify itself as 0.15.3 to the API.

Why nothing caught it: the existing version tests compare the header to __version__, which stays self-consistent no matter how stale it is, and nothing compared either to pyproject.toml. The release commits have always bumped both by hand (a41a37c, 2c78bd8) — which works right up until someone bumps only one.

This syncs the file and adds the missing comparison, so the next one-sided bump fails in CI rather than on PyPI.

sonilo-cli is unaffected: its own version and its sonilo>=0.15.0,<0.16 pin both still hold.

290 passed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SFP9xPKwk2S1Qjmk5krx6h

@lightsage-app

lightsage-app Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Lightsage docs evals

Waiting for the staging docs URL before running evals.

Lightsage will start the selected PR evals automatically when GitHub reports a successful docs deployment for this PR. This usually happens within 15 minutes.

Commit: 6d733ae
Status: waiting for staging docs URL

`#38` bumped `pyproject.toml` to 0.15.4 but left `src/sonilo/_version.py` on
0.15.3. `__version__` ships as the `x-sonilo-client-version` header, so a 0.15.4
release would have had every client on it identify itself as 0.15.3.

Nothing caught it: the existing version tests compare the header to
`__version__`, which stays self-consistent however stale it is, and no test
compared either to pyproject. The release commits have always bumped both by
hand (a41a37c, 2c78bd8), which works right up until someone bumps only one.

Adds the missing comparison so the next one-sided bump fails in CI instead of
on PyPI. sonilo-cli is unaffected — its own version and its
`sonilo>=0.15.0,<0.16` pin both still hold.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFP9xPKwk2S1Qjmk5krx6h
@spencer-zqian
spencer-zqian force-pushed the fix/version-sync-0.15.4 branch from dc4cdb7 to 6d733ae Compare September 4, 2026 23:28
@spencer-zqian
spencer-zqian merged commit 829ad11 into main Sep 4, 2026
2 checks passed
@spencer-zqian
spencer-zqian deleted the fix/version-sync-0.15.4 branch September 4, 2026 23:30
steven-panxd added a commit that referenced this pull request Sep 5, 2026
Both packages gained the dubbing `lipsync` parameter in #37, so both get a
minor bump.

_version.py moves with pyproject.toml, which the guard added in #39 now
enforces: `__version__` ships as the x-sonilo-client-version header, so a
release that bumped only pyproject would have every client on 0.16.0
identifying itself as 0.15.4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Y2tCTSPesvLbw3hcnQ5ct
spencer-zqian added a commit that referenced this pull request Sep 14, 2026
* feat: dubbing subtitles, export_srt and lipsync on the client

`POST /v1/dubbing` takes a target-language script per language as the
repeatable multipart field `subtitles[<language>]` (an https URL or an
uploaded .srt/.vtt), and `export_srt` returns a re-timed .srt per language
once the dub is delivered. `lipsync` has existed on the endpoint all along
and never reached this SDK.

`build_dubbing_parts` now returns a ready-made `close_after` as its third
element instead of an `opened` bool, the shape `build_ducking_parts` already
uses. One request can open the video plus one script per target language, and
a bool cannot say how many handles that is — nor which of them this builder
opened. Every handle is closed on the way out if a later open fails, and the
caller closes the rest through the `close_after` seam `_post_json` already
exposes, so the routine 422 from a rejected script set does not leak them.

Client-side checks are limited to the two guaranteed 422s: a script path
whose suffix is not .srt/.vtt, and `export_srt` with no scripts. The language
set is not checked against `languages` — the server owns that rule, its
message names the offending code, and a local copy would break a caller who
relies on the server default without passing `languages` at all.

`DubbingResult` gains `subtitles` (with save_subtitle / save_all_subtitles
mirroring save / save_all), `subtitle_preflight` and `subtitle_export`. The
two report maps are coerced but their values are left as they arrived: the
pipeline stores numbers as strings, so `cue_count` may be "5" and
`alignment_loss` "0.63", and guessing at a type here would only mislead. A
blocked export does not fail the task, so `subtitles` can lack a language
whose video was still delivered — save_all_subtitles iterates `subtitles`,
not `outputs`.

`lipsync` defaults ON server-side and every dubbing task ran that way before
the field existed, so it is sent only when the caller passes it: absent has
to keep meaning true.

Claude-Session: https://claude.ai/code/session_01WWHfnbRaXRdAH2sjm8mApk

* feat(cli): --subtitle and --export-srt on `sonilo dubbing`

`--subtitle <language>=<path-or-url>`, repeated once per target language,
carries the script to speak in it; `--export-srt` asks for a re-timed .srt
back. The `=` is split once only so a path or URL containing one survives.
Neither the codes nor the language set are checked here — the server owns
both rules and its message names what it refused; the SDK still refuses a
suffix that is not .srt/.vtt, which is a guaranteed 422.

Each .srt is written beside its video (clip.es.mp4 -> clip.es.srt) and every
requested language gets a status line, including a language whose export was
blocked: that still delivers the video, just without a script. The alignment
loss is formatted through float() because the pipeline stores numbers as
strings — "0.63" and 0.63 both have to print — and is omitted when it is
neither.

Claude-Session: https://claude.ai/code/session_01WWHfnbRaXRdAH2sjm8mApk

* chore: sonilo 0.16.0, sonilo-cli 0.15.0, and widen the CLI's pin

New parameters on an existing endpoint, so a minor bump on both packages —
and `__version__` is bumped alongside `pyproject.toml` in each, since it ships
as the `x-sonilo-client-version` header and CI now fails on a one-sided bump
(#39).

The CLI's `sonilo>=0.15.0,<0.16` pin has to widen to `>=0.16.0,<0.17` in this
same commit: it calls `dubbing.generate(subtitles=..., export_srt=...)`, which
only exists from 0.16.0, and left alone the pin would make the editable
install of both packages unresolvable.

Claude-Session: https://claude.ai/code/session_01WWHfnbRaXRdAH2sjm8mApk

* docs: subtitles, export_srt and lipsync in both READMEs

The dubbing section of each README gains the new parameters: the target-
language scripts and what the server checks about them, the re-timed .srt
`export_srt` returns, the three new maps on `DubbingResult` (including that a
blocked export still delivers the video), and `lipsync`, which has been on the
endpoint all along and was documented nowhere.

Also f-strings the two `subtitles[<language>]` field names, matching the rest
of the module.

Claude-Session: https://claude.ai/code/session_01WWHfnbRaXRdAH2sjm8mApk

* feat(cli): --ducking and --no-lipsync on `sonilo dubbing`

The JavaScript CLI's dubbing command has had --ducking since the ducking
round and has just gained --no-lipsync; the two CLIs mirror each other, so
this side was the one out of step.

--ducking goes through the same `_ducking()` resolution the sound commands
use, so --no-ducking keeps parsing as the explicit-default no-op it is
elsewhere and passing both still exits 1. Only --no-lipsync exists, with no
positive counterpart: lip sync is default-ON server-side, so the sole useful
direction is turning it off, and an absent flag sends nothing rather than
restating a default that could move.

Claude-Session: https://claude.ai/code/session_01WWHfnbRaXRdAH2sjm8mApk

* fix: return the 202's subtitle_preflight from dubbing submit()

`submit()` parsed the dubbing ack with the shared `parse_sfx_task`, which
builds an `SfxTask` of `task_id` and `status` — so the whole per-language
`subtitle_preflight` map was read off the wire and dropped. That map is the
free, pre-charge check: a language whose status is `review_required` had lines
changed in the script the caller submitted, and with `submit()` there was no
way to see it before the billed dub finished.

Adds `DubbingTask`, an additive subclass of `SfxTask` carrying the map, and
`parse_dubbing_task` to fill it — coerced through the same `_report_map_from`
the finished task's copy uses. `SfxTask` itself is untouched: it acks every
other async endpoint, and the preflight is dubbing's alone. Mirrors the
JavaScript SDK's `DubbingTask extends SfxTask`.

Also drops the unreachable `or "subtitles.srt"` fallback in
`_subtitle_filename` — an empty name has an empty suffix, so the extension
check raises before it — and fixes the README example, which read
`report["status"]` two lines under its own advice to read these defensively.

Claude-Session: https://claude.ai/code/session_01WWHfnbRaXRdAH2sjm8mApk

* fix(cli): refuse --output *.srt with --export-srt, and tighten three tests

`sonilo dubbing --export-srt --output clip.srt` wrote the dubbed video to
clip.es.srt and then wrote the subtitle over it, printing a "Wrote" line for
each: both files come from the one template and the subtitle goes second, so
the mp4 was silently truncated away. It is refused at parse time now, before
anything runs, with a message naming the cause and the fix — matched
case-insensitively, and the same refusal the JavaScript CLI makes.

The three tests that asserted only an exit code plus a flag name were passing
against `main` for the wrong reason: argparse's own "unrecognized arguments"
error quotes the flag, so they would not have caught a regression that deleted
the flags outright. Each now matches the message this CLI actually prints.

Claude-Session: https://claude.ai/code/session_01WWHfnbRaXRdAH2sjm8mApk
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant