tts-plugin-bridge 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ from .protocol import TTSRequest, TTSResponse, TTSConnector
2
+ from .factory import ConnectorFactory
3
+
4
+ __all__ = ["TTSRequest", "TTSResponse", "TTSConnector", "ConnectorFactory"]
@@ -0,0 +1,133 @@
1
+ import re
2
+ from typing import List, Protocol
3
+ from .protocol import ChunkResult, ChunkConfig
4
+
5
+
6
+ class TextChunker(Protocol):
7
+ def chunk(self, text: str, config: ChunkConfig) -> List[ChunkResult]: ...
8
+
9
+
10
+ class SentenceChunker:
11
+ """句点で分割"""
12
+
13
+ def chunk(self, text: str, config: ChunkConfig) -> List[ChunkResult]:
14
+ if not text:
15
+ return []
16
+
17
+ # 句点(。!?)を保持して分割するための正規表現
18
+ pattern = r"([。!?])"
19
+ parts = re.split(pattern, text)
20
+
21
+ combined_parts = []
22
+ for i in range(0, len(parts) - 1, 2):
23
+ combined_parts.append(parts[i] + parts[i + 1])
24
+
25
+ if len(parts) % 2 != 0 and parts[-1]:
26
+ combined_parts.append(parts[-1])
27
+
28
+ results = []
29
+ for idx, part in enumerate(combined_parts):
30
+ if part:
31
+ results.append(ChunkResult(text=part, index=idx, char_count=len(part)))
32
+ return results
33
+
34
+
35
+ class CharacterCountChunker:
36
+ """文字数で分割"""
37
+
38
+ def chunk(self, text: str, config: ChunkConfig) -> List[ChunkResult]:
39
+ if not text:
40
+ return []
41
+
42
+ max_chars = config.max_chars
43
+ results = []
44
+
45
+ for i in range(0, len(text), max_chars):
46
+ chunk_text = text[i : i + max_chars]
47
+ results.append(
48
+ ChunkResult(
49
+ text=chunk_text, index=len(results), char_count=len(chunk_text)
50
+ )
51
+ )
52
+ return results
53
+
54
+
55
+ class HybridChunker:
56
+ """原則sentence分割、max_chars超えりでchar分割"""
57
+
58
+ def __init__(self):
59
+ self.sentence_chunker = SentenceChunker()
60
+ self.char_chunker = CharacterCountChunker()
61
+
62
+ def chunk(self, text: str, config: ChunkConfig) -> List[ChunkResult]:
63
+ if not text:
64
+ return []
65
+
66
+ sentence_chunks = self.sentence_chunker.chunk(text, config)
67
+
68
+ final_results = []
69
+ current_index = 0
70
+
71
+ for s_chunk in sentence_chunks:
72
+ if len(s_chunk.text) <= config.max_chars:
73
+ final_results.append(
74
+ ChunkResult(
75
+ text=s_chunk.text,
76
+ index=current_index,
77
+ char_count=s_chunk.char_count,
78
+ is_partial=False,
79
+ )
80
+ )
81
+ current_index += 1
82
+ else:
83
+ sub_chunks = self.char_chunker.chunk(s_chunk.text, config)
84
+
85
+ for i, sub in enumerate(sub_chunks):
86
+ is_last = i == len(sub_chunks) - 1
87
+ final_results.append(
88
+ ChunkResult(
89
+ text=sub.text,
90
+ index=current_index,
91
+ char_count=sub.char_count,
92
+ is_partial=not is_last,
93
+ original_sentence=s_chunk.text,
94
+ )
95
+ )
96
+ current_index += 1
97
+
98
+ return final_results
99
+
100
+
101
+ class PauseMarkerChunker:
102
+ def chunk(self, text: str, config: ChunkConfig) -> List[ChunkResult]:
103
+ if not text:
104
+ return []
105
+
106
+ _split_re = re.compile(r"[、,・,;  ]+")
107
+ parts = _split_re.split(text)
108
+ parts = [p.strip() for p in parts if p.strip()]
109
+ if not parts:
110
+ return [ChunkResult(text=text, index=0, char_count=len(text))]
111
+
112
+ merged: list[str] = []
113
+ for p in parts:
114
+ if merged and len(merged[-1]) < config.min_chars:
115
+ merged[-1] += p
116
+ else:
117
+ merged.append(p)
118
+
119
+ if merged and len(merged) > 1 and len(merged[-1]) < config.min_chars:
120
+ tail = merged.pop()
121
+ merged[-1] += tail
122
+
123
+ results = []
124
+ for idx, chunk in enumerate(merged):
125
+ results.append(
126
+ ChunkResult(
127
+ text=chunk,
128
+ index=idx,
129
+ char_count=len(chunk),
130
+ is_partial=(idx < len(merged) - 1),
131
+ )
132
+ )
133
+ return results
@@ -0,0 +1,48 @@
1
+ import importlib.metadata
2
+ from typing import Type, Dict
3
+ from .protocol import TTSConnector
4
+
5
+
6
+ class ConnectorFactory:
7
+ _registry: Dict[str, Type[TTSConnector]] = {}
8
+ _discovered = False
9
+
10
+ @classmethod
11
+ def _discover(cls) -> None:
12
+ if cls._discovered:
13
+ return
14
+
15
+ eps = importlib.metadata.entry_points(group="tts_bridge.connectors")
16
+
17
+ for ep in eps:
18
+ try:
19
+ connector_cls = ep.load()
20
+ name = getattr(connector_cls, "ENGINE_NAME", ep.name)
21
+ if name in cls._registry:
22
+ print(
23
+ f"⚠️ Warning: Connector '{name}' already registered. Skipping {ep.value}"
24
+ )
25
+ continue
26
+ cls._registry[name] = connector_cls
27
+ except Exception as e:
28
+ print(f"❌ Failed to load plugin {ep.value}: {e}")
29
+
30
+ cls._discovered = True
31
+
32
+ @classmethod
33
+ def list_available(cls) -> list[str]:
34
+ cls._discover()
35
+ return list(cls._registry.keys())
36
+
37
+ @classmethod
38
+ def create(cls, engine: str, **kwargs) -> TTSConnector:
39
+ cls._discover()
40
+ connector_cls = cls._registry.get(engine.lower())
41
+ if not connector_cls:
42
+ available = ", ".join(cls.list_available()) or "none"
43
+ raise ValueError(
44
+ f"TTS Plugin '{engine}' not found. \n"
45
+ f"Available engines: {available}\n"
46
+ f"💡 To install, run: uv add tts-plugin-{engine}"
47
+ )
48
+ return connector_cls(**kwargs)
@@ -0,0 +1,104 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import AsyncIterator, ClassVar, Optional
3
+ from pydantic import BaseModel, Field
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+
7
+
8
+ class ChunkStrategy(Enum):
9
+ SENTENCE = "sentence" # 句点(。!?)で分割
10
+ CHARACTER_COUNT = "char" # 文字数で分割
11
+ HYBRID = "hybrid" # 原則sentence、max_chars超えりでchar分割
12
+ PAUSE_MARKERS = "pause" # 「、」など一時停止マーカーで分割
13
+
14
+
15
+ @dataclass
16
+ class ChunkConfig:
17
+ strategy: ChunkStrategy = ChunkStrategy.HYBRID
18
+ max_chars: int = 100 # HYBRID/CHARACTER_COUNT時
19
+ max_duration_sec: float = 30.0 # 目標最長時間
20
+ min_chars: int = 10 # 最小文字数(短すぎる分割を防ぐ)
21
+ preserve_punctuation: bool = True # 句読点を保持するか
22
+
23
+
24
+ @dataclass
25
+ class ChunkResult:
26
+ """分割されたテキストの情報を保持するクラス"""
27
+
28
+ text: str
29
+ index: int
30
+ char_count: int
31
+ is_partial: bool = False # 文の途中での分割
32
+ original_sentence: str = "" # 分割元の文(HYBRID時)
33
+
34
+
35
+ class TTSRequest(BaseModel):
36
+ """全エンジン共通のリクエストモデル"""
37
+
38
+ text: str = Field(..., min_length=1, description="合成テキスト")
39
+ speed: float = Field(
40
+ default=1.0, ge=0.1, le=3.0, description="話速: 1.0=標準, >1.0=速い"
41
+ )
42
+ pitch: Optional[float] = Field(
43
+ default=None, description="ピッチ補正(エンジン依存)"
44
+ )
45
+ volume: Optional[float] = Field(default=1.0, ge=0.0, le=3.0, description="音量倍率")
46
+ model: Optional[str] = Field(default=None, description="エンジン固有モデル名")
47
+ output_format: str = Field(default="wav", description="出力フォーマット")
48
+ chunk: bool = Field(default=False, description="テキスト分割を有効にするか")
49
+ chunk_config: Optional[ChunkConfig] = Field(default=None, description="分割の設定")
50
+ extra: dict = Field(default_factory=dict, description="エンジン固有パラメータ")
51
+
52
+
53
+ class TTSResponse(BaseModel):
54
+ """全エンジン共通のレスポンスモデル"""
55
+
56
+ success: bool
57
+ audio_data: Optional[bytes] = None
58
+ file_path: Optional[str] = None
59
+ duration_sec: Optional[float] = None
60
+ error: Optional[str] = None
61
+ metadata: dict = Field(default_factory=dict)
62
+
63
+ @classmethod
64
+ def ok(cls, audio_data: bytes, **kwargs) -> "TTSResponse":
65
+ return cls(success=True, audio_data=audio_data, **kwargs)
66
+
67
+ @classmethod
68
+ def fail(cls, error: str, **kwargs) -> "TTSResponse":
69
+ return cls(success=False, error=error, **kwargs)
70
+
71
+
72
+ class TTSConnector(ABC):
73
+ """TTSエンジン共通インターフェース"""
74
+
75
+ ENGINE_NAME: ClassVar[str] = "unknown"
76
+ SUPPORTED_PARAMS: ClassVar[list[str]] = []
77
+
78
+ @property
79
+ def name(self) -> str:
80
+ return self.ENGINE_NAME
81
+
82
+ @abstractmethod
83
+ async def synthesize(self, req: TTSRequest) -> TTSResponse:
84
+ """音声合成を実行"""
85
+ pass
86
+
87
+ @abstractmethod
88
+ async def synthesize_stream(self, req: TTSRequest) -> AsyncIterator[bytes]:
89
+ """音声合成をストリーミング形式で実行(チャンク単位で bytes を逐次返す)"""
90
+ if False:
91
+ yield b""
92
+
93
+ @abstractmethod
94
+ async def is_available(self) -> bool:
95
+ """エンジンサーバーが利用可能かチェック"""
96
+ pass
97
+
98
+ def get_supported_params(self) -> list[str]:
99
+ return self.SUPPORTED_PARAMS.copy()
100
+
101
+ @abstractmethod
102
+ async def close(self):
103
+ """リソースを解放"""
104
+ pass
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: tts-plugin-bridge
3
+ Version: 0.1.0
4
+ Summary: TTSプラグインの動的発見・管理・Agent連携のためのコアフレームワーク
5
+ Project-URL: Homepage, https://github.com/vox4ai/tts-plugin-bridge
6
+ Project-URL: Repository, https://github.com/vox4ai/tts-plugin-bridge
7
+ Project-URL: Issues, https://github.com/vox4ai/tts-plugin-bridge/issues
8
+ Author-email: utenadev <utena.cross+pypi@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: agent-skill,plugin-system,speech,synthesis,tts,tts-plugin,voice
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: pydantic-settings>=2.0
26
+ Requires-Dist: pydantic>=2.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ # tts-plugin-bridge
30
+
31
+ <p align="center">
32
+ <img src="https://via.placeholder.com/1200x400/1a1a1a/ffffff?text=tts-plugin-bridge" alt="tts-plugin-bridge Banner" width="1200">
33
+ </p>
34
+
35
+ <p align="center">
36
+ <img src="https://img.shields.io/badge/pypi-latest-blue.svg" alt="PyPI version">
37
+ <img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License">
38
+ <img src="https://img.shields.io/badge/python-3.10%2B-yellow.svg" alt="Python Version">
39
+ <img src="https://img.shields.io/badge/maintained%3F-yes-brightgreen.svg" alt="Maintained">
40
+ </p>
41
+
42
+ <p align="center">
43
+ <a href="https://github.com/vox4ai/tts-plugin-bridge">Website</a> •
44
+ <a href="https://github.com/vox4ai/tts-plugin-bridge/issues">Report Bug</a> •
45
+ <a href="https://github.com/vox4ai/tts-plugin-bridge/contributing">Contributing</a>
46
+ </p>
47
+
48
+ ---
49
+
50
+ ## 🚀 Overview
51
+
52
+ TTSエンジンのプラグイン化・動的発見・Agent連携を可能にするコアフレームワークです。
53
+
54
+ ### ✨ 特徴
55
+ - 🔌 **Entry Points による自動発見**: `uv add tts-plugin-xxx` するだけで自動的にブリッジへ登録
56
+ - 🔀 **エンジン非依存**: コアパッケージは特定のTTSに依存せず、軽量で安定
57
+ - 🤖 **Agent 最適化**: `TTSSkill` クラスで非同期呼び出し・パラメータ統一・Base64出力を標準提供
58
+ - 🛡️ **型安全**: Pydantic ベースのリクエスト/レスポンスでバリデーション自動実行
59
+ - 🎤 **vox4ai CLI**: 統一インターフェースの `vox4ai` コマンドを同梱(say/save/list/test + 環境診断)
60
+
61
+ ## 🧩 TTS Engine プラグイン
62
+
63
+ 各 TTS Engine は独立したプラグインとして提供されます。
64
+ `uv add tts-plugin-<name>` で導入するだけです。
65
+
66
+ | プラグイン | リポジトリ | バックエンド | 特徴 |
67
+ |---|---|---|---|
68
+ | **tts-plugin-edgetts** | [vox4ai/tts-plugin-edgetts](https://github.com/vox4ai/tts-plugin-edgetts) | [edge-tts](https://github.com/rany2/edge-tts) (Microsoft Edge TTS) | ローカルサーバー不要・APIキー不要・すぐ使える・多言語・ffplayストリーミング再生 |
69
+ | **tts-plugin-aivisspeech** | [vox4ai/tts-plugin-aivisspeech](https://github.com/vox4ai/tts-plugin-aivisspeech) | [AivisSpeech Engine](https://github.com/AivisProject/AivisSpeech-Engine) | VOICEVOX互換API・日本語高品質・Docker運用・複数話者・WAV出力 |
70
+ | **tts-plugin-kokoro** | [vox4ai/tts-plugin-kokoro](https://github.com/vox4ai/tts-plugin-kokoro) | [kokoro](https://github.com/hexgrad/kokoro) (ローカル推論) | 完全オフライン・espeak-ng必要・モデル別途DL・ローカル音声合成 |
71
+ | **tts-plugin-piperplus** | [vox4ai/tts-plugin-piperplus](https://github.com/vox4ai/tts-plugin-piperplus) | [Piper](https://github.com/rhasspy/piper) / Piper Plus HTTP Server | 軽量・Raspberry Piでも動作・ローカルHTTPサーバー |
72
+ | **tts-plugin-voisonatalk** | [vox4ai/tts-plugin-voisonatalk](https://github.com/vox4ai/tts-plugin-voisonatalk) | [VoiSona Talk Editor](https://resource.voisona.com/) (REST API) | 歌声・読み上げ対応・Windows/Mac・BasicAuth認証・直接スピーカー出力 |
73
+
74
+ > 全て `uv add tts-plugin-<name>` で bridge プロジェクトに追加できます。`vox4ai list` で導入済みプラグインを確認できます。
75
+
76
+ ## 📦 インストール
77
+
78
+ ```bash
79
+ uv add tts-plugin-bridge
80
+ ```
81
+
82
+ ## 🛠 Usage
83
+
84
+ ### 🧩 Python API
85
+
86
+ ```python
87
+ from tts_plugin_bridge import TTSSkill
88
+
89
+ async with TTSSkill(default_engine="edgetts") as skill:
90
+ # 音声ファイルに保存(save は synthesize の alias)
91
+ res = await skill.save(
92
+ text="こんにちは",
93
+ speed=1.2,
94
+ volume=1.0,
95
+ )
96
+ with open("output.mp3", "wb") as f:
97
+ f.write(base64.b64decode(res["audio_base64"]))
98
+
99
+ # 直接再生(say は play の alias)
100
+ res = await skill.say(
101
+ text="こんにちは",
102
+ model="ja-JP-KeitaNeural",
103
+ )
104
+ ```
105
+
106
+ #### メソッド一覧
107
+
108
+ | メソッド | alias | 戻り値 | 用途 |
109
+ |----------|-------|--------|------|
110
+ | `synthesize()` | `save()` | `dict` (Base64) | 音声合成してBase64として返す |
111
+ | `play()` | `say()` | `dict` | 直接再生(ストリーミング優先→全取得後再生) |
112
+ | `close()` | - | `None` | 全コネクタのリソース解放 |
113
+
114
+ - `say()` / `play()` は Engine が `synthesize_stream()` を実装していれば ffplay でストリーミング、なければ synthesize → paplay/aplay で再生
115
+ - Engine ごとに再生方式が異なるが、ユーザーは `say()` という同じインターフェースで使える
116
+
117
+ ### 🎤 vox4ai CLI
118
+
119
+ `vox4ai` は `tts-plugin-bridge` に同梱される統合TTS操作コマンドです。
120
+ サブコマンドで直感的に操作できます。
121
+
122
+ ```bash
123
+ vox4ai say "こんにちは"
124
+ vox4ai save "こんにちは" -o output.wav
125
+ vox4ai list
126
+ vox4ai test -e aivisspeech
127
+ ```
128
+
129
+ #### グローバルオプション
130
+
131
+ ```bash
132
+ vox4ai --commands # 利用可能なサブコマンド一覧
133
+ vox4ai --doctor # 環境診断(再生コマンド・プラグイン・パッケージ)
134
+ vox4ai --tts-plugin-list # TTS Engine 一覧(listと同じ)
135
+ ```
136
+
137
+ #### `vox4ai --doctor` で確認できる項目
138
+ - 再生コマンド: ffplay / paplay / aplay の有無
139
+ - 登録TTSプラグイン一覧
140
+ - Python パッケージ導入状況
141
+
142
+ #### サブコマンド
143
+
144
+ ##### `say` — テキストを読み上げる(ストリーミング再生優先)
145
+
146
+ ```bash
147
+ vox4ai say "こんにちは" # デフォルトエンジン
148
+ vox4ai say "Hello" -e edgetts # Edge TTS(ffplayストリーミング)
149
+ vox4ai say "こんにちは" -e aivisspeech # AivisSpeech
150
+ --server-url http://localhost:10101
151
+ --style-id 888753760
152
+ vox4ai say "Hello" -e edgetts # 話速・声指定
153
+ --speed 1.5
154
+ --model en-US-AndrewNeural
155
+ ```
156
+
157
+ ##### `save` — テキストを音声ファイルに保存
158
+
159
+ ```bash
160
+ vox4ai save "こんにちは" -o hello.wav # WAV保存
161
+ vox4ai save "こんにちは" -e edgetts -o out.mp3 # Edge TTS(MP3)
162
+ vox4ai save "こんにちは" -e aivisspeech # AivisSpeech
163
+ --server-url http://localhost:10101
164
+ --style-id 888753760
165
+ --output hello.wav
166
+ vox4ai save "こんにちは" --play # 保存後に再生
167
+ ```
168
+
169
+ ##### `list` — 利用可能なTTSプラグイン一覧
170
+
171
+ ```bash
172
+ vox4ai list
173
+ ```
174
+
175
+ ##### `test` — TTSエンジン接続テスト
176
+
177
+ ```bash
178
+ vox4ai test -e edgetts
179
+ vox4ai test -e aivisspeech --server-url http://localhost:10101 --style-id 888753760
180
+ ```
181
+
182
+ ### ヘルプ
183
+
184
+ ```bash
185
+ vox4ai --help # 全体ヘルプ
186
+ vox4ai say --help # say サブコマンドのヘルプ
187
+ vox4ai save --help # save サブコマンドのヘルプ
188
+ ```
189
+
190
+ ## 🔧 プラグイン開発者向け
191
+
192
+ 独自のTTSエンジンをプラグイン化するには、`pyproject.toml` にエントリーポイントを定義するだけです。
193
+ 詳細は各プラグインリポジトリのドキュメントを参照してください。
194
+
195
+ ## 🔍 検証環境
196
+
197
+ - **OS**: Windows 11 + WSL2 (Ubuntu)
198
+ - **確認日**: 2026-05-09
199
+ - **確認プラグイン**: aivisspeech (Engine v1.2.0), edgetts (edge-tts v7.2.8), kokoro
200
+ - **確認内容**:
201
+ - `vox4ai say` / `save` / `list` / `test` / `--doctor` / `--commands` 全動作確認
202
+ - 全ユニットテストパス: bridge 11件, aivisspeech 16件, edgetts 22件
203
+
204
+ ## 📜 ライセンス
205
+
206
+ MIT License
@@ -0,0 +1,8 @@
1
+ tts_plugin_bridge/__init__.py,sha256=j_sW8A59H4E6eVxitk5VF5z6KivVF3mpTmCRBmCoxgA,175
2
+ tts_plugin_bridge/chunker.py,sha256=wpSXO-vQgaVPHLgorqmp71FaXbdQzPgBG483oJzdzuk,4099
3
+ tts_plugin_bridge/factory.py,sha256=8KHdNpLnW8iZJrlXEWybS62OO2kkcXUFhSxF8Y0HhCw,1568
4
+ tts_plugin_bridge/protocol.py,sha256=pSXQkued_9vJl6ERRZhmLg-PCbykNlsCaca0Lszduyg,3602
5
+ tts_plugin_bridge-0.1.0.dist-info/METADATA,sha256=AykqfbJzuuUDn6AH_ZnrIdjAePtPwf9c0JXNRUQFtho,8741
6
+ tts_plugin_bridge-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ tts_plugin_bridge-0.1.0.dist-info/licenses/LICENSE,sha256=RnwZkqBFkeDYZDf3AlIh-qwLMPsJJajhB2YwaRRDWMU,1062
8
+ tts_plugin_bridge-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vox4ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.