feat: collapse CLI LLM config into the profile store and retire the legacy GUI service page (tickets 14-15) - #1190
Open
JsonBorn98 wants to merge 95 commits into
Conversation
Implement persistent Qwen workers, batch processing, VAD-aware chunking, MiMo transcript/alignment pipelining, anomaly checks, loudnorm wiring, and benchmark tooling. Add 20 minute Chinese and English ASR fixtures, document recorded Qwen/MiMo acceptance results, archive stale dev docs, and add a project ASR skill for future maintenance. Validation: pytest -m 'not integration' -q; pytest tests/test_asr/ -q -k 'not JianYingASR'; pytest tests/test_utils/test_asr_benchmark.py -q; ruff check .; pyright; bun run docs:build; Qwen and MiMo 20 minute benchmark acceptance passed.
Internalize the ass-subtitle-optimizer skill as opt-in, in-workflow subtitle cleanup and quality audit, shared by CLI and GUI. New videocaptioner/core/postprocess/ package: - placeholders (F1): drop [Music]/[音乐]/-style placeholder lines/sides - normalize (F2): Chinese quote normalization + weak trailing punctuation - timing (F3): close small gaps (extend/midpoint) to reduce flicker - audit (F4/F6): reading-speed CPS + long-duration anomalies (read-only) - compress (F5): LLM compression of over-fast Chinese lines - report (F7): Markdown QA report Wired into cli/commands/subtitle.py and ui/thread/subtitle_thread.py at load / post-optimize+translate (replacing remove_punctuation) / pre-save. All options default off and default output stays byte-identical; the enhanced normalize path only activates with --normalize-quotes. Config lands across CLI (config.py, main.py flags + process passthrough) and GUI (qconfig, task_factory, SubtitleConfig, settings page), with PostprocessConfig as the single source of defaults. Add tests/test_optimize/test_postprocess_* and include tests/test_optimize in CI; update CLI/config/workflow docs; archive the design plan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add optional shadow / margin_l / margin_r / wrap_style to SubtitleStyle and shadow / margin_bottom to SecondaryStyle, emitted by to_ass_string and round-tripped through JSON and legacy .txt parsing. Write ScaledBorderAndShadow: yes into ASS headers and scale Shadow/MarginL/R alongside the existing font/spacing/outline/MarginV scaling, so one 720p design grid renders consistently across resolutions. Existing presets stay byte-identical when the new fields are absent (shadow 0, margins 10/10). Add the bilingual "house" preset plus 1080p/4K presets, expose the new fields in `style list`, and document them in a new zh subtitle-style guide. Also carries the in-progress bilingual-SRT layout handling in ASRData.from_srt/from_subtitle_file that was already staged in the tree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Carry the in-progress subtitle view / SRT layout work that was already uncommitted in the tree: keep translate-on-top ordering when reloading bilingual SRT and in the subtitle interface, with matching test updates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the gap-tiered "tail dwell" model with a single monotonic clamped compensation curve: gaps above the max close gap get a lead-out that starts at min_compensation, ramps to max_compensation at max_compensation_gap, then saturates — both the added time and the remaining blank never decrease as the gap grows (slope <= 1). Removes the scene-cut cutoff and the tier knobs; the minimum blank is now emergent (max_gap - min_compensation). - config: tail_compensation + min/max_compensation(_gap)_ms with 4 constraints (ordering + slope <= 1); reuses max_gap_ms as the close/compensate boundary - timing: compensation_for_gap() + apply_tail_compensation(), runs after speed optimization on the final timeline, skips protected/word-level cues - profiles: migrate legacy tail_dwell_* fields forward, with a defaults fallback so an older persisted profile can never fail to load - fix: PostprocessProfileStore.reload() so settings-page edits reach the standalone postprocess page and the home workflow without an app restart (previously each held its own stale in-memory store) - ui: SliderSpinBoxSettingCard (slider + precise number entry) with live inter-knob clamping; convert all remaining slider-only timing settings - docs: CONTEXT.md glossary, ADR-0005, subtitle-postprocessing (zh/en) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bin dirs were prepended to PATH in list order, leaving the bundled dir first and shadowing a user's replacement in BIN_PATH -- the opposite of the intended 'user-downloaded binaries take precedence'. Prepend in reverse so BIN_PATH wins over the bundled fallback.
Design artifacts from the requirements grilling: the Handbrake-style encode-control task plan (16 sections incl. review revisions), ADR 0006-0008 (bundled/immutable ffmpeg + user replace; centralized command builder + two-way preview; curated encoder catalog + capability probing), and a new Video Synthesis Encoding bounded context (CONTEXT-MAP + glossary).
Several OpenAI-compatible gateways accept response_format.json_schema and then ignore it, answering with unconstrained JSON and no HTTP error that would reveal the downgrade. Route each dialect to the strongest schema contract it actually implements: json_schema for openai/qwen/gemini, a forced function call for deepseek/kimi/glm/anthropic, and bare JSON mode for generic, which stays the portable default for unidentified endpoints. A provider that rejects the forced tool request falls back to one JSON-mode retry, so models without function calling keep working. Input overflow is excluded from that fallback so it still surfaces as CONTEXT_LIMIT. A provider that ignores the forced tool choice degrades to message content, and proxies that send already-decoded tool arguments are accepted. Reuse one identifier for the structured contract across every transport, including the tool_use block Anthropic matches when reading the reply back. Verified against a live gateway: glm-5.3 now passes the structured capability probe it previously failed.
Add core/llm/utility.py with resolve_utility_profile, the single GUI/CLI entry point for utility-role LLM resolution (ADR-0014): an explicit utility binding wins, otherwise the profile derives from the main translation profile, and both paths strip the translation-only tuning fields (openai_endpoint, request_options, max_output_tokens) so utility request shape never silently inherits translation tuning. A lost binding or a missing main profile raises UtilityProfileError pointing at the utility model card instead of silently falling back. validate_utility_profile provides the local-only startup preflight (request-option validation plus tuning-default checks, no real request).
Gateway completions now cache to a fresh llm_gateway disk directory with an explicit allowlist key: full connection fields (api_key included, closing the old memoize hole), request-shaping fields, and a key_version escape hatch. Only successful completions are cached; hits replay text with all-None usage so billing reports stay truthful, and log a cache_hit request-log entry without usage or attempt. Probes bypass the cache in both directions so connection tests always exercise the live path. All cache failures fail open as misses.
…skeleton The adapter lets profile.max_output_tokens override the request-level cap in the request body, so two profiles differing only in that field would cross-hit the cache. Add it to the key allowlist with a regression test. Also extract the shared log entry skeleton used by both gateway log writers, and tidy test idioms (monkeypatch for KEY_VERSION, shared failing adapter, no private cache access).
LLMRequest gains timeout: Optional[float] (None = adapter constructor default). All three transports honor it: OpenAI-compatible passes it as a per-request SDK kwarg on both chat completions and responses endpoints; Anthropic and Gemini use it in place of the constructor default for the request-driven session.post calls (Gemini cache management keeps the constructor default). The OpenAI-compatible client's constructor default tightens from the SDK default Timeout(connect=5.0, read=600, ...) to 120s, aligning it with the native transports' existing 120s. Existing translation paths that never passed a timeout now get a 120s deadline backed by the gateway's four-attempt retry, per the gateway unification spec.
Code-review follow-up to ac0ff25: hoist the 120s constructor default shared by all three transports into one DEFAULT_TIMEOUT_SECONDS constant (the literal was copy-pasted per constructor and would drift silently), and merge the two identical timeout-validation raises in LLMRequest into a single condition.
Migrate the split and subtitle-optimize consumers off the legacy client and OPENAI_* env relay onto the model-profile system (ADR-0014): - SubtitleSplitter / split_by_llm / SubtitleOptimizer accept profile + gateway (lazily built), requests go through LLMGateway with stage/role labels (llm_split / llm_optimize, role=utility) - split keeps its 30s request timeout; optimize now omits timeout and falls to the adapter's 120s default with gateway retry - drop SubtitleConfig's six legacy connection scalars in favor of utility_llm_profile; add cfg utility_llm_profile_id binding key - task factory resolves the utility profile via resolve_utility_profile instead of expanding the old service-slot scalars - startup preflight (validate_utility_profile, local-only) sits at the old need_legacy_llm decision point; error copy points at the utility model card - profileless LLM translation now fails fast with guidance instead of silently reading env vars; both env-relay writes are gone - CLI bridges the legacy [llm] scalars into a profile to feed the new signatures (TODO(ticket-14) marks the bridge for removal) - tests: fake-gateway injection replaces the env-stub seam; conftest patches the gateway constructor on the migrated paths
Migrate the duration rewriter off the bare OpenAI client onto the
utility-role model profile seam: rewrite_segments_if_needed now takes
profile + gateway=None (lazy construct, owned instances closed), sends
the {items:[{index,text}]} response shape as a formal JSON Schema on
LLMRequest.response_schema, and logs with the llm_dub_rewrite/utility
stage-role tags. DubbingConfig drops its llm_api_key/llm_api_base/
llm_model triple for a single optional llm_profile; the CLI dub
command bridges the legacy [llm] table into that profile (TODO
ticket-14), leaving a disabled rewrite a silent no-op as before.
…N Schema - PostprocessConfig: retire llm_model, add runtime-injected utility_llm_profile shared by both consumers, excluded from persisted payloads - semantic.py: formalize rewrite/review window shapes as JSON Schema, route the default rewriter/reviewer through LLMGateway (60s timeout, utility role tags), collapse the legacy SDK-object response parsing to plain text - compress.py: route the agent loop through LLMGateway (no timeout, 120s adapter default), drop the dead llm_ctx parameter - GUI: resolve the utility profile via resolve_utility_profile (independent binding wins, else derive from the main profile); add utility_llm_profile_id - CLI postprocess: bridge legacy [llm] scalars into a profile (TODO ticket-14) - tests: fake-gateway consumer seams, schema tiering equivalence on generic dialect, task-factory profile resolution
…chema # Conflicts: # videocaptioner/ui/common/config.py
Old postprocess profile archives carrying the retired llm_model field crashed _config_from_dict with 'Unknown config field'; add it to the legacy drop set (no successor field, the model now comes from the utility profile) with a regression test. The dub [llm] bridge likewise required only api_key+model, letting a blank api_base silently fall back to api.openai.com; require all three so a running rewrite fails fast with guidance instead of hitting the wrong endpoint.
Add borrow_utility_gateway() to core.llm.utility as the shared seam for function-shaped utility consumers: an injected gateway is used as-is, a missing one is built lazily and closed on exit. Route the dub rewriter, compress and semantic repair through it, fixing the compress/semantic self-built gateways that were never closed; constructor-held consumers (split/optimize, translation) keep their long-lived ownership. Also hoist the twice-duplicated subtitle_thread error texts to module constants in utility.py, converge the thrice-duplicated dict-to-LLMMessage coercion into llm_messages_from_dicts(), and lift the subtitle.py build_legacy_llm_profile import to module top to match its sibling bridges.
The TOML [llm] section shrinks to profile_id / review_profile_id / utility_profile_id referencing the same model profile store the GUI edits (ADR-0015). Removed wholesale: - translate.llm.main/review inline tables and their alias normalizer - the [llm] scalar keys api_key/api_base/model/work_context_tokens/ max_concurrency - the entire [llm] -> main -> review inheritance chain in config.py (~250 lines: build_legacy_llm_profile, build_translation_llm_profile, build_translation_llm_profiles, translation_llm_role_allows_empty_api_key) - the OPENAI_* env mappings and the TRANSLATE_LLM_* env family Only VIDEOCAPTIONER_LLM_PROFILE_ID and its _REVIEW/_UTILITY variants select profiles; OPENAI_API_KEY is deliberately not recognized so a key exported for another tool is never silently adopted. VIDEOCAPTIONER_LLM_ API_KEY survives as a narrow credential-only override that swaps the resolved profile's api_key and nothing else; the request logger emits key_source only when set, so existing entries keep their shape. Leftover pre-profile keys in any layer now print a one-time stderr warning with migration guidance and the available profile ids. The data itself is tolerated, not migrated.
--llm-profile / --review-profile / --utility-profile now hang off all LLM-consuming subcommands (subtitle, process, dub, postprocess), resolving to llm.profile_id / llm.review_profile_id / utility_profile_id so the existing cli > env > TOML layer merge yields the required priority for free. process passes the three flags through to its subtitle, postprocess, and dub sub-stages. Naming tradeoff: postprocess and process already use --profile for the postprocessing template id (dest speed_profile), and argparse rejects one option string bound to two destinations, so the main-translation flag is --llm-profile rather than the spec's short --profile. The utility/review flags keep the spec names since they do not collide, and --profile keeps meaning the postprocess template where it exists. Also adds the top-level 'videocaptioner profile' group: list, show <id> (api_key masked, raw store file path hinted), and set-default <id> (validates the id exists, lists available ids on failure). It stays out of the config subcommand tree because the store and config.toml are separate files.
The ticket-11/12/13 bridges from [llm] scalars are gone. subtitle resolves main/review translation profiles and the utility profile (split/optimize) through the store; postprocess resolves its utility profile only when the resolved config actually issues utility LLM requests (needs_utility_llm), matching the GUI task factory gate; dub resolves the rewrite profile only when rewrite_too_long is enabled. Utility resolution goes through resolve_cli_utility_profile, which wraps the shared resolver so an agent never sees the GUI card wording (GUI: '翻译设置页·工具模型卡') on the CLI surface — the failure is restated as which llm.* key to set, the store file path, the profile object shape, and the available ids. Also removes the OPENAI_* environment relays in subtitle/postprocess and the zero-reader whisper-api relay in transcribe (the WhisperAPI consumer takes base_url/api_key as constructor args and never reads the environment).
validate_llm now checks the store resolves a utility profile (empty utility binding means derive, which is valid); api_key emptiness is no longer checked because the store holds the field value and keyless local services legitimately store "". validate_translation_llm checks main for both LLM modes and, for enhanced_llm only, a dedicated review profile — a blank review id fails fast instead of silently falling back to main, matching the GUI's missing_translation_roles. doctor's LLM checks become: store non-empty, profile_id set and resolvable, utility binding resolvable when set, review profile set and resolvable when enhanced translation is selected. The fix text points at 'videocaptioner profile list' instead of config set llm.api_key. config init's LLM prompt shrinks to a single profile_id question, the non-interactive --llm-api-key/--llm-api-base/--llm-model flags collapse to --llm-profile, and the onboarding template's [llm] block and comments describe the three profile-id keys.
Replaces the TestTranslationLLMProfiles class (15 cases over the deleted [llm] -> main -> review inheritance chain) with coverage of the profile surface: the three-key resolution against a seeded store, binding vs derivation, translation-tuning stripping on derived utility profiles, fail-fast guidance text (store path, key names, available ids, no GUI card wording), the env credential override, and the legacy-key warning across all three layers. test_translation_modes.py's three legacy-config tests now seed a temp store and use the three keys. test_postprocess_command.py seeds a main profile because the default balanced template enables semantic repair, which resolves its model from the store — the same requirement the GUI task factory has. Parser tests cover the new flags (including that --profile still means the postprocess template and coexists with --llm-profile), flag > env > TOML priority, and the profile list/show/set-default behavior including key masking and unknown-id guidance.
Address code-review findings on the CLI profile unification branch. Spec axis: - resolve_cli_utility_profile now passes through apply_env_api_key_override like the main and review roles (VIDEOCAPTIONER_LLM_API_KEY covers all three) - the override flips a request_logger marker so gateway log entries record key_source=env_override, giving the read-side branch its producer - doctor's enhanced_llm + missing review_profile_id check is now an error with binding guidance, matching the resolver's runtime fail-fast severity Standards axis: - profile_cmd run takes the run(args, config) -> int command shape - mask_credential shared helper replaces four mask copies - profile_cmd reuses cli.config's _profile_store - ProfileSelectionCard tr() wraps literals so Qt tools can extract them - doctor's repeated profile-list fix prefix collapsed into a helper - _warn_legacy_llm_keys drops the dead env_config/is_env flag params - _add_llm_profile_flags gains hidden= replacing _add_hidden_llm_options's copy - subtitle.py's three profile-model fallbacks collapsed into _profile_model
JsonBorn98
marked this pull request as ready for review
August 30, 2026 13:39
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.
实现 spec「LLM 配置面统一进模型配置方案体系」的 ticket 14/15(并行实现,见
.scratch/llm-profile-unification/,机器本地不入库):[llm]终局只剩profile_id/review_profile_id/utility_profile_id三键;硬切删除 inline 表、[llm]标量五键、TRANSLATE_LLM_*全套环境变量、OPENAI_*→llm.*映射与 config.py 整条继承链(约 250 行);凭证唯一来源方案库,仅留VIDEOCAPTIONER_LLM_API_KEY窄覆盖(三角色一致,请求日志记key_source=env_override);新增顶层profile命令组(list / show / set-default);三面旗子挂 subtitle/process/dub/postprocess;旧键残留一次性 stderr 警告;删 11-13 临时桥--profile已被后处理模板 id 占用,主翻译旗为--llm-profile(--review-profile/--utility-profile原名)utility_llm_profile_id绑定键,默认跟随主翻译模型);方案编辑器模型框升级可编辑下拉 + 「获取模型列表」按钮;_deleteProfile清理工具绑定依赖:tickets 11/12/13 已落地(
3028505)。Review fixes
双轴 code-review(spec 符合 + 编码规范)后的修复(
c263f2c):工具角色 env 凭证覆盖缺口、key_source=env_override生产侧落地、doctor 对 enhanced 缺校对方案标 error、命令形态对齐run(args, config)、掩码 helper 收敛四处拷贝、tr() 提取修复等 11 项。测试
uv run pytest -m "not integration"→ 1238 passed, 5 skipped(基线 1207,净增 31 用例)uv run ruff check .→ All checks passeduv run pyright→ 0 errors(20 warnings 均为未触碰文件的预存告警)