Commit 71a2a85
audio analysis script + multilingual MMS aligner + 4 new ASR backends (#510)
* Add analyze_audio.py: multi-task audio analysis with cache + LS export
Single-command developer script that runs senselab's full task suite
(diarization, AST, YAMNet, multi-backend features incl. squim quality
metrics, ASR, speaker embeddings) on an input audio file with and
without speech enhancement. Multiple models per task; AST and YAMNet
each driven at their native temporal resolution.
Output layout under artifacts/analyze_audio/<stem>_<timestamp>/:
- summary.json + per-(pass × task × model) JSON files
- labelstudio_tasks.json + labelstudio_config.xml for direct LS import
(one task per audio variant; one timeline track per analyzer × model)
Content-addressable cache under artifacts/analyze_audio_cache/ keyed by
sha256(audio_signature, task, model, params, wrapper_hash, senselab_ver,
schema_ver). Re-running with identical inputs replays prior outcomes
without invoking models. Per-model failures are captured in their own
JSON without aborting the run.
Forced-alignment auto-pairing for timestamp-less ASR models, the
multilingual aligner, NeMo Canary-Qwen, and Alibaba Qwen3-ASR backends
land in follow-up commits on this branch (see
specs/20260506-154425-audio-analysis-asr-extensions/tasks.md).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add uroman to nlp extra for forced-alignment ja/zh romanization
uroman (MIT, pure-Python, ~1.3.1.1) is needed by the upcoming MMS
forced-alignment backend to romanize Japanese and Chinese transcripts
before MMS's Roman-character tokenizer sees them.
Lives in the optional [nlp] extra so default `uv sync` is unaffected.
The MMS aligner imports uroman lazily, only when the language is ja or
zh — users who never align in those languages don't need the extra.
Verified default `uv sync` does not pull uroman; `uv sync --extra nlp`
does. uv.lock updated to record the new package; existing transitive
deps are unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Foundational: cache schema v2 + ASR dispatch table for new backends
scripts/analyze_audio.py:
- Bump _CACHE_SCHEMA_VERSION from 1 to 2. Comment documents that v2
introduces alignment as a separate cache entry from the ASR call that
produced its input transcript. v1 entries are inert and can be removed.
src/senselab/audio/tasks/speech_to_text/api.py:
- Add three new module-level prefix tables for upcoming backends:
_CANARY_PREFIXES = ("nvidia/canary-",) # NeMo SALM via subprocess venv
_QWEN_ASR_PREFIXES = ("Qwen/Qwen3-ASR",) # qwen-asr wrapper via subprocess venv
_TIMESTAMP_LESS_HF_MODELS = ("ibm-granite/granite-speech-",)
- Add stub dispatch branches for Canary and Qwen-ASR that raise
NotImplementedError pointing at the follow-up tasks (T025-T027 for
Canary, T030-T032 for Qwen). Lands the routing structure now so the
modules can be filled in independently without re-touching api.py.
- For models matching _TIMESTAMP_LESS_HF_MODELS, default
return_timestamps=False so the HF pipeline does not raise. Callers
can still override explicitly. This unblocks IBM Granite Speech 3.3
through the existing huggingface.py path (the analyze_audio script
then auto-aligns via the upcoming MMS forced-alignment backend).
Dispatch order: NeMo (existing) -> Canary (stub) -> Qwen-ASR (stub) ->
HF pipeline (default with timestamp-less knowledge). The four prefix
groups are disjoint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Script: separable ASR/alignment cache + auto-align CLI flags + smoke tests
Three-phase update to scripts/analyze_audio.py landing US1+US2+US3
script-side scope:
US1 (one-command E2E): no behavior change; smoke tests in
src/tests/scripts/analyze_audio_test.py exercise argparse defaults,
audio_signature stability, the cache-key composition contract, the LS
label-collection helper, and the serializer. 12 tests, no model loads,
runs in <1 s on default install.
US2 (re-run without re-computation):
- Add transcript_signature(text), align_cache_key(...), and
run_alignment_cached(...) — independent caching for the alignment
step that mirrors run_task_cached but keys on (audio_signature,
transcript_sha, language, aligner_model, aligner_params, wrapper,
senselab_ver).
- Failed alignments are NOT stored, so a future fix to the aligner /
senselab upgrade triggers a fresh attempt; the parent ASR cache is
unaffected per FR-024 / FR-025 / FR-026.
US3 (hierarchical LS export):
- Three-case LS-export branch for ASR: (a) ASR with native timestamps
-> per-segment regions from the ASR result; (b) ASR text-only with a
successful alignment outcome attached -> per-segment regions from
alignment; (c) ASR text-only with no/failed alignment -> single
full-audio TextArea region (graceful degradation per FR-025).
- Add _asr_has_timestamps() helper used by the LS export and
(eventually) by the auto-align stage to short-circuit cases where
alignment is unnecessary.
CLI surface (per contracts/cli.md, wired to the upcoming auto-align
stage that lands with US6):
- --no-align-asr (toggle off auto-align)
- --aligner-model (default facebook/mms-1b-all)
- --asr-language (force a specific language for the aligner)
- --qwen-asr-no-timestamps (skip Qwen's bundled forced-aligner)
- 'alignment' added to --skip choices.
Default --asr-models updated to include ibm-granite/granite-speech-3.3-8b
back into the comparison set; senselab's HF-pipeline path now defaults
return_timestamps=False for that model id (committed in da04a29), and
the upcoming MMS auto-align stage will add per-segment timestamps.
The auto-align stage itself is not yet wired (T021 lands with the MMS
backend in US6). Until then, --no-align-asr / --aligner-model /
--asr-language are accepted but unused.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* forced_alignment: MMS multilingual aligner backend (1100+ languages)
Adds Meta's MMS (Massively Multilingual Speech) as an opt-in aligner
backend for senselab.audio.tasks.forced_alignment, covering 1100+
languages via per-language adapters on a single base model. Selected
via the new ``align_transcriptions(..., aligner_model=...)`` keyword;
the legacy per-language wav2vec2 path stays the default for existing
callers (no behavior change for current code).
constants.py:
- MMS_MODEL_ID = "facebook/mms-1b-all" (CC-BY-NC 4.0 weights; override
the model id for commercial-friendly alternatives).
- ISO_1_TO_3 map for adapter selection (en, fr, de, es, pt, it, ja, zh,
plus the 19 other languages already in DEFAULT_ALIGN_MODELS_HF, which
MMS now also covers).
forced_alignment.py:
- _romanize_for_mms(text): lazy uroman import (optional dep, [nlp]
extra) for ja/zh transcripts. Without uroman installed, raises an
actionable ImportError pointing at `uv sync --extra nlp`.
- _load_mms_aligner(iso3, device, cache): one-time load of the MMS
base model + selected per-language adapter via processor.tokenizer
.set_target_lang(iso3) + model.load_adapter(iso3). Cached by
(model_id, iso3) so multiple calls on the same audio reuse weights.
- align_transcriptions: new ``aligner_model`` kwarg. When set to
MMS_MODEL_ID, dispatches to the MMS path; ja/zh transcripts go
through _romanize_for_mms before tokenization. Uses model_type=
"huggingface" internally — the wav2vec2 forward pass and CTC
trellis/backtrack code is identical, so no changes to
_get_prediction_matrix / _align_transcription / _assign_timestamps.
Output shape unchanged: List[List[ScriptLine | None]]. Existing 15
forced_alignment tests still pass (run via uv run pytest
src/tests/audio/tasks/forced_alignment_test.py).
speech_to_text/huggingface.py:
- _get_hf_asr_pipeline and transcribe_audios_with_transformers now
accept ``return_timestamps: Union[str, bool, None]`` (was Optional[str]).
HuggingFace's automatic-speech-recognition pipeline supports False
to disable timestamps; the existing senselab dispatcher (api.py,
committed earlier) defaults this to False for known-timestamp-less
HF models like ibm-granite/granite-speech-3.3-8b. Public docstring
updated to call out this case.
This commit lands T016-T020 from the spec-kit task plan. Next: T021
wires the analyze_audio script's auto-align stage to call
align_transcriptions(..., aligner_model=facebook/mms-1b-all) on every
timestamp-less ASR output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* analyze_audio: wire MMS auto-align stage; HF return_timestamps=False fix
scripts/analyze_audio.py:
- Wire the auto-align stage in run_pass(): after the ASR family runs,
iterate every successful ASR ModelRun. Skip when --no-align-asr is
set, when 'alignment' is in --skip, or when the ASR result already
has native timestamps (Whisper-style start/end or word chunks).
Otherwise call senselab.audio.tasks.forced_alignment.align_transcriptions
via run_alignment_cached(), with the resolved language (from
--asr-language, default "en"), the chosen aligner_model (default
facebook/mms-1b-all), and an alignment-specific provenance block
(transcript_sha, language, parent_asr_cache_key) per FR-026.
- Imports forced_alignment.align_transcriptions and the Language /
ScriptLine types from senselab.utils.data_structures.
- Add _extract_transcript_text() helper that handles both ScriptLine
objects (post-fresh-ASR) and the dict shape (post-cache-hit) so the
auto-align step works after either kind of ASR outcome.
- Update _asr_has_timestamps() to handle dict-shape ScriptLines too.
- Refresh the script docstring's ASR section: Granite is now in
defaults (auto-aligned), removing the older "intentionally NOT in
defaults" note that the spec clarification overrode. Add a paragraph
describing the auto-align stage and the --no-align-asr toggle.
src/senselab/audio/tasks/speech_to_text/huggingface.py:
- Fix _get_hf_asr_pipeline to OMIT return_timestamps from the kwargs
passed to transformers.pipeline() when the caller asked for False.
HF's AutomaticSpeechRecognitionPipeline behaves differently per
model family: CTC raises on False (only "char"/"word" allowed),
Whisper accepts False, and other generative seq2seq (Granite Speech)
raises on True/word/segment. Omitting the kwarg lets each pipeline
default sanely; omitting only happens when the caller explicitly
passed False (timestamps were not asked for). Other values
(str/None/True) are passed through unchanged.
src/tests/audio/tasks/forced_alignment/mms_test.py:
- New skipif-guarded smoke tests for the MMS aligner. Skipif checks
whether facebook/mms-1b-all is in the local HF cache (~1.6 GB
download otherwise; default CI install does not pull it). When
available: verify (a) English alignment produces well-shaped
ScriptLines via the real-speech fixture, (b) Japanese alignment
exercises the uroman romanization path, (c) an unknown language
code raises a clear ValueError pointing at the ISO_1_TO_3 registry.
Assertions are SHAPE-ONLY — synthetic transcripts on real audio do
not align meaningfully but the API contract is still verified.
src/tests/audio/tasks/speech_to_text/huggingface_no_timestamps_test.py:
- New skipif-guarded smoke test for return_timestamps=False. Uses
facebook/wav2vec2-base-960h (already cached locally for the existing
English aligner path) rather than Granite Speech (multi-GB).
Verifies: when return_timestamps=False, transcribe_audios_with_
transformers returns text-only ScriptLines without raising the HF
pipeline's CTC-rejects-False ValueError.
Existing 11 senselab speech_to_text_test.py tests still pass; existing
15 forced_alignment_test.py tests still pass.
This commit lands T021-T023. T024 (end-to-end Granite + Whisper
validation through the analyze_audio script with real model downloads)
is deferred to a follow-up session — needs ~16 GB Granite Speech
download plus ~1.6 GB MMS download.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* speech_to_text: NVIDIA Canary-Qwen 2.5B subprocess-venv backend
Adds CanaryQwenASR which loads SALM (Speech-Augmented Language Model)
from nemo.collections.speechlm2.models in an isolated nemo-canary-qwen
venv (kept separate from the existing nemo-diarization venv to avoid
destabilizing Sortformer/Conformer-CTC paths). The chat-style prompt
format using model.audio_locator_tag matches the published model card.
Canary-Qwen is text-only (no native timestamps); the analyze_audio
script's auto-align stage (US6) now adds per-segment timing via the
multilingual MMS forced-aligner downstream when this model is used.
Wires the api.py dispatch (replaces the NotImplementedError stub) and
adds a shape-only smoke test that auto-skips when the venv has not
been provisioned (~5 GB first-run install).
Marks T025-T028 done in specs tasks.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* speech_to_text: Alibaba Qwen3-ASR subprocess-venv backend
Adds QwenASR which loads Qwen3-ASR via Alibaba's qwen-asr Python wrapper
(Qwen3ASRModel.from_pretrained) inside an isolated qwen-asr venv,
optionally pairing with the bundled Qwen3-ForcedAligner-0.6B companion
to produce per-word / per-CJK-char timestamps.
Wires api.py dispatch (replaces the NotImplementedError stub) and
threads --qwen-asr-no-timestamps from analyze_audio.py through to the
ASR call so the script's MMS auto-align stage can take over when the
bundled aligner is undesirable. Adds shape-only smoke tests that
auto-skip until the qwen-asr venv has been provisioned.
Marks T030-T033 done in specs tasks.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* polish: lint cleanup + mark Phase 9 tasks done
- Add docstring to src/tests/scripts/__init__.py (D104).
- Mark T035-T037 done in specs tasks.md after running:
* full ruff check + mypy on changed paths (clean)
* full senselab pytest suite excluding scripts/ (421 passed,
25 skipped, no regressions; voice_cloning_test ERROR in
bulk run is a known order-dependent flake — passes in isolation,
unrelated to forced_alignment / speech_to_text changes).
T035 (CLAUDE.md update) is applied to the local file; CLAUDE.md is
not yet under version control in this repo so the section addition
lives in the working tree only — user decides whether to track it.
T038 (final E2E validation against a tutorial WAV) intentionally
deferred: requires the nemo-canary-qwen and qwen-asr venvs (~5-15 GB
combined) and the MMS aligner (~1.6 GB) to be provisioned locally.
The skipif-guarded smoke tests ride the venvs whenever the user
provisions them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* e2e validation: fix bugs surfaced by full-pipeline run
Validates T012/T015/T015a/T024/T029/T034/T038 end-to-end. Bugs found
and fixed during the validation cycle:
- canary_qwen.py worker: SALM.generate expects prompts as
list[list[dict]] (a batch of conversations), not list[dict]. Fixed.
Canary-Qwen now succeeds in ~70s on a 38s clip.
- forced_alignment.py: senselab's Language normalizes ISO-639-1 ('en')
to ISO-639-3 ('eng'). The MMS aligner now accepts either form.
- script_line.py from_dict: three pre-existing latent bugs surfaced by
MMS-aligner output:
* d["chunks"][0] accessed without checking non-empty (IndexError)
* d["timestamps"][0] accessed without checking length >= 2
(IndexError)
* recursive construction of empty subsegments with text="" failing
the "at least text or speaker" pydantic validator
Empty subsegments are now filtered before recursive construction.
- analyze_audio.py LS export: pre-existing bug where
getattr(seg, "start") returned None for cache-restored dicts,
silently dropping all regions on cache-hit runs. Added _seg_attr
helper that handles both Pydantic objects and JSON dicts. Also
unwrap align_transcriptions' List[List[ScriptLine]] to inner segment
list before passing to _asr_to_ls.
- mms_test: align test expectation with senselab.Language's stricter
ISO validation (Language("xx") raises before reaching MMS).
Validation results in artifacts/analyze_audio_e2e_validation.md (not
committed). Marks T012/T015/T015a/T024/T029/T034/T038 done. The two
known gaps documented in the validation note: YAMNet (corrupt local
tfhub cache, environmental) and Granite Speech 3.3 (HF pipeline
contract mismatch — needs a dedicated subprocess-venv backend in a
follow-up).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* speech_to_text: IBM Granite Speech 3.3 in-process backend
Adds GraniteSpeechASR which loads ``GraniteSpeechProcessor`` and
``AutoModelForSpeechSeq2Seq`` directly in-process — no subprocess venv
needed because transformers, torch, torchaudio, and peft all live in
senselab's core install (peft is required for Granite's LoRA adapter;
loading without it produces gibberish).
Why a dedicated backend rather than the HF ASR pipeline path: Granite
uses ``GraniteSpeechProcessor(text, audio, device, **kwargs)`` and a
``GraniteSpeechFeatureExtractor`` whose ``__call__(audios, device)``
signature does not accept the ``{array, sampling_rate}`` audio dict
that ``AutomaticSpeechRecognitionPipeline`` passes in. Routing through
the pipeline failed at the feature-extractor boundary in earlier E2E
runs.
Worker subtleties surfaced by the E2E:
- ``return_tensors="pt"`` is mandatory; without it Granite's processor
returns ``input_ids`` / ``attention_mask`` as nested Python lists,
which the underlying ``generate()`` then tries to call ``.shape`` on.
- The feature extractor expects a 1-D audio tensor; senselab's
``Audio.waveform`` is shape ``[channels, T]``, so we squeeze after
downmix.
- bfloat16 on accelerators (CUDA + MPS), float32 on CPU. CPU bf16 runs
but is ~40x slower than fp32 because PyTorch's CPU kernels lack a
native bf16 SIMD path. MPS bf16 was verified against torch 2.11.
Adds ``peft>=0.13`` to senselab's core deps. Wires api.py dispatch
(replaces the timestamp-less HF route), drops Granite from
``_TIMESTAMP_LESS_HF_MODELS``, and adds a shape-only smoke test that
auto-skips when the model weights are not in the local HF cache
(~16 GB).
Routed automatically by ``transcribe_audios`` when the model id starts
with ``ibm-granite/granite-speech-``. Granite is text-only; the
analyze_audio script's auto-align stage adds per-segment timestamps
via the multilingual MMS forced-aligner downstream.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* PR review: type annotations, MMS aligner cache, ruff format
Addresses pre-commit failures and Gemini's review feedback on PR #510:
- Test files: add ``model: HFModel`` annotations on canary_qwen_test,
qwen_test, granite_test, huggingface_no_timestamps_test (mypy 1.20.1
is stricter than 1.18.x and infers Self<HFModel> when the literal
HFModel(...) call is on the RHS — explicit annotation pins the type).
- ``forced_alignment.align_transcriptions``: hoist the
processor/model cache from a per-call local dict to a module-level
``_ALIGNER_CACHE``. The 1B-parameter MMS base weights (and any
per-language wav2vec2 variants) now load once per process instead of
once per call — fixes a perf cliff Gemini flagged in the review.
- ruff format: pick up the auto-applied formatting in
scripts/generate_model_registry.py + scripts/profile_model_tiers.py
(no functional changes; ruff 0.15.11 vs older local), plus a small
re-flow in script_line.py from_dict and granite.py.
Did NOT apply Gemini's two other suggestions:
- Drop ``device=`` arg in Granite ``processor(...)`` call: verified
Granite's ``GraniteSpeechProcessor.__call__`` accepts ``device``
explicitly in its signature; works on MPS in run 15.
- Drop ``input_len`` slicing on generate output: empirically verified
via standalone MPS run that with slicing the transcript decodes
correctly (``"so once upon a time there were four little rabbits..."``);
without slicing it would prepend the prompt. Granite is an LLM-style
multimodal generator, not a true seq2seq.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* analyze_audio: per-window features (1s/0.5s default), parquet output
Replaces the single global summary that the script was emitting for
opensmile / parselmouth / torchaudio-squim with proper time-series
output. The rest of the analysis (diarization, AST, YAMNet, ASR,
alignment, embeddings) is already temporal — features should be too.
Routing:
- opensmile uses ``LowLevelDescriptors`` (native ~10 ms frame grid).
Run once; one parquet row per opensmile frame.
- parselmouth + torchaudio-squim are summary statistics by design;
the senselab wrapper only exposes the aggregate form, so these run
in an external sliding window driven by the new
``--features-win-length`` (default 1.0 s) and
``--features-hop-length`` (default 0.5 s) flags. One parquet row
per window.
Each backend writes its own parquet sidecar
(``<pass>/features/<backend>.parquet``) — different columns and
different time grids don't share a schema; per-backend files keep
each grid clean and let downstream readers join on
``start``/``end`` if they want a unified view. The cache outcome
metadata stays in JSON for inspection parity with the other tasks
(``features.json`` now points to the parquet directory).
Verified end-to-end on a 38.4 s clip (run 16, both passes):
- opensmile.parquet: (3837, 27), 668 KB, 0–38.411 s
- parselmouth.parquet: (75, 43), 49 KB, 0–38 s
- torchaudio_squim.parquet: (75, 6), 6.5 KB, 0–38 s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* PR review: pre-commit fixes (codespell + pretty-format-toml)
- codespell: add ``vie`` (Vietnamese ISO-639-3 code in ISO_1_TO_3
registry, not the English noun) to ignore-words-list.
- pretty-format-toml: re-flow [tool.codespell] block so the ``vie``
addition lands on a properly-formatted line.
No code changes; all green pre-commit locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 6faab49 commit 71a2a85
21 files changed
Lines changed: 3215 additions & 55 deletions
File tree
- scripts
- specs/20260506-154425-audio-analysis-asr-extensions
- src
- senselab
- audio/tasks
- forced_alignment
- speech_to_text
- utils/data_structures
- tests
- audio/tasks
- forced_alignment
- speech_to_text
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
| 42 | + | |
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
| |||
59 | 60 | | |
60 | 61 | | |
61 | 62 | | |
62 | | - | |
| 63 | + | |
| 64 | + | |
63 | 65 | | |
64 | 66 | | |
65 | 67 | | |
| |||
196 | 198 | | |
197 | 199 | | |
198 | 200 | | |
199 | | - | |
| 201 | + | |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
| 34 | + | |
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
137 | 137 | | |
138 | 138 | | |
139 | 139 | | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
144 | | - | |
| 140 | + | |
145 | 141 | | |
146 | 142 | | |
147 | 143 | | |
| |||
259 | 255 | | |
260 | 256 | | |
261 | 257 | | |
262 | | - | |
263 | | - | |
264 | | - | |
265 | | - | |
266 | | - | |
| 258 | + | |
267 | 259 | | |
268 | 260 | | |
269 | 261 | | |
| |||
290 | 282 | | |
291 | 283 | | |
292 | 284 | | |
293 | | - | |
294 | | - | |
295 | | - | |
296 | | - | |
297 | | - | |
| 285 | + | |
298 | 286 | | |
299 | 287 | | |
300 | 288 | | |
| |||
532 | 520 | | |
533 | 521 | | |
534 | 522 | | |
535 | | - | |
536 | | - | |
| 523 | + | |
537 | 524 | | |
538 | 525 | | |
539 | 526 | | |
| |||
693 | 680 | | |
694 | 681 | | |
695 | 682 | | |
696 | | - | |
697 | | - | |
698 | | - | |
| 683 | + | |
699 | 684 | | |
700 | 685 | | |
701 | 686 | | |
| |||
Lines changed: 251 additions & 0 deletions
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
11 | 59 | | |
12 | 60 | | |
13 | 61 | | |
| |||
Lines changed: 113 additions & 12 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| 13 | + | |
13 | 14 | | |
14 | 15 | | |
| 16 | + | |
15 | 17 | | |
16 | 18 | | |
17 | 19 | | |
| |||
566 | 568 | | |
567 | 569 | | |
568 | 570 | | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
569 | 633 | | |
570 | 634 | | |
571 | 635 | | |
| 636 | + | |
572 | 637 | | |
573 | 638 | | |
574 | 639 | | |
| |||
577 | 642 | | |
578 | 643 | | |
579 | 644 | | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
580 | 654 | | |
581 | 655 | | |
582 | 656 | | |
583 | 657 | | |
584 | 658 | | |
585 | | - | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
586 | 663 | | |
| 664 | + | |
587 | 665 | | |
588 | 666 | | |
589 | 667 | | |
590 | 668 | | |
591 | 669 | | |
592 | 670 | | |
593 | | - | |
594 | | - | |
595 | | - | |
596 | | - | |
597 | | - | |
598 | | - | |
599 | | - | |
600 | | - | |
601 | | - | |
602 | | - | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
603 | 699 | | |
604 | 700 | | |
605 | 701 | | |
606 | 702 | | |
607 | 703 | | |
608 | 704 | | |
609 | 705 | | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
610 | 709 | | |
611 | 710 | | |
612 | 711 | | |
| |||
621 | 720 | | |
622 | 721 | | |
623 | 722 | | |
624 | | - | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
625 | 726 | | |
626 | 727 | | |
627 | 728 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| 11 | + | |
| 12 | + | |
11 | 13 | | |
12 | 14 | | |
| 15 | + | |
13 | 16 | | |
14 | 17 | | |
15 | 18 | | |
16 | 19 | | |
17 | 20 | | |
18 | 21 | | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
19 | 46 | | |
20 | 47 | | |
21 | 48 | | |
| |||
98 | 125 | | |
99 | 126 | | |
100 | 127 | | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
101 | 139 | | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
102 | 145 | | |
103 | 146 | | |
104 | 147 | | |
| |||
0 commit comments