├── main.py ├── utils.py ├── modules ├── split_data_to_chunks.py ├── audio_transcriber.py ├── youtube_scraper.py └── audio_splitter.py ├── README.md └── LICENSE /main.py: -------------------------------------------------------------------------------- 1 | from modules.audio_transcriber import AudioTranscriber 2 | 3 | def main(): 4 | name = 'ADD_NAME_HERE' 5 | target_lang = 'ADD_LANG_HERE' 6 | transcriber = AudioTranscriber(csv_filename=f'{name}.csv', target_lang=target_lang) 7 | 8 | folder_path = 'PATH/TO/AUDIOS' 9 | transcriber.transcribe_audio_folder(folder_path) 10 | 11 | if __name__ == '__main__': 12 | main() -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pydub import AudioSegment 3 | import soundfile as sf 4 | import pandas as pd 5 | 6 | def get_audio_durations(csv_path, folder_path): 7 | audio_durations = [] 8 | df = pd.read_csv(csv_path) 9 | for i, file_name in enumerate(df['splitted_audio_name'].tolist()): 10 | if i > 10: 11 | break 12 | file_path = os.path.join(folder_path, file_name) 13 | try: 14 | audio = AudioSegment.from_file(file_path) 15 | duration = audio.duration_seconds 16 | audio_durations.append(duration) 17 | except Exception as e: 18 | print(f"Error processing {file_name}: {e}") 19 | 20 | df['duration'] = audio_durations 21 | csv_path = csv_path.replace('.csv', '_new') + '.csv' 22 | df.to_csv(csv_path, encoding='utf-8-sig', index=False) 23 | 24 | def sample_rate_finder(folder_path): 25 | samples = [] 26 | for i, audio in enumerate(os.listdir(folder_path)): 27 | print(f'Processing audio {i}') 28 | audio_path = os.path.join(folder_path, audio) 29 | data, sample_rate = sf.read(audio_path) 30 | samples.append(len(data)) 31 | 32 | -------------------------------------------------------------------------------- /modules/split_data_to_chunks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | 5 | class SplitDataset: 6 | 7 | 8 | def __init__(self, folder_path, new_folder_path, chunk_size): 9 | self.folder_path = folder_path 10 | self.new_folder_path = new_folder_path 11 | self.chunk_size = chunk_size 12 | 13 | 14 | def split_into_chunks(self): 15 | audios = sorted(os.listdir(self.folder_path)) 16 | 17 | audios_chunks = [audios[i:i+self.chunk_size] for i in range(0, len(audios), self.chunk_size)] 18 | 19 | total_audio_files = sum(len(chunk) for chunk in audios_chunks) 20 | print(f"Total number of audio files: {total_audio_files}") 21 | 22 | for i, chunk in enumerate(audios_chunks): 23 | new_chunk_path = self.new_folder_path + f'_{i+1}' 24 | os.makedirs(new_chunk_path, exist_ok=True) 25 | for audio in chunk: 26 | current_audio_path = os.path.join(self.folder_path, audio) 27 | new_audio_path = os.path.join(new_chunk_path, audio) 28 | shutil.move(current_audio_path, new_audio_path) 29 | 30 | 31 | class MergeDataset: 32 | 33 | def __init__(self, folders_path, new_folder_path): 34 | self.folders_path = folders_path 35 | self.new_folder_path = new_folder_path 36 | 37 | 38 | def merge_folders(self): 39 | folders = os.listdir(self.folders_path) 40 | 41 | for folder in folders: 42 | if 'test' not in folder: 43 | continue 44 | for audio in os.listdir(folder): 45 | current_audio_path = os.path.join(folder, audio) 46 | new_audio_path = os.path.join(self.new_folder_path, audio) 47 | shutil.move(current_audio_path, new_audio_path) 48 | 49 | 50 | if __name__ == '__main__': 51 | # split = SplitDataset('./test', './test_folder', 10) 52 | # split.split_into_chunks() 53 | merge_dataset = MergeDataset('./', './audios/splitted_audios') 54 | merge_dataset.merge_folders() -------------------------------------------------------------------------------- /modules/audio_transcriber.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pandas as pd 3 | from transformers import pipeline 4 | from transformers import pipeline, AutoTokenizer 5 | import torch 6 | import time 7 | 8 | # Check if MPS is available and set device accordingly 9 | if torch.backends.mps.is_available(): 10 | device = torch.device("mps") 11 | else: 12 | device = torch.device("cpu") # Fallback to CPU if MPS is not available 13 | 14 | class AudioTranscriber: 15 | 16 | 17 | def __init__(self, csv_filename, model_name="facebook/seamless-m4t-v2-large", target_lang="arb", output_csv_dir='./datasets_csv/audio_text_datasets'): 18 | tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True) 19 | # Use the determined device 20 | self.transcription_pipeline = pipeline("automatic-speech-recognition", model=model_name, tokenizer=tokenizer,device=0) 21 | self.csv_filename = csv_filename 22 | self.target_lang = target_lang 23 | self.output_csv_dir = output_csv_dir 24 | 25 | 26 | def transcribe_audio(self, audio_path): 27 | transcription = self.transcription_pipeline(audio_path, generate_kwargs={"tgt_lang": self.target_lang}) 28 | return transcription 29 | 30 | 31 | def transcribe_audio_folder(self, folder_path): 32 | transcriptions = {} 33 | sorted_filenames = sorted(os.listdir(folder_path)) 34 | start = time.time() 35 | for filename in sorted_filenames: 36 | if filename.endswith(".wav"): 37 | audio_path = os.path.join(folder_path, filename) 38 | transcription = self.transcribe_audio(audio_path) 39 | transcriptions[filename] = transcription['text'] 40 | print(f"Finished processing {filename} , {i} out of {len(sorted_filenames)}") 41 | end = time.time() 42 | print(f"Time taken: {end - start}") 43 | keys, values = zip(*transcriptions.items()) 44 | 45 | df = pd.DataFrame({ 46 | 'filename': keys, 47 | 'text': values 48 | }) 49 | 50 | df.to_csv(os.path.join(self.output_csv_dir, self.csv_filename), encoding='utf-8-sig', index=False) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Egyptian-Text-To-Speech 2 | 3 | # EGTTS V0.1 4 | EGTTS V0.1 is a cutting-edge text-to-speech (TTS) model specifically designed for Egyptian Arabic. Built on the XTTS v2 architecture, it transforms written Egyptian Arabic text into natural-sounding speech, enabling seamless communication in various applications such as voice assistants, educational tools, and chatbots. 5 | 6 | ## Try It Out 7 | ✨ **Experience the magic of EGTTS V0.1 live!** Try the model directly through this [HuggingFace Space](https://huggingface.co/spaces/MohamedRashad/Egyptian-Arabic-TTS). 8 | 9 | ## Download The Model 10 | 📥 **Get the model now!** Download EGTTS V0.1 directly from [HuggingFace](https://huggingface.co/OmarSamir/EGTTS-V0.1). 11 | 12 | ## Quick Start 13 | ### Dependencies to install 14 | ```bash 15 | pip install git+https://github.com/coqui-ai/TTS 16 | 17 | pip install transformers 18 | 19 | pip install deepspeed 20 | ``` 21 | ### Inference 22 | #### Load the model 23 | ```python 24 | import os 25 | import torch 26 | import torchaudio 27 | from TTS.tts.configs.xtts_config import XttsConfig 28 | from TTS.tts.models.xtts import Xtts 29 | 30 | CONFIG_FILE_PATH = 'path/to/config.json' 31 | VOCAB_FILE_PATH = 'path/to/vocab.json' 32 | MODEL_PATH = 'path/to/model' 33 | SPEAKER_AUDIO_PATH = 'path/to/speaker.wav' 34 | 35 | print("Loading model...") 36 | config = XttsConfig() 37 | config.load_json(CONFIG_FILE_PATH) 38 | model = Xtts.init_from_config(config) 39 | model.load_checkpoint(config, checkpoint_dir=MODEL_PATH, use_deepspeed=True, vocab_path=VOCAB_FILE_PATH) 40 | model.cuda() 41 | 42 | print("Computing speaker latents...") 43 | gpt_cond_latent, speaker_embedding = model.get_conditioning_latents(audio_path=[SPEAKER_AUDIO_PATH]) 44 | ``` 45 | 46 | #### Run the model 47 | ```python 48 | from IPython.display import Audio, display 49 | 50 | text = "صباح الخير" 51 | print("Inference...") 52 | out = model.inference( 53 | text, 54 | "ar", 55 | gpt_cond_latent, 56 | speaker_embedding, 57 | temperature=0.75, 58 | ) 59 | 60 | AUDIO_OUTPUT_PATH = "path/to/output_audio.wav" 61 | torchaudio.save("xtts_audio.wav", torch.tensor(out["wav"]).unsqueeze(0), 24000) 62 | display(Audio(AUDIO_OUTPUT_PATH, autoplay=True)) 63 | ``` 64 | 65 | ## Citation 66 | 67 | ```bibtex 68 | @misc{omarsamir, 69 | author = {Omar Samir, Youssef Waleed, Youssef Tamer ,and Amir Mohamed}, 70 | title = {Fine-Tuning XTTS V2 for Egyptian Arabic}, 71 | year = {2024}, 72 | url = {https://github.com/joejoe03/Egyptian-Text-To-Speech}, 73 | } 74 | ``` 75 | -------------------------------------------------------------------------------- /modules/youtube_scraper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | import pandas as pd 5 | from selenium import webdriver 6 | from selenium.webdriver.common.by import By 7 | 8 | 9 | class YouTubeScraper: 10 | 11 | 12 | def __init__(self, channel_name, channel_url, voice, output_dir, csv_name): 13 | self.channel_name = channel_name 14 | self.channel_url = channel_url 15 | self.voice = voice 16 | self.output_dir = output_dir 17 | self.csv_name = csv_name 18 | self._driver = webdriver.Chrome() 19 | 20 | 21 | def _scroll_to_end(self): 22 | last_height = self._driver.execute_script("return document.documentElement.scrollHeight") 23 | 24 | while True: 25 | self._driver.execute_script("window.scrollTo(0, document.documentElement.scrollHeight);") 26 | time.sleep(2) 27 | new_height = self._driver.execute_script("return document.documentElement.scrollHeight") 28 | 29 | if new_height == last_height: 30 | break 31 | 32 | last_height = new_height 33 | 34 | 35 | def _split_time_ago(self, text): 36 | splitted_text = text.split() 37 | num = splitted_text[0] 38 | time_period = splitted_text[1].replace('s', '') 39 | return num, time_period 40 | 41 | 42 | def collect_data(self): 43 | self._driver.get(self.channel_url) 44 | time.sleep(2) 45 | 46 | self._scroll_to_end() 47 | 48 | video_title_lst = [] 49 | release_date_lst1 = [] 50 | release_date_lst2 = [] 51 | video_link_lst = [] 52 | video_duration_lst = [] 53 | 54 | video_details = self._driver.find_elements(By.ID, 'video-title-link') 55 | video_time_dates = self._driver.find_elements(By.XPATH, '//*[@id="metadata-line"]/span[2]') 56 | video_durations = self._driver.find_elements(By.XPATH, '//*[@id="length"]') 57 | 58 | for i, video_detail in enumerate(video_details): 59 | video_link_lst.append(video_detail.get_attribute('href')) 60 | video_title_lst.append(video_detail.get_attribute('title')) 61 | video_duration_lst.append(video_durations[i].get_attribute('aria-label').split()[0]) 62 | 63 | num, time_period = self._split_time_ago(video_time_dates[i].text) 64 | release_date_lst1.append(num) 65 | release_date_lst2.append(time_period) 66 | 67 | self._driver.quit() 68 | 69 | df = pd.DataFrame({ 70 | 'channel_name': [self.channel_name]*len(video_link_lst), 71 | 'video_title': video_title_lst, 72 | 'release_date_1': release_date_lst1, 73 | 'release_date_2': release_date_lst2, 74 | 'video_link': video_link_lst, 75 | 'video_duration (min)': video_duration_lst, 76 | 'voice': [self.voice]*len(video_link_lst), 77 | }) 78 | 79 | df.to_csv(os.path.join(self.output_dir, self.csv_name), encoding='utf-8-sig', index=False) -------------------------------------------------------------------------------- /modules/audio_splitter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import yt_dlp 3 | import pandas as pd 4 | from pydub import AudioSegment 5 | from tqdm import tqdm 6 | from pydub.silence import split_on_silence 7 | 8 | 9 | class AudioSplitter: 10 | 11 | 12 | def __init__(self, 13 | csv_path, 14 | audio_name, 15 | channel_name, 16 | output_csv_name, 17 | silence_len=200, 18 | silence_thresh=-40, 19 | output_splitted_audio_dir='./audios/splitted_audios', 20 | output_audio_dir='./audios', 21 | output_csv_dir='./datasets_csv/audio_datasets', 22 | conditional_function=None, 23 | ): 24 | self.audio_name = audio_name 25 | self.channel_name = channel_name 26 | self.output_csv_name = output_csv_name 27 | self.silence_len = silence_len 28 | self.silence_thresh = silence_thresh 29 | self.output_splitted_audio_dir = output_splitted_audio_dir 30 | self.output_audio_dir = output_audio_dir 31 | self.output_csv_dir = output_csv_dir 32 | self.conditional_function = conditional_function 33 | self.ydl_opts = { 34 | 'format': 'bestaudio', 35 | 'extractaudio': True, 36 | 'audioformat': 'wav', 37 | 'verbose': True, 38 | 'writesubtitles': False, 39 | 'writeautomaticsub': False, 40 | 'skip_download': False, 41 | 'postprocessors': [{ 42 | 'key': 'FFmpegExtractAudio', 43 | 'preferredcodec': 'wav', 44 | }] 45 | } 46 | 47 | self.df = pd.read_csv(csv_path, encoding='utf-8-sig') 48 | 49 | 50 | def _conditional_function_caller(self, func): 51 | return func() 52 | 53 | 54 | def _match_target_amplitude(self, aChunk, target_dBFS): 55 | change_in_dBFS = target_dBFS - aChunk.dBFS 56 | return aChunk.apply_gain(change_in_dBFS) 57 | 58 | 59 | def split_audio(self, filename): 60 | audio = AudioSegment.from_file(os.path.join(self.output_audio_dir, filename+'.wav'), format="wav") 61 | chunks = split_on_silence ( 62 | audio, 63 | min_silence_len = self.silence_len, 64 | silence_thresh = self.silence_thresh 65 | ) 66 | original_audio_name_lst = [] 67 | splitted_audio_name_lst = [] 68 | for i, chunk in tqdm(enumerate(chunks)): 69 | silence_chunk = AudioSegment.silent(duration=50) 70 | audio_chunk = silence_chunk + chunk + silence_chunk 71 | normalized_chunk = self._match_target_amplitude(audio_chunk, -20.0) 72 | new_filename = filename+f'_chunk_{i}' 73 | normalized_chunk.export( 74 | os.path.join(self.output_splitted_audio_dir, new_filename+'.wav'), 75 | bitrate = "192k", 76 | format = "wav" 77 | ) 78 | 79 | splitted_audio_name_lst.append(new_filename) 80 | 81 | original_audio_name_lst.extend([filename]*len(splitted_audio_name_lst)) 82 | 83 | if self.conditional_function != None: 84 | lst, _ = self._conditional_function_caller(lambda: self.conditional_function(splitted_audio_name_lst, original_audio_name_lst, self.output_splitted_audio_dir)) 85 | splitted_audio_name_lst, original_audio_name_lst = lst[0], lst[1] 86 | 87 | return splitted_audio_name_lst, original_audio_name_lst 88 | 89 | 90 | def process_videos(self): 91 | video_title_lst = [] 92 | splitted_audio_name_lst = [] 93 | original_audio_name_lst = [] 94 | voice_lst = [] 95 | os.makedirs(self.output_audio_dir, exist_ok=True) 96 | 97 | for index, row in self.df.iterrows(): 98 | filename = f"{row['channel_name'].lower()}_{self.audio_name}_{index}" 99 | self.ydl_opts['outtmpl'] = os.path.join(self.output_audio_dir, f'{filename}') 100 | 101 | try: 102 | with yt_dlp.YoutubeDL(self.ydl_opts) as ydl: 103 | ydl.download([row['video_link']]) 104 | print(f"\n\nSuccessfully downloaded: {row['video_title']}") 105 | 106 | except Exception as e: 107 | print(f"\n\nError downloading {row['video_title']}: {str(e)}") 108 | 109 | splitted_audio_lst, original_audio_lst = self.split_audio(filename) 110 | splitted_audio_name_lst.extend(splitted_audio_lst) 111 | original_audio_name_lst.extend(original_audio_lst) 112 | video_title_lst.extend([row['video_title']]*len(original_audio_lst)) 113 | voice_lst.extend([row['voice']]*len(original_audio_lst)) 114 | 115 | output_df = pd.DataFrame({ 116 | 'channel_name': [self.channel_name]*len(video_title_lst), 117 | 'video_title': video_title_lst, 118 | 'original_audio_name': original_audio_name_lst, 119 | 'splitted_audio_name': splitted_audio_name_lst, 120 | 'voice': voice_lst, 121 | }) 122 | 123 | output_df.to_csv(os.path.join(self.output_csv_dir, self.output_csv_name), encoding='utf-8-sig', index=False) 124 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------