Skip to content

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
WEIFENG2333:masterfrom
JsonBorn98:feature/llm-profile-unification-14-15
Open

feat: collapse CLI LLM config into the profile store and retire the legacy GUI service page (tickets 14-15)#1190
JsonBorn98 wants to merge 95 commits into
WEIFENG2333:masterfrom
JsonBorn98:feature/llm-profile-unification-14-15

Conversation

@JsonBorn98

@JsonBorn98 JsonBorn98 commented Aug 30, 2026

Copy link
Copy Markdown

实现 spec「LLM 配置面统一进模型配置方案体系」的 ticket 14/15(并行实现,见 .scratch/llm-profile-unification/,机器本地不入库):

  • 14 CLI 配置面坍缩进方案库:TOML [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 临时桥
    • 旗名取舍:postprocess/process 的 --profile 已被后处理模板 id 占用,主翻译旗为 --llm-profile--review-profile / --utility-profile 原名)
  • 15 GUI 旧服务页移除与工具模型卡:旧「通用 LLM 工具配置」服务页四层全删(22 张凭证卡、cfg 22 键、检查连接回调线程);新增翻译设置页顶层共享「工具模型」卡(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 passed
  • uv run pyright → 0 errors(20 warnings 均为未触碰文件的预存告警)

JsonBorn98 and others added 30 commits June 14, 2026 02:18
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
JsonBorn98 marked this pull request as ready for review August 30, 2026 13:39
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