mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-22 07:10:28 +02:00
extract conversion_engine: shared TTS segment iteration loop for WebUI and PyQt
- domain/conversion_engine.py: run_tts_segment_loop() with CancelChecker, SegmentStats, SegmentInfo protocols; on_segment callback for per-segment subtitle processing; process_and_write_subtitles() helper - conversion_runner.py: emit_text() delegates TTS iteration to engine - pyqt/conversion.py: inner tts_segments loop replaced with engine call, on_segment handles dual merged+chapter subtitle writers - routes/utils/settings.py: re-exports load_settings, coerce_int/float, llm_ready, settings_defaults from domain for backward compat
This commit is contained in:
@@ -0,0 +1,190 @@
|
|||||||
|
"""Shared TTS iteration loop used by both WebUI and PyQt conversion runners.
|
||||||
|
|
||||||
|
The core pattern is identical across both UIs:
|
||||||
|
|
||||||
|
for seg in tts_segments(text, backend, voice, speed, split_pattern, current_time):
|
||||||
|
check_cancel()
|
||||||
|
update_progress(seg)
|
||||||
|
write_audio(seg, sink)
|
||||||
|
accumulate_subtitles(seg)
|
||||||
|
|
||||||
|
After the loop, the caller processes accumulated subtitle tokens.
|
||||||
|
|
||||||
|
This module provides ``run_tts_segment_loop`` which encapsulates that
|
||||||
|
iteration, while leaving UI-specific concerns (progress display, subtitle
|
||||||
|
processing strategy) to the caller via callbacks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Callable, List, Optional, Protocol
|
||||||
|
|
||||||
|
from abogen.domain.audio_sink import AudioSink
|
||||||
|
from abogen.domain.conversion_pipeline import tts_segments
|
||||||
|
from abogen.domain.progress import calc_etr_str
|
||||||
|
from abogen.domain.subtitle_generation import process_subtitle_tokens
|
||||||
|
|
||||||
|
|
||||||
|
class CancelChecker(Protocol):
|
||||||
|
"""Returns True if conversion has been cancelled."""
|
||||||
|
def __call__(self) -> bool: ...
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SegmentStats:
|
||||||
|
"""Running statistics updated per TTS segment."""
|
||||||
|
processed_chars: int = 0
|
||||||
|
current_time: float = 0.0
|
||||||
|
etr_start_time: float = field(default_factory=time.time)
|
||||||
|
total_characters: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SegmentInfo:
|
||||||
|
"""Read-only info about a TTS segment, passed to on_segment callback."""
|
||||||
|
graphemes: str
|
||||||
|
audio: Any
|
||||||
|
tokens: list
|
||||||
|
duration: float
|
||||||
|
chunk_start: float
|
||||||
|
|
||||||
|
|
||||||
|
def run_tts_segment_loop(
|
||||||
|
*,
|
||||||
|
text: str,
|
||||||
|
backend: Any,
|
||||||
|
voice: Any,
|
||||||
|
speed: float,
|
||||||
|
split_pattern: str,
|
||||||
|
stats: SegmentStats,
|
||||||
|
check_cancel: CancelChecker,
|
||||||
|
on_progress: Callable[[int, str], None],
|
||||||
|
chapter_sink: Optional[AudioSink] = None,
|
||||||
|
audio_sink: Optional[AudioSink] = None,
|
||||||
|
preview_callback: Optional[Callable[[str], None]] = None,
|
||||||
|
on_segment: Optional[Callable[[SegmentInfo], None]] = None,
|
||||||
|
subtitle_mode: str = "Disabled",
|
||||||
|
max_subtitle_words: int = 5,
|
||||||
|
lang_code: str = "a",
|
||||||
|
use_spacy_segmentation: bool = False,
|
||||||
|
) -> tuple[int, list]:
|
||||||
|
"""Run the core TTS segment iteration loop.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Normalized text to synthesize.
|
||||||
|
backend: TTS pipeline instance (Kokoro or Supertonic).
|
||||||
|
voice: Voice name/id for the backend.
|
||||||
|
speed: Speech speed multiplier.
|
||||||
|
split_pattern: Regex pattern used by the TTS engine for sentence splitting.
|
||||||
|
stats: Running character/timing stats (mutated in place).
|
||||||
|
check_cancel: Called each segment; if it returns True, iteration stops.
|
||||||
|
on_progress: Called with (percent, etr_str) after each segment.
|
||||||
|
chapter_sink: Optional audio sink for the current chapter.
|
||||||
|
audio_sink: Optional audio sink for the merged output.
|
||||||
|
preview_callback: Called with a short preview string per segment.
|
||||||
|
on_segment: Called with a SegmentInfo for each segment *before*
|
||||||
|
audio is written. Useful for callers that need per-segment
|
||||||
|
subtitle processing (e.g. PyQt dual-writer pattern).
|
||||||
|
When provided, the default subtitle accumulation is skipped.
|
||||||
|
subtitle_mode: Subtitle mode string (e.g. "Disabled", "Sentence").
|
||||||
|
max_subtitle_words: Max words per subtitle entry.
|
||||||
|
lang_code: Language code for subtitle processing.
|
||||||
|
use_spacy_segmentation: Whether spaCy sentence boundaries are active.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (segment_count, accumulated_subtitle_tokens).
|
||||||
|
The caller is responsible for processing subtitle tokens via
|
||||||
|
``process_subtitle_tokens`` and writing entries to subtitle writers.
|
||||||
|
"""
|
||||||
|
local_segments = 0
|
||||||
|
accumulated_tokens: list[dict] = []
|
||||||
|
|
||||||
|
for seg in tts_segments(
|
||||||
|
text,
|
||||||
|
backend=backend,
|
||||||
|
voice=voice,
|
||||||
|
speed=speed,
|
||||||
|
split_pattern=split_pattern,
|
||||||
|
current_time=stats.current_time,
|
||||||
|
):
|
||||||
|
if check_cancel():
|
||||||
|
break
|
||||||
|
|
||||||
|
local_segments += 1
|
||||||
|
stats.processed_chars += len(seg.graphemes)
|
||||||
|
|
||||||
|
# Progress
|
||||||
|
if stats.total_characters:
|
||||||
|
percent = min(int(stats.processed_chars / stats.total_characters * 100), 99)
|
||||||
|
else:
|
||||||
|
percent = 0 if stats.processed_chars == 0 else 99
|
||||||
|
|
||||||
|
etr_str = calc_etr_str(
|
||||||
|
time.time() - stats.etr_start_time,
|
||||||
|
stats.processed_chars,
|
||||||
|
stats.total_characters,
|
||||||
|
)
|
||||||
|
on_progress(percent, etr_str)
|
||||||
|
|
||||||
|
# Preview / log
|
||||||
|
if preview_callback:
|
||||||
|
preview_callback(seg.graphemes or "[silence]")
|
||||||
|
|
||||||
|
# Per-segment callback (for callers needing segment-level access)
|
||||||
|
if on_segment:
|
||||||
|
info = SegmentInfo(
|
||||||
|
graphemes=seg.graphemes,
|
||||||
|
audio=seg.audio,
|
||||||
|
tokens=list(seg.tokens) if seg.tokens else [],
|
||||||
|
duration=seg.duration,
|
||||||
|
chunk_start=getattr(seg, "chunk_start", stats.current_time),
|
||||||
|
)
|
||||||
|
on_segment(info)
|
||||||
|
|
||||||
|
# Write audio
|
||||||
|
if chapter_sink:
|
||||||
|
chapter_sink.write(seg.audio)
|
||||||
|
if audio_sink:
|
||||||
|
audio_sink.write(seg.audio)
|
||||||
|
|
||||||
|
# Accumulate subtitle tokens (default path; skipped if on_segment handles it)
|
||||||
|
if not on_segment and subtitle_mode != "Disabled" and seg.tokens:
|
||||||
|
accumulated_tokens.extend(seg.tokens)
|
||||||
|
|
||||||
|
# Update timing
|
||||||
|
if audio_sink:
|
||||||
|
stats.current_time += seg.duration
|
||||||
|
|
||||||
|
return local_segments, accumulated_tokens
|
||||||
|
|
||||||
|
|
||||||
|
def process_and_write_subtitles(
|
||||||
|
accumulated_tokens: list[dict],
|
||||||
|
subtitle_writer: Any,
|
||||||
|
*,
|
||||||
|
subtitle_mode: str,
|
||||||
|
max_subtitle_words: int,
|
||||||
|
lang_code: str,
|
||||||
|
use_spacy_segmentation: bool,
|
||||||
|
fallback_end_time: float,
|
||||||
|
) -> None:
|
||||||
|
"""Process accumulated subtitle tokens and write entries to a subtitle writer.
|
||||||
|
|
||||||
|
This is the standard subtitle post-processing step shared by both UIs.
|
||||||
|
"""
|
||||||
|
if not accumulated_tokens or not subtitle_writer:
|
||||||
|
return
|
||||||
|
new_entries: list[tuple] = []
|
||||||
|
process_subtitle_tokens(
|
||||||
|
accumulated_tokens,
|
||||||
|
new_entries,
|
||||||
|
max_subtitle_words,
|
||||||
|
subtitle_mode,
|
||||||
|
lang_code,
|
||||||
|
use_spacy_segmentation=use_spacy_segmentation,
|
||||||
|
fallback_end_time=fallback_end_time,
|
||||||
|
)
|
||||||
|
for start, end, text in new_entries:
|
||||||
|
subtitle_writer.write_entry(start=start, end=end, text=text)
|
||||||
+43
-44
@@ -36,7 +36,7 @@ from abogen.domain.output_paths import (
|
|||||||
)
|
)
|
||||||
from abogen.domain.audio_helpers import build_ffmpeg_command, to_float32
|
from abogen.domain.audio_helpers import build_ffmpeg_command, to_float32
|
||||||
from abogen.domain.audio_sink import AudioSink, open_audio_sink
|
from abogen.domain.audio_sink import AudioSink, open_audio_sink
|
||||||
from abogen.domain.conversion_pipeline import tts_segments
|
from abogen.domain.conversion_engine import run_tts_segment_loop, SegmentStats, SegmentInfo
|
||||||
from abogen.domain.audio_buffer import (
|
from abogen.domain.audio_buffer import (
|
||||||
create_silence,
|
create_silence,
|
||||||
mix_audio,
|
mix_audio,
|
||||||
@@ -950,41 +950,32 @@ class ConversionThread(QThread):
|
|||||||
(f"Warning: Text normalization failed: {exc}", "orange")
|
(f"Warning: Text normalization failed: {exc}", "orange")
|
||||||
)
|
)
|
||||||
|
|
||||||
for seg in tts_segments(
|
def _qt_check_cancel() -> bool:
|
||||||
text_segment,
|
|
||||||
backend=self.backend,
|
|
||||||
voice=loaded_voice,
|
|
||||||
speed=self.speed,
|
|
||||||
split_pattern=active_split_pattern,
|
|
||||||
current_time=current_time,
|
|
||||||
):
|
|
||||||
if self.cancel_requested:
|
if self.cancel_requested:
|
||||||
sink_stack.close()
|
sink_stack.close()
|
||||||
self.conversion_finished.emit("Cancelled", None)
|
self.conversion_finished.emit("Cancelled", None)
|
||||||
return
|
return True
|
||||||
current_segment += 1
|
return False
|
||||||
self.processed_char_count += len(seg.graphemes)
|
|
||||||
|
def _qt_on_progress(pct: int, etr: str) -> None:
|
||||||
|
self.processed_char_count = stats.processed_chars
|
||||||
|
self.progress_updated.emit(pct, etr)
|
||||||
|
|
||||||
|
def _qt_on_segment(info: SegmentInfo) -> None:
|
||||||
|
nonlocal chapter_current_time
|
||||||
self.log_updated.emit(
|
self.log_updated.emit(
|
||||||
f"\n{self.processed_char_count:,}/{self.total_char_count:,}: {seg.graphemes}"
|
f"\n{stats.processed_chars:,}/{self.total_char_count:,}: {info.graphemes}"
|
||||||
)
|
)
|
||||||
|
if self.subtitle_mode != "Disabled" and info.tokens:
|
||||||
# Write audio
|
tokens_with_timestamps = list(info.tokens)
|
||||||
if merge_chapters_at_end and merged_sink:
|
|
||||||
merged_sink.write(seg.audio)
|
|
||||||
if chapter_sink:
|
|
||||||
chapter_sink.write(seg.audio)
|
|
||||||
|
|
||||||
# Subtitle logic
|
|
||||||
if self.subtitle_mode != "Disabled" and seg.tokens:
|
|
||||||
tokens_with_timestamps = list(seg.tokens)
|
|
||||||
chapter_tokens_with_timestamps = [
|
chapter_tokens_with_timestamps = [
|
||||||
{
|
{
|
||||||
"start": chapter_current_time + (t["start"] - seg.chunk_start),
|
"start": chapter_current_time + (t["start"] - info.chunk_start),
|
||||||
"end": chapter_current_time + (t["end"] - seg.chunk_start),
|
"end": chapter_current_time + (t["end"] - info.chunk_start),
|
||||||
"text": t["text"],
|
"text": t["text"],
|
||||||
"whitespace": t["whitespace"],
|
"whitespace": t["whitespace"],
|
||||||
}
|
}
|
||||||
for t in seg.tokens
|
for t in info.tokens
|
||||||
]
|
]
|
||||||
if merge_chapters_at_end:
|
if merge_chapters_at_end:
|
||||||
new_entries = []
|
new_entries = []
|
||||||
@@ -992,7 +983,7 @@ class ConversionThread(QThread):
|
|||||||
tokens_with_timestamps,
|
tokens_with_timestamps,
|
||||||
new_entries,
|
new_entries,
|
||||||
self.max_subtitle_words,
|
self.max_subtitle_words,
|
||||||
fallback_end_time=seg.chunk_start + seg.duration,
|
fallback_end_time=info.chunk_start + info.duration,
|
||||||
)
|
)
|
||||||
if merged_subtitle_writer:
|
if merged_subtitle_writer:
|
||||||
for start, end, text in new_entries:
|
for start, end, text in new_entries:
|
||||||
@@ -1003,29 +994,37 @@ class ConversionThread(QThread):
|
|||||||
chapter_tokens_with_timestamps,
|
chapter_tokens_with_timestamps,
|
||||||
new_chapter_entries,
|
new_chapter_entries,
|
||||||
self.max_subtitle_words,
|
self.max_subtitle_words,
|
||||||
fallback_end_time=chapter_current_time + seg.duration,
|
fallback_end_time=chapter_current_time + info.duration,
|
||||||
)
|
)
|
||||||
if chapter_subtitle_writer:
|
if chapter_subtitle_writer:
|
||||||
for start, end, text in new_chapter_entries:
|
for start, end, text in new_chapter_entries:
|
||||||
chapter_subtitle_writer.write_entry(start, end, text)
|
chapter_subtitle_writer.write_entry(start, end, text)
|
||||||
|
|
||||||
# Update timing
|
|
||||||
if merge_chapters_at_end:
|
|
||||||
current_time += seg.duration
|
|
||||||
if chapter_sink:
|
if chapter_sink:
|
||||||
chapter_current_time += seg.duration
|
chapter_current_time += info.duration
|
||||||
|
|
||||||
# Progress
|
stats = SegmentStats(
|
||||||
percent = min(
|
processed_chars=self.processed_char_count,
|
||||||
int(self.processed_char_count / self.total_char_count * 100),
|
current_time=current_time,
|
||||||
99,
|
etr_start_time=self.etr_start_time,
|
||||||
)
|
total_characters=self.total_char_count,
|
||||||
etr_str = calc_etr_str(
|
)
|
||||||
time.time() - self.etr_start_time,
|
|
||||||
self.processed_char_count,
|
run_tts_segment_loop(
|
||||||
self.total_char_count,
|
text=text_segment,
|
||||||
)
|
backend=self.backend,
|
||||||
self.progress_updated.emit(percent, etr_str)
|
voice=loaded_voice,
|
||||||
|
speed=self.speed,
|
||||||
|
split_pattern=active_split_pattern,
|
||||||
|
stats=stats,
|
||||||
|
check_cancel=_qt_check_cancel,
|
||||||
|
on_progress=_qt_on_progress,
|
||||||
|
chapter_sink=chapter_sink,
|
||||||
|
audio_sink=merged_sink if merge_chapters_at_end else None,
|
||||||
|
on_segment=_qt_on_segment,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.processed_char_count = stats.processed_chars
|
||||||
|
current_time = stats.current_time
|
||||||
|
|
||||||
# Add silence between chapters for merged output (except after the last chapter)
|
# Add silence between chapters for merged output (except after the last chapter)
|
||||||
if merge_chapters_at_end and chapter_idx < total_chapters:
|
if merge_chapters_at_end and chapter_idx < total_chapters:
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ from abogen.domain.output_paths import (
|
|||||||
from abogen.domain.device import select_device as _select_device
|
from abogen.domain.device import select_device as _select_device
|
||||||
from abogen.domain.split_pattern import get_split_pattern
|
from abogen.domain.split_pattern import get_split_pattern
|
||||||
from abogen.domain.progress import ProgressTracker, calc_etr_str
|
from abogen.domain.progress import ProgressTracker, calc_etr_str
|
||||||
from abogen.domain.subtitle_generation import process_subtitle_tokens
|
|
||||||
from abogen.domain.audio_helpers import (
|
from abogen.domain.audio_helpers import (
|
||||||
build_ffmpeg_command as _build_ffmpeg_command,
|
build_ffmpeg_command as _build_ffmpeg_command,
|
||||||
to_float32 as _to_float32,
|
to_float32 as _to_float32,
|
||||||
@@ -118,7 +118,7 @@ from abogen.domain.audio_buffer import (
|
|||||||
)
|
)
|
||||||
from abogen.domain.audio_sink import AudioSink, open_audio_sink
|
from abogen.domain.audio_sink import AudioSink, open_audio_sink
|
||||||
from abogen.domain.pipeline_factory import PipelinePool
|
from abogen.domain.pipeline_factory import PipelinePool
|
||||||
from abogen.domain.conversion_pipeline import tts_segments
|
from abogen.domain.conversion_engine import run_tts_segment_loop, process_and_write_subtitles, SegmentStats
|
||||||
from abogen.domain.voice_utils import resolve_voice_target as _resolve_voice_target
|
from abogen.domain.voice_utils import resolve_voice_target as _resolve_voice_target
|
||||||
|
|
||||||
|
|
||||||
@@ -434,59 +434,56 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
effective_speed = float(speed_override if speed_override is not None else job.speed)
|
effective_speed = float(speed_override if speed_override is not None else job.speed)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
accumulated_tokens: List[dict] = []
|
stats = SegmentStats(
|
||||||
|
processed_chars=processed_chars,
|
||||||
|
current_time=current_time,
|
||||||
|
etr_start_time=etr_start_time,
|
||||||
|
total_characters=job.total_characters or 0,
|
||||||
|
)
|
||||||
|
prefix = f"{preview_prefix} · " if preview_prefix else ""
|
||||||
|
|
||||||
for seg in tts_segments(
|
def _on_progress(pct: int, etr: str) -> None:
|
||||||
normalized,
|
nonlocal processed_chars
|
||||||
|
processed_chars = stats.processed_chars
|
||||||
|
job.processed_characters = processed_chars
|
||||||
|
if stats.total_characters:
|
||||||
|
job.progress = min(processed_chars / stats.total_characters, 0.999)
|
||||||
|
else:
|
||||||
|
job.progress = 0.0 if processed_chars == 0 else 0.999
|
||||||
|
job.etr_str = etr
|
||||||
|
|
||||||
|
def _preview(text: str) -> None:
|
||||||
|
job.add_log(f"{prefix}{stats.processed_chars:,}/{job.total_characters or '—'}: {text[:80]}")
|
||||||
|
|
||||||
|
local_segments, accumulated_tokens = run_tts_segment_loop(
|
||||||
|
text=normalized,
|
||||||
backend=backend,
|
backend=backend,
|
||||||
voice=resolved_voice,
|
voice=resolved_voice,
|
||||||
speed=effective_speed,
|
speed=effective_speed,
|
||||||
split_pattern=split_pattern,
|
split_pattern=split_pattern,
|
||||||
current_time=current_time,
|
stats=stats,
|
||||||
):
|
check_cancel=canceller,
|
||||||
canceller()
|
on_progress=_on_progress,
|
||||||
local_segments += 1
|
chapter_sink=chapter_sink,
|
||||||
processed_chars += len(seg.graphemes)
|
audio_sink=audio_sink,
|
||||||
job.processed_characters = processed_chars
|
preview_callback=_preview,
|
||||||
if job.total_characters:
|
subtitle_mode=job.subtitle_mode if (subtitle_writer and audio_sink) else "Disabled",
|
||||||
job.progress = min(processed_chars / job.total_characters, 0.999)
|
max_subtitle_words=job.max_subtitle_words,
|
||||||
job.etr_str = calc_etr_str(
|
lang_code=job.language,
|
||||||
time.time() - etr_start_time,
|
use_spacy_segmentation=job.subtitle_mode not in ("Disabled", "Line"),
|
||||||
processed_chars,
|
)
|
||||||
job.total_characters,
|
current_time = stats.current_time
|
||||||
)
|
|
||||||
else:
|
|
||||||
job.progress = 0.0 if processed_chars == 0 else 0.999
|
|
||||||
|
|
||||||
preview_text = seg.graphemes or "[silence]"
|
|
||||||
prefix = f"{preview_prefix} · " if preview_prefix else ""
|
|
||||||
job.add_log(f"{prefix}{processed_chars:,}/{job.total_characters or '—'}: {preview_text[:80]}")
|
|
||||||
|
|
||||||
if chapter_sink:
|
|
||||||
chapter_sink.write(seg.audio)
|
|
||||||
if audio_sink:
|
|
||||||
audio_sink.write(seg.audio)
|
|
||||||
|
|
||||||
if subtitle_writer and audio_sink:
|
|
||||||
accumulated_tokens.extend(seg.tokens)
|
|
||||||
|
|
||||||
if audio_sink:
|
|
||||||
current_time += seg.duration
|
|
||||||
|
|
||||||
if subtitle_writer and audio_sink and accumulated_tokens:
|
if subtitle_writer and audio_sink and accumulated_tokens:
|
||||||
_use_spacy = job.subtitle_mode not in ("Disabled", "Line")
|
process_and_write_subtitles(
|
||||||
new_entries: List[tuple] = []
|
|
||||||
process_subtitle_tokens(
|
|
||||||
accumulated_tokens,
|
accumulated_tokens,
|
||||||
new_entries,
|
subtitle_writer,
|
||||||
job.max_subtitle_words,
|
subtitle_mode=job.subtitle_mode,
|
||||||
job.subtitle_mode,
|
max_subtitle_words=job.max_subtitle_words,
|
||||||
job.language,
|
lang_code=job.language,
|
||||||
use_spacy_segmentation=_use_spacy,
|
use_spacy_segmentation=job.subtitle_mode not in ("Disabled", "Line"),
|
||||||
fallback_end_time=current_time,
|
fallback_end_time=current_time,
|
||||||
)
|
)
|
||||||
for start, end, text in new_entries:
|
|
||||||
subtitle_writer.write_entry(start=start, end=end, text=text)
|
|
||||||
|
|
||||||
except OverflowError as exc:
|
except OverflowError as exc:
|
||||||
job.add_log(
|
job.add_log(
|
||||||
|
|||||||
@@ -8,8 +8,16 @@ from abogen.domain.settings_core import (
|
|||||||
CHUNK_LEVEL_OPTIONS,
|
CHUNK_LEVEL_OPTIONS,
|
||||||
CHUNK_LEVEL_VALUES,
|
CHUNK_LEVEL_VALUES,
|
||||||
DEFAULT_ANALYSIS_THRESHOLD,
|
DEFAULT_ANALYSIS_THRESHOLD,
|
||||||
|
SAVE_MODE_LABELS,
|
||||||
|
_NORMALIZATION_BOOLEAN_KEYS,
|
||||||
|
_NORMALIZATION_STRING_KEYS,
|
||||||
coerce_bool,
|
coerce_bool,
|
||||||
|
coerce_float,
|
||||||
|
coerce_int,
|
||||||
integration_defaults,
|
integration_defaults,
|
||||||
|
load_settings,
|
||||||
|
llm_ready,
|
||||||
|
settings_defaults,
|
||||||
)
|
)
|
||||||
|
|
||||||
_NORMALIZATION_GROUPS = [
|
_NORMALIZATION_GROUPS = [
|
||||||
|
|||||||
Reference in New Issue
Block a user