diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..718c7d994139563c28cc492e9ca19c11e36f1f68 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,35 +1,6 @@ -*.7z filter=lfs diff=lfs merge=lfs -text -*.arrow filter=lfs diff=lfs merge=lfs -text -*.bin filter=lfs diff=lfs merge=lfs -text -*.bz2 filter=lfs diff=lfs merge=lfs -text -*.ckpt filter=lfs diff=lfs merge=lfs -text -*.ftz filter=lfs diff=lfs merge=lfs -text +*.q4 filter=lfs diff=lfs merge=lfs -text +*.q8 filter=lfs diff=lfs merge=lfs -text *.gz filter=lfs diff=lfs merge=lfs -text -*.h5 filter=lfs diff=lfs merge=lfs -text -*.joblib filter=lfs diff=lfs merge=lfs -text -*.lfs.* filter=lfs diff=lfs merge=lfs -text -*.mlmodel filter=lfs diff=lfs merge=lfs -text -*.model filter=lfs diff=lfs merge=lfs -text -*.msgpack filter=lfs diff=lfs merge=lfs -text -*.npy filter=lfs diff=lfs merge=lfs -text -*.npz filter=lfs diff=lfs merge=lfs -text -*.onnx filter=lfs diff=lfs merge=lfs -text -*.ot filter=lfs diff=lfs merge=lfs -text -*.parquet filter=lfs diff=lfs merge=lfs -text -*.pb filter=lfs diff=lfs merge=lfs -text -*.pickle filter=lfs diff=lfs merge=lfs -text -*.pkl filter=lfs diff=lfs merge=lfs -text -*.pt filter=lfs diff=lfs merge=lfs -text -*.pth filter=lfs diff=lfs merge=lfs -text -*.rar filter=lfs diff=lfs merge=lfs -text -*.safetensors filter=lfs diff=lfs merge=lfs -text -saved_model/**/* filter=lfs diff=lfs merge=lfs -text -*.tar.* filter=lfs diff=lfs merge=lfs -text -*.tar filter=lfs diff=lfs merge=lfs -text -*.tflite filter=lfs diff=lfs merge=lfs -text -*.tgz filter=lfs diff=lfs merge=lfs -text -*.wasm filter=lfs diff=lfs merge=lfs -text -*.xz filter=lfs diff=lfs merge=lfs -text -*.zip filter=lfs diff=lfs merge=lfs -text -*.zst filter=lfs diff=lfs merge=lfs -text -*tfevents* filter=lfs diff=lfs merge=lfs -text +*.gguf filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +assets/banner.png filter=lfs diff=lfs merge=lfs -text diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..340cf0c959586320b003a9ed4ad61ec65f8f90e5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,158 @@ +Creative Commons Attribution-NonCommercial 4.0 International + + Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. + +Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors. + +Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public. + +Creative Commons Attribution-NonCommercial 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. + +Section 1 – Definitions. + + a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + + d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. + + g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. + + i. NonCommercial means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. + + j. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. + + k. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. + + l. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. + +Section 2 – Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: + + A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and + + B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only. + + 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. + + 3. Term. The term of this Public License is specified in Section 6(a). + + 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. + + 5. Downstream recipients. + + A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. + + B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. + + 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this Public License. + + 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. + +Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified form), You must: + + A. retain the following if it is supplied by the Licensor with the Licensed Material: + + i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of warranties; + + v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; + + B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and + + C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. + + 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. + +Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only; + + b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. +For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. + +Section 5 – Disclaimer of Warranties and Limitation of Liability. + + a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. + + b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. + + c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. + +Section 6 – Term and Termination. + + a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +Section 7 – Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. + +Section 8 – Interpretation. + + a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. + + c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. + + d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. + +Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/LICENSE-APACHE-2.0 b/LICENSE-APACHE-2.0 new file mode 100644 index 0000000000000000000000000000000000000000..011cab2a541afeac0bd0bacd5903496485d412bf --- /dev/null +++ b/LICENSE-APACHE-2.0 @@ -0,0 +1,209 @@ +AksharaLP by Sourojit Dhua — https://huggingface.co/sourojitdotcom/AksharaLP-1M-v2 +Copyright 2026 Sourojit Dhua + +Licensed under the Apache License, Version 2.0 (the full text follows). Commercial use is permitted. +Redistributions and derivative works must include the attribution notices in the accompanying +NOTICE file, as required by Section 4(d) of the License. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000000000000000000000000000000000000..37cd22d5ed7451df3557970469f82a269656d5ce --- /dev/null +++ b/NOTICE @@ -0,0 +1,75 @@ +AksharaLP-MultiIndic-8M — Sourojit Dhua, 2026 +https://huggingface.co/sourojitdotcom/AksharaLP-MultiIndic-8M + +Combined model-card licence: CC-BY-NC-4.0. NON-COMMERCIAL RELEASE. +The new Bengali/Telugu/Kannada/Tamil weights include Samanantar-derived Aksharantar rows. The upstream Samanantar card specifies CC-BY-NC-4.0; other publisher pages describe CC0 packaging. This release does not certify those weights as commercially clean. An Apache-2.0 clean-data retrain is planned. Preserve source attributions and applicable upstream terms. Selecting CC-BY-NC does not resolve all derivative-rights questions. + +Original Hindi word/sentence components and runtime code retain Apache-2.0 and their existing notices. They are independently available at: +https://huggingface.co/sourojitdotcom/AksharaLP-1M-v2 +https://huggingface.co/sourojitdotcom/AksharaLP-Sentence-2.8M + +Attributions: +Aksharantar — AI4Bharat, Madhani et al. (2022), CC0 mined/existing packaging and CC-BY manual data. +Samanantar — AI4Bharat, Ramesh et al. (2022), upstream card CC-BY-NC-4.0. +Dakshina — Google Research, Roark et al., Processing South Asian Languages Written in the Latin Script, LREC 2020, https://github.com/google-research-datasets/dakshina ; CC-BY-SA-4.0 https://creativecommons.org/licenses/by-sa/4.0/ . Source-attributed training pairs appear inside Aksharantar. Retain the upstream terms. +IndicCorp v2 — AI4Bharat, Doddapaneni et al. (ACL 2023), CC0. Dictionaries and Hindi LM training. +IndicXlit — AI4Bharat, MIT. Distillation teacher and comparison model, not redistributed. +Inter and Noto Sans Indic fonts in the Space — SIL Open Font License 1.1 (included beside each font). + +For the unchanged Hindi components, retain the following original notice as well: + +AksharaLP-Sentence-2.8M by Sourojit Dhua +https://huggingface.co/sourojitdotcom/AksharaLP-Sentence-2.8M +Copyright 2026 Sourojit Dhua + +AksharaLP-Sentence-2.8M is licensed under the Apache License, Version 2.0 (see LICENSE). +Commercial use is permitted. + +What is in here + * The v2 word model: a 989K-parameter character Transformer, its reranker, the dictionary pack + (lexicon + word-bigram language model) and the JavaScript/Python runtime, unchanged from + AksharaLP-1M-v2 (https://huggingface.co/sourojitdotcom/AksharaLP-1M-v2). + * The sentence add-on: nlm.q8 (a 2,770,752-parameter Hindi neural language model trained only on + IndicCorp v2 Hindi, CC0, with our own tokenizer, training code and runtime), plus nlm.mjs, v3.mjs + and sent.mjs. This add-on and its training data are commercially clean; the full system still + inherits the v2 word package's terms, which are Apache-2.0 as set out here. + +ATTRIBUTION +Under Section 4(d) of the Apache License, any redistribution of this work or of a Derivative Work +(for example: shipping these files in an app, a web page, a browser extension or an SDK, or +releasing a fine-tuned, quantized or converted model based on it) must include a readable copy of +this NOTICE file. Please use the following attribution text: + + Transliteration by AksharaLP (Sourojit Dhua) — https://huggingface.co/sourojitdotcom/AksharaLP-Sentence-2.8M + +Recommended in-app credit (e.g. in an About, Settings or Licenses screen): + + Hindi typing powered by AksharaLP by Sourojit Dhua + +The model files in this repository were built with the help of the following +third-party resources. No third-party data is redistributed in this repository +other than as learned model parameters and derived word lists. + +------------------------------------------------------------------------------ +IndicCorp v2 — AI4Bharat + https://github.com/AI4Bharat/IndicBERT/tree/main#indiccorp-v2 + License: CC0 + Used for: training the sentence language model (nlm.q8); the word-model lexicon and + word-bigram language model. + +Aksharantar — AI4Bharat + https://huggingface.co/datasets/ai4bharat/Aksharantar + License: CC0 (mined data and existing sources) / CC-BY (manually collected data) + Used for: romanized <-> Devanagari word pairs for training the word model. + +IndicXlit — AI4Bharat + https://github.com/AI4Bharat/IndicXlit + License: MIT + Used for: teacher model for word-model distillation. + +CMUdict — Carnegie Mellon University + https://github.com/cmusphinx/cmudict + License: BSD-2-Clause-style + Used for: English word cues in the word model. + +Fonts served by the demo Space (Inter, Noto Sans Devanagari) are under the SIL Open Font License 1.1. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9c33ba0fb447592f6015c8a4f09120b7780c070e --- /dev/null +++ b/README.md @@ -0,0 +1,305 @@ +--- +license: cc-by-nc-4.0 +language: +- hi +- bn +- te +- kn +- ta +tags: +- transliteration +- indic +- multilingual +- tiny +- browser +- javascript +- offline +- input-method +- gguf +- custom-architecture +datasets: +- ai4bharat/Aksharantar +- ai4bharat/IndicCorpV2 +metrics: +- accuracy +--- + +

AksharaLP MultiIndic — five scripts, one small footprint

+

Try the private browser demo ↗ · Measured benchmarks · 13 GGUF files · Licence & data

+ +# AksharaLP-MultiIndic-8M + +### Five word transliterators. One optional Hindi sentence model. Your device. + +**Hindi · Bengali · Telugu · Kannada · Tamil** — familiar Roman letters in, native script out. Five independent ~989K-parameter character Transformers, a lazily loaded dictionary per language, and the already-shipped **2,770,752-parameter Hindi sentence LM**. No inference server and no GPU required for inference. This is **transliteration, not translation or a general-purpose language model**. + +> **Private review · non-commercial release.** This bundle's model-card licence is **CC-BY-NC-4.0**, not Apache-2.0. The new Bengali/Telugu/Kannada/Tamil weights include Samanantar-derived Aksharantar rows with non-commercial upstream terms. A commercially clean Apache-2.0 retrain is planned. The Hindi word and sentence components remain independently available under Apache-2.0 in their original repositories. See [Licence & data](#licence--data). + +

4.94M

five word models combined

7.72M

with the optional Hindi LM

20.55 MB

all word payloads, gzip-9

23.48 MB

including Hindi sentence mode
+ +The **8M name rounds 7,715,520 neural parameters**, not a billion-parameter model. Dictionaries and small candidate-reranker coefficients are separate from that count. Loading one language does not load the other four. Browser inference uses float32 arithmetic after loading low-bit weights: small files do **not** mean equally small RAM use. + +## What this release does — and does not prove + +- Every word Transformer is **988,928 parameters**, except Bengali (**989,056**, with one additional output character). The five sum to **4,944,768**; optional Hindi LM adds **2,770,752**. +- New word weights are about **0.54 MB gzip-9 per language**; model-plus-dictionary payloads are about **3.05–3.23 MB** each. Hindi keeps its larger, established 3M-word dictionary pack. +- IndicXlit has **11,487,744 neural parameters** in the comparison port. Its per-language original dictionaries are much larger; the exact byte comparison below uses their released JSON formats, not an optimized repacking. +- **We trail IndicXlit's full package in every new language on top-1, top-3 and top-8.** This release is a storage/accuracy trade-off, not a state-of-the-art or “best in class” claim. +- Sentence-level neural rescoring is **Hindi-only**. Bengali, Telugu, Kannada and Tamil convert word by word. The separate Bengali real-chat research front-end is **not** part of this bundle. + +## Benchmarks + +Per-language top-1 test comparison, including confidence intervals + +**Aksharantar test, exact match, any accepted reference.** Top-1 %, 95% bootstrap intervals in brackets; paired differences are percentage points, ours minus IndicXlit. + +| Language | Roman words | Ours + dictionary | IndicXlit full package | Paired gap [95% CI] | +|---|---:|---:|---:|---:| +| Hindi | 9,991 | 70.04 [69.16, 70.90] | 69.33 [68.44, 70.23] | +0.71 [-0.04, +1.40] | +| Bengali | 13,367 | 64.64 [63.83, 65.44] | 70.52 [69.76, 71.34] | -5.88 [-6.52, -5.22] | +| Telugu | 9,936 | 78.74 [77.93, 79.53] | 82.42 [81.65, 83.14] | -3.67 [-4.27, -3.07] | +| Kannada | 11,165 | 77.37 [76.61, 78.13] | 82.61 [81.92, 83.29] | -5.24 [-5.83, -4.67] | +| Tamil | 11,405 | 71.78 [71.01, 72.61] | 75.95 [75.20, 76.73] | -4.16 [-4.75, -3.59] | + +| Language | Ours top-1 / top-3 / top-8 % | IndicXlit full top-1 / top-3 / top-8 % | +|---|---:|---:| +| Hindi | 70.04 / 88.31 / 93.63 | 69.33 / 85.40 / 88.29 | +| Bengali | 64.64 / 75.66 / 77.97 | 70.52 / 82.25 / 85.44 | +| Telugu | 78.74 / 86.87 / 88.63 | 82.42 / 90.63 / 92.75 | +| Kannada | 77.37 / 85.80 / 87.68 | 82.61 / 90.68 / 92.41 | +| Tamil | 71.78 / 80.05 / 82.61 | 75.95 / 84.21 / 87.26 | + +**Protocol and caveats** + +1. For Bengali/Telugu/Kannada/Tamil, the shipped q4 file, top-500k IndicCorp frequency list and eight-feature linear reranker were frozen before test. The reranker was fitted on **all VALID**, not on test, using the same fixed feature design and L2 regularization selected during validation work. Quantized VALID candidates were regenerated before that fit. There was **one inference pass on the full test per frozen artifact**, with cached predictions scored once; no post-test tuning. +2. NFC exact match, ZWJ/ZWNJ removed, any reference accepted. Top-k means at least one reference among the first k candidates, not “k times more accurate.” Intervals use **2,000 bootstrap resamples, seed 0**; paired intervals resample the same words together. Full top-k intervals, hashes and frozen configurations are in [evaluation/](evaluation/). +3. IndicXlit scores reuse existing **beam-8 PyTorch-port predictions with the full official frequency dictionary formula**, rescored on exactly the same rows. This is not a fresh run of the official fairseq package. The port masks output to each language's script block. Bengali has one empty Roman input skipped by that runner; its missing prediction is counted incorrect and the row is not dropped. +4. Hindi numbers are copied from the measured [AksharaLP-1M-v2 release](https://huggingface.co/sourojitdotcom/AksharaLP-1M-v2), **not re-tested**. Its candidate generation/reranker and 3M-word dictionary differ from the four new models. The old Hindi full-package comparison interval includes zero; it does not establish a significant top-1 win. +5. Training reports record **zero valid/test native-word and Roman-form overlap** for all four new training files. The general IndicCorp frequency dictionaries are not test-filtered and can naturally contain test reference words. Distillation teachers may have their own historical overlap. This is not an unseen-vocabulary-only benchmark, and Aksharantar words are not a WhatsApp sentence benchmark. + +## Hindi sentence mode — the shipped add-on, unchanged + +The [AksharaLP-Sentence-2.8M](https://huggingface.co/sourojitdotcom/AksharaLP-Sentence-2.8M) LM chooses jointly among candidates already proposed for each typed Hindi word. It cannot translate, invent a missing candidate, reorder words, or answer questions. The runtime exposes the existing candidate-membership guard. + +| Dakshina Hindi sentences, 4,976 test items | Existing Hindi v2 word package + bigram context | With the neural sentence LM | +|---|---:|---:| +| Word accuracy % | 89.02 | 89.44 | +| Entire sentences exactly right % | 25.70 | 26.73 | +| Character error rate % | 4.07 | 3.93 | +| Word error rate % | 11.20 | 10.78 | + +These are the **existing one-time published results, not new evaluations**. The 89.02/25.70 baseline already uses the older Hindi bigram context decoder; it is not the context-off demo toggle. Paired reported gain: word accuracy **+0.42 [0.33, 0.51]** points, sentence exact match **+1.02 [0.40, 1.65]** points (the difference of rounded sentence percentages is 1.03). This release retains the default **q8** LM and original settings. No sentence claims are made for the other four languages. + +## Footprint & latency + +Canonical uncompressed per-language payload comparison on a logarithmic scale + +| Language payload | Canonical raw bytes | Sum gzip-9 bytes | JS CPU p50 / p95 ms per word | +|---|---:|---:|---:| +| Hindi | 16,221,771 | 7,905,041 (7.91 MB) | 207.02 / 319.99 | +| Bengali | 14,658,574 | 3,049,313 (3.05 MB) | 51.61 / 74.00 | +| Telugu | 16,579,274 | 3,222,365 (3.22 MB) | 51.77 / 87.87 | +| Kannada | 17,376,245 | 3,224,222 (3.22 MB) | 55.35 / 100.77 | +| Tamil | 18,695,389 | 3,130,045 (3.13 MB) | 60.98 / 103.39 | +| Shared word JS runtime + root configuration | 68,781 | 23,186 | — | +| **All five word packages + shared runtime** | **83,600,034** | **20,554,172 (20.55 MB)** | — | +| Optional Hindi q8 sentence LM + JS add-on | 3,256,675 | 2,928,839 (2.93 MB) | Not re-timed | +| **All languages + Hindi sentence add-on** | **86,856,709** | **23,483,011 (23.48 MB)** | — | + +MB = 1,000,000 bytes. Canonical raw dictionary size means decompressed frequency TSV for new languages, and native lexicon/bigram binaries for Hindi. Browser downloads use gzip companions where provided. gzip-9 values are sums of per-file compression, not actual RAM, not total repository storage, and not a promise about HTTP Content-Encoding. Charts/docs/fonts/Python/GGUF alternatives and duplicated compressed companions are excluded. All exact component sizes are in [sizes.json](evaluation/sizes.json). + +Latency: **100 evenly spaced VALID words per language**, five warmups, cleared candidate memo per word, single JavaScript thread in **Node v24.22.1 on Intel(R) Xeon(R) Processor, Linux**; initialization excluded. This is **Node CPU latency, not a browser or phone speed claim**. Loading the large Hindi dictionary is substantially more expensive. Browser demo inference happens in a Web Worker and reports live timings. The 15 initial example word candidate lists matched Python exactly; a broader independent GGUF validation sample is below. + +IndicXlit size comparison: **45,950,976 bytes of FP32 neural parameters**, plus original dictionary JSON raw bytes: Hindi 358,118,856; Bengali 403,487,845; Telugu 761,940,466; Kannada 1,045,504,407; Tamil 1,044,620,827. These compare different precisions, vocabularies and data encodings; IndicXlit was not requantized or repacked. They do not prove that ours is the smallest possible system. + +## Real outputs + +Generated by the bundled JavaScript runtime, **not hand-written target strings**. These are fixed illustrative examples, not an accuracy suite or proof of sentence understanding. Multiword inputs outside Hindi are independent word conversions. Some outputs are wrong: in particular, Tamil `naan` currently produces **ணன்**, not the intended நான். We show it rather than hiding the failure. + +### Hindi + +| Roman input | Actual browser-runtime output | +|---|---| +| `namaste` | नमस्ते | +| `kaise` | कैसे | +| `dhanyavaad` | धन्यवाद | +| `mera naam rahul hai` | मेरा नाम राहुल है | + +### Bengali + +| Roman input | Actual browser-runtime output | +|---|---| +| `nomoskar` | নমস্কার | +| `ami` | আমি | +| `bhalo` | ভালো | +| `tumi kemon acho` | তুমি কেমন আছো | + +### Telugu + +| Roman input | Actual browser-runtime output | +|---|---| +| `namaskaram` | నమస్కారం | +| `nenu` | నేను | +| `manchi` | మంచి | +| `meeru ela unnaru` | మీరు ఎలా ఉన్నారు | + +### Kannada + +| Roman input | Actual browser-runtime output | +|---|---| +| `namaskara` | ನಮಸ್ಕಾರ | +| `nanu` | ನನು | +| `chennagide` | ಚೆನ್ನಾಗಿದೆ | +| `neevu hegiddira` | ನೀವು ಹೇಗಿದ್ದೀರಾ | + +### Tamil + +| Roman input | Actual browser-runtime output | +|---|---| +| `vanakkam` | வணக்கம் | +| `naan` | ணன் | +| `nandri` | நன்றி | +| `neenga eppadi irukeenga` | நீங்க எப்படி இருகீங்க | + +### Hindi sentence examples + +| Input | Actual output with the optional sentence LM | +|---|---| +| `main theek hoon` | मैं ठीक हूं | +| `ghar main kaun hai` | घर में कौन है | + +## JavaScript — browser, worker, or Node + +Copy `multiindic.mjs`, `aksharalp.mjs`, the `models/` tree and (for Hindi sentence mode) `nlm.mjs`, `v3.mjs`, `sent.mjs`. Serve over HTTPS or localhost; ES modules do not reliably work from `file://`. + +```js +import { MultiIndic } from './multiindic.mjs'; + +// Loads Bengali's weights + dictionary, not every language. +const engine = await MultiIndic.load('./', { language: 'bn' }); +console.log(engine.word('nomoskar', 8)); // ranked {text, score} objects +console.log(engine.transliterate('tumi kemon acho').output); + +await engine.setLanguage('hi'); +await engine.setSentence(true); // lazily loads the q8 Hindi LM +console.log(engine.transliterate('main theek hoon').output); +await engine.setSentence(false); // word-by-word, context off +``` + +```js +// Node >= 18: no dependencies, uses local files. +import {loadFromDir} from './multiindic.mjs'; +const engine = await loadFromDir('./', {language: 'ta'}); +console.log(engine.word('nandri', 3)); +``` + +Browser gzip decoding requires `DecompressionStream` support; Node uses its built-in zlib. Switching languages retains already loaded models in memory for reuse. Recreate the engine to release that cache. The demo preserves URLs, email addresses, @mentions, #tags, digits and already-native text. Hindi retains the plain reference convention of changing a final full stop to danda. Roman English words are otherwise treated as transliteration input; there is no multilingual code-switch detector. + +## Python reference + +Requires `numpy` and `torch`. For a private review repository, authenticate with Hugging Face normally; never put a write token in browser code. + +```python +from pathlib import Path +import sys +from huggingface_hub import snapshot_download + +root = Path(snapshot_download('sourojitdotcom/AksharaLP-MultiIndic-8M', token=True)) +sys.path.insert(0, str(root / 'python')) +from multiindic import MultiIndic + +engine = MultiIndic(root, language='te', threads=1) +print(engine.word('namaskaram', k=8)) +engine.set_language('hi').set_sentence(True) +print(engine.transliterate('main theek hoon')) +``` + +The Python reference returns candidate strings from `word()`; JavaScript additionally exposes uncalibrated ranking scores. Those scores are **not probabilities**. Hindi uses the plain reference token conversion in both runtimes, not the older demo's extra brand/acronym heuristics. The model/decoder parity samples are measured separately. + +## GGUF containers + +**These are custom-architecture GGUF v3 tensor containers. They do NOT run in llama.cpp, Ollama, or LM Studio.** Use the bundled [gguf_read.py](gguf_read.py) plus our decoder. A GGUF is not a standalone dictionary or language-model runtime. + +The writer/reader follows the [existing v2 export](https://huggingface.co/sourojitdotcom/AksharaLP-1M-v2). Word **q4 GGUF carries the byte-exact shipped mixed q4/q8/f16 blob in an I8 tensor**; it is not standard GGML Q4_0. F16 materializes those already-quantized weights as half floats; it does not restore an original full-precision checkpoint. Exact reranker configuration is retained as JSON metadata, and the external dictionary/runtime remain required. + +For Hindi sentence mode, **q8 GGUF preserves the test-measured default byte-exactly**. An existing **q4 LM** and F16 materialization are included as alternatives with validation parity only. **Do not attach the default q8 test accuracies to q4 or F16 alternatives.** The q4 LM is not the demo default. + +| GGUF file | Raw bytes | gzip-9 bytes | +|---|---:|---:| +| [aksharalp-hi-word-q4.gguf](gguf/aksharalp-hi-word-q4.gguf) | 632,416 | 562,579 | +| [aksharalp-hi-word-f16.gguf](gguf/aksharalp-hi-word-f16.gguf) | 2,026,048 | 1,348,288 | +| [aksharalp-bn-word-q4.gguf](gguf/aksharalp-bn-word-q4.gguf) | 591,136 | 541,220 | +| [aksharalp-bn-word-f16.gguf](gguf/aksharalp-bn-word-f16.gguf) | 1,984,864 | 1,321,177 | +| [aksharalp-te-word-q4.gguf](gguf/aksharalp-te-word-q4.gguf) | 591,008 | 542,744 | +| [aksharalp-te-word-f16.gguf](gguf/aksharalp-te-word-f16.gguf) | 1,984,608 | 1,329,379 | +| [aksharalp-kn-word-q4.gguf](gguf/aksharalp-kn-word-q4.gguf) | 591,008 | 540,642 | +| [aksharalp-kn-word-f16.gguf](gguf/aksharalp-kn-word-f16.gguf) | 1,984,608 | 1,319,622 | +| [aksharalp-ta-word-q4.gguf](gguf/aksharalp-ta-word-q4.gguf) | 591,008 | 542,094 | +| [aksharalp-ta-word-f16.gguf](gguf/aksharalp-ta-word-f16.gguf) | 1,984,608 | 1,328,348 | +| [aksharalp-hi-sentence-q8.gguf](gguf/aksharalp-hi-sentence-q8.gguf) | 3,488,192 | 2,981,426 | +| [aksharalp-hi-sentence-q4.gguf](gguf/aksharalp-hi-sentence-q4.gguf) | 2,110,912 | 1,596,808 | +| [aksharalp-hi-sentence-f16.gguf](gguf/aksharalp-hi-sentence-f16.gguf) | 5,790,336 | 5,159,347 | + +### Validation-only parity — not a second test evaluation + +First **40 ASCII-alphabetic Roman words per language from full VALID** (200 total). Same dictionary and reranker; no fitting or tuning during parity. “Top-8 order” compares the complete ordered candidate list. + +| Language | q4 top-1 identical | q4 top-8 order identical | F16 top-1 identical | F16 top-8 order identical | +|---|---:|---:|---:|---:| +| Hindi | 40/40 | 40/40 | 40/40 | 40/40 | +| Bengali | 40/40 | 40/40 | 40/40 | 40/40 | +| Telugu | 40/40 | 40/40 | 40/40 | 40/40 | +| Kannada | 40/40 | 40/40 | 40/40 | 40/40 | +| Tamil | 40/40 | 40/40 | 40/40 | 40/40 | + +Hindi LM: first **12 Dakshina VALID sentences with at most 12 Roman words**, fixed word-model lattices. q8 reproduces **12/12** source decoder outputs; F16 **12/12**; q4 **11/12**. Maximum absolute sequence-score deltas vs q8 are 0.000000, 0.008702, and 6.343046, respectively. This is conversion agreement, **not accuracy**. Across the same validation sample, JavaScript and Python matched **200/200 top-1 and complete top-8 ordered lists**, and **12/12 Hindi sentence outputs**, after using the same plain-reference token policy. Both our structural reader and independent `gguf-py` read all 13 files; complete data is in [gguf_parity.json](evaluation/gguf_parity.json). + +```python +# After the Python setup above: +engine.set_language('bn') +engine.load_word_gguf(root / 'gguf/aksharalp-bn-word-q4.gguf') +print(engine.word('nomoskar')) +engine.set_language('hi') +engine.load_sentence_gguf(root / 'gguf/aksharalp-hi-sentence-q8.gguf') +print(engine.transliterate('main theek hoon')) +``` + +## Licence & data + +**Model-card licence: CC-BY-NC-4.0. This release is non-commercial.** The Bengali, Telugu, Kannada and Tamil weights contain `aks-Samanantar` Aksharantar training rows. The [current upstream Samanantar card](https://huggingface.co/datasets/ai4bharat/samanantar) states **CC-BY-NC-4.0**. Aksharantar and the publisher's Samanantar packaging page describe CC0 packaging, so provenance terms conflict; this release does **not** certify those weights as commercially clean. A commercially clean **Apache-2.0 retrain is planned**, and would be separately evaluated before replacement. + +| Resource | Terms / role | +|---|---| +| [Aksharantar, AI4Bharat](https://huggingface.co/datasets/ai4bharat/Aksharantar) | CC0 mined/existing packaging; CC-BY manual data. New training includes Samanantar-derived rows: Bengali **114,558**, Telugu **42,413**, Kannada **36,482**, Tamil **34,447**; per-language training reports are bundled. | +| [Samanantar, AI4Bharat](https://huggingface.co/datasets/ai4bharat/samanantar) | CC-BY-NC-4.0 upstream card; reason for this release's non-commercial restriction. | +| [Dakshina, Google Research](https://github.com/google-research-datasets/dakshina) | CC-BY-SA-4.0 upstream. Source-attributed pairs appear within Aksharantar; original terms remain, and derivative-rights questions are not resolved by selecting a more restrictive model-card licence. Cite Roark et al., LREC 2020. | +| [IndicCorp v2, AI4Bharat](https://huggingface.co/datasets/ai4bharat/IndicCorpV2) | CC0; frequency dictionaries and Hindi LM training. | +| [IndicXlit, AI4Bharat](https://github.com/AI4Bharat/IndicXlit) | MIT teacher for distillation, and comparison baseline; original checkpoint not redistributed. | +| Runtime code / original Hindi components | Apache-2.0, retained with their existing notices. Inter and Noto Indic demo fonts: SIL OFL 1.1. | + +The **Hindi components are also separately available under Apache-2.0**: [AksharaLP-1M-v2](https://huggingface.co/sourojitdotcom/AksharaLP-1M-v2) and [AksharaLP-Sentence-2.8M](https://huggingface.co/sourojitdotcom/AksharaLP-Sentence-2.8M). This combined release does not revoke or replace those existing grants. Code, dictionaries and individual components retain their own upstream terms; see [NOTICE](NOTICE), [LICENSE](LICENSE) and [LICENSE-APACHE-2.0](LICENSE-APACHE-2.0). This documentation is engineering provenance, not legal advice. + +## FAQ + +**Is this a multilingual LLM?** No. It cannot answer questions, reason generally, summarize, or translate between languages. It changes Romanized spellings into native script. + +**Why 8M when a word model has 989K parameters?** Five independent word models total 4,944,768; optional Hindi LM adds 2,770,752; total 7,715,520, rounded to 8M. That count excludes small reranker coefficients and dictionary data. Only selected components load. + +**Is it the best model for these languages?** Not established. It trails IndicXlit's full package on all four new test languages. We claim measured footprint and measured results, not smallest-ever or best-in-class status. + +**Does every language understand a sentence?** No. Only Hindi has neural sentence rescoring. Others concatenate independent word predictions, preserving text layout. Hindi rescoring cannot recover a spelling outside its candidate list. + +**Can I use it commercially?** Not this combined release. Its model-card licence is CC-BY-NC-4.0. Use independently Apache-licensed Hindi releases where appropriate, or wait for a cleared retrain. Retain all source attributions and review applicable component terms. + +**Does text leave the device?** The demo fetches local model assets from the Space when loading a language. Typed text is processed in the browser and is not sent to an inference API or telemetry service. Once the selected assets are loaded, typing needs no network. This is not a promise to avoid the initial asset download. + +**Can I run the GGUFs with llama.cpp?** No. GGUF is a container here, not a supported llama.cpp architecture. Use our reader, dictionaries and decoding code. + +## Limitations & responsible use + +Names, ambiguous short words, unseen vocabulary, English code-switching, dialects and chat abbreviations can fail. The output must be user-reviewable; do not silently replace important legal, medical or financial text. Candidate suggestions are uncalibrated. Input encoding truncates a word after 31 Roman characters. Type-as-you-go inference is debounced; older phones and long sentences may be slow. All measured compute here is CPU, not an on-device mobile benchmark. Tiny low-bit downloads still expand to float32 arrays and large dictionary structures in RAM. + +This private release records the current artifacts. No “best in class,” general-LLM capability, sub-5-MB complete bundle, commercial-clean, or multilingual-sentence claim is made. + +--- +**AksharaLP · Sourojit Dhua · 2026-11-10 (IST)** · Private review copy. Do not make public without the owner's approval. diff --git a/SHA256SUMS b/SHA256SUMS new file mode 100644 index 0000000000000000000000000000000000000000..3d69acc958333615d362c89f25d8c02509aac2ba --- /dev/null +++ b/SHA256SUMS @@ -0,0 +1,101 @@ +d39ccdda1d2005a1b51113aec99f0c29a55d70dc0e35399656866ce0d9438e94 .gitattributes +cc97a2c4ff7723a1f25179dc7c18f676fa84191bbb36ea7d0061acaf3c102559 LICENSE +e61722325c93e58388069e84e208e43ca5b6294050676ee27194921cf453a071 LICENSE-APACHE-2.0 +8dd4007ec7ce19774061f88c9173ef176951e329083c079728c901ce15b15f13 NOTICE +edc975f244807c134d306d1376fdfb8b29073d5db540e30db0bc90371b3c1e45 README.md +f6484f5caaebaf2acb68540cc2cb29402030aee22a7e45b25d37424872f769a8 aksharalp.mjs +e29d4e04be4a7235d4f2b94b6578cd580486960a7068b048e1bf6c1b1799679a assets/accuracy.png +c5378130af32052f6020b7cdc5a491aa5f1d11de763c37ee5491ba1eaef0bf49 assets/accuracy.svg +27da3ac213f72636663b84026093cf2a1c80f825d8d67f110466ae9df0d3d6fc assets/banner.png +a9f8a61342401feb1e4bb6c1f82d0831d3fcbf35e3a6788eb7027d193b8de129 assets/banner.svg +70d7aee1fe5e1e61dc7154c0aa4e33c818ff30ebbbe5d78fb5f52ba09fad1e04 assets/sizes.png +e6a92bb1337fb4997c5d657e12d0b34208d0240e006aa3eb58bce6d3a9b53bec assets/sizes.svg +f5689263059364546fe23824634183d8781defb4691f36d951fff63558f16afc config.json +13350ec5c2ffa334de1dd6fb11e318fb1eeb5b80791506768546c3e43607539d evaluation/bn_frozen.json +79b140c2feac29957461d9ee879db331ea30885dee334675694169315f0d7535 evaluation/examples.json +8d7b75ab880b89c749765cba386fcf0c3a290cdb6c1d2c9491be4a341a5453bc evaluation/gguf_independent_validation.json +4f898f14ad3912b8ecfa2025476f17ddad7aaa307bcac381620ffa17f2652a27 evaluation/gguf_parity.json +bf7eba31593b49e8223d56726022d6e68627ad437661003771144995b5cba07f evaluation/gguf_sizes.json +392f25d66278a10a0e4f0093539d8f117727737bfd1b3b2d0e4dc31cfb5c792d evaluation/hindi_existing_results.json +8e29063e276abfa4320d1fc299b3a5560ec2bbada77bb7147a8fba97563b3acd evaluation/indicxlit_storage.json +157f7694918ba21b4e5268edbcf54f3fa5f1416506234ef5b78f1b9a502755da evaluation/kn_frozen.json +8a9b660df627f538d67decc52b16b5349dc416d0cb0b25fb0b793f7752f54763 evaluation/latency.json +50f1594f15182c2ade9a10d4b971418932e98d7413a40590d2fc96f30ff6b80b evaluation/multiindic_test.json +25b2cc588a557f77ee852427fe06c77b108cad74fb6082d299b72cc1e1d58fe0 evaluation/release_summary.json +a8912129bb1143d06e7f8761b515516b6e503d2f36951e684ee2f602b7911285 evaluation/sizes.json +4798b6de7e820251a93924186934287d70407705df242573910c74b60d85e431 evaluation/ta_frozen.json +eb327b20a375b534bad337dbf90b4e054b58a788fc17a34b142668ccbdae0cca evaluation/te_frozen.json +7a0b78e133bb3ed773f6a8bae5e3f1af4b75f5158de4e814efadeaa0002ed580 gguf/aksharalp-bn-word-f16.gguf +8311c77889381ce559d483b965027c2d409787ac090b146ae1af08060e4a4552 gguf/aksharalp-bn-word-q4.gguf +446ff8f003c3680e9098df94727f8eb67a6a05a7c418888fb0bfeeb2742e098e gguf/aksharalp-hi-sentence-f16.gguf +8ecc83bc618b9c67173328a34f0f40e5c83ce77a993a6279acdeae6d724690db gguf/aksharalp-hi-sentence-q4.gguf +5ff23bc8d131b560ee29c79ee16d15294600149200948433d76f29f4b8c3e496 gguf/aksharalp-hi-sentence-q8.gguf +3b6670429a226a39d357160691c634160667728bae8cfc7a711067424a2d224e gguf/aksharalp-hi-word-f16.gguf +737d14197514a2625eb9505eb0a22ebd62cd2a4b764c65761f4084599bb6eb36 gguf/aksharalp-hi-word-q4.gguf +c9d273c8339864bd37c2b051cab60d2741c1d71e56e8c27d00a981ff28ef3864 gguf/aksharalp-kn-word-f16.gguf +0e000f7fc0e52bbcb1f05753a46e6de50d1a1bdd6bb0ef46303361b9c39138ed gguf/aksharalp-kn-word-q4.gguf +3baf8764ecc77250666eb3f8eb83f2d7a33763e13227e2d6db8272868dd5a864 gguf/aksharalp-ta-word-f16.gguf +f270e77e6355d53dc140f7cc7282551f5126302bcfbdbb17d802861510896c04 gguf/aksharalp-ta-word-q4.gguf +3c556cec413db8f1485a1ee9ea237406f967937019e0d9d6076c3ae41b7a6231 gguf/aksharalp-te-word-f16.gguf +56b9f956b69e4fcf9e8d3f23c9391a091208651e90b06e2bde6d7f46d7cb6a60 gguf/aksharalp-te-word-q4.gguf +9348b0d8b522e9e3e45843a95c47087a3331f811a26c52813fa177a506f5cdcf gguf_read.py +edb2ad0bd7de4e87bd4d49ac406a9ef01011451bf23ac82d4c063bb9cce1dc2b models/bn/dict.tsv.gz +7007c674102f7d242b71832c877ea1d58b6b83393ac1ea2d2814f93dec499cda models/bn/inventory.json +e0bd564ad61f6dcef8eca6bf3d64f8b231146c9885aa4c5b282a2b2420606c4f models/bn/params.json +7d85a15252ea42676289a216bb775c2e48fea96634b32d2f2f0dce214951ada3 models/bn/training_report.json +9f099f0b9524f446debf1d3f0ae1dc7ed1e88625c33f6dbb7b45e0348bf76376 models/bn/xlit.q4 +1c87e62a4249a63c333942c1c8736eb2c89f9ee3148ef8fc422784ba6ce21e43 models/bn/xlit.q4.gz +73bd5436b77db4ee433c564521f19a2b78cb6a45c429f274d43bef909708347f models/hi/dict/lex.bin +3b4dbff703f395e97b4503646fde7782bab0f3d79b9731ef703df6b961e38f55 models/hi/dict/lex.bin.gz +254e34abe7c1e6ed82f74d5cbf175aba1aedbee316363fe2a67e06291ba589ee models/hi/dict/lm.bin +ab3f6b4135862b5bbf42815cc1219274eed711b5a7dfc66f09fc7b0fe32ceed6 models/hi/dict/lm.bin.gz +6d60eaf2b4e89a847a750a226680021460e454c6d1a06070a56e8f8664500565 models/hi/dict/params.json +bc7dd23ba83ec6bc5841b03fa7b8e3cb934d0322dda652863537ce03958ea79d models/hi/dict/params.json.gz +620ab5ebf213fc4b8469b88338368b34616390154f72f64f3dcd49a550f01472 models/hi/manifest.json +acb2bd2c0c12813d18c9696058260d57ea504db76db6d2d0810054a9e65a875e models/hi/manifest.json.gz +1dc58564cd649278c819641a65141e9e96a42f3fc7fba555b9072628674c2935 models/hi/params.json +8e42f8aec0204bca50891244478d94ac3ba1801f8cc774c475b84c5581377c73 models/hi/params.json.gz +2c90f300c0114354fcf0c5a12300b9ba98ce71b4580b54f0b870a9395e869227 models/hi/sentence/nlm.q4 +b8f3850506d93f3b0531beb37362cf1ad7c02a9e1d8c81ebc128efee9e44c17e models/hi/sentence/nlm.q8 +7face18513d342c59b005f3ea57a0e86310dc863778a828e7743cea0d5c68526 models/hi/sentence/nlm.q8.gz +652eafe0c1e2b46fec85663825621284f0a5911f2d7cc6e13b1182ce47cd9803 models/hi/xlit.q4 +191eb28f75f29c45ec29f044031a6ae5aedd6a80edc3f3a9d1ae78a53c04fb9d models/hi/xlit.q4.gz +97035264d30eddb5810467dc5bbf088614221ea6ba2c3fff349c6bbd196bb644 models/kn/dict.tsv.gz +cc2085a1f34c929678658159bc22eaa7641ce4aecada94fa3336dc82e1e553a3 models/kn/inventory.json +6076d9126ffce66630b634e47ac5b38d03cda3bce7ab4d07162aeb0cbc925356 models/kn/params.json +bfae38dd1fb1c799e9005750b3e7ed93860a2be8051489a3cd0ba9ead17ae30f models/kn/training_report.json +3788fd545c297febb7dbbdbdd780e831042395b4ec99260b95c963607afc8664 models/kn/xlit.q4 +bedc5a698dc5c83a964f03586bc0ded67e88fa0b644cdd82fafc21f1e50925d9 models/kn/xlit.q4.gz +4d14f514020e7737144c22991e9324a29734b5ab203bef3b68ec6d9f51e7cddb models/ta/dict.tsv.gz +e1ea48ffbd41b725a435c611d0ebd1da454904bc90e2368eb2fbaecee9f1870b models/ta/inventory.json +f4520a8320f02573e6b8b6530a372c980903fbf757999540698957e8eec58385 models/ta/params.json +0c93e563d879dfef97ca0d36ae6c6ce4ca21dd417741be90478b7191214b0165 models/ta/training_report.json +5dd185a033db79835fede66ab41a25f5897f24ad5d7c8ecc9864ca6ec167fdc0 models/ta/xlit.q4 +c15be889468520e6d5ee8dfbc4c77cad781a79f454d4681026fc082ae217afc2 models/ta/xlit.q4.gz +20612a52ccf39c816d3068e78e9e9a3aa9aed9156f10de45ebbd543363943640 models/te/dict.tsv.gz +ff20b9635ecef52f692a53807be1c6a5acf3922212d2bc14f7dd9283002bc690 models/te/inventory.json +653f897efe1af78bb52149f2a18dc804b49ecac9c93add867871e64f56e07470 models/te/params.json +da8bc7af7e481e995e3ada79f21585bdbcf7df8fc751fdf151d8a774f8f0f343 models/te/training_report.json +aea094e46a3c61eb9487d1aaba3824d3d9dc60f3f1cf6b6f99cadf04fd4318f9 models/te/xlit.q4 +3513df30b5d3bfa07085baa19f0c9c1076e85268a5df88fcc4b492630b1b4eb2 models/te/xlit.q4.gz +037d1f4e40f61457e4e33a9b8a034d2509316b723f27907849d55f0fb9cf23cf multiindic.mjs +a0bc14ac9629a0bb6b69fe94609eb922a9b71bbd327b17553581b91648d970c2 nlm.mjs +e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 python/bhc/__init__.py +af77db2406b19466f538ebc35be31b6ffcf23de2d8447e48247a8ba58c1c2697 python/bhc/agree.py +aebfdd405693b5096744cbab03a30662e440308583302e9b5b54575ce0498231 python/bhc/casual.py +b07229b0d484048f6f75fd8304dc62e2a05d2dbc450289bfb57f6efd669c9ffc python/bhc/lexlm.py +5b7c49b45309ac3f324906bd2546c35cfc4d5a199a9311a9d845233337a41b39 python/bhc/lextrie.py +ba9c1c4b326328cbbc65085b379835fac522d116e1ea163625bfcadf5c0de723 python/bhc/nlm.py +39e674d163ebcb7e7adcf02dd267fd6c9b188be1346486f2695736240c4f42ff python/bhc/skel.py +09617bd0f9b90a4179628dd40d786a30ba6a3a2e043d3993ecf36cf223524483 python/bhc/text.py +67de6da4dc780c3128ca5c2da4461d1e0616d3f1c65619cc62ef59699a28c2d2 python/bhc/translit2.py +e93f9cc572853158f7414a46ac24815aea40c76e0939a2b79a6034542aeb2615 python/bhc/translit3.py +8eb7235292a0b42a4cdfc07c548809fe312e2eec2c622317ff40e0c203ff5766 python/bhc/xlit_model.py +1a577ff4b9075b6ee73debed2955840d30a5796a846ba74af58561866681307f python/bhc/xlit_q.py +9348b0d8b522e9e3e45843a95c47087a3331f811a26c52813fa177a506f5cdcf python/gguf_read.py +e22e0dfcd2cee31d13d5eee303e07dea973032bfdfbd09b2c4060d69b0f12367 python/multiindic.py +72cabbf383d3b033df0eb37be00bb7079f51b0d08d793f0ac9d7eec4709dffbb python/requirements.txt +de4599eedf54731c4d61f2bee9afb0d3776aaa5a49516fa4d2bf720bc2fdbb7a sent.mjs +f62025ba5b1c6b7a0e3559a0b6a6dc6f53773f188f8be118f9d0e6083b4e5211 tools/build_gguf.py +9fcbb1848e3a46daaf9d9b317f240a1d04d7442a7242af99df7a23d24fd48940 tools/gguf_format.py +dcd348d0f8f26b3316123228ae534d37155bfe552520141a38aa55b14707f0d9 v3.mjs diff --git a/aksharalp.mjs b/aksharalp.mjs new file mode 100644 index 0000000000000000000000000000000000000000..f6664a2477d0c235c853bf09608ba2393186ad09 --- /dev/null +++ b/aksharalp.mjs @@ -0,0 +1,822 @@ +/*! + * BhashaCore JS runtime — romanized Hindi / Hinglish -> Devanagari, fully offline (browser, Web Worker, Node >= 18). + * Zero dependencies. Pure-JS port of the reference Python runtime (bhc/translit2.py, bhc/xlit_q.py, bhc/lexlm.py, + * bhc/lextrie.py, bhc/lid.py, bhc/hinglish.py) over the exported asset formats: + * xlit.q4 | xlit.q8 | xlit.int8 char Transformer (pre-LN enc/dec), 4/8-bit grouped or per-row INT8 weights + * lex.bin Devanagari lexicon (LX4 alphabetical front-coded + 4-bit bands, or v2 bucketed) + * lm.bin pruned backoff word-bigram LM (varint CSR + 4-bit codebooks) + * params.json decoding params + reranker (linear w or MLP w1/b1/w2/b2; mu / sd) + * lid.int8 char-CNN + BiGRU token / sentence language ID (optional) + * loan.tsv, natural_hi.tsv Hinglish loanword lexicon, Natural-Hindi glossary (optional) + * Bundle layout: a small CORE (model + params.json) that works standalone, plus an optional dictionary pack in + * dict/ (lex.bin + lm.bin + params.json overriding the core params), fetched on first use, cached with the Cache API + * when available, and attached with engine.loadDict(); if it cannot be fetched the engine stays in core-only mode. + * + * AksharaLP by Sourojit Dhua — https://huggingface.co/sourojitdotcom/AksharaLP-1M-v2 + * Copyright 2026 Sourojit Dhua. Licensed under the Apache License, Version 2.0; commercial use permitted. + * Attribution required (Apache-2.0 §4(d)): keep this notice and the NOTICE file in redistributions and derivatives: + * "Transliteration by AksharaLP (Sourojit Dhua) — https://huggingface.co/sourojitdotcom/AksharaLP-1M-v2" + */ + +// ------------------------------------------------------------------ vocab +const PAD = 0, BOS = 1, EOS = 2, UNK = 3, MAXLEN = 32, V = 132; +const NEG = -Infinity; +const encSrc = (w) => { + const s = w.toLowerCase().slice(0, MAXLEN - 1), ids = []; + for (const c of s) { const k = c.charCodeAt(0); ids.push(c.length === 1 && k >= 97 && k <= 122 ? k - 97 + 4 : UNK); } + ids.push(EOS); return ids; +}; +const t2i = (ch) => { const k = ch.codePointAt(0); return k >= 0x900 && k < 0x980 ? k - 0x900 + 4 : -1; }; +const encTgt = (w) => { const ids = []; for (const c of [...w].slice(0, MAXLEN - 2)) { const i = t2i(c); ids.push(i < 0 ? UNK : i); } ids.push(EOS); return ids; }; +const decTgt = (ids) => { let s = ""; for (const i of ids) if (i > UNK) s += String.fromCharCode(0x900 + i - 4); return s; }; + +// ------------------------------------------------------------------ binary helpers +function f16(h) { + const s = (h & 0x8000) ? -1 : 1, e = (h >> 10) & 0x1f, f = h & 0x3ff; + if (e === 0) return s * f * 5.960464477539063e-8; + if (e === 31) return f ? NaN : s * Infinity; + return s * (1 + f / 1024) * Math.pow(2, e - 15); +} +function readHeader(buf) { + const u8 = new Uint8Array(buf), dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength); + const n = dv.getUint32(0, true); + const meta = JSON.parse(new TextDecoder().decode(u8.subarray(4, 4 + n))); + const data = u8.subarray(4 + n); + return { meta, data, dv: new DataView(data.buffer, data.byteOffset, data.byteLength) }; +} +const prod = (a) => a.reduce((x, y) => x * y, 1); +const f16arr = (dv, off, n) => { const o = new Float32Array(n); for (let i = 0; i < n; i++) o[i] = f16(dv.getUint16(off + 2 * i, true)); return o; }; + +/** Parse xlit.int8 / xlit.q4 / xlit.q8 / lid.int8 into {cfg, extra, T: {name: {shape, data: Float32Array}}}. */ +export function parseWeights(buf) { + const { meta, data, dv } = readHeader(buf); const T = {}; + for (const t of meta.tensors) { + const [k, shape, kind, off] = t; const cnt = prod(shape); let w; + if (kind === "f16") w = f16arr(dv, off, cnt); + else if (kind === "i8") { // per-row symmetric INT8 (+ fp16 row scale) + const r = shape[0], c = cnt / r, sc = f16arr(dv, off + cnt, r); w = new Float32Array(cnt); + for (let i = 0; i < r; i++) for (let j = 0; j < c; j++) { const q = (data[off + i * c + j] << 24) >> 24; w[i * c + j] = q * sc[i]; } + } else { // q4 / q8: group-wise symmetric (G inputs per fp16 scale) + const bits = kind.length > 1 ? parseInt(kind.slice(1)) : meta.bits, G = meta.G, ln = t[4]; + const [r, c] = shape, ng = Math.ceil(c / G), sc = f16arr(dv, off + ln, r * ng); w = new Float32Array(cnt); + for (let i = 0; i < cnt; i++) { + let q; + if (bits === 4) { const b = data[off + (i >> 1)]; q = ((i & 1) ? (b & 15) : (b >> 4)) - 8; } + else q = (data[off + i] << 24) >> 24; + const row = (i / c) | 0, col = i - row * c; w[i] = q * sc[row * ng + ((col / G) | 0)]; + } + } + T[k] = { shape, data: w }; + } + return { cfg: meta.cfg, extra: meta.extra || {}, T }; +} + +// ------------------------------------------------------------------ math kernels +function linear(x, xo, W, b, out, oo, nin, nout) { // out[oo..] = W x + b ; W row-major [nout, nin] + for (let o = 0; o < nout; o++) { + let s = b ? b[o] : 0; const r = o * nin; + for (let i = 0; i < nin; i++) s += W[r + i] * x[xo + i]; + out[oo + o] = s; + } +} +function layerNorm(x, xo, g, b, out, oo, d) { + let m = 0; for (let i = 0; i < d; i++) m += x[xo + i]; m /= d; + let v = 0; for (let i = 0; i < d; i++) { const t = x[xo + i] - m; v += t * t; } v /= d; + const r = 1 / Math.sqrt(v + 1e-5); + for (let i = 0; i < d; i++) out[oo + i] = (x[xo + i] - m) * r * g[i] + b[i]; +} +function erf(x) { // |err| < 1.2e-7 (Numerical Recipes erfc) + const z = Math.abs(x), t = 1 / (1 + 0.5 * z); + const r = t * Math.exp(-z * z - 1.26551223 + t * (1.00002368 + t * (0.37409196 + t * (0.09678418 + t * (-0.18628806 + + t * (0.27886807 + t * (-1.13520398 + t * (1.48851587 + t * (-0.82215223 + t * 0.17087277))))))))); + return x >= 0 ? 1 - r : r - 1; +} +const gelu = (x) => 0.5 * x * (1 + erf(x / Math.SQRT2)); +const sigmoid = (x) => 1 / (1 + Math.exp(-x)); + +// ------------------------------------------------------------------ char Transformer +export class XlitModel { + constructor(w) { + const c = w.cfg; this.V = w.T["temb.weight"].shape[0]; + const start = {hi:0x900,bn:0x980,te:0xc00,kn:0xc80,ta:0xb80}[c.vocab || "hi"]; + this.target = Array.from({length:128}, (_,i)=>String.fromCharCode(start+i)); + if (c.vocab === "bn") this.target.push("\u0964"); + this.decTgt = ids => ids.filter(i=>i>3).map(i=>this.target[i-4]).join(""); + this.d = c.d; this.h = c.h; this.dk = c.d / c.h; this.ff = c.ff; this.ne = c.enc; this.nd = c.dec; + const g = (k) => { const t = w.T[k]; if (!t) throw new Error("missing tensor " + k); return t.data; }; + this.semb = g("semb.weight"); this.temb = g("temb.weight"); this.spos = g("spos.weight"); this.tpos = g("tpos.weight"); + const att = (p) => ({ q: g(p + ".q.weight"), qb: g(p + ".q.bias"), kv: g(p + ".kv.weight"), kvb: g(p + ".kv.bias"), o: g(p + ".o.weight"), ob: g(p + ".o.bias") }); + const ln = (p) => ({ g: g(p + ".weight"), b: g(p + ".bias") }); + const ffn = (p) => ({ w1: g(p + ".ff.0.weight"), b1: g(p + ".ff.0.bias"), w2: g(p + ".ff.2.weight"), b2: g(p + ".ff.2.bias") }); + this.enc = []; for (let i = 0; i < c.enc; i++) { const p = "enc." + i; this.enc.push({ n1: ln(p + ".n1"), att: att(p + ".att"), n2: ln(p + ".n2"), ff: ffn(p) }); } + this.dec = []; for (let i = 0; i < c.dec; i++) { const p = "dec." + i; this.dec.push({ n1: ln(p + ".n1"), sa: att(p + ".sa"), n2: ln(p + ".n2"), ca: att(p + ".ca"), n3: ln(p + ".n3"), ff: ffn(p) }); } + this.en = ln("en"); this.dn = ln("dn"); this.scale = Math.sqrt(c.d); + this.nParams = Object.values(w.T).reduce((a, t) => a + t.data.length, 0); + const d = this.d; this._y = new Float32Array(d); this._a = new Float32Array(d); this._f = new Float32Array(this.ff); this._q = new Float32Array(d); this._kv = new Float32Array(2 * d); + this._sc = new Float64Array(MAXLEN); + } + _ffn(L, x, xo, n) { // x[xo] += FF(LN(x)) + const d = this.d, y = this._y, f = this._f, a = this._a; + layerNorm(x, xo, n.g, n.b, y, 0, d); linear(y, 0, L.w1, L.b1, f, 0, d, this.ff); + for (let i = 0; i < this.ff; i++) f[i] = gelu(f[i]); + linear(f, 0, L.w2, L.b2, a, 0, this.ff, d); for (let i = 0; i < d; i++) x[xo + i] += a[i]; + } + /** attention of one query q (len d) over K/V rows (stride 2d in kvbuf: K at +0, V at +d) -> out (len d) */ + _attend(q, kvbuf, kvo, n, out) { + const { h, dk, d } = this, sc = n <= MAXLEN ? this._sc : new Float64Array(n), inv = 1 / Math.sqrt(dk); + for (let hh = 0; hh < h; hh++) { + let mx = -Infinity; + for (let j = 0; j < n; j++) { let s = 0; const ko = kvo + j * 2 * d + hh * dk; for (let i = 0; i < dk; i++) s += q[hh * dk + i] * kvbuf[ko + i]; s *= inv; sc[j] = s; if (s > mx) mx = s; } + let z = 0; for (let j = 0; j < n; j++) { sc[j] = Math.exp(sc[j] - mx); z += sc[j]; } + for (let i = 0; i < dk; i++) { let s = 0; for (let j = 0; j < n; j++) s += sc[j] * kvbuf[kvo + j * 2 * d + d + hh * dk + i]; out[hh * dk + i] = s / z; } + } + } + /** encode one word -> {S, cross: per decoder layer K/V of memory (Float32Array S*2d)} */ + encode(ids) { + const d = this.d, S = ids.length, x = new Float32Array(S * d), y = new Float32Array(S * d), kv = new Float32Array(S * 2 * d), att = new Float32Array(d), o = this._a; + for (let t = 0; t < S; t++) for (let i = 0; i < d; i++) x[t * d + i] = this.semb[ids[t] * d + i] * this.scale + this.spos[t * d + i]; + for (const L of this.enc) { + for (let t = 0; t < S; t++) { layerNorm(x, t * d, L.n1.g, L.n1.b, y, t * d, d); linear(y, t * d, L.att.kv, L.att.kvb, kv, t * 2 * d, d, 2 * d); } + const nx = new Float32Array(x); + for (let t = 0; t < S; t++) { + linear(y, t * d, L.att.q, L.att.qb, this._q, 0, d, d); this._attend(this._q, kv, 0, S, att); + linear(att, 0, L.att.o, L.att.ob, o, 0, d, d); for (let i = 0; i < d; i++) nx[t * d + i] += o[i]; + } + for (let t = 0; t < S; t++) this._ffn(L.ff, nx, t * d, L.n2); + x.set(nx); + } + const mem = new Float32Array(S * d); for (let t = 0; t < S; t++) layerNorm(x, t * d, this.en.g, this.en.b, mem, t * d, d); + const cross = this.dec.map((L) => { const c = new Float32Array(S * 2 * d); for (let t = 0; t < S; t++) linear(mem, t * d, L.ca.kv, L.ca.kvb, c, t * 2 * d, d, 2 * d); return c; }); + return { S, cross }; + } + newCache(K) { const d = this.d; return this.dec.map(() => new Float32Array(K * MAXLEN * 2 * d)); } + /** one decoder step for beam rows `rows` (indices into cache) with tokens tok[row] at position t -> logprobs[row] (Float64Array V) */ + step(enc, cache, rows, tok, t, out) { + const V = this.V, d = this.d, x = new Float32Array(d), y = this._y, att = new Float32Array(d), o = this._a; + for (const r of rows) { + for (let i = 0; i < d; i++) x[i] = this.temb[tok[r] * d + i] * this.scale + this.tpos[t * d + i]; + for (let l = 0; l < this.nd; l++) { + const L = this.dec[l], C = cache[l], base = r * MAXLEN * 2 * d; + layerNorm(x, 0, L.n1.g, L.n1.b, y, 0, d); + linear(y, 0, L.sa.kv, L.sa.kvb, C, base + t * 2 * d, d, 2 * d); + linear(y, 0, L.sa.q, L.sa.qb, this._q, 0, d, d); this._attend(this._q, C, base, t + 1, att); + linear(att, 0, L.sa.o, L.sa.ob, o, 0, d, d); for (let i = 0; i < d; i++) x[i] += o[i]; + layerNorm(x, 0, L.n2.g, L.n2.b, y, 0, d); + linear(y, 0, L.ca.q, L.ca.qb, this._q, 0, d, d); this._attend(this._q, enc.cross[l], 0, enc.S, att); + linear(att, 0, L.ca.o, L.ca.ob, o, 0, d, d); for (let i = 0; i < d; i++) x[i] += o[i]; + this._ffn(L.ff, x, 0, L.n3); + } + layerNorm(x, 0, this.dn.g, this.dn.b, y, 0, d); + const lp = out[r]; let mx = -Infinity; + for (let v = 0; v < V; v++) { let s = 0; for (let i = 0; i < d; i++) s += y[i] * this.temb[v * d + i]; lp[v] = s; if (s > mx) mx = s; } + let z = 0; for (let v = 0; v < V; v++) z += Math.exp(lp[v] - mx); const lz = mx + Math.log(z); + for (let v = 0; v < V; v++) lp[v] -= lz; + } + } + /** beam search for one word (identical procedure to bhc.xlit_model.beam_search_fast for one batch row) */ + beam(enc, S, { beam: K = 8, nbest = 8, trie = null } = {}) { + const V = this.V, d = this.d, cache = this.newCache(K), out = Array.from({ length: K }, () => new Float64Array(V)); + let score = new Array(K).fill(NEG); score[0] = 0; let tok = new Array(K).fill(BOS); let hist = Array.from({ length: K }, () => []); + let nodes = trie ? new Array(K).fill(0) : null; const done = []; const L = Math.min(MAXLEN - 1, S + 8); + const tmp = new Float32Array(MAXLEN * 2 * d); + for (let t = 0; t < L; t++) { + const rows = []; for (let k = 0; k < K; k++) if (score[k] > NEG) rows.push(k); + this.step(enc, cache, rows, tok, t, out); + const cand = []; // (total, beam, v) + for (const k of rows) { + const lp = out[k]; let allow = null; + if (trie) { const n = nodes[k]; if (n === null) continue; allow = trie.allowed(n); } + if (allow) { for (const v of allow) if (v !== PAD && v !== BOS && v !== UNK) cand.push([score[k] + lp[v], k, v]); } + else { cand.push([score[k] + lp[EOS], k, EOS]); for (let v = 4; v < V; v++) cand.push([score[k] + lp[v], k, v]); } + } + cand.sort((a, b) => b[0] - a[0] || (a[1] * V + a[2]) - (b[1] * V + b[2])); + const top = cand.slice(0, 2 * K); + const ns = new Array(K).fill(NEG), nt = new Array(K).fill(EOS), par = new Array(K).fill(0), nn = trie ? new Array(K).fill(null) : null; + let j = 0; + for (const [sc, b, v] of top) { + if (!(sc > NEG)) continue; + if (v === EOS) { if (done.length < nbest) done.push([this.decTgt(hist[b]), sc]); continue; } + if (j < K) { ns[j] = sc; nt[j] = v; par[j] = b; if (trie) nn[j] = trie.child(nodes[b], v); j++; } + } + let alive = true; + if (done.length >= nbest && (j === 0 || ns[0] < Math.min(...done.map((x) => x[1])))) alive = false; + else if (!j) alive = false; + if (!alive) break; + for (const C of cache) { // reorder self-attention caches by parent beam + const n = (t + 1) * 2 * d, olds = []; + for (let k = 0; k < j; k++) { const src = par[k] * MAXLEN * 2 * d; olds.push(C.slice(src, src + n)); } + for (let k = 0; k < j; k++) C.set(olds[k], k * MAXLEN * 2 * d); + } + hist = Array.from({ length: K }, (_, k) => (k < j ? hist[par[k]].concat([nt[k]]) : [])); + score = ns; tok = nt; nodes = nn; + } + const best = new Map(); + for (const [s, sc] of done) if (!best.has(s) || sc > best.get(s)) best.set(s, sc); + return [...best.entries()].sort((a, b) => b[1] - a[1]).slice(0, nbest); + } + /** teacher-forced ln P(tgt | src) (as bhc.xlit_model.score_pairs) */ + scorePair(enc, tgt) { + const V = this.V, ids = [...tgt].slice(0,MAXLEN-2).map(c=>{const i=this.target.indexOf(c);return i<0?UNK:i+4;}).concat([EOS]), cache = this.newCache(1), out = [new Float64Array(V)]; let tok = [BOS], s = 0; + for (let t = 0; t < ids.length; t++) { this.step(enc, cache, [0], tok, t, out); s += out[0][ids[t]]; tok = [ids[t]]; } + return s; + } +} + +// ------------------------------------------------------------------ lexicon + bigram LM +function readVarints(b, n, pos) { + const out = new Uint32Array(n); + for (let i = 0; i < n; i++) { let v = 0, sh = 0, c; do { c = b[pos++]; v += (c & 0x7f) * 2 ** sh; sh += 7; } while (c >= 0x80); out[i] = v; } + return [out, pos]; +} +const unpack4 = (b, n) => { const o = new Uint8Array(n); for (let i = 0; i < n; i++) o[i] = (i & 1) ? b[i >> 1] & 15 : b[i >> 1] >> 4; return o; }; + +export class LexLM { + constructor(lexBuf, lmBuf) { + if (!lexBuf) { // core-only: empty lexicon, no LM + this.words = []; this.sortedWords = []; this.uni = new Float32Array(0); this.N = 0; this.unk = 0; this.alpha = true; this.hasLM = false; return; + } + const b = new Uint8Array(lexBuf), dv = new DataView(b.buffer, b.byteOffset, b.byteLength); const words = []; + const decodeFC = (P, S, nWords) => { let prev = [], j = 0, pi = 0; + while (words.length < nWords) { const k = P ? P[pi++] : S[j++]; const cur = prev.slice(0, k); + for (;;) { const c = S[j++], ch = c & 0x7f; if (ch !== 0x7f) cur.push(ch); if (c & 0x80) break; } + words.push(String.fromCharCode(...cur.map((x) => 0x900 + x))); prev = cur; } }; + if (b[0] === 0x4c && b[1] === 0x58 && b[2] === 0x34 && b[3] === 0) { // "LX4\0" + const N = dv.getUint32(4, true), nfc = dv.getUint32(8, true), NB = dv.getUint32(16, true); this.unk = dv.getFloat32(12, true); + let pos = 20; const ucb = new Float32Array(NB); for (let i = 0; i < NB; i++) ucb[i] = dv.getFloat32(pos + 4 * i, true); pos += 4 * NB; + const band = unpack4(b.subarray(pos, pos + ((N + 1) >> 1)), N); pos += (N + 1) >> 1; + this.uni = new Float32Array(N); for (let i = 0; i < N; i++) this.uni[i] = ucb[band[i]]; + decodeFC(b.subarray(pos, pos + N), b.subarray(pos + N, pos + N + nfc), N); this.N = N; this.alpha = true; + } else { // v2 (frequency-bucketed) + const N = dv.getUint32(0, true), nb = dv.getUint32(4, true), nfc = dv.getUint32(8, true); this.unk = dv.getFloat32(12, true); + let [bn, pos] = readVarints(b, nb, 16); const blp = []; for (let i = 0; i < nb; i++) blp.push(f16(dv.getUint16(pos + 2 * i, true))); pos += 2 * nb; + this.uni = new Float32Array(N); let w = 0; for (let i = 0; i < nb; i++) for (let k = 0; k < bn[i]; k++) this.uni[w++] = blp[i]; + decodeFC(null, b.subarray(pos, pos + nfc), N); this.N = N; this.alpha = false; + } + this.words = words; + let sorted = this.alpha; for (let i = 1; sorted && i < words.length; i++) if (!(words[i - 1] < words[i])) sorted = false; + if (!sorted) { this.map = new Map(); words.forEach((w, i) => this.map.set(w, i)); } + this.sortedWords = sorted ? words : [...new Set(words)].sort(); + this.hasLM = false; + if (lmBuf) this._loadLM(new Uint8Array(lmBuf)); + } + id(w) { + if (this.map) { const i = this.map.get(w); return i === undefined ? null : i; } + let lo = 0, hi = this.words.length; while (lo < hi) { const m = (lo + hi) >> 1; if (this.words[m] < w) lo = m + 1; else hi = m; } + return lo < this.words.length && this.words[lo] === w ? lo : null; + } + _loadLM(b) { + const dv = new DataView(b.buffer, b.byteOffset, b.byteLength), N = this.N; + const nbg = dv.getUint32(4, true), nh = dv.getUint32(16, true); let pos = 20; + const cb = [], cbb = []; for (let i = 0; i < 16; i++) cb.push(dv.getFloat32(pos + 4 * i, true)); pos += 64; + for (let i = 0; i < 16; i++) cbb.push(dv.getFloat32(pos + 4 * i, true)); pos += 64; + let cnt, delta; [cnt, pos] = readVarints(b, N + 1, pos); [delta, pos] = readVarints(b, nbg, pos); + const q = unpack4(b.subarray(pos, pos + ((nbg + 1) >> 1)), nbg); pos += (nbg + 1) >> 1; + const qb = unpack4(b.subarray(pos, pos + ((nh + 1) >> 1)), nh); + this.off = new Uint32Array(N + 2); this.succ = new Uint32Array(nbg); this.bq = q; this.cb = Float32Array.from(cb); + this.bow = new Float32Array(N + 1); let i = 0, hi = 0; + for (let h = 0; h <= N; h++) { + this.off[h] = i; const n = cnt[h]; if (!n) continue; + this.bow[h] = cbb[qb[hi++]]; let acc = -1; + for (let k = 0; k < n; k++) { acc = acc < 0 ? delta[i] : acc + delta[i] + 1; this.succ[i++] = acc; } + } + this.off[N + 1] = i; this.hasLM = nbg > 0; + } + unigram(wid) { return wid === null ? this.unk : wid === this.N ? Math.log(0.04) : this.uni[wid]; } + /** ln P(wid | prev); prev null = sentence start; wid null = OOV; wid N = */ + logp(prev, wid) { + const h = prev === null ? this.N : prev; + if (wid !== null && this.hasLM) { + let lo = this.off[h], hi = this.off[h + 1]; + while (lo < hi) { const m = (lo + hi) >> 1; if (this.succ[m] < wid) lo = m + 1; else hi = m; } + if (lo < this.off[h + 1] && this.succ[lo] === wid) return this.cb[this.bq[lo]]; + } + return (this.hasLM ? this.bow[h] : 0) + this.unigram(wid); + } +} + +/** lazy lexicon trie over the sorted word list (same node semantics as bhc/lextrie.py) */ +class LazyTrie { + constructor(sortedWords) { this.w = sortedWords; this.nodes = [[0, sortedWords.length, 0]]; this.ids = new Map([["0,0," + sortedWords.length, 0]]); this.cache = new Map(); this.allow = new Map(); } + _bisect(x, lo, hi) { const w = this.w; while (lo < hi) { const m = (lo + hi) >> 1; if (w[m] < x) lo = m + 1; else hi = m; } return lo; } + children(n) { + let c = this.cache.get(n); if (c) return c; + const [lo, hi, d] = this.nodes[n], w = this.w; c = new Map(); let pos = lo; + if (pos < hi && w[pos].length === d) pos++; + while (pos < hi) { + const ch = w[pos].charCodeAt(d), pre = w[pos].slice(0, d); + const end = this._bisect(pre + String.fromCharCode(ch + 1), pos, hi); + const key = pos + "," + (d + 1) + "," + end; let nid = this.ids.get(key); + if (nid === undefined) { nid = this.nodes.length; this.nodes.push([pos, end, d + 1]); this.ids.set(key, nid); } + c.set(ch - 0x900 + 4, nid); pos = end; + } + if (this.cache.size > 400000) { this.cache.clear(); this.allow.clear(); } + this.cache.set(n, c); return c; + } + final(n) { const [lo, , d] = this.nodes[n]; return lo < this.w.length && this.w[lo].length === d; } + allowed(n) { let a = this.allow.get(n); if (!a) { a = [...this.children(n).keys()]; if (this.final(n)) a.push(EOS); this.allow.set(n, a); } return a; } + child(n, v) { return this.children(n).get(v); } +} + +// ------------------------------------------------------------------ language ID (char-CNN + BiGRU) +const LID_CH = {}; [..."abcdefghijklmnopqrstuvwxyz'"].forEach((c, i) => (LID_CH[c] = i + 3)); +const WMAX = 16, MODES3 = ["english", "romanized_hindi", "hinglish"]; +const RE_LATIN_APOS = /^[A-Za-z]+(?:'[A-Za-z]+)?$/, RE_DEVA = /^[\u0900-\u0963\u0971-\u097F]+$/; +function whash(w, n) { let h = 2166136261; for (const ch of new TextEncoder().encode(w.toLowerCase())) h = Math.imul(h ^ ch, 16777619) >>> 0; return h % n; } + +export class LID { + constructor(w) { + this.cfg = w.cfg; this.x = w.extra || {}; const g = (k) => w.T[k] && w.T[k].data; + Object.assign(this, { ce: g("ce.weight"), c1: g("c1.weight"), c1b: g("c1.bias"), c2: g("c2.weight"), c2b: g("c2.bias"), wemb: g("wemb.weight"), + wih: g("gru.weight_ih_l0"), whh: g("gru.weight_hh_l0"), bih: g("gru.bias_ih_l0"), bhh: g("gru.bias_hh_l0"), + wihR: g("gru.weight_ih_l0_reverse"), whhR: g("gru.weight_hh_l0_reverse"), bihR: g("gru.bias_ih_l0_reverse"), bhhR: g("gru.bias_hh_l0_reverse"), + tw: g("tok.weight"), tb: g("tok.bias"), s0: g("sent.0.weight"), s0b: g("sent.0.bias"), s2: g("sent.2.weight"), s2b: g("sent.2.bias") }); + this.nParams = Object.values(w.T).reduce((a, t) => a + t.data.length, 0); + } + _wvec(word) { + const { ce: E, cf } = this.cfg; const ids = new Array(WMAX).fill(0); const w = [...word.toLowerCase()].slice(0, WMAX - 2); + ids[0] = 1; w.forEach((c, i) => (ids[i + 1] = LID_CH[c] ?? 2)); ids[w.length + 1] = 2; + const x = new Float32Array(E * WMAX); for (let t = 0; t < WMAX; t++) for (let i = 0; i < E; i++) x[i * WMAX + t] = this.ce[ids[t] * E + i]; + const conv = (inp, cin, W, b) => { const o = new Float32Array(cf * WMAX); + for (let oc = 0; oc < cf; oc++) for (let t = 0; t < WMAX; t++) { let s = b[oc]; + for (let ic = 0; ic < cin; ic++) for (let k = 0; k < 3; k++) { const tt = t + k - 1; if (tt >= 0 && tt < WMAX) s += W[(oc * cin + ic) * 3 + k] * inp[ic * WMAX + tt]; } + o[oc * WMAX + t] = s > 0 ? s : 0; } return o; }; + const h1 = conv(x, E, this.c1, this.c1b), h2 = conv(h1, cf, this.c2, this.c2b); + const wh = this.cfg.wh, wd = this.cfg.wd, v = new Float32Array(cf + (wh ? wd : 0)); + for (let oc = 0; oc < cf; oc++) { let m = -Infinity; for (let t = 0; t < WMAX; t++) { const val = ids[t] === 0 ? -1e4 : h2[oc * WMAX + t] + h1[oc * WMAX + t]; if (val > m) m = val; } v[oc] = m; } + if (wh) { const r = whash(word, wh); for (let i = 0; i < wd; i++) v[cf + i] = this.wemb[r * wd + i]; } + return v; + } + _gru(xs, Wih, Whh, bih, bhh, rev) { + const H = this.cfg.hid, nin = xs[0].length, T = xs.length, out = new Array(T); let h = new Float32Array(H); + const gi = new Float32Array(3 * H), gh = new Float32Array(3 * H); + for (let s = 0; s < T; s++) { + const t = rev ? T - 1 - s : s; linear(xs[t], 0, Wih, bih, gi, 0, nin, 3 * H); linear(h, 0, Whh, bhh, gh, 0, H, 3 * H); + const nh = new Float32Array(H); + for (let i = 0; i < H; i++) { const r = sigmoid(gi[i] + gh[i]), z = sigmoid(gi[H + i] + gh[H + i]), n = Math.tanh(gi[2 * H + i] + r * gh[2 * H + i]); nh[i] = (1 - z) * n + z * h[i]; } + h = nh; out[t] = h; + } + return out; + } + /** Latin words -> {pen: P(EN) per word, ps: [english, romanized_hindi, hinglish]} */ + run(words) { + if (!words.length) return { pen: [], ps: null }; + const H = this.cfg.hid, xs = words.map((w) => this._wvec(w)), T = xs.length, nin = xs[0].length; + const f = this._gru(xs, this.wih, this.whh, this.bih, this.bhh, false), r = this._gru(xs, this.wihR, this.whhR, this.bihR, this.bhhR, true); + const pen = [], hm = new Float32Array(2 * H), hx = new Float32Array(2 * H).fill(-1e4), z = new Float32Array(2 * H + nin), lo = new Float32Array(2); + for (let t = 0; t < T; t++) { + for (let i = 0; i < H; i++) { z[i] = f[t][i]; z[H + i] = r[t][i]; } z.set(xs[t], 2 * H); + linear(z, 0, this.tw, this.tb, lo, 0, 2 * H + nin, 2); const m = Math.max(lo[0], lo[1]), e0 = Math.exp(lo[0] - m), e1 = Math.exp(lo[1] - m); pen.push(e0 / (e0 + e1)); + for (let i = 0; i < 2 * H; i++) { hm[i] += z[i] / T; if (z[i] > hx[i]) hx[i] = z[i]; } + } + const sin = new Float32Array(4 * H + 1); sin.set(hm, 0); sin.set(hx, 2 * H); sin[4 * H] = pen.reduce((a, b) => a + b, 0) / T; + const h1 = new Float32Array(32); linear(sin, 0, this.s0, this.s0b, h1, 0, 4 * H + 1, 32); for (let i = 0; i < 32; i++) h1[i] = Math.max(0, h1[i]); + const s = new Float32Array(3); linear(h1, 0, this.s2, this.s2b, s, 0, 32, 3); const m = Math.max(...s); const e = [...s].map((v) => Math.exp(v - m)); const Z = e.reduce((a, b) => a + b, 0); + return { pen, ps: e.map((v) => v / Z) }; + } + tag(tokens) { + const lat = tokens.filter((t) => RE_LATIN_APOS.test(t)); const { pen } = this.run(lat); let k = 0; const th = this.x.tok_th ?? 0.5; + return tokens.map((t) => (RE_DEVA.test(t) ? "DEV" : RE_LATIN_APOS.test(t) ? (pen[k++] >= th ? "EN" : "HI") : "OTHER")); + } + mode(tokens) { + const nd = tokens.filter((t) => RE_DEVA.test(t)).length, nl = tokens.filter((t) => RE_LATIN_APOS.test(t)).length; + if (nd && !nl) return "devanagari"; + if (nd && nl) return nd / (nd + nl) >= (this.x.dev_frac ?? 0.5) ? "devanagari" : "mixed_script"; + if (!nl) return "english"; + const { ps } = this.run(tokens.filter((t) => RE_LATIN_APOS.test(t))); const bias = this.x.mode_bias || [0, 0, 0]; + let best = 0, bv = -Infinity; ps.forEach((p, i) => { const v = Math.log(p + 1e-9) + bias[i]; if (v > bv) { bv = v; best = i; } }); + return MODES3[best]; + } +} + +// ------------------------------------------------------------------ tokenizer + policy tables +export const TOKEN_RE = /(https?:\/\/\S+|www\.\S+|[\p{L}\p{N}_.+-]+@[\p{L}\p{N}_-]+\.[\p{L}\p{N}_.]+|[@#][\p{L}\p{N}_]+|[A-Za-z]+(?:'[A-Za-z]+)?|[\u0900-\u0963\u0971-\u097F]+|[0-9\u0966-\u096F]+(?:[.,:][0-9\u0966-\u096F]+)*|[\u0964\u0965]|[^\sA-Za-z0-9\u0900-\u097F])/gu; +const RE_LATIN = /^[A-Za-z]+$/; +const RE_ACRO = /^[b-df-hj-np-tv-z]{2,7}$/; +const LETTER = Object.fromEntries([..."abcdefghijklmnopqrstuvwxyz"].map((c, i) => [c, ("\u090f \u092c\u0940 \u0938\u0940 \u0921\u0940 \u0908 \u090f\u092b \u091c\u0940 \u090f\u091a \u0906\u0908 \u091c\u0947 \u0915\u0947 \u090f\u0932 \u090f\u092e \u090f\u0928 " + + "\u0913 \u092a\u0940 \u0915\u094d\u092f\u0942 \u0906\u0930 \u090f\u0938 \u091f\u0940 \u092f\u0942 \u0935\u0940 \u0921\u092c\u094d\u0932\u094d\u092f\u0942 \u090f\u0915\u094d\u0938 \u0935\u093e\u0908 \u091c\u0947\u0921").split(" ")[i]])); +// Hinglish policy letter names (bhc/hinglish baseline): f -> एफ़, z -> ज़ेड +const LETTERS_H = { ...LETTER, f: "\u090f\u092b\u093c", z: "\u091c\u093c\u0947\u0921" }; +const VAR_SWAPS = [["\u0949", "\u094b"], ["\u094b", "\u0949"], ["\u093e", "\u0949"], ["\u0949", "\u093e"], ["\u0903", ""], ["\u092b\u093c", "\u092b"], ["\u092b", "\u092b\u093c"], + ["\u091c\u093c", "\u091c"], ["\u091c", "\u091c\u093c"], ["\u0901", "\u0902"], ["\u0902", "\u0901"]]; +export const FEATS = ["sc", "scn", "inlex", "uni", "free0", "infree", "incon", "aw", "visarga", "nukta", "chandra", "acro", "variant", "frank", + "skel", "skmatch", "lev", "sc_rel", "scn_rel", "uni_rel", "lenr", "visr", "hfin", "acro_nv", + "ck1", "ck2", "ckr", "abbr", "abbr_r", "novow"]; // v2 casual features (append-only; older params use the first mu.length) +const BRANDS = new Set(`whatsapp google meet paytm amazon flipkart youtube instagram facebook gmail excel uber ola swiggy zomato +netflix linkedin github python java jira slack zoom iphone android windows microsoft apple samsung twitter telegram +chrome firefox safari office365 outlook teams spotify hotstar jio airtel phonepe gpay`.split(/\s+/)); +const KEEP_RE = /^(https?:\/\/|www\.|[@#])|@.+\.|_|\d.*[A-Za-z]|[A-Za-z].*\d|^[a-z]+[A-Z]/; +const isUpper = (t) => /[A-Z]/.test(t) && t === t.toUpperCase(); +const DEFAULTS = { beam: 8, nbest: 8, cbeam: 8, variants: true, acro: true, w: null, mu: null, sd: null, w_lm: 0.8, oov_pen_s: 4.0, w_ch_s: 1.0, use_lm: true, + skel: false, skel_k: 200, skel_m: 16, skel_wu: 0.03, visr: false, acro_v: false, + casual: false, cas_top: 300000, cas_n1: 6, cas_n2: 4, abbr: false, squeeze: 0, k1gate: 99, k2gate: 99 }; +const parseTSV = (txt) => { const m = new Map(); for (const l of txt.split("\n")) { const i = l.indexOf("\t"); if (i > 0) m.set(l.slice(0, i).trim(), l.slice(i + 1).trim()); } return m; }; +const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now()); + +// Sizes are checked against the manifest, never against Content-Length (a CDN redirect or an auth error page has its +// own, unrelated length); a wrong-size body (e.g. an HTML/plain-text error page served with 200) is rejected. +const checkSize = (b, expect, url) => { if (expect && b.length !== expect) throw new Error(`fetch ${url}: got ${b.length} bytes, manifest says ${expect}`); return b; }; +async function fetchBytes(base, name, fetchImpl, expect = 0) { + const url = base.endsWith("/") ? base + name : base + "/" + name; + const r = await fetchImpl(url); if (!r.ok) throw new Error(`fetch ${url}: ${r.status}`); + if (name.endsWith(".gz") && typeof DecompressionStream !== "undefined") return new Uint8Array(await new Response(r.body.pipeThrough(new DecompressionStream("gzip"))).arrayBuffer()); + return checkSize(new Uint8Array(await r.arrayBuffer()), expect, url); +} + +// ------------------------------------------------------------------ consonant-skeleton index (bhc/skel.py; tables generated from it) +const SK_DEV = {"\u0915": "K", "\u0916": "K", "\u0917": "G", "\u0918": "G", "\u091a": "C", "\u091b": "C", "\u091c": "J", "\u091d": "J", "\u091f": "T", "\u0920": "T", "\u0924": "T", "\u0925": "T", "\u0921": "D", "\u0922": "D", "\u0926": "D", "\u0927": "D", "\u0919": "N", "\u091e": "N", "\u0923": "N", "\u0928": "N", "\u0929": "N", "\u092e": "N", "\u0902": "N", "\u0901": "N", "\u092a": "P", "\u092b": "P", "\u092c": "B", "\u092d": "B", "\u0930": "R", "\u0931": "R", "\u090b": "R", "\u0943": "R", "\u0932": "L", "\u0933": "L", "\u0935": "V", "\u0936": "S", "\u0937": "S", "\u0938": "S", "\u0939": "H", "\u0903": "H"}; +const SK_ROM1 = {"k": "K", "q": "K", "g": "G", "j": "J", "z": "J", "t": "T", "d": "D", "n": "N", "m": "N", "p": "P", "f": "P", "b": "B", "r": "R", "l": "L", "v": "V", "w": "V", "s": "S", "h": "H"}; +const SK_ASP = "bcdgjkpstz"; +const DR_C = {"\u0915": "k", "\u0916": "kh", "\u0917": "g", "\u0918": "gh", "\u0919": "n", "\u091a": "ch", "\u091b": "chh", "\u091c": "j", "\u091d": "jh", "\u091e": "n", "\u091f": "t", "\u0920": "th", "\u0921": "d", "\u0922": "dh", "\u0923": "n", "\u0924": "t", "\u0925": "th", "\u0926": "d", "\u0927": "dh", "\u0928": "n", "\u092a": "p", "\u092b": "ph", "\u092c": "b", "\u092d": "bh", "\u092e": "m", "\u092f": "y", "\u0930": "r", "\u0932": "l", "\u0935": "v", "\u0936": "sh", "\u0937": "sh", "\u0938": "s", "\u0939": "h", "\u0933": "l"}; +const DR_V = {"\u0905": "a", "\u0906": "aa", "\u0907": "i", "\u0908": "ee", "\u0909": "u", "\u090a": "oo", "\u090b": "ri", "\u090f": "e", "\u0910": "ai", "\u0913": "o", "\u0914": "au", "\u0911": "o"}; +const DR_M = {"\u093e": "a", "\u093f": "i", "\u0940": "ee", "\u0941": "u", "\u0942": "oo", "\u0943": "ri", "\u0947": "e", "\u0948": "ai", "\u094b": "o", "\u094c": "au", "\u0949": "o", "\u0945": "e"}; +const NUKTA_ROM = { k: "q", kh: "kh", g: "g", j: "z", ph: "f", d: "r", dh: "rh" }; +export function devSkel(w) { let o = ""; for (const ch of w) { const c = SK_DEV[ch]; if (c && o[o.length - 1] !== c) o += c; } return o; } +export function romSkels(w, maxk = 4) { + w = w.toLowerCase(); let outs = [""]; let i = 0; const n = w.length; + while (i < n) { + const ch = w[i]; let step = 1, opts; + if ((ch === "t" || ch === "s") && w.startsWith("ion", i + 1)) { opts = ch === "t" ? ["SN", "TN"] : ["SN"]; step = 4; } + else if (ch === "c") { + const nx = i + 1 < n ? w[i + 1] : ""; + if (nx === "h") { opts = ["C", "K"]; step = 2; while (i + step < n && (w[i + step] === "c" || w[i + step] === "h")) step++; } + else if (nx === "e" || nx === "i" || nx === "y") opts = ["S", "K"]; + else if (nx === "k") { opts = ["K"]; step = 2; } + else opts = ["K", "S"]; + } else if (ch === "x") opts = ["KS"]; + else if (SK_ASP.includes(ch) && i + 1 < n && w[i + 1] === "h") { opts = [SK_ROM1[ch] || ""]; step = 2; while (i + step < n && w[i + step] === "h") step++; } + else opts = [SK_ROM1[ch] || ""]; + const nw = []; + for (const o of outs) for (const q of opts) { let s = o; for (const c of q) if (!s || s[s.length - 1] !== c) s += c; nw.push(s); } + outs = [...new Set(nw)].slice(0, maxk); i += step; + } + return outs; +} +export function devRom(w) { + const out = [], a = [...w], L = a.length; + for (let j = 0; j < L; j++) { + const ch = a[j]; + if (DR_C[ch] !== undefined) { + out.push(DR_C[ch]); let nx = j + 1 < L ? a[j + 1] : ""; + if (nx === "\u093c") nx = j + 2 < L ? a[j + 2] : ""; + if (nx && DR_M[nx] === undefined && nx !== "\u094d" && j + 1 < L - 1) out.push("a"); + } else if (DR_V[ch] !== undefined) out.push(DR_V[ch]); + else if (DR_M[ch] !== undefined) out.push(DR_M[ch]); + else if (ch === "\u0902" || ch === "\u0901") out.push("n"); + else if (ch === "\u0903") out.push("h"); + else if (ch === "\u093c" && out.length) { const l = out[out.length - 1]; out[out.length - 1] = NUKTA_ROM[l] ?? l; } + } + return out.join(""); +} +export function lev(a, b) { + if (a.length < b.length) [a, b] = [b, a]; + let prev = Array.from({ length: b.length + 1 }, (_, j) => j); + for (let i = 1; i <= a.length; i++) { + const cur = [i]; + for (let j = 1; j <= b.length; j++) cur.push(Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] !== b[j - 1] ? 1 : 0))); + prev = cur; + } + return prev[b.length]; +} +class SkelIndex { + constructor(lm) { + const b = new Map(), N = lm.words.length; + for (let i = 0; i < N; i++) { const k = devSkel(lm.words[i]); if (k) { let v = b.get(k); if (!v) b.set(k, (v = [])); v.push(i); } } + for (const v of b.values()) v.sort((x, y) => (lm.uni[y] - lm.uni[x]) || (x - y)); + this.b = b; + } + lookup(roman, topk = 24) { + const seen = [], S = new Set(); + for (const k of romSkels(roman)) { const v = this.b.get(k); if (!v) continue; for (const i of v.slice(0, topk)) if (!S.has(i)) { S.add(i); seen.push(i); } } + return seen; + } +} +// ------------------------------------------------------------------ casual / chat-romanization keys (bhc/casual.py; keep 1:1) +const CAS_DC = {"\u0915": "k", "\u0916": "k", "\u0917": "g", "\u0918": "g", "\u091a": "c", "\u091b": "c", "\u091c": "j", "\u091d": "j", "\u091f": "t", "\u0920": "t", "\u0924": "t", "\u0925": "t", "\u0921": "d", "\u0922": "d", "\u0926": "d", "\u0927": "d", "\u0923": "n", "\u0928": "n", "\u0929": "n", "\u092a": "p", "\u092b": "p", "\u092c": "b", "\u092d": "b", "\u092e": "m", "\u092f": "y", "\u0930": "r", "\u0931": "r", "\u0932": "l", "\u0933": "l", "\u0935": "v", "\u0936": "s", "\u0937": "s", "\u0938": "s", "\u0939": "h", "\u0958": "k", "\u0959": "k", "\u095a": "g", "\u095b": "j", "\u095c": "R", "\u095d": "R", "\u095e": "p", "\u095f": "y"}; +const CAS_DV = {"\u0905": "a", "\u0906": "a", "\u093e": "a", "\u0907": "i", "\u0908": "i", "\u093f": "i", "\u0940": "i", "\u0909": "u", "\u090a": "u", "\u0941": "u", "\u0942": "u", "\u090f": "e", "\u0910": "e", "\u0947": "e", "\u0948": "e", "\u0945": "e", "\u090d": "e", "\u0913": "o", "\u0914": "o", "\u0911": "o", "\u094b": "o", "\u094c": "o", "\u0949": "o", "\u090b": "r", "\u0943": "r"}; +export const ABBR = {"h": ["\u0939\u0948", "\u0939\u0948\u0902"], "hy": ["\u0939\u0948"], "hn": ["\u0939\u093e\u0901", "\u0939\u0948\u0902"], "hm": ["\u0939\u092e", "\u0939\u092e\u094d\u092e"], "k": ["\u0915\u0947", "\u0915\u0940", "\u0913\u0915\u0947"], "ki": ["\u0915\u0940", "\u0915\u093f"], "n": ["\u0928\u093e", "\u0928"], "ni": ["\u0928\u0939\u0940\u0902", "\u0928\u0940"], "nai": ["\u0928\u0939\u0940\u0902", "\u0928\u0908"], "nhi": ["\u0928\u0939\u0940\u0902"], "nhe": ["\u0928\u0939\u0940\u0902"], "nahi": ["\u0928\u0939\u0940\u0902"], "kr": ["\u0915\u0930"], "krna": ["\u0915\u0930\u0928\u093e"], "kro": ["\u0915\u0930\u094b"], "tm": ["\u0924\u0941\u092e"], "tu": ["\u0924\u0942"], "mai": ["\u092e\u0948\u0902", "\u092e\u0947\u0902"], "me": ["\u092e\u0947\u0902", "\u092e\u0948\u0902"], "m": ["\u092e\u0948\u0902", "\u092e\u0947\u0902"], "thk": ["\u0920\u0940\u0915"], "tk": ["\u0920\u0940\u0915", "\u0924\u0915"], "yr": ["\u092f\u093e\u0930"], "yrr": ["\u092f\u093e\u0930"], "plz": ["\u092a\u094d\u0932\u0940\u091c\u093c"], "pls": ["\u092a\u094d\u0932\u0940\u091c\u093c"], "plzz": ["\u092a\u094d\u0932\u0940\u091c\u093c"], "tq": ["\u0925\u0948\u0902\u0915 \u092f\u0942"], "ty": ["\u0925\u0948\u0902\u0915 \u092f\u0942"], "thx": ["\u0925\u0948\u0902\u0915\u094d\u0938"], "thnx": ["\u0925\u0948\u0902\u0915\u094d\u0938"], "thnks": ["\u0925\u0948\u0902\u0915\u094d\u0938"], "gn": ["\u0917\u0941\u0921 \u0928\u093e\u0907\u091f"], "gm": ["\u0917\u0941\u0921 \u092e\u0949\u0930\u094d\u0928\u093f\u0902\u0917"], "ok": ["\u0913\u0915\u0947"], "okk": ["\u0913\u0915\u0947"], "kk": ["\u0913\u0915\u0947"], "sry": ["\u0938\u0949\u0930\u0940"], "bro": ["\u092c\u094d\u0930\u094b"], "msg": ["\u092e\u0948\u0938\u0947\u091c"], "bday": ["\u092c\u0930\u094d\u0925\u0921\u0947"], "clg": ["\u0915\u0949\u0932\u0947\u091c"], "ofc": ["\u0911\u092b\u093c\u093f\u0938"], "pic": ["\u092a\u093f\u0915"], "bhai": ["\u092d\u093e\u0908"], "bht": ["\u092c\u0939\u0941\u0924"], "bhot": ["\u092c\u0939\u0941\u0924"], "bohot": ["\u092c\u0939\u0941\u0924"], "pta": ["\u092a\u0924\u093e"], "kb": ["\u0915\u092c"], "jb": ["\u091c\u092c"], "sb": ["\u0938\u092c"], "mtlb": ["\u092e\u0924\u0932\u092c"], "kch": ["\u0915\u0941\u091b"], "kuch": ["\u0915\u0941\u091b"]}; +const CAS_VOW = new Set(["a", "e", "i", "o", "u"]), CAS_ASP = new Set([..."kgjtdpbc"]); +function casFin(s) { let o = ""; for (let i = 0; i < s.length; i++) { const ch = s[i]; if (ch === "a" && i > 0) continue; if (o && o[o.length - 1] === ch) continue; o += ch; } return o; } +export function k2Of(k1) { + if (!k1) return ""; + let s = k1[0]; for (const c of k1.slice(1)) if (!CAS_VOW.has(c)) s += c; + if (s && CAS_VOW.has(s[0])) s = s.slice(1) || s; + let o = ""; for (const ch of s) if (!(o && o[o.length - 1] === ch)) o += ch; return o; +} +export function devKeys(w) { + const a = [...w], raw = []; + for (let i = 0; i < a.length; i++) { + const ch = a[i]; + if (ch === "\u093c") { if (raw.length && raw[raw.length - 1] === "d" && i > 0 && (a[i - 1] === "\u0921" || a[i - 1] === "\u0922")) raw[raw.length - 1] = "R"; continue; } + if (CAS_DC[ch] !== undefined) raw.push(CAS_DC[ch]); else if (CAS_DV[ch] !== undefined) raw.push(CAS_DV[ch]); + } + const s = raw.join(""); if (!s) return []; + const ks = [...new Set([casFin(s.split("R").join("d")), casFin(s.split("R").join("r"))])].filter((k) => k); + return ks.sort(); +} +export function romKeys(t, cap = 12) { + t = t.toLowerCase().replace(/[^a-z]/g, ""); if (!t) return [[], []]; + t = t.replace(/(.)\1{2,}/g, "$1"); + t = t.split("chh").join("c").split("cch").join("c").split("ch").join("c").split("sh").join("s"); + t = t.split("x").join("ks").split("q").join("k").split("z").join("j").split("f").join("p").split("w").join("v"); + const alts = [], L = t.length, V = (c) => CAS_VOW.has(c); let i = 0; + while (i < L) { + const c = t[i], nx = i + 1 < L ? t[i + 1] : "", prev = i > 0 ? t[i - 1] : ""; + if (c === "h" && CAS_ASP.has(prev) && i > 0) { alts.push(["", "h"]); i += 1; continue; } + if (nx && (c === "a" || c === "e") && (nx === "i" || nx === "y") && (i + 2 >= L || !V(t[i + 2]))) { + if (c === "a" && nx === "i") { alts.push(["e", "ai"]); i += 2; continue; } + if (c === "a" && nx === "y" && i + 2 >= L) { alts.push(["e"]); i += 2; continue; } + if (c === "e" && nx === "y" && i + 2 >= L) { alts.push(["e"]); i += 2; continue; } + } + if (nx && (c === "a" || c === "o") && "uvw".includes(nx) && (i + 2 >= L || !V(t[i + 2])) && c + nx !== "av") { alts.push(["o", c + "u"]); i += 2; continue; } + if (c === "o" && nx === "u") { alts.push(["o", "u"]); i += 2; continue; } + if (c === "e" && nx === "e") { alts.push(["i"]); i += 2; continue; } + if (c === "o" && nx === "o") { alts.push(["u", "o"]); i += 2; continue; } + if (c === "e" && nx === "i" && (i + 2 >= L || !V(t[i + 2]))) { alts.push(["e"]); i += 2; continue; } + if (c === "e" && nx === "h" && i + 2 < L && !V(t[i + 2]) && i > 0) { alts.push(["e", ""]); i += 1; continue; } + if (nx && c === "i" && "aeou".includes(nx) && i > 0) { alts.push(["y", "i"]); i += 1; continue; } + if (c === "y" && i === L - 1 && prev && !V(prev)) { alts.push(["e", "i"]); i += 1; continue; } + if (c === "n" && V(prev) && i > 0 && (i === L - 1 || (nx && !V(nx) && nx !== "y"))) { alts.push(["n", ""]); i += 1; continue; } + if ((c === "i" || c === "u") && prev === "r" && i >= 2 && !V(t[i - 2])) { alts.push([c, ""]); i += 1; continue; } + alts.push([c]); i += 1; + } + let outs = [""]; + for (const a of alts) { const nxt = []; for (const o of outs) for (const x of a) nxt.push(o + x); outs = [...new Set(nxt)].slice(0, 64); } + const k1 = [], seen = new Set(); + for (const o of outs) { const k = casFin(o); if (k && !seen.has(k)) { seen.add(k); k1.push(k); } } + const k1c = k1.slice(0, cap); const k2 = [...new Set(k1c.map(k2Of).filter((k) => k))]; + return [k1c, k2]; +} +export function squeezeIn(t, mode = 0) { + if (!mode) return t; + if (mode === 1) return t.replace(/([a-z])\1{2,}/g, "$1"); + return t.replace(/([a-z])\1{2,}/g, (m, c) => (/[aeo]/.test(c) ? c + c : c)); +} +class CasualIndex { + constructor(lm, topN = 300000, perKey = 8) { + const N = lm.words.length, uni = lm.uni, order = Array.from({ length: N }, (_, i) => i); + order.sort((x, y) => (uni[y] - uni[x]) || (x - y)); + this.k1 = new Map(); this.k2 = new Map(); + const put = (m, k, i) => { let b = m.get(k); if (!b) m.set(k, (b = [])); if (b.length < perKey && !b.includes(i)) b.push(i); }; + for (let r = 0; r < Math.min(N, topN); r++) { const i = order[r]; for (const k of devKeys(lm.words[i])) { put(this.k1, k, i); const k2 = k2Of(k); if (k2) put(this.k2, k2, i); } } + } + lookup(roman, n1 = 6, n2 = 4) { + const [k1, k2] = romKeys(roman), r1 = [], r2 = []; + for (const k of k1) for (const i of this.k1.get(k) || []) if (!r1.includes(i)) r1.push(i); + for (const k of k2) for (const i of this.k2.get(k) || []) if (!r2.includes(i)) r2.push(i); + return [r1.slice(0, n1 * 2), r2.slice(0, n2 * 2)]; + } +} +const VIS_RE = /[aeiou]h$/; + +// ------------------------------------------------------------------ the engine +export class BhashaCore { + /** + * Load a bundle (directory with manifest.json) from a URL (browser / worker) or via a custom fetch (Node: file loader). + * dict: true (default) = also load the dictionary pack and wait for it; false = core only (call loadDict() later). + * cache: Cache API cache name for the dictionary pack (browser / worker; default "aksharalp-dict"), or false. + */ + static async load(base, { fetch: fetchImpl = globalThis.fetch, onProgress, dict = true, cache = "aksharalp-dict" } = {}) { + const t0 = now(); const txt = (b) => new TextDecoder().decode(b); + let man; try { man = JSON.parse(txt(await fetchBytes(base, "manifest.json", fetchImpl))); } catch { man = { files: { model: "xlit.q4", lex: "lex.bin", lm: "lm.bin", params: "params.json", lid: "lid.int8", loan: "loan.tsv", natural: "natural_hi.tsv" } }; } + const F = man.files, SZ = man.sizes || {}, get = async (n, opt) => { if (!n) return null; try { const b = await fetchBytes(base, n, fetchImpl, SZ[n] && SZ[n].bytes); onProgress && onProgress(n); return b; } catch (e) { if (opt) return null; throw e; } }; + const [mb, lexb, lmb, pb, lidb, loanb, natb] = await Promise.all([get(F.model), get(F.lex, true), get(F.lm, true), get(F.params, true), get(F.lid, true), get(F.loan, true), get(F.natural, true)]); + const e = new BhashaCore({ model: mb, lex: lexb, lm: lmb, params: pb ? JSON.parse(txt(pb)) : {}, lid: lidb, loan: loanb ? txt(loanb) : null, natural: natb ? txt(natb) : null, manifest: man }); + e.base = base; e.fetchImpl = fetchImpl; + if (dict && man.packs && man.packs.dict) await e.loadDict({ onProgress, cache }); + e.loadMs = now() - t0; return e; + } + /** Fetch (or read from cache) the dictionary pack and attach it. Returns true on success; on any failure the engine + * keeps working in core-only mode and the error is kept in this.dictError. */ + async loadDict({ onProgress, cache = "aksharalp-dict" } = {}) { + const pk = this.manifest && this.manifest.packs && this.manifest.packs.dict; if (!pk) return false; + const dir = pk.dir.endsWith("/") ? pk.dir : pk.dir + "/", fetchImpl = this.fetchImpl || globalThis.fetch; + const useCache = cache && typeof caches !== "undefined"; + const get = async (n) => { + if (!n) return null; + const ver = (pk.sizes && pk.sizes[n] && pk.sizes[n].sha256) || "", expect = (pk.sizes && pk.sizes[n] && pk.sizes[n].bytes) || 0; + if (useCache) { + try { + const url = new URL(dir + n, new URL(this.base.endsWith("/") ? this.base : this.base + "/", globalThis.location && globalThis.location.href)).href; + const c = await caches.open(cache), key = url + "?v=" + ver.slice(0, 16), hit = await c.match(key); + if (hit) { const hb = new Uint8Array(await hit.arrayBuffer()); if (!expect || hb.length === expect) { onProgress && onProgress(n, "cache"); return hb; } await c.delete(key); } + const r = await fetchImpl(url); if (!r.ok) throw new Error(`fetch ${url}: ${r.status}`); + const b = checkSize(new Uint8Array(await r.arrayBuffer()), expect, url); try { await c.put(key, new Response(b)); } catch (_) {} // stored as a fresh, non-redirected response + onProgress && onProgress(n, "network"); return b; + } catch (e) { if (String(e).includes("fetch ")) throw e; } + } + const b = await fetchBytes(this.base, dir + n, fetchImpl, expect); onProgress && onProgress(n, "network"); return b; + }; + try { + const [lexb, lmb, pb] = await Promise.all([get(pk.files.lex), get(pk.files.lm), get(pk.files.params)]); + this.attachDict({ lex: lexb, lm: lmb, params: pb ? JSON.parse(new TextDecoder().decode(pb)) : {} }); + this.dictInfo = { name: pk.name || "dict", words: this.lm.N, bytes: pk.bytes, gz: pk.gz }; return true; + } catch (e) { this.dictError = String(e && e.message || e); return false; } + } + /** attach an already-fetched dictionary pack: {lex, lm, params} (params override the core params) */ + attachDict({ lex, lm = null, params = {} }) { + this.p = { ...DEFAULTS, ...this.coreParams, ...params }; + this._setLex(lex, lm); this.cache = new Map(); this.hasDict = this.lm.N > 0; + } + _setLex(lex, lm) { + this.lm = new LexLM(lex, lm && this.p.use_lm ? lm : null); + if (!this.lm.N) { this.p.cbeam = 0; this.p.skel = false; } + this.cix = this.p.casual && this.lm.N ? new CasualIndex(this.lm, this.p.cas_top) : null; + // the trie never contains the empty string (as in Python's LazyTrie), otherwise "" would take a constrained-beam slot + const sw = this.lm.sortedWords, tw = sw.length && sw[0] === "" ? sw.slice(1) : sw; + this.trie = this.p.cbeam ? new LazyTrie(tw) : null; + this.skix = this.p.skel ? new SkelIndex(this.lm) : null; + } + constructor({ model, lex = null, lm = null, params = {}, lid = null, loan = null, natural = null, manifest = null }) { + this.coreParams = params; this.p = { ...DEFAULTS, ...params }; this.manifest = manifest; + this.model = new XlitModel(parseWeights(model)); + this._setLex(lex, lm); this.hasDict = this.lm.N > 0; this.dictInfo = null; this.dictError = null; + this.lid = lid ? new LID(parseWeights(lid)) : null; + this.loan = loan ? parseTSV(loan) : new Map(); + this.natural = natural ? parseTSV(natural) : new Map(); + this.cache = new Map(); + } + info() { + const s = this.manifest && this.manifest.sizes ? this.manifest.sizes : {}; + const used = Object.values(this.manifest ? this.manifest.files : {}).filter((f) => f && s[f]); + return { params: this.model.nParams + (this.lid ? this.lid.nParams : 0), xlitParams: this.model.nParams, lidParams: this.lid ? this.lid.nParams : 0, + lexWords: this.lm.N, bytes: used.reduce((a, f) => a + s[f].bytes, 0), gz: used.reduce((a, f) => a + s[f].gz, 0), hasLID: !!this.lid, hasLM: this.lm.hasLM, + hasDict: this.hasDict, dict: this.dictInfo, dictError: this.dictError, hasLoan: this.loan.size > 0, hasNatural: this.natural.size > 0 }; + } + + /** candidate rows with reranker features for one (lower-cased) Latin word: [[devanagari, fv, wid], ...] */ + candFeats(word) { + const p = this.p, w = squeezeIn(word.toLowerCase(), p.squeeze); const hit = this.cache.get(w); if (hit) return hit; + const m = this.model, ids = encSrc(w), enc = m.encode(ids), S = ids.length; + const free = m.beam(enc, S, { beam: p.beam, nbest: p.nbest }); + const con = p.cbeam && this.trie ? m.beam(enc, S, { beam: p.beam, nbest: p.nbest, trie: this.trie }) : []; + const d = new Map(); + free.forEach(([s, sc], i) => { if (s) d.set(s, { sc, infree: 1, frank: i, free0: +(i === 0), beamEntry: 1 }); }); + for (const [s, sc] of con) if (s) { if (!d.has(s)) d.set(s, { sc, infree: 0, frank: p.nbest, free0: 0, beamEntry: 1 }); d.get(s).incon = 1; } + if (p.variants) { + const top = [...d.keys()].sort((a, b) => d.get(b).sc - d.get(a).sc).slice(0, 4); + for (const s of top) for (const [a, b] of VAR_SWAPS) if (s.includes(a)) { + const v = s.split(a).join(b); if (!d.has(v) && this.lm.id(v) !== null) d.set(v, { sc: m.scorePair(enc, v), infree: 0, frank: p.nbest, free0: 0, variant: 1 }); + } + } + if (p.acro && (RE_ACRO.test(w) || (p.acro_v && w.length >= 2 && w.length <= 4))) { + const v = [...w].map((c) => LETTER[c]).join(""); + if (!d.has(v)) d.set(v, { sc: m.scorePair(enc, v), infree: 0, frank: p.nbest, free0: 0, acro: 1 }); else d.get(v).acro = 1; + } + if (p.visr && VIS_RE.test(w)) { // Aksharantar convention: literal word-final h -> visarga + const top = [...d.keys()].filter((x) => d.get(x).beamEntry).sort((x, y) => d.get(y).sc - d.get(x).sc).slice(0, 2); + for (const s of top) { + const v = s.endsWith("\u0939") ? s.slice(0, -1) + "\u0903" : (!s.endsWith("\u0903") ? s + "\u0903" : null); + if (v && !d.has(v)) d.set(v, { sc: m.scorePair(enc, v), infree: 0, frank: p.nbest, free0: 0, visr: 1 }); + } + } + if (this.skix) { + const L = w.length + 1, words = this.lm.words, uni = this.lm.uni; + const ids = this.skix.lookup(w, p.skel_k).map((i) => [i, lev(w, devRom(words[i])) / L - p.skel_wu * uni[i]]) + .sort((x, y) => x[1] - y[1]).slice(0, p.skel_m); + for (const [i] of ids) { const v = words[i]; if (!d.has(v)) d.set(v, { sc: m.scorePair(enc, v), infree: 0, frank: p.nbest, free0: 0, skel: 1 }); else d.get(v).skel = 1; } + } + if (this.cix) { + const [r1, r2] = this.cix.lookup(w, p.cas_n1, p.cas_n2), words = this.lm.words, nv = (w.match(/[aeiou]/g) || []).length; + const ab = new Set(p.abbr && Object.prototype.hasOwnProperty.call(ABBR, w) ? ABBR[w] : []), s1 = new Set(r1.slice(0, p.cas_n1).map((i) => words[i])); + for (const [key, ids, n] of [["ck1", r1, p.cas_n1], ["ck2", r2, p.cas_n2]]) ids.slice(0, n).forEach((i, rk) => { + const v = words[i]; + if (!d.has(v)) { + if (!ab.has(v) && ((s1.has(v) && nv > p.k1gate) || (!s1.has(v) && nv > p.k2gate))) return; // vowel gate + d.set(v, { sc: m.scorePair(enc, v), infree: 0, frank: p.nbest, free0: 0 }); + } + const e = d.get(v); e[key] = 1; e.ckr = Math.min(e.ckr ?? 1, rk / n); + }); + } + if (p.abbr && Object.prototype.hasOwnProperty.call(ABBR, w)) ABBR[w].forEach((v, rk) => { + if (!d.has(v)) d.set(v, { sc: m.scorePair(enc, v), infree: 0, frank: p.nbest, free0: 0 }); + const e = d.get(v); e.abbr = 1; e.abbr_r = rk; + }); + const rows = [], sks = new Set(romSkels(w)), Lw = w.length + 1, vis = VIS_RE.test(w), novow = !/[aeiou]/.test(w); + for (const [s, e] of d) { + const wid = this.lm.id(s), L = [...s].length; + rows.push([s, [e.sc, e.sc / (L + 1), +(wid !== null), wid !== null ? this.lm.unigram(wid) : 0, e.free0 || 0, e.infree || 0, e.incon || 0, + +(s.includes("\u0949") || s.includes("\u0911")), +s.includes("\u0903"), +s.includes("\u093c"), +s.includes("\u0901"), e.acro || 0, e.variant || 0, (e.frank ?? p.nbest) / p.nbest, + e.skel || 0, +sks.has(devSkel(s)), lev(w, devRom(s)) / Lw, 0, 0, 0, L / Lw, e.visr || 0, +(vis && s.endsWith("\u0903")), +(!!e.acro && novow), + e.ck1 || 0, e.ck2 || 0, e.ckr ?? 1, e.abbr || 0, e.abbr_r ?? 3, +!/[aeiouy]/.test(w)], wid]); + } + if (rows.length) { + let msc = -Infinity, mscn = -Infinity, mu = null; + for (const [, fv] of rows) { msc = Math.max(msc, fv[0]); mscn = Math.max(mscn, fv[1]); if (fv[2]) mu = mu === null ? fv[3] : Math.max(mu, fv[3]); } + for (const [, fv] of rows) { fv[17] = fv[0] - msc; fv[18] = fv[1] - mscn; fv[19] = fv[2] ? fv[3] - (mu ?? 0) : 0; } + } + if (this.cache.size > 20000) this.cache.clear(); + this.cache.set(w, rows); return rows; + } + score(fv) { + const p = this.p; if (!p.w && !p.w1) return fv[0] + 0.2 * (fv[2] ? fv[3] : -25.0); + const n = p.mu.length, z = new Array(n); for (let i = 0; i < n; i++) z[i] = (fv[i] - p.mu[i]) / p.sd[i]; + if (p.w1) { // 1-hidden-layer tanh MLP reranker (fit on VALID only) + let s = p.b2 || 0; + for (let h = 0; h < p.w1.length; h++) { const r = p.w1[h]; let a = p.b1[h]; for (let i = 0; i < n; i++) a += r[i] * z[i]; s += p.w2[h] * Math.tanh(a); } + return s; + } + let s = 0; for (let i = 0; i < Math.min(n, p.w.length); i++) s += p.w[i] * z[i]; return s; + } + /** word mode: top-k Devanagari candidates for a romanized word */ + word(w, k = 8) { + if (!RE_LATIN.test(w)) return [{ text: w, score: 0 }]; + const rows = this.candFeats(w).map(([s, fv]) => ({ text: s, score: this.score(fv) })); + return rows.sort((a, b) => b.score - a.score).slice(0, k); + } + /** sentence Viterbi over candidate lattices with the bigram LM (bhc/translit2.py decode_slots) */ + decodeSlots(slots, context = true) { + const p = this.p, lm = this.lm, lat = []; + for (const [kind, t] of slots) { + if (kind === "x") { + let sc = this.candFeats(t).map(([s, fv, wid]) => [s, this.score(fv), wid]); + if (sc.length) { + const mx = Math.max(...sc.map((x) => x[1])), lse = mx + Math.log(sc.reduce((a, x) => a + Math.exp(x[1] - mx), 0)); + sc = sc.map(([s, v, wid]) => [s, p.w_ch_s * (v - lse), wid]).sort((a, b) => b[1] - a[1]).slice(0, 12); + } + lat.push(sc.length ? sc : [[t, 0, null]]); + } else { + const wid = RE_DEVA.test(t) ? lm.id(t) ?? -1 : -2; lat.push([[t, 0, wid]]); + } + } + const post = lat.map((o) => o.map(([s, v]) => [s, v])); // per-slot candidate list (for UI chips) + if (!context || !p.use_lm || !lm.hasLM) return { out: lat.map((o) => o.reduce((a, b) => (b[1] > a[1] ? b : a))[0]), lat: post }; + let beams = [[0, null, []]]; + for (const opts of lat) { + if (opts[0][2] === -2 && opts.length === 1) { beams = beams.map(([s, h, ch]) => [s, h, ch.concat([opts[0][0]])]); continue; } + const best = new Map(); + for (const [s, h, ch] of beams) for (const [cand, csc, wid] of opts) { + const w = wid === null || wid === -1 ? null : wid; + const tot = s + csc + p.w_lm * (lm.logp(h, w) - (w === null ? p.oov_pen_s : 0)); + if (!best.has(cand) || tot > best.get(cand)[0]) best.set(cand, [tot, w, ch.concat([cand])]); + } + beams = [...best.values()].sort((a, b) => b[0] - a[0]).slice(0, 16); + } + let bb = null, bv = -Infinity; + for (const b of beams) { const v = b[0] + p.w_lm * lm.logp(b[1], lm.N); if (v > bv) { bv = v; bb = b; } } + return { out: bb[2], lat: post }; + } + /** plain Track-B sentence conversion (bhc/translit2.py convert) */ + convertPlain(text) { + const spans = [...text.matchAll(TOKEN_RE)].map((m) => [m.index, m.index + m[0].length, m[0]]); + const { out } = this.decodeSlots(spans.map(([, , t]) => (RE_LATIN.test(t) ? ["x", t] : ["fix", t]))); + if (out.length && out[out.length - 1] === ".") out[out.length - 1] = "\u0964"; + let s = "", pos = 0; spans.forEach(([a, b], i) => { s += text.slice(pos, a) + out[i]; pos = b; }); return s + text.slice(pos); + } + + /** + * Full pipeline: tokenise -> language ID -> policy (keep / acronym / loanword / glossary) -> contextual decoding. + * @param {string} text + * @param {{mode?: 'conversational'|'natural'|'strict', k?: number, context?: boolean, overrides?: Object}} opt + * context: false = word-by-word (no sentence LM) + * overrides: {tokenStartOffset: chosenDevanagari} pins a user-picked suggestion (re-decodes the rest in context) + * @returns {{output: string, tokens: Array<{text,start,end,output,lang,source,candidates:Array<{text,score}>}>, sentence: string|null, ms: number}} + */ + transliterate(text, { mode = "conversational", k = 5, overrides = {}, context = true } = {}) { + const t0 = now(); const policy = mode === "strict" ? "strict" : "conversational"; + const spans = [...text.matchAll(TOKEN_RE)].map((m) => [m.index, m.index + m[0].length, m[0]]); + const toks = spans.map((s) => s[2]); + const tags = this.lid ? this.lid.tag(toks) : toks.map((t) => (RE_DEVA.test(t) ? "DEV" : RE_LATIN_APOS.test(t) ? (this.loan.has(t.toLowerCase()) ? "EN" : "HI") : "OTHER")); + const slots = [], src = []; let prevBrand = false; + for (let i = 0; i < toks.length; i++) { + const t = toks[i], tag = tags[i], nxt = toks[i + 1] || "", low = t.toLowerCase(), ov = overrides[spans[i][0]]; + if (ov !== undefined) { slots.push(["fix", ov]); src.push("user"); prevBrand = false; continue; } + if (!RE_LATIN_APOS.test(t) || KEEP_RE.test(t) || nxt === "(") { + if (t === "." && (i === toks.length - 1 || /^[A-Z]/.test(toks[i + 1] || ""))) slots.push(["fix", "\u0964"]); else slots.push(["fix", t]); + src.push(RE_DEVA.test(t) ? "devanagari" : KEEP_RE.test(t) ? "keep" : "punct"); prevBrand = false; continue; + } + const isBrand = BRANDS.has(low) || (prevBrand && /^[A-Z]/.test(t)); prevBrand = isBrand; + if (policy === "strict" && isBrand) { slots.push(["fix", t]); src.push("brand"); continue; } + if (mode === "natural" && tag === "EN" && this.natural.has(low)) { slots.push(["fix", this.natural.get(low)]); src.push("glossary"); continue; } + if (isUpper(t) && t.length >= 2 && t.length <= 5 && !this.loan.has(low)) { slots.push(["fix", [...low].map((c) => LETTERS_H[c]).join("")]); src.push("acronym"); continue; } + if ((tag === "EN" || isBrand) && this.loan.has(low)) { slots.push(["fix", this.loan.get(low)]); src.push("loanword"); continue; } + slots.push(["x", t]); src.push("model"); + } + const { out, lat } = this.decodeSlots(slots, context); + let output = "", pos = 0; spans.forEach(([a, b], i) => { output += text.slice(pos, a) + out[i]; pos = b; }); output = (output + text.slice(pos)).trim(); + const tokens = spans.map(([a, b, t], i) => { + let cands = []; + if (slots[i][0] === "x") { + const mx = lat[i][0] ? lat[i][0][1] : 0; + cands = lat[i].slice(0, k).map(([s, v]) => ({ text: s, score: Math.exp(v - mx) })); + } else if (RE_LATIN_APOS.test(t) && !KEEP_RE.test(t)) { + cands = [{ text: out[i], score: 1 }]; + for (const c of this.word(t, k)) if (c.text !== out[i]) cands.push({ text: c.text, score: null }); + if (out[i] !== t) cands.push({ text: t, score: null }); + cands = cands.slice(0, k); + } + const isW = RE_LATIN_APOS.test(t) || RE_DEVA.test(t); + return { text: t, start: a, end: b, output: out[i], lang: isW ? tags[i] : "OTHER", source: src[i], candidates: cands }; + }); + let sentence = null; if (this.lid && toks.length) try { sentence = this.lid.mode(toks); } catch { sentence = null; } + return { output, tokens, sentence, ms: now() - t0 }; + } +} + +/** Node helper: load a bundle from a local directory. */ +export async function loadFromDir(dir, opts = {}) { + const fs = await import("node:fs/promises"), path = await import("node:path"); + const f = async (u) => { try { const b = await fs.readFile(path.resolve(u)); return new Response(b); } catch { return new Response(null, { status: 404 }); } }; + return BhashaCore.load(dir, { fetch: f, cache: false, ...opts }); +} +export { BhashaCore as AksharaLP }; +export default BhashaCore; diff --git a/assets/accuracy.png b/assets/accuracy.png new file mode 100644 index 0000000000000000000000000000000000000000..1adfa063d187b1cb6c1cbe06309d27a4b08ed40b Binary files /dev/null and b/assets/accuracy.png differ diff --git a/assets/accuracy.svg b/assets/accuracy.svg new file mode 100644 index 0000000000000000000000000000000000000000..067a2889b3ae9b933234f51db2f89bc89ea7f41f --- /dev/null +++ b/assets/accuracy.svg @@ -0,0 +1 @@ +Accuracy is a trade-off. Here is the whole picture.Aksharantar test · top-1 exact match (%) · 95% bootstrap CIs · beam 8 + dictionary0%20%40%60%80%100%Hindi70.0469.33Bengali64.6470.52Telugu78.7482.42Kannada77.3782.61Tamil71.7875.95AksharaLP MultiIndicIndicXlit full package (PyTorch port)Hindi reused; four new languages evaluated once \ No newline at end of file diff --git a/assets/banner.png b/assets/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..4f28b993a2e4c8896e5d8e12f20072d3d5803a6c --- /dev/null +++ b/assets/banner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27da3ac213f72636663b84026093cf2a1c80f825d8d67f110466ae9df0d3d6fc +size 210405 diff --git a/assets/banner.svg b/assets/banner.svg new file mode 100644 index 0000000000000000000000000000000000000000..2ab02673bff75defb0dffd1308e5534aecc88143 --- /dev/null +++ b/assets/banner.svg @@ -0,0 +1 @@ +AKSHARALP / MULTIINDIC / 8MFive scripts.One small footprint.Roman letters → native words. On your device.5 word models · 4.94M params+ Hindi sentence LM · 2.77MHIHindiBNBengaliTETeluguKNKannadaTATamil7,715,520EXACT NEURAL PARAMETERS, INCLUDING OPTIONAL LMPRIVATE REVIEW · NON-COMMERCIAL RELEASE · CC-BY-NC-4.0by Sourojit Dhua \ No newline at end of file diff --git a/assets/sizes.png b/assets/sizes.png new file mode 100644 index 0000000000000000000000000000000000000000..193fb32710d1f2a7e007dc478b9bf6005d470368 Binary files /dev/null and b/assets/sizes.png differ diff --git a/assets/sizes.svg b/assets/sizes.svg new file mode 100644 index 0000000000000000000000000000000000000000..723544ba496d372b385cdc30c52141235e37bd75 --- /dev/null +++ b/assets/sizes.svg @@ -0,0 +1 @@ +Smaller payloads. Not a smaller disclaimer.Model + dictionary, canonical uncompressed MB · logarithmic axis · released formats, not repacked baselines0.1 MB1 MB10 MB100 MB1000 MBHindi16.22404.07Bengali14.66449.44Telugu16.58807.89Kannada17.381091.46Tamil18.701090.57Ours: low-bit weights + compact dictionary dataIndicXlit: FP32 parameters + original JSON dictionary \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000000000000000000000000000000000000..88a53fd502fce281e740029ebe55155a2669c5dd --- /dev/null +++ b/config.json @@ -0,0 +1,27 @@ +{ + "name": "AksharaLP-MultiIndic-8M", + "version": "1.0.0", + "format": "aksharalp-multiindic/1", + "languages": [ + "hi", + "bn", + "te", + "kn", + "ta" + ], + "parameters": { + "hi": 988928, + "bn": 989056, + "te": 988928, + "kn": 988928, + "ta": 988928, + "word_total": 4944768, + "hindi_sentence_lm": 2770752, + "total": 7715520 + }, + "sentence_languages": [ + "hi" + ], + "license": "cc-by-nc-4.0", + "requires_bundled_decoder": true +} diff --git a/evaluation/bn_frozen.json b/evaluation/bn_frozen.json new file mode 100644 index 0000000000000000000000000000000000000000..eab8c7e736aa25fbc784e5ec340ba98b1cbea6be --- /dev/null +++ b/evaluation/bn_frozen.json @@ -0,0 +1,74 @@ +{ + "files": { + "xlit.q4": "9f099f0b9524f446debf1d3f0ae1dc7ed1e88625c33f6dbb7b45e0348bf76376", + "dict.tsv.gz": "edb2ad0bd7de4e87bd4d49ac406a9ef01011451bf23ac82d4c063bb9cce1dc2b", + "params.json": "e0bd564ad61f6dcef8eca6bf3d64f8b231146c9885aa4c5b282a2b2420606c4f" + }, + "fit": "VALID only; frozen before test", + "config": { + "format": "aksharalp-multiindic/1", + "language": "bn", + "beam": 8, + "nbest": 8, + "model": "xlit.q4", + "dictionary": "dict.tsv.gz", + "dictionary_words": 500000, + "dictionary_total": 29467775, + "unk": -17.198807885289952, + "reranker": { + "type": "linear", + "features": [ + "sc", + "scn", + "inlex", + "uni", + "free0", + "rank", + "sc_rel", + "uni_rel" + ], + "w": [ + 0.7719883322715759, + 0.7239015102386475, + 2.2432072162628174, + 0.3994755744934082, + -0.1507813185453415, + -0.07777873426675797, + 0.7293219566345215, + 0.42816251516342163 + ], + "mu": [ + -5.078212233229818, + -0.8147631729600909, + 0.31161147274689055, + -4.2673892678692775, + 0.13265511803554703, + 0.4336724409822265, + -3.7508561308261763, + -0.6986245748643944 + ], + "sd": [ + 2.216960295304686, + 0.6165648295196909, + 0.46315298671618116, + 6.490492456336254, + 0.33920261805997143, + 0.28614764292216904, + 2.341120778815271, + 1.748762448987244 + ], + "fit_split": "all Aksharantar VALID", + "fit_n": 11188, + "l2": 0.001, + "seed": 0 + } + }, + "inventory": { + "parameters": 989056, + "model_raw": 589040, + "model_gz9": 540107, + "dict_raw": 14068340, + "dict_gz9": 2508610, + "dict_words": 500000 + } +} diff --git a/evaluation/examples.json b/evaluation/examples.json new file mode 100644 index 0000000000000000000000000000000000000000..90a342e040985963e855937ccd6f2a37fa4d7978 --- /dev/null +++ b/evaluation/examples.json @@ -0,0 +1,274 @@ +{ + "hi": [ + { + "input": "namaste", + "output": "नमस्ते", + "top8": [ + "नमस्ते", + "नमसते", + "नाश्ते", + "नामस्ते", + "नमास्ते", + "नमस्तें", + "नास्ते", + "नम्स्ते" + ], + "ms": 405.55157300000064 + }, + { + "input": "kaise", + "output": "कैसे", + "top8": [ + "कैसे", + "केसे", + "कइसे", + "कैस", + "कईसे", + "किसे", + "काइसे", + "कैशे" + ], + "ms": 146.41421700000137 + }, + { + "input": "dhanyavaad", + "output": "धन्यवाद", + "top8": [ + "धन्यवाद", + "धन्यावाद", + "धन्यवाद्", + "धान्यवाद", + "धनयवाद", + "धन्यववाद", + "धन्यवादी", + "धन्येवाद" + ], + "ms": 234.02473099999952 + }, + { + "input": "mera naam rahul hai", + "output": "मेरा नाम राहुल है", + "top8": null, + "ms": 623.3726830000014 + } + ], + "hi_sentence": [ + { + "input": "main theek hoon", + "output": "मैं ठीक हूं" + }, + { + "input": "ghar main kaun hai", + "output": "घर में कौन है" + } + ], + "bn": [ + { + "input": "nomoskar", + "output": "নমস্কার", + "top8": [ + "নমস্কার", + "নমষ্কার", + "নোমস্কার", + "নোমসকার", + "নমসকার", + "নমস্কর", + "নোমস্কর" + ], + "ms": 62.31514000000061 + }, + { + "input": "ami", + "output": "আমি", + "top8": [ + "আমি", + "অমি", + "এমি", + "আমী", + "অামি", + "এএমই", + "এএম" + ], + "ms": 42.25174700000025 + }, + { + "input": "bhalo", + "output": "ভালো", + "top8": [ + "ভালো", + "ভাল", + "ভালও", + "ভলো", + "বলো", + "ভল", + "ভা", + "ভলও" + ], + "ms": 36.55845999999838 + }, + { + "input": "tumi kemon acho", + "output": "তুমি কেমন আছো", + "top8": null, + "ms": 95.42537099999936 + } + ], + "te": [ + { + "input": "namaskaram", + "output": "నమస్కారం", + "top8": [ + "నమస్కారం", + "నమస్కరం", + "నామస్కారం", + "నమస్కారమ్", + "నామస్కరం", + "నమాస్కారం", + "నమస్కారంా", + "నమస్కారంో" + ], + "ms": 57.50473300000158 + }, + { + "input": "nenu", + "output": "నేను", + "top8": [ + "నేను", + "నెను", + "నేనూ", + "నను", + "నే", + "ఏను", + "నేన", + "నె" + ], + "ms": 27.787594999999783 + }, + { + "input": "manchi", + "output": "మంచి", + "top8": [ + "మంచి", + "మాంచి", + "మంచీ", + "మన్చి", + "మం", + "మంఛి", + "మంచ" + ], + "ms": 34.4755350000014 + }, + { + "input": "meeru ela unnaru", + "output": "మీరు ఎలా ఉన్నారు", + "top8": null, + "ms": 103.91343400000005 + } + ], + "kn": [ + { + "input": "namaskara", + "output": "ನಮಸ್ಕಾರ", + "top8": [ + "ನಮಸ್ಕಾರ", + "ನಮಸ್ಕರ", + "ನಮಸ್ಕರಾ", + "ನಮಾಸ್ಕರ", + "ನಮಸಕರ", + "ನಾಮಸ್ಕರ", + "ನಮಸಕಾರ", + "ನಮಸ್ಕಕರ" + ], + "ms": 46.95310299999983 + }, + { + "input": "nanu", + "output": "ನನು", + "top8": [ + "ನನು", + "ನಂ", + "ನರು", + "ನಣು", + "ನಮು", + "ನಾನ", + "ಣನು", + "ನ" + ], + "ms": 27.71208200000001 + }, + { + "input": "chennagide", + "output": "ಚೆನ್ನಾಗಿದೆ", + "top8": [ + "ಚೆನ್ನಾಗಿದೆ", + "ಚನ್ನಾಗಿದೆ", + "ಚೆನ್ನಗಿದೆ", + "ಚೆಣ್ಣಾಗಿದೆ", + "ಚೆನ್ನಾಗಿಡೆ", + "ಚೇನ್ನಾಗಿದೆ", + "ಚೆನ್ನಾಗಿದೇ", + "ಚೆನ್ನಾಗಿದಾ" + ], + "ms": 61.05912199999875 + }, + { + "input": "neevu hegiddira", + "output": "ನೀವು ಹೇಗಿದ್ದೀರಾ", + "top8": null, + "ms": 84.23852799999986 + } + ], + "ta": [ + { + "input": "vanakkam", + "output": "வணக்கம்", + "top8": [ + "வணக்கம்", + "வனக்கம்", + "வண", + "வணாக்கம்", + "வாணக்கம்", + "வானக்கம்", + "வநக்கம்", + "வணக்கம" + ], + "ms": 61.89169000000038 + }, + { + "input": "naan", + "output": "ணன்", + "top8": [ + "ணன்", + "னா", + "னன்", + "ணண்", + "ணண", + "ணன", + "ண", + "ன" + ], + "ms": 24.088208000001032 + }, + { + "input": "nandri", + "output": "நன்றி", + "top8": [ + "நன்றி", + "நந்திரி", + "நந்த்ரி", + "நான்றி", + "னன்றி", + "நண்ட்ரி", + "நனத்ரி", + "ந" + ], + "ms": 41.495796000001064 + }, + { + "input": "neenga eppadi irukeenga", + "output": "நீங்க எப்படி இருகீங்க", + "top8": null, + "ms": 139.49065300000075 + } + ] +} diff --git a/evaluation/gguf_independent_validation.json b/evaluation/gguf_independent_validation.json new file mode 100644 index 0000000000000000000000000000000000000000..0fc5bfbba200332b1e0b1c3dc6fd63cc98f66d11 --- /dev/null +++ b/evaluation/gguf_independent_validation.json @@ -0,0 +1,54 @@ +{ + "aksharalp-hi-word-q4.gguf": { + "tensors": 1, + "metadata_keys": 18 + }, + "aksharalp-hi-sentence-q4.gguf": { + "tensors": 1, + "metadata_keys": 18 + }, + "aksharalp-ta-word-f16.gguf": { + "tensors": 94, + "metadata_keys": 16 + }, + "aksharalp-kn-word-q4.gguf": { + "tensors": 1, + "metadata_keys": 17 + }, + "aksharalp-bn-word-f16.gguf": { + "tensors": 94, + "metadata_keys": 16 + }, + "aksharalp-te-word-f16.gguf": { + "tensors": 94, + "metadata_keys": 16 + }, + "aksharalp-kn-word-f16.gguf": { + "tensors": 94, + "metadata_keys": 16 + }, + "aksharalp-ta-word-q4.gguf": { + "tensors": 1, + "metadata_keys": 17 + }, + "aksharalp-te-word-q4.gguf": { + "tensors": 1, + "metadata_keys": 17 + }, + "aksharalp-hi-sentence-f16.gguf": { + "tensors": 12, + "metadata_keys": 16 + }, + "aksharalp-bn-word-q4.gguf": { + "tensors": 1, + "metadata_keys": 17 + }, + "aksharalp-hi-word-f16.gguf": { + "tensors": 94, + "metadata_keys": 17 + }, + "aksharalp-hi-sentence-q8.gguf": { + "tensors": 1, + "metadata_keys": 18 + } +} \ No newline at end of file diff --git a/evaluation/gguf_parity.json b/evaluation/gguf_parity.json new file mode 100644 index 0000000000000000000000000000000000000000..406e4cb986c600ffa06d8c283e7d9839b0dd3146 --- /dev/null +++ b/evaluation/gguf_parity.json @@ -0,0 +1,262 @@ +{ + "sample": "First 40 alphabetic ASCII roman words per full Aksharantar VALID; first 12 Dakshina VALID sentences with <=12 input words. No test used.", + "word": { + "hi": { + "max_abs_f16_weight_delta": 0.00091552734375, + "q4": { + "n": 40, + "same_top1": 40, + "same_top8_order": 40, + "same_top8_set": 40 + }, + "f16": { + "n": 40, + "same_top1": 40, + "same_top8_order": 40, + "same_top8_set": 40 + } + }, + "bn": { + "max_abs_f16_weight_delta": 0.0009765625, + "q4": { + "n": 40, + "same_top1": 40, + "same_top8_order": 40, + "same_top8_set": 40 + }, + "f16": { + "n": 40, + "same_top1": 40, + "same_top8_order": 40, + "same_top8_set": 40 + } + }, + "te": { + "max_abs_f16_weight_delta": 0.0009765625, + "q4": { + "n": 40, + "same_top1": 40, + "same_top8_order": 40, + "same_top8_set": 40 + }, + "f16": { + "n": 40, + "same_top1": 40, + "same_top8_order": 40, + "same_top8_set": 40 + } + }, + "kn": { + "max_abs_f16_weight_delta": 0.0009765625, + "q4": { + "n": 40, + "same_top1": 40, + "same_top8_order": 40, + "same_top8_set": 40 + }, + "f16": { + "n": 40, + "same_top1": 40, + "same_top8_order": 40, + "same_top8_set": 40 + } + }, + "ta": { + "max_abs_f16_weight_delta": 0.0009765625, + "q4": { + "n": 40, + "same_top1": 40, + "same_top8_order": 40, + "same_top8_set": 40 + }, + "f16": { + "n": 40, + "same_top1": 40, + "same_top8_order": 40, + "same_top8_set": 40 + } + } + }, + "sentence": { + "n": 12, + "source_search_outputs": [ + [ + "विलुप्ति", + "की", + "कगार", + "पर", + "गुणकारी", + "तिखुर" + ], + [ + "शाहनामा", + "नौरोज", + "के", + "त्योहार", + "को", + "महान", + "जमशेद", + "के", + "शासनकाल", + "से", + "जोड़ता", + "है", + "." + ], + [ + "मेहरोत्रा", + ",", + "दर", + "न", + "." + ], + [ + "दुर्गा", + "भाभी" + ], + [ + "स्वामी", + "बृहस्पति" + ], + [ + "बॉलीवुड", + ",", + "हिंदी", + "चलचित्र", + "उद्योग", + "भी", + "मुंबई", + "में", + "ही", + "स्थित", + "है", + "." + ], + [ + "क्योंकि", + "ऐसे", + "शब्द", + "भाषा", + "के", + "मूल", + "शब्द", + "होते", + "हैं", + "." + ], + [ + "अंडे", + "के", + "छिलके", + "अन्य", + "एकांकी", + "तथा", + "बीज", + "नाटक", + "-", + "मोहन", + "राकेश" + ], + [ + "तीसरा", + "सप्तक", + "(", + "कविता", + "संग्रह", + ")", + "," + ], + [ + "राह", + "चलते", + "एक", + "वैद्य", + "को", + "शंकर", + "मिल", + "जाता", + "है", + "." + ], + [ + "अति", + "विशिष्ट", + "सेवा", + "मेडल" + ], + [ + "सारे", + "ललहते", + "खेत", + ",", + "पेड़", + "-", + "पौधे", + ",", + "महल", + "एयर", + "झोपड़ियां", + "-", + "सब", + "की", + "सब", + "उजाड़", + "जाएंगी", + "." + ] + ], + "q8": { + "same_decoder_outputs": 12, + "max_abs_sequence_score_delta": 0.0, + "max_abs_weight_delta": 0.0 + }, + "q4": { + "same_decoder_outputs": 11, + "max_abs_sequence_score_delta": 6.343046009540558, + "max_abs_weight_delta": 0.2186431884765625 + }, + "f16": { + "same_decoder_outputs": 12, + "max_abs_sequence_score_delta": 0.008701682090759277, + "max_abs_weight_delta": 0.0009307861328125 + } + }, + "js_vs_python": { + "hi": { + "n": 40, + "same_top1": 40, + "same_top8_order": 40, + "same_top8_set": 40 + }, + "bn": { + "n": 40, + "same_top1": 40, + "same_top8_order": 40, + "same_top8_set": 40 + }, + "te": { + "n": 40, + "same_top1": 40, + "same_top8_order": 40, + "same_top8_set": 40 + }, + "kn": { + "n": 40, + "same_top1": 40, + "same_top8_order": 40, + "same_top8_set": 40 + }, + "ta": { + "n": 40, + "same_top1": 40, + "same_top8_order": 40, + "same_top8_set": 40 + } + }, + "js_sentence_vs_python": { + "n": 12, + "same_output": 12, + "mismatches": [], + "policy": "Both runtimes use plain reference token conversion; older UI brand/acronym heuristics are not used." + } +} diff --git a/evaluation/gguf_sizes.json b/evaluation/gguf_sizes.json new file mode 100644 index 0000000000000000000000000000000000000000..8e91128e2697afe5f89fddb26182013ee340c8ba --- /dev/null +++ b/evaluation/gguf_sizes.json @@ -0,0 +1,67 @@ +{ + "aksharalp-hi-word-q4.gguf": { + "raw": 632416, + "gzip9": 562579, + "sha256": "737d14197514a2625eb9505eb0a22ebd62cd2a4b764c65761f4084599bb6eb36" + }, + "aksharalp-hi-word-f16.gguf": { + "raw": 2026048, + "gzip9": 1348288, + "sha256": "3b6670429a226a39d357160691c634160667728bae8cfc7a711067424a2d224e" + }, + "aksharalp-bn-word-q4.gguf": { + "raw": 591136, + "gzip9": 541220, + "sha256": "8311c77889381ce559d483b965027c2d409787ac090b146ae1af08060e4a4552" + }, + "aksharalp-bn-word-f16.gguf": { + "raw": 1984864, + "gzip9": 1321177, + "sha256": "7a0b78e133bb3ed773f6a8bae5e3f1af4b75f5158de4e814efadeaa0002ed580" + }, + "aksharalp-te-word-q4.gguf": { + "raw": 591008, + "gzip9": 542744, + "sha256": "56b9f956b69e4fcf9e8d3f23c9391a091208651e90b06e2bde6d7f46d7cb6a60" + }, + "aksharalp-te-word-f16.gguf": { + "raw": 1984608, + "gzip9": 1329379, + "sha256": "3c556cec413db8f1485a1ee9ea237406f967937019e0d9d6076c3ae41b7a6231" + }, + "aksharalp-kn-word-q4.gguf": { + "raw": 591008, + "gzip9": 540642, + "sha256": "0e000f7fc0e52bbcb1f05753a46e6de50d1a1bdd6bb0ef46303361b9c39138ed" + }, + "aksharalp-kn-word-f16.gguf": { + "raw": 1984608, + "gzip9": 1319622, + "sha256": "c9d273c8339864bd37c2b051cab60d2741c1d71e56e8c27d00a981ff28ef3864" + }, + "aksharalp-ta-word-q4.gguf": { + "raw": 591008, + "gzip9": 542094, + "sha256": "f270e77e6355d53dc140f7cc7282551f5126302bcfbdbb17d802861510896c04" + }, + "aksharalp-ta-word-f16.gguf": { + "raw": 1984608, + "gzip9": 1328348, + "sha256": "3baf8764ecc77250666eb3f8eb83f2d7a33763e13227e2d6db8272868dd5a864" + }, + "aksharalp-hi-sentence-q8.gguf": { + "raw": 3488192, + "gzip9": 2981426, + "sha256": "5ff23bc8d131b560ee29c79ee16d15294600149200948433d76f29f4b8c3e496" + }, + "aksharalp-hi-sentence-q4.gguf": { + "raw": 2110912, + "gzip9": 1596808, + "sha256": "8ecc83bc618b9c67173328a34f0f40e5c83ce77a993a6279acdeae6d724690db" + }, + "aksharalp-hi-sentence-f16.gguf": { + "raw": 5790336, + "gzip9": 5159347, + "sha256": "446ff8f003c3680e9098df94727f8eb67a6a05a7c418888fb0bfeeb2742e098e" + } +} diff --git a/evaluation/hindi_existing_results.json b/evaluation/hindi_existing_results.json new file mode 100644 index 0000000000000000000000000000000000000000..296ce6f2f1057fef53476f3a31c01ab500f88de9 --- /dev/null +++ b/evaluation/hindi_existing_results.json @@ -0,0 +1,43 @@ +{ + "word": { + "ours": { + "n": 9991, + "top1": 70.04, + "top1_ci95": [ + 69.16, + 70.9 + ], + "top3": 88.31, + "top8": 93.63 + }, + "indicxlit_full": { + "top1": 69.33, + "top1_ci95": [ + 68.44, + 70.23 + ], + "top3": 85.4, + "top8": 88.29 + }, + "paired_ours_minus_indicxlit": { + "top1": [ + 0.71, + -0.04, + 1.4 + ] + } + }, + "sentence": { + "n": 4976, + "baseline_word_accuracy": 89.02, + "word_accuracy": 89.44, + "baseline_sentence_em": 25.7, + "sentence_em": 26.73, + "baseline_cer": 4.07, + "cer": 3.93, + "baseline_wer": 11.2, + "wer": 10.78, + "source": "https://huggingface.co/sourojitdotcom/AksharaLP-Sentence-2.8M", + "not_retested": true + } +} diff --git a/evaluation/indicxlit_storage.json b/evaluation/indicxlit_storage.json new file mode 100644 index 0000000000000000000000000000000000000000..32fd2880f195d6d77c9906137c42ab7dded87a61 --- /dev/null +++ b/evaluation/indicxlit_storage.json @@ -0,0 +1,27 @@ +{ + "parameters": 11487744, + "fp32_parameter_bytes": 45950976, + "dictionaries": { + "hi": { + "raw": 358118856, + "zip_compressed": 42255573 + }, + "bn": { + "raw": 403487845, + "zip_compressed": 45529651 + }, + "te": { + "raw": 761940466, + "zip_compressed": 84164136 + }, + "kn": { + "raw": 1045504407, + "zip_compressed": 114183601 + }, + "ta": { + "raw": 1044620827, + "zip_compressed": 104546002 + } + }, + "note": "Published dictionary JSON archive member bytes, without repacking; ZIP is not gzip-9. FP32 parameter bytes excludes serialization overhead. IndicXlit supports more languages than this bundle." +} diff --git a/evaluation/kn_frozen.json b/evaluation/kn_frozen.json new file mode 100644 index 0000000000000000000000000000000000000000..ec0da2cfa84e8170b2fbd6ccd2b563b8a9f80aec --- /dev/null +++ b/evaluation/kn_frozen.json @@ -0,0 +1,74 @@ +{ + "files": { + "xlit.q4": "3788fd545c297febb7dbbdbdd780e831042395b4ec99260b95c963607afc8664", + "dict.tsv.gz": "97035264d30eddb5810467dc5bbf088614221ea6ba2c3fff349c6bbd196bb644", + "params.json": "6076d9126ffce66630b634e47ac5b38d03cda3bce7ab4d07162aeb0cbc925356" + }, + "fit": "VALID only; frozen before test", + "config": { + "format": "aksharalp-multiindic/1", + "language": "kn", + "beam": 8, + "nbest": 8, + "model": "xlit.q4", + "dictionary": "dict.tsv.gz", + "dictionary_words": 500000, + "dictionary_total": 23038668, + "unk": -16.952684623026045, + "reranker": { + "type": "linear", + "features": [ + "sc", + "scn", + "inlex", + "uni", + "free0", + "rank", + "sc_rel", + "uni_rel" + ], + "w": [ + 0.7762413620948792, + 0.47590601444244385, + 1.8370015621185303, + 0.37460821866989136, + -0.14645907282829285, + -0.39526867866516113, + 0.7561540603637695, + 0.40543532371520996 + ], + "mu": [ + -5.910224747519752, + -0.7570656360672512, + 0.21765999600123598, + -2.8589205075172495, + 0.12688805278368503, + 0.43655597360815745, + -4.8503386403114055, + -0.3713378327997356 + ], + "sd": [ + 2.385781122067686, + 0.5672953341562836, + 0.4126559674266211, + 5.516374375872727, + 0.3328485249185093, + 0.2863515606523184, + 2.4442090407344486, + 1.2445700746967023 + ], + "fit_split": "all Aksharantar VALID", + "fit_n": 6981, + "l2": 0.001, + "seed": 0 + } + }, + "inventory": { + "parameters": 988928, + "model_raw": 588904, + "model_gz9": 539543, + "dict_raw": 16786145, + "dict_gz9": 2684083, + "dict_words": 500000 + } +} diff --git a/evaluation/latency.json b/evaluation/latency.json new file mode 100644 index 0000000000000000000000000000000000000000..cfb78af8f11ca7873630f2efddb3ad769971624b --- /dev/null +++ b/evaluation/latency.json @@ -0,0 +1,43 @@ +{ + "device": "Intel(R) Xeon(R) Processor", + "node": "v24.22.1", + "platform": "linux", + "method": "100 evenly spaced Aksharantar VALID words per language; 5 warmups; cache cleared per word; beam 8 with dictionary; Node single JS thread; load time excluded", + "languages": { + "hi": { + "n": 100, + "p50_ms": 207.02, + "p95_ms": 319.99, + "mean_ms": 217.33, + "load_ms": 7697.54 + }, + "bn": { + "n": 100, + "p50_ms": 51.61, + "p95_ms": 74, + "mean_ms": 52.41, + "load_ms": 0.02 + }, + "te": { + "n": 100, + "p50_ms": 51.77, + "p95_ms": 87.87, + "mean_ms": 54.86, + "load_ms": 398.74 + }, + "kn": { + "n": 100, + "p50_ms": 55.35, + "p95_ms": 100.77, + "mean_ms": 59.86, + "load_ms": 380.85 + }, + "ta": { + "n": 100, + "p50_ms": 60.98, + "p95_ms": 103.39, + "mean_ms": 63.09, + "load_ms": 451.39 + } + } +} diff --git a/evaluation/multiindic_test.json b/evaluation/multiindic_test.json new file mode 100644 index 0000000000000000000000000000000000000000..8a432e5f2b1d881bfe1064a809385720f040f435 --- /dev/null +++ b/evaluation/multiindic_test.json @@ -0,0 +1,318 @@ +{ + "protocol": "Frozen q4 beam-8 + eight-feature linear reranker fitted on all VALID; one final test per artifact; any-reference NFC and zero-width stripped; 2000 bootstrap replicates, seed 0 (scorer defaults)", + "languages": { + "bn": { + "ours": { + "n": 13367, + "top1": 64.64, + "top1_ci95": [ + 63.83, + 65.44 + ], + "top3": 75.66, + "top3_ci95": [ + 74.9, + 76.38 + ], + "top8": 77.97, + "top8_ci95": [ + 77.23, + 78.66 + ] + }, + "indicxlit_full": { + "n": 13367, + "top1": 70.52, + "top1_ci95": [ + 69.76, + 71.34 + ], + "top3": 82.25, + "top3_ci95": [ + 81.61, + 82.89 + ], + "top8": 85.44, + "top8_ci95": [ + 84.86, + 86.06 + ] + }, + "paired_ours_minus_indicxlit": { + "top1": [ + -5.88, + -6.52, + -5.22 + ], + "top3": [ + -6.59, + -7.18, + -5.97 + ], + "top8": [ + -7.47, + -8.05, + -6.89 + ] + }, + "frozen_sha256": { + "xlit.q4": "9f099f0b9524f446debf1d3f0ae1dc7ed1e88625c33f6dbb7b45e0348bf76376", + "dict.tsv.gz": "edb2ad0bd7de4e87bd4d49ac406a9ef01011451bf23ac82d4c063bb9cce1dc2b", + "params.json": "e0bd564ad61f6dcef8eca6bf3d64f8b231146c9885aa4c5b282a2b2420606c4f" + }, + "test_rows_sha256": "de2dc6a45db144a32074c586b2b92f6af07e9b3b4fef495150d62c59c31c9dfd", + "baseline_predictions_sha256": "e3e72158035ae04c46691f75eb85df3b59959e9d801cd9099af26148cf7a5c9e", + "baseline_missing_empty_inputs": 1, + "baseline_note": "Existing IndicXlit PyTorch port beam-8 + full dictionary predictions, rescored on exactly the same rows; no new baseline inference.", + "size": { + "parameters": 989056, + "model_raw": 589040, + "model_gz9": 540107, + "dict_raw": 14068340, + "dict_gz9": 2508610, + "dict_words": 500000 + } + }, + "te": { + "ours": { + "n": 9936, + "top1": 78.74, + "top1_ci95": [ + 77.93, + 79.53 + ], + "top3": 86.87, + "top3_ci95": [ + 86.2, + 87.52 + ], + "top8": 88.63, + "top8_ci95": [ + 88.0, + 89.25 + ] + }, + "indicxlit_full": { + "n": 9936, + "top1": 82.42, + "top1_ci95": [ + 81.65, + 83.14 + ], + "top3": 90.63, + "top3_ci95": [ + 90.04, + 91.18 + ], + "top8": 92.75, + "top8_ci95": [ + 92.21, + 93.26 + ] + }, + "paired_ours_minus_indicxlit": { + "top1": [ + -3.67, + -4.27, + -3.07 + ], + "top3": [ + -3.76, + -4.27, + -3.21 + ], + "top8": [ + -4.13, + -4.59, + -3.62 + ] + }, + "frozen_sha256": { + "xlit.q4": "aea094e46a3c61eb9487d1aaba3824d3d9dc60f3f1cf6b6f99cadf04fd4318f9", + "dict.tsv.gz": "20612a52ccf39c816d3068e78e9e9a3aa9aed9156f10de45ebbd543363943640", + "params.json": "653f897efe1af78bb52149f2a18dc804b49ecac9c93add867871e64f56e07470" + }, + "test_rows_sha256": "698de748186eb92a43baed29e1e0aa9a15acbd5b20a79b3a4fd7db4b336ceda1", + "baseline_predictions_sha256": "6c05dfc219ebc012fd96bac5f5b9586fd5ac35d0856a36af28c7362b04a31c82", + "baseline_note": "Existing IndicXlit PyTorch port beam-8 + full dictionary predictions, rescored on exactly the same rows; no new baseline inference.", + "size": { + "parameters": 988928, + "model_raw": 588904, + "model_gz9": 541242, + "dict_raw": 15989177, + "dict_gz9": 2680527, + "dict_words": 500000 + } + }, + "kn": { + "ours": { + "n": 11165, + "top1": 77.37, + "top1_ci95": [ + 76.61, + 78.13 + ], + "top3": 85.8, + "top3_ci95": [ + 85.16, + 86.46 + ], + "top8": 87.68, + "top8_ci95": [ + 87.08, + 88.32 + ] + }, + "indicxlit_full": { + "n": 11165, + "top1": 82.61, + "top1_ci95": [ + 81.92, + 83.29 + ], + "top3": 90.68, + "top3_ci95": [ + 90.16, + 91.2 + ], + "top8": 92.41, + "top8_ci95": [ + 91.94, + 92.9 + ] + }, + "paired_ours_minus_indicxlit": { + "top1": [ + -5.24, + -5.83, + -4.67 + ], + "top3": [ + -4.87, + -5.39, + -4.39 + ], + "top8": [ + -4.73, + -5.19, + -4.26 + ] + }, + "frozen_sha256": { + "xlit.q4": "3788fd545c297febb7dbbdbdd780e831042395b4ec99260b95c963607afc8664", + "dict.tsv.gz": "97035264d30eddb5810467dc5bbf088614221ea6ba2c3fff349c6bbd196bb644", + "params.json": "6076d9126ffce66630b634e47ac5b38d03cda3bce7ab4d07162aeb0cbc925356" + }, + "test_rows_sha256": "5bfe000c741316d700f4caae046ed05c4cdcaf485ef31707f6254bcc4c9abbd9", + "baseline_predictions_sha256": "928c3375ffdbca7811a36284ac4afb70b8b5192d28aa35f88a6291f941373407", + "baseline_note": "Existing IndicXlit PyTorch port beam-8 + full dictionary predictions, rescored on exactly the same rows; no new baseline inference.", + "size": { + "parameters": 988928, + "model_raw": 588904, + "model_gz9": 539543, + "dict_raw": 16786145, + "dict_gz9": 2684083, + "dict_words": 500000 + } + }, + "ta": { + "ours": { + "n": 11405, + "top1": 71.78, + "top1_ci95": [ + 71.01, + 72.61 + ], + "top3": 80.05, + "top3_ci95": [ + 79.37, + 80.78 + ], + "top8": 82.61, + "top8_ci95": [ + 81.95, + 83.31 + ] + }, + "indicxlit_full": { + "n": 11405, + "top1": 75.95, + "top1_ci95": [ + 75.2, + 76.73 + ], + "top3": 84.21, + "top3_ci95": [ + 83.56, + 84.89 + ], + "top8": 87.26, + "top8_ci95": [ + 86.66, + 87.87 + ] + }, + "paired_ours_minus_indicxlit": { + "top1": [ + -4.16, + -4.75, + -3.59 + ], + "top3": [ + -4.16, + -4.66, + -3.63 + ], + "top8": [ + -4.65, + -5.12, + -4.16 + ] + }, + "frozen_sha256": { + "xlit.q4": "5dd185a033db79835fede66ab41a25f5897f24ad5d7c8ecc9864ca6ec167fdc0", + "dict.tsv.gz": "4d14f514020e7737144c22991e9324a29734b5ab203bef3b68ec6d9f51e7cddb", + "params.json": "f4520a8320f02573e6b8b6530a372c980903fbf757999540698957e8eec58385" + }, + "test_rows_sha256": "464c11fec7cfa71680a01c57899aeb691d88f1a871b1c7976184b0a21e2d75a6", + "baseline_predictions_sha256": "0c2272e9423eb22e3fa04037a9973a777d50070cb9f3bcd03d4037865fd71bc3", + "baseline_note": "Existing IndicXlit PyTorch port beam-8 + full dictionary predictions, rescored on exactly the same rows; no new baseline inference.", + "size": { + "parameters": 988928, + "model_raw": 588904, + "model_gz9": 540818, + "dict_raw": 18105290, + "dict_gz9": 2588634, + "dict_words": 500000 + } + }, + "hi": { + "ours": { + "n": 9991, + "top1": 70.04, + "top1_ci95": [ + 69.16, + 70.9 + ], + "top3": 88.31, + "top8": 93.63 + }, + "indicxlit_full": { + "top1": 69.33, + "top1_ci95": [ + 68.44, + 70.23 + ], + "top3": 85.4, + "top8": 88.29 + }, + "paired_ours_minus_indicxlit": { + "top1": [ + 0.71, + -0.04, + 1.4 + ] + } + } + } +} diff --git a/evaluation/release_summary.json b/evaluation/release_summary.json new file mode 100644 index 0000000000000000000000000000000000000000..7b0442cde34c0f63538862401bc03fbc3cd84d6f --- /dev/null +++ b/evaluation/release_summary.json @@ -0,0 +1,51 @@ +{ + "name": "AksharaLP-MultiIndic-8M", + "parameters": 7715520, + "word_parameters": 4944768, + "sizes": { + "languages": { + "hi": { + "raw": 16221771, + "gzip9": 7905041 + }, + "bn": { + "raw": 14658574, + "gzip9": 3049313 + }, + "te": { + "raw": 16579274, + "gzip9": 3222365 + }, + "kn": { + "raw": 17376245, + "gzip9": 3224222 + }, + "ta": { + "raw": 18695389, + "gzip9": 3130045 + } + }, + "shared_js_and_config": { + "raw": 68781, + "gzip9": 23186 + }, + "word_only": { + "raw": 83600034, + "gzip9": 20554172 + }, + "sentence_addon": { + "raw": 3256675, + "gzip9": 2928839 + }, + "with_sentence": { + "raw": 86856709, + "gzip9": 23483011 + }, + "note": "Canonical payload bytes and sum of per-file gzip -9 equivalents; MB=1,000,000 bytes. Excludes documentation, Python reference, fonts, charts, GGUF alternatives and duplicate compressed companions. Dictionaries are frequency data, not neural parameters. Runtime computes float32 after dequantization; RAM is not file size." + }, + "gguf_files": 13, + "gguf_total": { + "raw": 24350752, + "gzip9": 19113674 + } +} diff --git a/evaluation/sizes.json b/evaluation/sizes.json new file mode 100644 index 0000000000000000000000000000000000000000..9c2f7088dfedb4a3c5b9fc4bb0e1d36d08629b12 --- /dev/null +++ b/evaluation/sizes.json @@ -0,0 +1,41 @@ +{ + "languages": { + "hi": { + "raw": 16221771, + "gzip9": 7905041 + }, + "bn": { + "raw": 14658574, + "gzip9": 3049313 + }, + "te": { + "raw": 16579274, + "gzip9": 3222365 + }, + "kn": { + "raw": 17376245, + "gzip9": 3224222 + }, + "ta": { + "raw": 18695389, + "gzip9": 3130045 + } + }, + "shared_js_and_config": { + "raw": 68781, + "gzip9": 23186 + }, + "word_only": { + "raw": 83600034, + "gzip9": 20554172 + }, + "sentence_addon": { + "raw": 3256675, + "gzip9": 2928839 + }, + "with_sentence": { + "raw": 86856709, + "gzip9": 23483011 + }, + "note": "Canonical payload bytes and sum of per-file gzip -9 equivalents; MB=1,000,000 bytes. Excludes documentation, Python reference, fonts, charts, GGUF alternatives and duplicate compressed companions. Dictionaries are frequency data, not neural parameters. Runtime computes float32 after dequantization; RAM is not file size." +} diff --git a/evaluation/ta_frozen.json b/evaluation/ta_frozen.json new file mode 100644 index 0000000000000000000000000000000000000000..36ac83f31ada1a5e631fd3cf2ee84e1c5cb2797e --- /dev/null +++ b/evaluation/ta_frozen.json @@ -0,0 +1,74 @@ +{ + "files": { + "xlit.q4": "5dd185a033db79835fede66ab41a25f5897f24ad5d7c8ecc9864ca6ec167fdc0", + "dict.tsv.gz": "4d14f514020e7737144c22991e9324a29734b5ab203bef3b68ec6d9f51e7cddb", + "params.json": "f4520a8320f02573e6b8b6530a372c980903fbf757999540698957e8eec58385" + }, + "fit": "VALID only; frozen before test", + "config": { + "format": "aksharalp-multiindic/1", + "language": "ta", + "beam": 8, + "nbest": 8, + "model": "xlit.q4", + "dictionary": "dict.tsv.gz", + "dictionary_words": 500000, + "dictionary_total": 20232595, + "unk": -16.82280552518196, + "reranker": { + "type": "linear", + "features": [ + "sc", + "scn", + "inlex", + "uni", + "free0", + "rank", + "sc_rel", + "uni_rel" + ], + "w": [ + 0.9623245596885681, + 0.5414344072341919, + 1.828786015510559, + 0.003193225245922804, + -0.12186560034751892, + -0.003971157129853964, + 0.9330659508705139, + 0.5227358937263489 + ], + "mu": [ + -6.01308456526665, + -0.7381095081752549, + 0.22912272701362366, + -2.9371551977749273, + 0.1254346462976686, + 0.43728267685116573, + -4.8824671233512555, + -0.38838364800108277 + ], + "sd": [ + 2.449121527790252, + 0.5435378258591739, + 0.42026937018709404, + 5.483689004938199, + 0.3312121045929875, + 0.28639834850311835, + 2.5225466225846818, + 1.2477123699412511 + ], + "fit_split": "all Aksharantar VALID", + "fit_n": 8802, + "l2": 0.001, + "seed": 0 + } + }, + "inventory": { + "parameters": 988928, + "model_raw": 588904, + "model_gz9": 540818, + "dict_raw": 18105290, + "dict_gz9": 2588634, + "dict_words": 500000 + } +} diff --git a/evaluation/te_frozen.json b/evaluation/te_frozen.json new file mode 100644 index 0000000000000000000000000000000000000000..8f60cdfd8a86cc0b2b204cdc6c51c4488912da33 --- /dev/null +++ b/evaluation/te_frozen.json @@ -0,0 +1,74 @@ +{ + "files": { + "xlit.q4": "aea094e46a3c61eb9487d1aaba3824d3d9dc60f3f1cf6b6f99cadf04fd4318f9", + "dict.tsv.gz": "20612a52ccf39c816d3068e78e9e9a3aa9aed9156f10de45ebbd543363943640", + "params.json": "653f897efe1af78bb52149f2a18dc804b49ecac9c93add867871e64f56e07470" + }, + "fit": "VALID only; frozen before test", + "config": { + "format": "aksharalp-multiindic/1", + "language": "te", + "beam": 8, + "nbest": 8, + "model": "xlit.q4", + "dictionary": "dict.tsv.gz", + "dictionary_words": 500000, + "dictionary_total": 24117810, + "unk": -16.998461171144285, + "reranker": { + "type": "linear", + "features": [ + "sc", + "scn", + "inlex", + "uni", + "free0", + "rank", + "sc_rel", + "uni_rel" + ], + "w": [ + 0.8893349170684814, + 0.42266765236854553, + 1.647674560546875, + 0.01816709153354168, + -0.09264160692691803, + -0.17859779298305511, + 0.8579440116882324, + 0.4983164668083191 + ], + "mu": [ + -5.577303872867701, + -0.7360878454967777, + 0.22874146800712822, + -3.057939543170047, + 0.12881207760330857, + 0.43559396119834565, + -4.512450547449157, + -0.4069890521202824 + ], + "sd": [ + 2.345458571867553, + 0.5172883363068768, + 0.4200233908567531, + 5.716765533673338, + 0.33499283014943054, + 0.28628677366288935, + 2.4278868542564216, + 1.326799761380314 + ], + "fit_split": "all Aksharantar VALID", + "fit_n": 7662, + "l2": 0.001, + "seed": 0 + } + }, + "inventory": { + "parameters": 988928, + "model_raw": 588904, + "model_gz9": 541242, + "dict_raw": 15989177, + "dict_gz9": 2680527, + "dict_words": 500000 + } +} diff --git a/gguf/aksharalp-bn-word-f16.gguf b/gguf/aksharalp-bn-word-f16.gguf new file mode 100644 index 0000000000000000000000000000000000000000..7e7eb2431eca863d2bf7441bf394a81d9ca62c47 --- /dev/null +++ b/gguf/aksharalp-bn-word-f16.gguf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a0b78e133bb3ed773f6a8bae5e3f1af4b75f5158de4e814efadeaa0002ed580 +size 1984864 diff --git a/gguf/aksharalp-bn-word-q4.gguf b/gguf/aksharalp-bn-word-q4.gguf new file mode 100644 index 0000000000000000000000000000000000000000..d7f5984730d8a61d8d27fd5c604f3584393a290e --- /dev/null +++ b/gguf/aksharalp-bn-word-q4.gguf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8311c77889381ce559d483b965027c2d409787ac090b146ae1af08060e4a4552 +size 591136 diff --git a/gguf/aksharalp-hi-sentence-f16.gguf b/gguf/aksharalp-hi-sentence-f16.gguf new file mode 100644 index 0000000000000000000000000000000000000000..01a809db7edc29175ba888569c5cf06b1b05341a --- /dev/null +++ b/gguf/aksharalp-hi-sentence-f16.gguf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:446ff8f003c3680e9098df94727f8eb67a6a05a7c418888fb0bfeeb2742e098e +size 5790336 diff --git a/gguf/aksharalp-hi-sentence-q4.gguf b/gguf/aksharalp-hi-sentence-q4.gguf new file mode 100644 index 0000000000000000000000000000000000000000..3dbf505a095c602c134fbefc40ce75ec4c46096e --- /dev/null +++ b/gguf/aksharalp-hi-sentence-q4.gguf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ecc83bc618b9c67173328a34f0f40e5c83ce77a993a6279acdeae6d724690db +size 2110912 diff --git a/gguf/aksharalp-hi-sentence-q8.gguf b/gguf/aksharalp-hi-sentence-q8.gguf new file mode 100644 index 0000000000000000000000000000000000000000..fa7de25b6fe1a2424b5693c83586addc9c3dbdcb --- /dev/null +++ b/gguf/aksharalp-hi-sentence-q8.gguf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ff23bc8d131b560ee29c79ee16d15294600149200948433d76f29f4b8c3e496 +size 3488192 diff --git a/gguf/aksharalp-hi-word-f16.gguf b/gguf/aksharalp-hi-word-f16.gguf new file mode 100644 index 0000000000000000000000000000000000000000..3f41ca3d639a19c4a5c0c8de08469549caead97b --- /dev/null +++ b/gguf/aksharalp-hi-word-f16.gguf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b6670429a226a39d357160691c634160667728bae8cfc7a711067424a2d224e +size 2026048 diff --git a/gguf/aksharalp-hi-word-q4.gguf b/gguf/aksharalp-hi-word-q4.gguf new file mode 100644 index 0000000000000000000000000000000000000000..c248b579bbb002f2dfee7fef403aa09666d5137a --- /dev/null +++ b/gguf/aksharalp-hi-word-q4.gguf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:737d14197514a2625eb9505eb0a22ebd62cd2a4b764c65761f4084599bb6eb36 +size 632416 diff --git a/gguf/aksharalp-kn-word-f16.gguf b/gguf/aksharalp-kn-word-f16.gguf new file mode 100644 index 0000000000000000000000000000000000000000..24308391d847baafb8980b64c268395494f76088 --- /dev/null +++ b/gguf/aksharalp-kn-word-f16.gguf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9d273c8339864bd37c2b051cab60d2741c1d71e56e8c27d00a981ff28ef3864 +size 1984608 diff --git a/gguf/aksharalp-kn-word-q4.gguf b/gguf/aksharalp-kn-word-q4.gguf new file mode 100644 index 0000000000000000000000000000000000000000..436ce59cab72db1878d22132240e30359330debb --- /dev/null +++ b/gguf/aksharalp-kn-word-q4.gguf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e000f7fc0e52bbcb1f05753a46e6de50d1a1bdd6bb0ef46303361b9c39138ed +size 591008 diff --git a/gguf/aksharalp-ta-word-f16.gguf b/gguf/aksharalp-ta-word-f16.gguf new file mode 100644 index 0000000000000000000000000000000000000000..02ee3294365755ab43297c571ed440abf0300ab1 --- /dev/null +++ b/gguf/aksharalp-ta-word-f16.gguf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3baf8764ecc77250666eb3f8eb83f2d7a33763e13227e2d6db8272868dd5a864 +size 1984608 diff --git a/gguf/aksharalp-ta-word-q4.gguf b/gguf/aksharalp-ta-word-q4.gguf new file mode 100644 index 0000000000000000000000000000000000000000..4eb6b307ab3afdae2299887388b48cea2ee13010 --- /dev/null +++ b/gguf/aksharalp-ta-word-q4.gguf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f270e77e6355d53dc140f7cc7282551f5126302bcfbdbb17d802861510896c04 +size 591008 diff --git a/gguf/aksharalp-te-word-f16.gguf b/gguf/aksharalp-te-word-f16.gguf new file mode 100644 index 0000000000000000000000000000000000000000..10f76362b811e78be9fa51757b1f0d7dd6abc325 --- /dev/null +++ b/gguf/aksharalp-te-word-f16.gguf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c556cec413db8f1485a1ee9ea237406f967937019e0d9d6076c3ae41b7a6231 +size 1984608 diff --git a/gguf/aksharalp-te-word-q4.gguf b/gguf/aksharalp-te-word-q4.gguf new file mode 100644 index 0000000000000000000000000000000000000000..7542ff84ebe78df8b32b91dbd733a38236794d0c --- /dev/null +++ b/gguf/aksharalp-te-word-q4.gguf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56b9f956b69e4fcf9e8d3f23c9391a091208651e90b06e2bde6d7f46d7cb6a60 +size 591008 diff --git a/gguf_read.py b/gguf_read.py new file mode 100644 index 0000000000000000000000000000000000000000..b4fcc2503e032288110de803263bf33b2bea5d4b --- /dev/null +++ b/gguf_read.py @@ -0,0 +1,383 @@ +"""Minimal, dependency-light reader for the GGUF files shipped with AksharaLP-MultiIndic-8M. + +GGUF here is used as what it is: a portable, single-file tensor container -- a header, typed +key/value metadata, a tensor table, and aligned little-endian tensor data. It is NOT a +llama.cpp-loadable model: AksharaLP is a custom character-level encoder-decoder transformer with a +bespoke decoding stack (lexicon-trie candidate beam, hand-built candidate features, gates, MLP +reranker), and llama.cpp has no such architecture. This reader is the decoder-side tool: it returns +the weights our reference implementation (python/bhc/xlit_*.py) expects. + +Only numpy is required. No torch, no llama.cpp, no gguf/gguf-py. + + python gguf_read.py aksharalp-1m-v2-f16.gguf # summary + python gguf_read.py --json aksharalp-1m-v2-q4.gguf # metadata + tensor table + + import gguf_read + w = gguf_read.load("aksharalp-1m-v2-f16.gguf") # -> Weights(cfg, state_dict, metadata) + w.state_dict["semb.weight"].shape # (30, 128) float32 + +Two container layouts are understood, both GGUF v3: + + * aksharalp-1m-v2-q4.gguf one I8 tensor, ``aksharalp.q4.blob``: the byte-exact xlit.q4 file + (4-byte header length, JSON tensor table, then the payload). Decoded + here exactly as bhc/xlit_q.py:load_q does. + * aksharalp-1m-v2-f16.gguf every weight under its state_dict name at F16. +""" +import json +import struct +import sys + +import numpy as np + +MAGIC = b"GGUF" +SUPPORTED_VERSIONS = (3,) + +# GGUF metadata value types +GGUF_UINT8, GGUF_INT8, GGUF_UINT16, GGUF_INT16 = 0, 1, 2, 3 +GGUF_UINT32, GGUF_INT32, GGUF_FLOAT32, GGUF_BOOL = 4, 5, 6, 7 +GGUF_STRING, GGUF_ARRAY, GGUF_UINT64, GGUF_INT64, GGUF_FLOAT64 = 8, 9, 10, 11, 12 + +# GGML tensor types we can read: F32, F16, I8 (the three this model uses) +_GGML_F32, _GGML_F16, _GGML_I8 = 0, 1, 24 +_TENSOR_DTYPES = {_GGML_F32: np.dtype(" len(self.buf): + raise GGUFError("truncated file: wanted %d bytes at %d, file is %d" % (n, self.pos, len(self.buf))) + b = self.buf[self.pos:self.pos + n] + self.pos += n + return b + + def unpack(self, fmt): + n = struct.calcsize(fmt) + return struct.unpack(fmt, self.take(n))[0] + + def string(self): + """GGUF strings: u64 length + UTF-8 bytes (not NUL-terminated).""" + n = self.unpack("Q") + return self.take(n).decode("utf-8") + + +class Tensor: + """One tensor: name, logical shape (GGML order reversed back to row-major), dtype, raw bytes.""" + + __slots__ = ("name", "shape", "type_id", "offset", "data") + + def __init__(self, name, shape, type_id, offset, data): + self.name, self.shape, self.type_id, self.offset, self.data = name, shape, type_id, offset, data + + @property + def dtype(self): + return _TENSOR_DTYPES[self.type_id] + + @property + def type_name(self): + return _TENSOR_NAMES[self.type_id] + + def array(self): + return np.frombuffer(self.data, dtype=self.dtype).reshape(self.shape) + + def __repr__(self): + return "" % (self.name, self.type_name, tuple(self.shape)) + + +class GGUF: + """Parsed GGUF file: header fields, typed metadata, tensor table, tensor data.""" + + def __init__(self, path): + self.path = str(path) + with open(self.path, "rb") as f: + self.buf = f.read() + self._parse() + + # ---------------------------------------------------------------- parsing + def _parse(self): + c = _Cursor(self.buf) + self.magic = c.take(4) + if self.magic != MAGIC: + raise GGUFError("%s: not a GGUF file (magic %r)" % (self.path, self.magic)) + self.version = c.unpack("I") + if self.version not in SUPPORTED_VERSIONS: + raise GGUFError("%s: GGUF version %d not supported (this reader knows %s)" + % (self.path, self.version, SUPPORTED_VERSIONS)) + self.tensor_count = c.unpack("Q") + self.kv_count = c.unpack("Q") + + self.metadata = {} + for _ in range(self.kv_count): + key = c.string() + self.metadata[key] = self._value(c) + + self.alignment = int(self.metadata.get("general.alignment", 32)) + if self.alignment <= 0 or self.alignment & (self.alignment - 1): + raise GGUFError("%s: general.alignment %d is not a power of two" % (self.path, self.alignment)) + + infos = [] + for _ in range(self.tensor_count): + name = c.string() + n_dims = c.unpack("I") + if n_dims > 8: + raise GGUFError("%s: tensor %s declares %d dims" % (self.path, name, n_dims)) + # GGML stores dims fastest-varying first; reverse to get the row-major shape. + dims = [c.unpack("Q") for _ in range(n_dims)] + type_id = c.unpack("I") + offset = c.unpack("Q") + infos.append((name, list(reversed(dims)), type_id, offset)) + + pad = (-c.pos) % self.alignment + self.data_start = c.pos + pad + if self.data_start > len(self.buf): + raise GGUFError("%s: tensor info block overruns the file" % self.path) + + self.tensors = {} + self.order = [] + for name, shape, type_id, offset in infos: + if type_id not in _TENSOR_DTYPES: + raise GGUFError("%s: tensor %s has GGML type %d, which this reader does not decode " + "(only F32/F16/I8 are used by these files)" % (self.path, name, type_id)) + if name in self.tensors: + raise GGUFError("%s: duplicate tensor name %r" % (self.path, name)) + numel = int(np.prod(shape, dtype=np.int64)) if shape else 1 + nbytes = numel * _TENSOR_DTYPES[type_id].itemsize + start = self.data_start + offset + end = start + nbytes + if start < self.data_start or end > len(self.buf): + raise GGUFError("%s: tensor %s spans [%d, %d) outside the data section [%d, %d)" + % (self.path, name, start, end, self.data_start, len(self.buf))) + if offset % self.alignment: + raise GGUFError("%s: tensor %s offset %d is not aligned to %d" + % (self.path, name, offset, self.alignment)) + self.tensors[name] = Tensor(name, shape, type_id, offset, self.buf[start:end]) + self.order.append(name) + + def _value(self, c, vtype=None): + if vtype is None: + vtype = c.unpack("I") + if vtype == GGUF_STRING: + return c.string() + if vtype == GGUF_ARRAY: + sub = c.unpack("I") + n = c.unpack("Q") + return [self._value(c, sub) for _ in range(n)] + if vtype in _KV_FMT: + return c.unpack(_KV_FMT[vtype]) + raise GGUFError("%s: unknown metadata value type %d" % (self.path, vtype)) + + # ---------------------------------------------------------------- checks + def validate(self): + """Structural checks; returns a list of human-readable results, raises GGUFError on failure.""" + notes = [] + if len(self.tensors) != self.tensor_count: + raise GGUFError("tensor count mismatch: header says %d, parsed %d" % (self.tensor_count, len(self.tensors))) + notes.append("magic GGUF, version %d" % self.version) + notes.append("%d tensors, %d metadata keys" % (self.tensor_count, len(self.metadata))) + notes.append("alignment %d, data section at byte %d" % (self.alignment, self.data_start)) + declared = self.metadata.get("general.architecture") + if declared is None: + raise GGUFError("general.architecture is missing") + for name in self.order: + t = self.tensors[name] + if t.offset + len(t.data) > len(self.buf) - self.data_start: + raise GGUFError("tensor %s overruns the data section" % name) + notes.append("all tensor offsets aligned and inside the data section") + arch = self.metadata.get("general.architecture") + notes.append("general.architecture = %r (custom; not a llama.cpp architecture)" % arch) + return notes + + # ---------------------------------------------------------------- weights + def state_dict(self): + """Return {state_dict name: float32 ndarray} for the reference XlitTransformer.""" + cfg, sd = self._weights() + return cfg, sd + + def _weights(self): + if "aksharalp.nlm.blob" in self.tensors: + return _weights_from_nlm_blob(bytes(self.tensors["aksharalp.nlm.blob"].data)) + if _BLOB_TENSOR in self.tensors: + return _weights_from_q4_blob(bytes(self.tensors[_BLOB_TENSOR].data)) + cfg = _cfg_from_metadata(self.metadata) + sd = {} + for name in self.order: + if name.startswith(RERANKER_PREFIX): + continue # reranker matrices are carried separately; see reranker() + a = self.tensors[name].array() + sd[name] = np.ascontiguousarray(a, dtype=np.float32) + return cfg, sd + + def reranker(self): + """{('core'|'pack', 'w1'|'b1'|'w2'|'b2'): float32 ndarray} from the F16 variant, else {}.""" + out = {} + for name in self.order: + if not name.startswith(RERANKER_PREFIX): + continue + _, part, param = name.split(".") + out[(part, param)] = np.ascontiguousarray(self.tensors[name].array(), dtype=np.float32) + return out + + +_BLOB_TENSOR = "aksharalp.q4.blob" +RERANKER_PREFIX = "reranker." + + +def _cfg_from_metadata(md): + if "aksharalp.config_json" in md: return json.loads(md["aksharalp.config_json"]) + def g(key, default=None): + v = md.get("aksharalp." + key, default) + if v is None: + raise GGUFError("metadata is missing aksharalp.%s" % key) + return v + cfg = {"d": int(g("d_model")), "h": int(g("n_heads")), "enc": int(g("encoder_layers")), + "dec": int(g("decoder_layers")), "ff": int(g("ff_dim")), "p": float(g("dropout", 0.1))} + return cfg + + +def _weights_from_q4_blob(blob): + """Decode the carried xlit.q4 bytes exactly as python/bhc/xlit_q.py:load_q does.""" + n = int.from_bytes(blob[:4], "little") + meta = json.loads(blob[4:4 + n]) + data = blob[4 + n:] + bits_default, G = int(meta["bits"]), int(meta["G"]) + cfg = dict(meta["cfg"]) + sd = {} + for k, shape, kind, off, ln in meta["tensors"]: + cnt = int(np.prod(shape)) + if kind.startswith("q"): + bits = int(kind[1:]) if len(kind) > 1 else bits_default + if bits == 4: + raw = np.frombuffer(data, np.uint8, ln, off) + q = np.stack([raw >> 4, raw & 15], 1).reshape(-1)[:cnt].astype(np.int16) - 8 + elif bits == 8: + q = np.frombuffer(data, np.int8, ln, off).astype(np.int16) + else: + raise GGUFError("unsupported q width %d for %s" % (bits, k)) + r, c = shape + ng = (c + G - 1) // G + sc = np.frombuffer(data, np.float16, r * ng, off + ln).astype(np.float32).reshape(r, ng) + w = q.reshape(r, c).astype(np.float32) * np.repeat(sc, G, 1)[:, :c] + sd[k] = w + elif kind == "f16": + sd[k] = np.frombuffer(data, np.float16, cnt, off).astype(np.float32).reshape(shape).copy() + else: + raise GGUFError("unsupported tensor kind %r for %s" % (kind, k)) + return cfg, sd + + +class Weights: + """Weights + config, in the shape the reference implementation consumes. + + ``state_dict`` maps state_dict names (as in python/bhc/xlit_q.py:load_q) to float32 ndarrays; + ``cfg`` is the XlitTransformer config. To build the torch model the reference uses: + + import torch + from bhc.xlit_model import XlitTransformer + m = XlitTransformer(**w.cfg) + m.load_state_dict({k: torch.from_numpy(v) for k, v in w.state_dict.items()}) + m.eval() + """ + + def __init__(self, cfg, state_dict, metadata=None, source=None, reranker=None): + self.cfg, self.state_dict, self.metadata, self.source = cfg, state_dict, metadata or {}, source + self.reranker = reranker or {} + + @property + def n_params(self): + return int(sum(v.size for v in self.state_dict.values())) + + def __repr__(self): + return "" % (self.source, len(self.state_dict), self.n_params) + + +def load(path): + """Parse either GGUF variant and return Weights (cfg + state_dict of float32 arrays).""" + g = GGUF(path) + cfg, sd = g.state_dict() + return Weights(cfg, sd, g.metadata, g.path, g.reranker()) + + +def tensors(path): + """Yield (name, shape, type_name, float32 array) for every tensor in the file.""" + g = GGUF(path) + for name in g.order: + t = g.tensors[name] + yield name, tuple(t.shape), t.type_name, t.array() + + +def q4_blob(path): + """Bytes of the carried xlit.q4 file from the compact GGUF (byte-identical to the original).""" + g = GGUF(path) + if _BLOB_TENSOR not in g.tensors: + raise GGUFError("%s carries no %s tensor (that is the F16 variant)" % (path, _BLOB_TENSOR)) + return bytes(g.tensors[_BLOB_TENSOR].data) + + +def main(argv): + args = [a for a in argv[1:] if not a.startswith("-")] + flags = {a for a in argv[1:] if a.startswith("-")} + if not args: + print(__doc__) + return 2 + g = GGUF(args[0]) + if "--json" in flags: + print(json.dumps({"version": g.version, "tensors": g.tensor_count, + "metadata": g.metadata, + "tensor_table": [{"name": n, "shape": g.tensors[n].shape, + "type": g.tensors[n].type_name, + "offset": g.tensors[n].offset} for n in g.order]}, + indent=1, ensure_ascii=False)) + return 0 + for line in g.validate(): + print(line) + print("general.name = %r" % g.metadata.get("general.name")) + print("-- tensors --") + for n in g.order: + t = g.tensors[n] + print(" %-28s %-4s %-18s %d B" % (n, t.type_name, tuple(t.shape), len(t.data))) + w = load(args[0]) + print("-- weights --") + print("cfg = %s" % (w.cfg,)) + print("%d tensors, %d parameters" % (len(w.state_dict), w.n_params)) + return 0 + + + + + +def _weights_from_nlm_blob(blob): + n=int.from_bytes(blob[:4],"little");md=json.loads(blob[4:4+n]);data=blob[4+n:];W={};G=md["G"] + for k,shape,kind,off,ln in md["tensors"]: + count=int(np.prod(shape)) + if kind in ("f16","f32"): + W[k]=np.frombuffer(data,np.float16 if kind=="f16" else np.float32,count,off).astype(np.float32).reshape(shape).copy() + else: + r,c=shape;ng=(c+G-1)//G + if kind=="q8":q=np.frombuffer(data,np.int8,r*c,off).astype(np.float32).reshape(r,c);end=off+r*c + else: + nb=(r*c+1)//2;u=np.frombuffer(data,np.uint8,nb,off);q=(np.stack([u>>4,u&15],1).reshape(-1)[:r*c].astype(np.int16)-8).astype(np.float32).reshape(r,c);end=off+nb + end+=(-end)%4;sc=np.frombuffer(data,np.float16,r*ng,end).astype(np.float32).reshape(r,ng);W[k]=q*np.repeat(sc,G,1)[:,:c] + return md["cfg"],W + +def load_word_model(path): + import torch + from bhc.xlit_model import XlitTransformer, set_vocab + w=load(path);set_vocab(w.cfg.get("vocab","hi"));m=XlitTransformer(**w.cfg);m.load_state_dict({k:torch.from_numpy(v.copy()) for k,v in w.state_dict.items()});return m.eval() + +def load_sentence_lm(path): + from bhc.nlm import NLM + w=load(path);vocab=json.loads(w.metadata["aksharalp.tokenizer_json"]) + return NLM(w.cfg,w.state_dict,vocab,w.cfg.get("norm","self")) + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/models/bn/dict.tsv.gz b/models/bn/dict.tsv.gz new file mode 100644 index 0000000000000000000000000000000000000000..00bc293b6e6bb2f0b9d3a98afc78440bf94aa8ce --- /dev/null +++ b/models/bn/dict.tsv.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:edb2ad0bd7de4e87bd4d49ac406a9ef01011451bf23ac82d4c063bb9cce1dc2b +size 2508610 diff --git a/models/bn/inventory.json b/models/bn/inventory.json new file mode 100644 index 0000000000000000000000000000000000000000..fc7e52c91c5e67fe8b99d0061e6e7fe1b0c6a5dc --- /dev/null +++ b/models/bn/inventory.json @@ -0,0 +1,8 @@ +{ + "parameters": 989056, + "model_raw": 589040, + "model_gz9": 540107, + "dict_raw": 14068340, + "dict_gz9": 2508610, + "dict_words": 500000 +} diff --git a/models/bn/params.json b/models/bn/params.json new file mode 100644 index 0000000000000000000000000000000000000000..49e6342005e68f1ec620cf46fcea4dd4cca4d8d3 --- /dev/null +++ b/models/bn/params.json @@ -0,0 +1,58 @@ +{ + "format": "aksharalp-multiindic/1", + "language": "bn", + "beam": 8, + "nbest": 8, + "model": "xlit.q4", + "dictionary": "dict.tsv.gz", + "dictionary_words": 500000, + "dictionary_total": 29467775, + "unk": -17.198807885289952, + "reranker": { + "type": "linear", + "features": [ + "sc", + "scn", + "inlex", + "uni", + "free0", + "rank", + "sc_rel", + "uni_rel" + ], + "w": [ + 0.7719883322715759, + 0.7239015102386475, + 2.2432072162628174, + 0.3994755744934082, + -0.1507813185453415, + -0.07777873426675797, + 0.7293219566345215, + 0.42816251516342163 + ], + "mu": [ + -5.078212233229818, + -0.8147631729600909, + 0.31161147274689055, + -4.2673892678692775, + 0.13265511803554703, + 0.4336724409822265, + -3.7508561308261763, + -0.6986245748643944 + ], + "sd": [ + 2.216960295304686, + 0.6165648295196909, + 0.46315298671618116, + 6.490492456336254, + 0.33920261805997143, + 0.28614764292216904, + 2.341120778815271, + 1.748762448987244 + ], + "fit_split": "all Aksharantar VALID", + "fit_n": 11188, + "l2": 0.001, + "seed": 0 + } +} diff --git a/models/bn/training_report.json b/models/bn/training_report.json new file mode 100644 index 0000000000000000000000000000000000000000..30d8e30bd97920ea62a374bed3930c2b3c1d5b5e --- /dev/null +++ b/models/bn/training_report.json @@ -0,0 +1,31 @@ +{ + "pairs": 1835520, + "counts": { + "aks-Dakshina": 76330, + "aks-AK-Freq": 7365, + "aks-Existing": 5478, + "aks-Wikidata": 18249, + "aks-Samanantar": 114558, + "aks-IndicCorp": 1009448, + "ie": 538301, + "heldout_drop_ie": 8171, + "dup_ie": 149395, + "ie_words": 189824, + "dup_ei": 185794, + "ei": 65791, + "heldout_drop_ei": 665, + "ei_words": 137774 + }, + "unique_natives": 1242898, + "unique_romans": 1513122, + "overlap_valid_test_natives": 0, + "overlap_valid_test_romans": 0, + "heldout_natives_total": 11576, + "heldout_romans_total": 24322, + "margins": { + "ie": 0.6, + "ei": 0.35, + "maxv_ie": 4, + "maxv_ei": 2 + } +} \ No newline at end of file diff --git a/models/bn/xlit.q4 b/models/bn/xlit.q4 new file mode 100644 index 0000000000000000000000000000000000000000..e2e2c0f22ec8779c2614157f8bc2e6a5db983638 --- /dev/null +++ b/models/bn/xlit.q4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f099f0b9524f446debf1d3f0ae1dc7ed1e88625c33f6dbb7b45e0348bf76376 +size 589040 diff --git a/models/bn/xlit.q4.gz b/models/bn/xlit.q4.gz new file mode 100644 index 0000000000000000000000000000000000000000..95c7e39271c54d79329fed60e61e7c6eafe60e53 --- /dev/null +++ b/models/bn/xlit.q4.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c87e62a4249a63c333942c1c8736eb2c89f9ee3148ef8fc422784ba6ce21e43 +size 540107 diff --git a/models/hi/dict/lex.bin b/models/hi/dict/lex.bin new file mode 100644 index 0000000000000000000000000000000000000000..50d10f133976f6858c59708dafb69aa57917e230 --- /dev/null +++ b/models/hi/dict/lex.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73bd5436b77db4ee433c564521f19a2b78cb6a45c429f274d43bef909708347f +size 11675371 diff --git a/models/hi/dict/lex.bin.gz b/models/hi/dict/lex.bin.gz new file mode 100644 index 0000000000000000000000000000000000000000..e250f0e37a3d567705489ad4d18f430d24b33394 --- /dev/null +++ b/models/hi/dict/lex.bin.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b4dbff703f395e97b4503646fde7782bab0f3d79b9731ef703df6b961e38f55 +size 6472998 diff --git a/models/hi/dict/lm.bin b/models/hi/dict/lm.bin new file mode 100644 index 0000000000000000000000000000000000000000..1873dd3931b3e47596671ed3c35b8d21a22bf090 --- /dev/null +++ b/models/hi/dict/lm.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:254e34abe7c1e6ed82f74d5cbf175aba1aedbee316363fe2a67e06291ba589ee +size 3913284 diff --git a/models/hi/dict/lm.bin.gz b/models/hi/dict/lm.bin.gz new file mode 100644 index 0000000000000000000000000000000000000000..26039582e2fe23420b3a96f0d952ae03cd1b16ad --- /dev/null +++ b/models/hi/dict/lm.bin.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab3f6b4135862b5bbf42815cc1219274eed711b5a7dfc66f09fc7b0fe32ceed6 +size 869227 diff --git a/models/hi/dict/params.json b/models/hi/dict/params.json new file mode 100644 index 0000000000000000000000000000000000000000..a9fb01d0864f17702a11d597a1fa110dfcd0611b --- /dev/null +++ b/models/hi/dict/params.json @@ -0,0 +1,1207 @@ +{ + "skel": true, + "visr": true, + "acro_v": true, + "beam": 16, + "nbest": 16, + "cbeam": 12, + "model_src": "models/xlit/kaggle_S/best.pt (q4)", + "mu": [ + -10.521347126777764, + -1.6728469504597467, + 0.7538328448077046, + -12.970011146902019, + 0.02594688324476713, + 0.40626001812473145, + 0.4151501319162741, + 0.025044474492837667, + 0.004313954033614008, + 0.06483084381438514, + 0.012691139838906162, + 0.003765618705029087, + 0.0037751882570113715, + 0.7810726343349783, + 0.38245480061359965, + 0.7430479597236697, + 0.3587281554450812, + -8.926633537828142, + -1.4409213087071673, + -5.745570805576459, + 0.7320955089664059, + 0.00013110286215730238, + 0.0008823126927666627, + 0.0007435541890235325, + 0.060991539559092464, + 0.0451960370571331, + 0.937351652007662, + 0.00010813593739981876, + 2.9997004730229544, + 0.029567044759665487 + ], + "sd": [ + 7.091673980821669, + 1.1249789220975193, + 0.43077807331662826, + 8.092773450586368, + 0.15897786150672746, + 0.49113521362781, + 0.49274890702965507, + 0.1562611957952297, + 0.06553987269542878, + 0.24622814209751304, + 0.11193881670497922, + 0.0612499903651332, + 0.061327472347272156, + 0.32132746822913416, + 0.4859877550713982, + 0.4369537311641961, + 0.25644745953949705, + 7.120945222146196, + 1.1268166367816894, + 4.65015790729656, + 0.2181961259258999, + 0.011450265225161555, + 0.02969164191124488, + 0.027259050484096686, + 0.23931579616231363, + 0.20773485687199444, + 0.2143319909975035, + 0.010399280820363304, + 0.029199584986460532, + 0.16939059420091407 + ], + "feats": [ + "sc", + "scn", + "inlex", + "uni", + "free0", + "infree", + "incon", + "aw", + "visarga", + "nukta", + "chandra", + "acro", + "variant", + "frank", + "skel", + "skmatch", + "lev", + "sc_rel", + "scn_rel", + "uni_rel", + "lenr", + "visr", + "hfin", + "acro_nv" + ], + "rerank_hid": 32, + "reranker_fit_on": "Aksharantar hin_valid (x3) + synthetic casual corruptions of valid romans + internal casual dev half A + casual seed train split + valid with val-natives-removed lexicon (frac 0.15)", + "val_rerank_cv2": 69.69, + "k1gate": 2, + "k2gate": 0, + "casual": true, + "abbr": true, + "squeeze": 2, + "nfeat": 30, + "w1": [ + [ + 0.2978273630142212, + -0.12524424493312836, + 0.11281786113977432, + -2.095041036605835, + 0.011366930790245533, + -0.057106778025627136, + -0.08888763189315796, + -0.02623431757092476, + -0.008364331908524036, + 0.03523550555109978, + -0.32832279801368713, + 0.031755391508340836, + -0.18945248425006866, + -0.03855879604816437, + -0.07282370328903198, + 0.004111294634640217, + -0.15331575274467468, + 0.06938236206769943, + 0.03218870982527733, + -0.18719182908535004, + 0.06670068949460983, + -0.0018233258742839098, + 0.0007599865202791989, + -0.03192363306879997, + -0.011909550055861473, + 0.01853865757584572, + 0.1060149297118187, + -0.02053777687251568, + 0.007109370082616806, + 0.0774591937661171 + ], + [ + -0.015218627639114857, + 0.06436186283826828, + -0.27220481634140015, + -0.1558413952589035, + -0.1648024320602417, + 0.06645617634057999, + 0.17393523454666138, + 0.03716582432389259, + -0.023398850113153458, + 0.014711493626236916, + 0.021292448043823242, + -0.160580113530159, + -0.10599923133850098, + 0.09534041583538055, + -0.4242168664932251, + -0.17817474901676178, + 0.11704500764608383, + -0.009878351353108883, + -0.0034402625169605017, + 0.0359056256711483, + 0.08866402506828308, + 0.003516272408887744, + 0.03308955952525139, + 0.04392077773809433, + 0.3541436791419983, + -0.21139748394489288, + 0.17253346741199493, + -0.026407228782773018, + 0.025974996387958527, + 0.07903912663459778 + ], + [ + 0.004851491190493107, + 0.0015358320670202374, + 0.0163769219070673, + 0.0027937949635088444, + 0.011398920789361, + -0.006382606457918882, + -0.0016012770356610417, + 0.007944178767502308, + -0.0023601858410984278, + 0.0069118826650083065, + -0.012052054516971111, + 0.0029139569960534573, + -0.008684785105288029, + -0.002064894651994109, + 0.004399779252707958, + 0.0055933548137545586, + 0.010477577336132526, + 0.003925456199795008, + 0.001485816785134375, + 0.01089472509920597, + -0.011359399184584618, + 0.000777803361415863, + -0.00013434136053547263, + 0.005471780896186829, + 0.012079771608114243, + 0.005524762906134129, + -0.022310465574264526, + 0.004354824312031269, + -0.0021751539316028357, + -0.0021792424377053976 + ], + [ + 0.1546557992696762, + -0.22023016214370728, + 0.026433231309056282, + -0.3074091374874115, + 0.03142368048429489, + -0.001414265250787139, + 0.250213623046875, + 0.02635425515472889, + -0.025518791750073433, + 0.07727637887001038, + 0.1250922977924347, + 0.1549404263496399, + -0.06325443834066391, + 0.1676981896162033, + 0.2107115089893341, + 0.023393569514155388, + -0.07689705491065979, + -0.0959274023771286, + -0.21012301743030548, + -0.27987271547317505, + -0.5363252758979797, + -5.837770731886849e-05, + -0.027652518823742867, + 0.10613229870796204, + 0.08651300519704819, + -0.00655867625027895, + 0.07840493321418762, + -0.01836014911532402, + 0.014346407726407051, + 0.08706853538751602 + ], + [ + 0.18826177716255188, + -0.009053463116288185, + -0.31065717339515686, + 0.12534351646900177, + 0.04372546449303627, + -0.03333894908428192, + -0.30128827691078186, + -0.06759382039308548, + 0.08113513141870499, + 0.014073670841753483, + -0.279991090297699, + -0.07876414060592651, + -0.01601807214319706, + -0.09550463408231735, + 0.0012678971979767084, + -0.2498188614845276, + 0.10350498557090759, + 0.11099839955568314, + -0.009730542078614235, + 0.28837817907333374, + -0.14156962931156158, + 0.0020480784587562084, + -0.056828007102012634, + 0.01283149141818285, + -0.16832418739795685, + -0.1467394381761551, + 0.1770872324705124, + -0.021060630679130554, + -0.06769103556871414, + -0.03401108458638191 + ], + [ + -0.11754447966814041, + -0.029808275401592255, + -0.1512550711631775, + -0.11373015493154526, + -0.11048572510480881, + -0.17563050985336304, + 0.32049867510795593, + -0.03267872706055641, + -0.10488305240869522, + 0.14843808114528656, + -0.019326267763972282, + -0.09003283828496933, + -0.001043589785695076, + -0.06198497116565704, + -0.02670750580728054, + -0.23743438720703125, + -0.22184354066848755, + -0.05037761107087135, + -0.07410146296024323, + 0.27122747898101807, + 0.49611490964889526, + 0.003490318777039647, + -0.01802603341639042, + 0.028976958245038986, + -0.25906312465667725, + 0.13117507100105286, + 0.1753552258014679, + -0.03953361511230469, + 0.04228513687849045, + -0.08250270038843155 + ], + [ + 0.10288158804178238, + 0.09264303743839264, + 0.1902378648519516, + -0.027377843856811523, + -0.009417884983122349, + -0.09616657346487045, + -0.056310057640075684, + -0.1494356244802475, + -0.06837408989667892, + 0.23289616405963898, + -0.08375770598649979, + -0.00040237419307231903, + -0.11441013962030411, + 0.12468060106039047, + -0.019334953278303146, + 0.22956791520118713, + -1.4520599506795406e-05, + 0.027223318815231323, + 0.06727354228496552, + -0.1135883703827858, + -0.03107711859047413, + -0.002018857281655073, + 0.00917847827076912, + 0.000580518099013716, + -0.3089872896671295, + 0.03467949479818344, + -0.014521819539368153, + 0.029316266998648643, + -0.020077496767044067, + 0.02363729104399681 + ], + [ + -0.041116416454315186, + -0.2365289181470871, + -0.22092482447624207, + -0.023430000990629196, + -0.06117008253931999, + 0.0180368572473526, + -0.04412784427404404, + -0.0070696608163416386, + 0.07610896974802017, + 0.06204422190785408, + 0.03766728192567825, + 0.013954287394881248, + 0.019880810752511024, + 0.08508526533842087, + 0.017565736547112465, + 0.2397090494632721, + -0.0932067260146141, + 0.03273257613182068, + -0.0738687664270401, + -0.1392388939857483, + 0.4667511284351349, + 0.0025568881537765265, + -0.016340259462594986, + -0.02498924359679222, + 0.31997644901275635, + -0.11028777807950974, + 0.43853846192359924, + -0.051532767713069916, + 0.050680991262197495, + 0.19535787403583527 + ], + [ + 0.33466964960098267, + 0.02687048725783825, + 0.2653488218784332, + -0.02545027807354927, + 0.04630497470498085, + 0.019789546728134155, + 0.1422019600868225, + -0.04931357875466347, + -0.07480935007333755, + -0.010642027482390404, + -0.009178480133414268, + 0.06842169165611267, + -0.013987762853503227, + -0.09246459603309631, + 0.37448567152023315, + 0.1166202500462532, + -0.028120124712586403, + 0.27045363187789917, + 0.23964692652225494, + -0.03982849419116974, + -0.1351834535598755, + -0.0004796762077603489, + 0.008671257644891739, + 0.0689004510641098, + 0.0060224891640245914, + -0.2252589464187622, + 0.29317355155944824, + -0.04284771531820297, + -0.015715429559350014, + -0.07563797384500504 + ], + [ + -0.1948905885219574, + -0.06836901605129242, + -0.06071034073829651, + 0.17876936495304108, + -0.02517947554588318, + 0.004684338346123695, + 0.3768179416656494, + 0.0746963769197464, + -0.0026411819271743298, + 0.1542917639017105, + -0.023636970669031143, + 0.05739671364426613, + -0.08756262063980103, + -0.15020489692687988, + 0.1690479964017868, + -0.02700330875813961, + -0.27638229727745056, + -0.33039161562919617, + -0.21426960825920105, + -0.036147501319646835, + -0.4114132821559906, + -0.0016613593325018883, + -0.07422971725463867, + 0.012680871412158012, + -0.2701949179172516, + -0.07551289349794388, + -0.015061997808516026, + 0.022278647869825363, + 0.03522580862045288, + -0.2765999734401703 + ], + [ + 0.11008555442094803, + -0.006589978933334351, + 0.4071297347545624, + -0.36271584033966064, + -0.054420106112957, + 0.16820485889911652, + 0.05241335183382034, + 0.0012270126026123762, + -0.06601029634475708, + 0.10006329417228699, + -0.19624856114387512, + 0.1140369176864624, + -0.09873856604099274, + -0.11243139952421188, + -0.24534428119659424, + 0.3282925486564636, + -0.48944753408432007, + 0.03921837359666824, + 0.14451849460601807, + -0.2924042046070099, + -0.13455374538898468, + -0.000331229210132733, + 0.04286456108093262, + 0.010504919104278088, + -0.3971524238586426, + 0.019514674320816994, + 0.052226945757865906, + 0.019799765199422836, + 0.009336386807262897, + 0.2063215672969818 + ], + [ + -0.331403911113739, + -0.04248686507344246, + -0.1742829531431198, + -0.054567597806453705, + -0.010023543611168861, + -0.09257467091083527, + -0.043399494141340256, + 0.1145426481962204, + 0.022823648527264595, + -0.12922437489032745, + -0.02116081491112709, + -0.014364399015903473, + 0.047502823173999786, + 0.16297870874404907, + 0.14818622171878815, + 0.21975566446781158, + 0.018450461328029633, + -0.22422853112220764, + -0.028471672907471657, + 0.08202888071537018, + 0.08384975045919418, + -0.0001631450286367908, + 0.023598164319992065, + -0.03857981041073799, + -0.16346950829029083, + 0.02116750180721283, + 0.07288042455911636, + 0.028233397752046585, + 0.006980113219469786, + 0.13414007425308228 + ], + [ + -0.11374436318874359, + 0.029442576691508293, + -0.11658085137605667, + 0.38891634345054626, + 0.27945610880851746, + -0.023148799315094948, + -0.12111373245716095, + 0.11448338627815247, + 0.07090019434690475, + -0.0585474856197834, + -0.11256012320518494, + 0.10155794769525528, + -0.15389187633991241, + 0.13555829226970673, + -0.13662731647491455, + 0.01267039030790329, + -0.1267690509557724, + 0.03053329885005951, + -0.025454195216298103, + -0.02522820420563221, + 0.5375521779060364, + -0.0016045188531279564, + 0.014284935779869556, + 0.0015696983318775892, + 0.08597452938556671, + -0.13576777279376984, + -0.004649235866963863, + 0.037172526121139526, + -0.05200475826859474, + 0.09373147040605545 + ], + [ + -0.029431937262415886, + -0.2620309889316559, + -0.302036851644516, + -0.02678726427257061, + -0.060088150203228, + 0.11497970670461655, + -0.016540294513106346, + -0.28534334897994995, + 0.006799261551350355, + 0.05705795809626579, + 0.0010234479559585452, + -0.1447167992591858, + 0.07319372147321701, + -0.18381135165691376, + -0.015908673405647278, + -0.15757401287555695, + 0.16871140897274017, + 0.062292929738759995, + -0.18093398213386536, + -0.05533643066883087, + -0.07152833789587021, + 0.006225141696631908, + 0.01381989661604166, + 0.062143463641405106, + 0.0060385470278561115, + 0.05931759253144264, + -0.5278472900390625, + 0.044890351593494415, + 0.0603656992316246, + 0.021104032173752785 + ], + [ + -0.08289032429456711, + 0.010489634238183498, + -0.15753856301307678, + 0.1578647494316101, + -0.1520807445049286, + -0.006744581274688244, + -0.21280339360237122, + -0.07963788509368896, + 0.02312246896326542, + 0.17505905032157898, + -0.031054100021719933, + 0.050754114985466, + 0.1856665462255478, + 0.21133340895175934, + 0.03555568307638168, + -0.16340608894824982, + -0.011286195367574692, + -0.17731770873069763, + -0.1558055728673935, + 0.01001640036702156, + -0.4405674636363983, + -0.0007858822355046868, + 0.060133811086416245, + 0.12372671067714691, + -0.24311712384223938, + 0.12147476524114609, + 0.0719279795885086, + -0.01516275480389595, + 0.013173088431358337, + -0.10736092180013657 + ], + [ + -0.02461673691868782, + -0.033535923808813095, + 0.04467519372701645, + 0.17701135575771332, + -0.13619090616703033, + -0.06061656028032303, + 0.3366754353046417, + -0.08483055233955383, + 0.05314863845705986, + -0.07403695583343506, + -0.015225897543132305, + -0.0862615630030632, + 0.11477895826101303, + 0.11675471812486649, + 0.0206782054156065, + 0.030554944649338722, + 0.5412853956222534, + 0.028187474235892296, + -0.06224275007843971, + 0.03471064567565918, + 0.28326016664505005, + -0.003337741130962968, + -0.021662497892975807, + -0.16106656193733215, + 0.11502524465322495, + 0.03413642570376396, + 0.2151215523481369, + 0.020399991422891617, + -0.016254127025604248, + 0.0517471544444561 + ], + [ + -0.07012061029672623, + 0.010917849838733673, + 0.15196256339550018, + 0.322708398103714, + -0.22805741429328918, + 0.09942539781332016, + 0.26415935158729553, + -0.1787206530570984, + -0.023527758195996284, + 0.25548166036605835, + -0.2276124358177185, + -0.04554101452231407, + 0.13465531170368195, + -0.16299594938755035, + 0.08794410526752472, + 0.35724157094955444, + -0.07113544642925262, + -0.024448543787002563, + 0.05222854018211365, + 0.1859729290008545, + 0.09545399993658066, + -0.0025332551449537277, + -0.009556661359965801, + 0.0772150307893753, + -0.012442230246961117, + -0.019003134220838547, + 0.2470007687807083, + 0.014394563622772694, + -0.03273707255721092, + -0.005577665753662586 + ], + [ + 0.10807406902313232, + 0.10408087074756622, + -0.26516106724739075, + -0.14970353245735168, + 0.02540256455540657, + 0.06080946698784828, + 0.01794132962822914, + -0.038598500192165375, + 0.027654403820633888, + 0.03587906435132027, + 0.2089749127626419, + 0.06664592772722244, + 0.044442806392908096, + -0.37682294845581055, + -0.1572580486536026, + -0.03694003075361252, + 0.03499995544552803, + 0.1262999027967453, + 0.11232877522706985, + -0.23201511800289154, + 0.4309413433074951, + 0.0038779794704169035, + 0.002699619624763727, + -0.05958323925733566, + -0.20616133511066437, + 0.1609780639410019, + 0.16190771758556366, + -0.02789650484919548, + 0.020086074247956276, + 0.07073075324296951 + ], + [ + 0.006996219512075186, + 0.03400345891714096, + 0.1543908417224884, + -0.007444288581609726, + 0.08196599036455154, + -0.008490710519254208, + -0.08009248226881027, + 0.12440887093544006, + 0.019961031153798103, + 0.1257852166891098, + 0.048060204833745956, + -0.07055604457855225, + 0.061722416430711746, + -0.05149455368518829, + 0.008484847843647003, + 0.11561670154333115, + 0.2172529548406601, + 0.037951596081256866, + 0.03992628678679466, + 0.04459511861205101, + -0.004068531561642885, + -0.0020207033958286047, + -0.004832305014133453, + 0.0006540190661326051, + -0.10632411390542984, + 0.16884131729602814, + -0.07619648426771164, + -0.026168160140514374, + 0.017908411100506783, + -0.013091475702822208 + ], + [ + -0.20183679461479187, + 0.09394951164722443, + -0.06837902218103409, + 0.3966565728187561, + -0.08733834326267242, + 0.00375247816555202, + -0.1088104322552681, + 0.05278634652495384, + -0.050863511860370636, + 0.054866623133420944, + -0.04225798323750496, + 0.011660507880151272, + 0.02623550407588482, + -0.12409938126802444, + -0.3320227563381195, + -0.28528183698654175, + -0.21174274384975433, + 0.0518014095723629, + 0.04707464575767517, + 0.49259650707244873, + 0.1384994089603424, + -0.0025354258250445127, + -0.07498032599687576, + -0.03429447486996651, + 0.034742970019578934, + -0.015903383493423462, + 0.07173606753349304, + -0.00840385165065527, + -0.016444213688373566, + 0.17811930179595947 + ], + [ + -0.005009941756725311, + -0.002124645747244358, + -0.02421066164970398, + -0.0048532867804169655, + -0.01484320592135191, + 0.009254788048565388, + 0.005000927951186895, + -0.009545867331326008, + 0.0029280090238898993, + -0.007147208787500858, + 0.014578479342162609, + 0.0005799255450256169, + 0.008558865636587143, + 0.00028763487352989614, + -0.005970140919089317, + -0.005843811202794313, + -0.007050391752272844, + -0.0039084916934370995, + -0.0010358188301324844, + -0.014289230108261108, + 0.01355541031807661, + 0.0012916265986859798, + 0.0007600969984196126, + -0.0023508684244006872, + -0.009323712438344955, + -0.0029848776757717133, + 0.019896771758794785, + -0.00410780543461442, + 0.0010361883323639631, + 0.004762246739119291 + ], + [ + -0.012774190865457058, + -0.018942734226584435, + -0.11251771450042725, + 0.019434144720435143, + -0.011331307701766491, + 0.007678790017962456, + 0.007547513581812382, + -0.061995729804039, + 0.03405012562870979, + -0.054970525205135345, + -0.05692291632294655, + -0.0005997982807457447, + -0.004191168583929539, + 0.013564752414822578, + -0.05143298581242561, + -0.09229299426078796, + 0.0031138239428400993, + 0.016548657789826393, + -0.01669246517121792, + -0.025574499741196632, + 0.05329960957169533, + 0.0009290038142353296, + -0.0014467722503468394, + -0.008538647554814816, + -0.002785967430099845, + -0.09404794126749039, + 0.059979960322380066, + 0.017218327149748802, + -0.01839030720293522, + 0.0368536040186882 + ], + [ + 0.13817264139652252, + 0.05768146738409996, + 0.2810722887516022, + -0.1336238831281662, + -0.020433520898222923, + -0.022262845188379288, + -0.22902441024780273, + 0.04042287915945053, + -0.04194024205207825, + 0.052723661065101624, + 0.05492428317666054, + 0.0030093498062342405, + -0.017766088247299194, + -0.11627964675426483, + 0.2707958519458771, + 0.3692748248577118, + 0.12485955655574799, + -0.056481827050447464, + 0.07885903120040894, + 0.1400105357170105, + -0.14929717779159546, + -0.0005992194055579603, + 0.010729140602052212, + 0.010986294597387314, + 0.14774632453918457, + 0.13031838834285736, + -0.19673748314380646, + -0.0031795212998986244, + 0.03459145873785019, + 0.008430790156126022 + ], + [ + 0.4591112434864044, + 0.0025790133513510227, + 0.25633737444877625, + 0.2674988806247711, + -0.0451095886528492, + -0.1987534910440445, + 0.23540718853473663, + -0.0024211835116147995, + -0.05394670367240906, + -0.18480446934700012, + -0.06402574479579926, + -0.026236604899168015, + -0.006443975027650595, + -0.04793023318052292, + -0.24826540052890778, + -0.24948164820671082, + -0.06005451828241348, + 0.2147718220949173, + -0.10956361144781113, + 0.3251183032989502, + -0.059907492250204086, + -0.00654930854216218, + 0.03770732134580612, + 0.0900811180472374, + 0.02229798212647438, + 0.10015444457530975, + -0.13280200958251953, + 0.045921050012111664, + -0.047279007732868195, + 0.004736369010061026 + ], + [ + 0.24877531826496124, + 0.09107162803411484, + 0.1673077642917633, + -0.18705332279205322, + 0.0927852913737297, + 0.02446102723479271, + -0.03718196973204613, + -0.15125590562820435, + -0.09532417356967926, + 0.016231965273618698, + -0.03530269116163254, + -0.08297035843133926, + -0.08234328776597977, + 0.002841780660673976, + -0.35036754608154297, + 0.04587160050868988, + 0.06278622150421143, + 0.19165585935115814, + 0.1635713428258896, + -0.33520224690437317, + -0.20163007080554962, + 0.0009035415132530034, + -0.02194751612842083, + 0.055562879890203476, + 0.219464510679245, + 0.2697101831436157, + -0.02586711384356022, + -0.055024728178977966, + -0.029787302017211914, + -0.10946609079837799 + ], + [ + -0.2924965023994446, + -0.00993357878178358, + -0.2653579115867615, + 0.16539999842643738, + 0.1087416410446167, + -0.11873558908700943, + -0.3984578251838684, + 0.32359859347343445, + -0.04011141136288643, + -0.021019507199525833, + -0.07874271273612976, + 0.1229449212551117, + -0.12061803787946701, + 0.3016977906227112, + -0.1609334647655487, + -0.05772622674703598, + -0.09362296015024185, + -0.1770908385515213, + -0.1246710941195488, + -0.1669032722711563, + -0.556264340877533, + 0.0033323306124657393, + 0.017697351053357124, + 0.10091863572597504, + 0.13055913150310516, + -0.0007065890822559595, + 0.13776889443397522, + 0.02825685776770115, + 0.03610856458544731, + -0.25887566804885864 + ], + [ + -0.6498939990997314, + -0.570142388343811, + -0.09348710626363754, + 0.10502799600362778, + -0.0010723592713475227, + 0.27705782651901245, + -0.08424247056245804, + -0.13540084660053253, + -0.09929133206605911, + -0.026345640420913696, + -0.03949467092752457, + 0.08228645473718643, + 0.007659286260604858, + 0.10000486671924591, + 0.02055215835571289, + 0.44263848662376404, + 0.2704412341117859, + -0.39337337017059326, + -0.4581047296524048, + -0.24843794107437134, + -0.03707090765237808, + 0.007195556070655584, + 0.02235628478229046, + -0.053947798907756805, + 0.09798818081617355, + -0.007347901351749897, + -0.19071193039417267, + -0.08718398213386536, + 0.09261944890022278, + 0.18475641310214996 + ], + [ + -0.008125352673232555, + -0.014405515976250172, + -0.10802251100540161, + 0.03259724751114845, + -0.08692625910043716, + 0.02846444584429264, + 0.05434923619031906, + -0.04812580719590187, + 0.033300112932920456, + 0.03922724723815918, + 0.06522151082754135, + -0.07150112092494965, + 0.057582758367061615, + 0.037744637578725815, + -0.11136531084775925, + -0.09772961586713791, + 0.011466114781796932, + -0.04866102337837219, + -0.042589105665683746, + -0.13253262639045715, + 0.10261378437280655, + 0.0012789112515747547, + -0.007382560055702925, + -0.0386480912566185, + 0.09579290449619293, + 0.07787111401557922, + -0.03437710925936699, + -0.022561732679605484, + 0.03427862375974655, + -0.04894588887691498 + ], + [ + 0.19641371071338654, + -0.0004457634349819273, + 0.42898422479629517, + -0.3109084665775299, + 0.023015540093183517, + -0.030388157814741135, + 0.016041671857237816, + 0.049095023423433304, + -0.08631545305252075, + 0.029774822294712067, + 0.08273293077945709, + -0.03961489722132683, + -0.14475280046463013, + -0.2634407579898834, + 0.10352513194084167, + 0.12892627716064453, + -0.14932452142238617, + 0.0002653998089954257, + 0.14986489713191986, + -0.14165998995304108, + 0.14919297397136688, + -0.0020301176700741053, + 0.03245501592755318, + 0.030653124675154686, + 0.20543573796749115, + 0.09449265152215958, + -0.12122594565153122, + 0.030303027480840683, + -0.03031827136874199, + -0.04865238070487976 + ], + [ + -0.5960131883621216, + -0.10798998922109604, + -0.2643187642097473, + 0.11755231022834778, + 0.10250407457351685, + -0.1922222375869751, + 0.1440320760011673, + -0.0010857033776119351, + -0.03235757350921631, + 0.004977218806743622, + 0.0048888074234128, + 0.011324840597808361, + -0.002030604053288698, + 0.12053726613521576, + -0.18381474912166595, + 0.05327345430850983, + -0.07517174631357193, + -0.42148804664611816, + -0.17396032810211182, + -0.11963854730129242, + 0.0972416028380394, + 0.005354877095669508, + -0.024641545489430428, + -0.03697052225470543, + -0.326669842004776, + 0.296438604593277, + 0.31222113966941833, + -0.0683341920375824, + 0.07314322888851166, + 0.19110500812530518 + ], + [ + 0.01856222376227379, + 0.3088688254356384, + 0.2287462204694748, + -0.35537827014923096, + -0.010479615069925785, + -0.30885910987854004, + -0.36486321687698364, + 0.11301260441541672, + 0.014579768292605877, + 0.008242469280958176, + 0.26824134588241577, + 0.10678158700466156, + 0.01691117323935032, + 0.1525404155254364, + 0.3289874196052551, + 0.05752164125442505, + -0.28336283564567566, + -0.035387687385082245, + 0.11145664006471634, + -0.14698174595832825, + 0.3178817629814148, + -0.005325492471456528, + 0.006087847985327244, + -0.06780464947223663, + -0.005475239362567663, + 0.12878170609474182, + 0.21892914175987244, + 0.01651044562458992, + 0.03148592263460159, + -0.035293299704790115 + ], + [ + 0.3846608102321625, + 0.11729733645915985, + -0.09179908037185669, + 0.17007967829704285, + 0.11335381865501404, + 0.14137855172157288, + 0.09570345282554626, + -0.14108647406101227, + 0.03668607398867607, + 0.05555937811732292, + 0.12136383354663849, + -0.16928109526634216, + -0.08710984140634537, + -0.0749814584851265, + -0.06574045121669769, + 0.1974145621061325, + 0.22125230729579926, + 0.265985906124115, + 0.1317518651485443, + 0.15454895794391632, + -0.19146312773227692, + 0.0022830197121948004, + -0.021833697333931923, + -0.0475747250020504, + -0.005283071659505367, + 0.07824162393808365, + -0.18995541334152222, + 0.02367176115512848, + 0.03805243596434593, + -0.0012313274201005697 + ] + ], + "b1": [ + -1.2538763284683228, + -0.35510221123695374, + 0.1701766550540924, + -0.5261530876159668, + -0.3044930100440979, + 0.6120124459266663, + 0.09416700154542923, + -0.23872992396354675, + -0.3090171217918396, + 0.41894909739494324, + -0.18880058825016022, + 0.7128903269767761, + 0.44998398423194885, + 0.22322070598602295, + -0.4774796962738037, + -0.1421545296907425, + -0.11969872564077377, + -0.5750750303268433, + -0.35053399205207825, + 0.374132364988327, + -0.0662507489323616, + -0.3004978895187378, + 0.10778029263019562, + -0.7059808969497681, + -0.07251366227865219, + 0.1130848154425621, + 2.298213005065918, + 0.12174320966005325, + 0.529291033744812, + 0.992193877696991, + 0.199993297457695, + 0.39610981941223145 + ], + "w2": [ + -1.6143524646759033, + -0.6882335543632507, + 0.04837442561984062, + -0.7366872429847717, + -0.7074232697486877, + -0.7606724500656128, + 0.4774588942527771, + -0.7977778315544128, + 0.6916497349739075, + -0.756131649017334, + 0.7962300777435303, + -0.5795010924339294, + 0.7254323959350586, + -0.7123520970344543, + -0.6687820553779602, + 0.6352038383483887, + 0.6475100517272949, + -0.7353212237358093, + 0.40447404980659485, + 0.7017884254455566, + -0.06201210245490074, + -0.1934119313955307, + 0.7135710120201111, + 0.7929962873458862, + 0.6543982028961182, + -0.8076037168502808, + -1.187669038772583, + -0.2901042699813843, + 0.6971926093101501, + -1.0045663118362427, + 0.7298798561096191, + -0.7038688659667969 + ], + "b2": -0.012386741116642952, + "w_lm": 0.5, + "w_ch_s": 1.5, + "oov_pen_s": 4.0, + "sent_weights_note": "sent_grid on val_dakshina_sent (600) + casual dev A: w_lm 0.5, w_ch_s 1.5 (val 88.80, devA tok 92.37)" +} \ No newline at end of file diff --git a/models/hi/dict/params.json.gz b/models/hi/dict/params.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..896f8b17845cfd488a6737367247d9f9e656b393 --- /dev/null +++ b/models/hi/dict/params.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc7dd23ba83ec6bc5841b03fa7b8e3cb934d0322dda652863537ce03958ea79d +size 11868 diff --git a/models/hi/manifest.json b/models/hi/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..b08845295daf082226113b76fd04c560570d280f --- /dev/null +++ b/models/hi/manifest.json @@ -0,0 +1,76 @@ +{ + "format": "bhashacore-bundle/1", + "name": "AksharaLP-Sentence-2.8M", + "note": "AksharaLP-Sentence-2.8M v3.0.0 (v2 word package 2.0.0 + sentence LM 2,770,752 params)", + "files": { + "model": "xlit.q4", + "lex": null, + "lm": null, + "params": "params.json", + "lid": null, + "loan": null, + "natural": null + }, + "sizes": { + "params.json": { + "bytes": 14780, + "gz": 6367, + "sha256": "1dc58564cd649278c819641a65141e9e96a42f3fc7fba555b9072628674c2935" + }, + "xlit.q4": { + "bytes": 588891, + "gz": 543838, + "sha256": "652eafe0c1e2b46fec85663825621284f0a5911f2d7cc6e13b1182ce47cd9803" + } + }, + "model_name": "AksharaLP-Sentence-2.8M", + "version": "3.0.0", + "dict_name": "dict-3M", + "packs": { + "dict": { + "dir": "dict/", + "name": "dict-3M", + "files": { + "lex": "lex.bin", + "lm": "lm.bin", + "params": "params.json" + }, + "sizes": { + "lex.bin": { + "bytes": 11675371, + "gz": 6472998, + "sha256": "73bd5436b77db4ee433c564521f19a2b78cb6a45c429f274d43bef909708347f" + }, + "lm.bin": { + "bytes": 3913284, + "gz": 869227, + "sha256": "254e34abe7c1e6ed82f74d5cbf175aba1aedbee316363fe2a67e06291ba589ee" + }, + "params.json": { + "bytes": 27828, + "gz": 11868, + "sha256": "6d60eaf2b4e89a847a750a226680021460e454c6d1a06070a56e8f8664500565" + } + }, + "bytes": 15616483, + "gz": 7354093 + } + }, + "sentence_lm": { + "file": "nlm.q8", + "params": 2770752, + "norm": "self", + "cfg": { + "w_nlm": 0.4, + "w_lm": 0.25, + "w_ch_s": 1.5, + "w_agn": 2, + "w_agr": 0, + "nlm_margin": 2, + "nlm_k": 6, + "beam_s": 8, + "lat_k": 12, + "rec_n": 3 + } + } +} \ No newline at end of file diff --git a/models/hi/manifest.json.gz b/models/hi/manifest.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..3bcd847dc3e1229a2e62cd3c55036de67da01232 --- /dev/null +++ b/models/hi/manifest.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acb2bd2c0c12813d18c9696058260d57ea504db76db6d2d0810054a9e65a875e +size 743 diff --git a/models/hi/params.json b/models/hi/params.json new file mode 100644 index 0000000000000000000000000000000000000000..40cd20898323befd38f5714296918ca656ecb86e --- /dev/null +++ b/models/hi/params.json @@ -0,0 +1,658 @@ +{ + "visr": true, + "acro_v": true, + "beam": 16, + "nbest": 16, + "model_file": "xlit.q4", + "model_src": "models/xlit/kaggle_S/best.pt (q4)", + "feats": [ + "sc", + "scn", + "inlex", + "uni", + "free0", + "infree", + "incon", + "aw", + "visarga", + "nukta", + "chandra", + "acro", + "variant", + "frank", + "skel", + "skmatch", + "lev", + "sc_rel", + "scn_rel", + "uni_rel", + "lenr", + "visr", + "hfin", + "acro_nv", + "ck1", + "ck2", + "ckr", + "abbr", + "abbr_r", + "novow" + ], + "rerank_hid": 16, + "reranker_fit_on": "Aksharantar hin_valid (all distinct romans)", + "w1": [ + [ + -0.09569721668958664, + -0.06976903229951859, + 7.370932144112885e-05, + 6.590761768165976e-05, + 0.03393518179655075, + -0.04138208553195, + 1.7743354874255601e-06, + 0.04049277305603027, + 0.030535239726305008, + -0.03615963086485863, + 0.06760071963071823, + -0.050459809601306915, + 8.555630483897403e-05, + 0.12352440506219864, + 3.691729943966493e-05, + -0.03898712620139122, + 0.05472588911652565, + -0.0722658708691597, + -0.03084373287856579, + 3.899929652106948e-05, + 0.09371104836463928, + 0.0006122469785623252, + 0.009943258948624134, + 0.0035983887501060963, + 1.4434878721658606e-05, + -9.476167178945616e-06, + -8.109111513476819e-05, + 8.307790994876996e-05, + 5.63784378755372e-05, + 0.033583711832761765 + ], + [ + -0.06520714610815048, + -0.22002042829990387, + 5.8050100051332265e-05, + 4.122480095247738e-05, + 0.157881498336792, + -0.08535762131214142, + 5.227849032962695e-05, + -0.10667268931865692, + 0.0449322871863842, + -0.0830828994512558, + 0.1274748593568802, + -0.16234727203845978, + -1.5151351362874266e-05, + 0.04462355375289917, + 6.470967491623014e-05, + -0.46928101778030396, + -0.06409385055303574, + -0.1443071961402893, + -0.10042543709278107, + 3.231388518543099e-06, + 0.506575882434845, + 0.0001223099243361503, + 0.001166749862022698, + -0.124892458319664, + -6.004522219882347e-05, + 5.272938869893551e-05, + -1.668776349106338e-05, + 6.943113112356514e-05, + 6.206922262208536e-05, + 0.03463549539446831 + ], + [ + 0.10320857167243958, + 0.03564881905913353, + 5.304890873958357e-05, + -2.705583392526023e-05, + 0.0204799622297287, + 0.03376350179314613, + -3.419404038140783e-06, + -0.012314163148403168, + -0.020435109734535217, + 0.04108770936727524, + -0.03224874287843704, + 0.06798811256885529, + -3.519669553497806e-05, + -0.0850144475698471, + -7.793389522703364e-05, + 0.026164377108216286, + -0.04806457832455635, + 0.085055872797966, + 0.02192525751888752, + 5.6025866797426715e-05, + -0.07257550209760666, + -0.0008326452225446701, + -0.007208347320556641, + -0.0033632635604590178, + -1.6762169252615422e-05, + 1.5086284292920027e-05, + 1.4737893252458889e-05, + 4.09946842410136e-05, + -3.4439559385646135e-05, + -0.013663604855537415 + ], + [ + 0.09898629039525986, + 0.08176817744970322, + -6.411002686945722e-05, + -3.348607424413785e-05, + -0.11127989739179611, + -0.013069065287709236, + -4.471569991437718e-05, + -0.04852429777383804, + -0.03373131528496742, + 0.02113977074623108, + -0.07466695457696915, + -0.03130970150232315, + 3.904753612005152e-05, + -0.13018809258937836, + 8.530828927177936e-05, + 0.002361613791435957, + -0.02581200562417507, + 0.08082696050405502, + 0.05275065079331398, + -1.3446571756503545e-05, + -0.11102613061666489, + -0.0013248512987047434, + -0.011110929772257805, + 0.05770425498485565, + -6.137053424026817e-05, + 7.551998714916408e-05, + 2.2288904801825993e-05, + -4.0409154280496296e-06, + -1.3065729035588447e-05, + -0.03673354536294937 + ], + [ + -0.269260972738266, + 0.3251459300518036, + 4.369729504105635e-05, + -4.237536631990224e-05, + -0.3263976573944092, + -0.01859147660434246, + 2.2418435037252493e-05, + -0.2223610281944275, + -0.0014950090553611517, + 0.15405474603176117, + -0.05801631137728691, + -0.10004965215921402, + 5.8344110584584996e-05, + 0.3145333528518677, + -6.41081205685623e-05, + -0.15777987241744995, + -0.3469451665878296, + -0.11161395907402039, + 0.15116912126541138, + -5.694388528354466e-05, + 0.36025646328926086, + 0.0005174149991944432, + -0.035536207258701324, + 0.018017007037997246, + 8.045231516007334e-05, + 4.245597301633097e-05, + -6.097873847465962e-05, + 5.805411547044059e-07, + 4.451270797289908e-05, + 0.06907041370868683 + ], + [ + 0.022240441292524338, + 0.2273552566766739, + 1.8164721041102894e-05, + -4.911405630991794e-05, + 0.17769457399845123, + -0.08192356675863266, + -5.586499901255593e-05, + 0.048712145537137985, + -0.02366526797413826, + -0.1359623223543167, + -0.13797058165073395, + -0.2785131335258484, + 8.03629300207831e-05, + -0.0347527340054512, + 1.4013784493727144e-05, + 0.3173225522041321, + 0.4152546226978302, + 0.229934424161911, + 0.1303340345621109, + 6.043950634193607e-06, + -0.06547214835882187, + -0.001481640269048512, + 0.031032465398311615, + 0.02227460779249668, + 8.082203567028046e-05, + -5.6661094276933e-05, + 1.0334381840948481e-05, + 3.9977760025067255e-05, + -7.161345274653286e-05, + -0.0508415587246418 + ], + [ + 0.09429118037223816, + 0.06885714828968048, + -6.395364471245557e-05, + -2.4998951630550437e-05, + -0.027124423533678055, + 0.04007512703537941, + 2.153534660465084e-05, + -0.032153401523828506, + -0.029090870171785355, + 0.03400539606809616, + -0.0627259761095047, + 0.0530974306166172, + 6.317307270364836e-05, + -0.11811400204896927, + 6.312993355095387e-05, + 0.03953547403216362, + -0.04927203431725502, + 0.07272028177976608, + 0.028571121394634247, + 1.953853143277229e-06, + -0.09079796075820923, + -0.0007623485289514065, + -0.009378188289701939, + -0.005076464265584946, + 5.200117448111996e-05, + -6.268163269851357e-05, + 3.219073187210597e-05, + -7.553756586275995e-05, + -3.2382464269176126e-05, + -0.03175865113735199 + ], + [ + 0.09341835975646973, + 0.12944768369197845, + -1.1236212230869569e-05, + 2.044896245934069e-05, + -0.03125547990202904, + 0.008514725603163242, + 7.004891813267022e-05, + -0.015319893136620522, + -0.05173759162425995, + -0.02196366898715496, + -0.12502722442150116, + -0.08452509343624115, + -3.2351006666431203e-05, + -0.19605429470539093, + 7.760905282339081e-05, + 0.06072349473834038, + 0.12377873808145523, + 0.14147983491420746, + 0.09260926395654678, + 1.3261968888400588e-05, + -0.05319736897945404, + -0.0006486521451734006, + -0.026158668100833893, + 0.025145594030618668, + -5.478331877384335e-05, + 5.661814066115767e-05, + -4.016574894194491e-05, + 6.330598989734426e-05, + 3.795664451899938e-05, + -0.04009797051548958 + ], + [ + 0.14615890383720398, + 0.6110277771949768, + 2.88290193566354e-05, + -1.433652698779042e-07, + -0.030410602688789368, + 0.08663705736398697, + -6.474498513853177e-05, + 0.290285587310791, + -0.023639924824237823, + 0.04415418952703476, + 0.12880535423755646, + 0.2596578001976013, + -7.405548967653885e-05, + 0.06234123557806015, + 5.3167372243478894e-05, + -0.07243198156356812, + -0.4852181375026703, + 0.2230229675769806, + 0.552479088306427, + 5.976505781291053e-05, + 0.20912130177021027, + -0.0007544575491920114, + -0.009291751310229301, + -0.028257571160793304, + -3.2483749237144366e-05, + 2.9907767384429462e-05, + 2.5025836293934844e-05, + -2.6458508727955632e-05, + -7.361648749792948e-05, + 0.22839832305908203 + ], + [ + -0.3818569779396057, + -0.1401883214712143, + 8.467176667181775e-05, + -1.9348055502632633e-05, + -0.06677248328924179, + 0.06494887918233871, + -7.852890848880634e-05, + 0.06737758964300156, + 0.06014402210712433, + 0.24596485495567322, + -0.033661797642707825, + 0.15194131433963776, + 7.238896796479821e-05, + -0.19444407522678375, + -1.458264796383446e-05, + 0.541691780090332, + 0.17984287440776825, + -0.4631941616535187, + -0.10273690521717072, + 3.982477664976614e-06, + -0.3709455132484436, + 0.0014663306064903736, + 0.05002697929739952, + -0.07670951634645462, + 2.0141340428381227e-05, + 5.520493505173363e-05, + -6.193004082888365e-05, + 6.666780973318964e-05, + -3.6687841202365234e-05, + 0.17054851353168488 + ], + [ + 0.07255902886390686, + 0.035203978419303894, + 4.652183997677639e-05, + -2.063363353954628e-05, + -0.020438503473997116, + 0.035957034677267075, + -1.4910645404597744e-05, + -0.021891864016652107, + -0.02124514803290367, + 0.04210743308067322, + -0.045299943536520004, + -0.06603967398405075, + -3.110277248197235e-05, + -0.08671364188194275, + -3.3993335819104686e-05, + -0.02041228674352169, + -0.082717165350914, + 0.058610934764146805, + 0.031886789947748184, + -7.019236818450736e-06, + -0.07084228098392487, + 0.00041868281550705433, + -0.007279503624886274, + 0.024585433304309845, + -6.201326323207468e-05, + -8.070007606875151e-05, + -5.263454659143463e-05, + -1.2001257346128114e-05, + -4.1824565414572135e-05, + -0.021190185099840164 + ], + [ + -0.027166031301021576, + -0.01833399571478367, + -8.934030483942479e-05, + -5.684884308720939e-05, + 0.14226768910884857, + -0.016369251534342766, + -6.763099372619763e-05, + 0.26558563113212585, + 0.0009422461153008044, + 0.04254531115293503, + 0.05502381920814514, + 0.13352437317371368, + -7.352827378781512e-05, + 0.2544839382171631, + 5.3895608289167285e-05, + -0.006741601973772049, + -0.18674622476100922, + -0.027773601934313774, + -0.02901112474501133, + -2.5693636416690424e-05, + -0.41145721077919006, + 0.0025886783841997385, + -0.04026828706264496, + -0.04075672850012779, + -1.5906958651612513e-05, + -3.8103684346424416e-05, + 3.0432984203798696e-05, + -4.3665528210112825e-05, + 6.255014159251004e-05, + 0.1999475061893463 + ], + [ + -0.12301916629076004, + -0.09077896922826767, + 7.191824988694862e-05, + -7.069959247019142e-05, + 0.022427506744861603, + -0.04203149303793907, + -7.174642087193206e-05, + 0.07856646180152893, + 0.029309313744306564, + -0.023069821298122406, + 0.06668419390916824, + -0.09253831207752228, + 6.770532490918413e-05, + 0.11860214918851852, + 8.651690586702898e-05, + -0.005733694415539503, + 0.08224576711654663, + -0.07185926288366318, + -0.030827904120087624, + -6.914838013472036e-05, + 0.18779835104942322, + 0.0007796170539222658, + 0.005076499655842781, + 0.0053651100024580956, + -2.962657526950352e-05, + -8.952312055043876e-05, + -4.644940054276958e-05, + -5.567237167269923e-05, + 3.134322105324827e-05, + 0.037619393318891525 + ], + [ + 0.10036638379096985, + 0.060135334730148315, + 5.048117964179255e-05, + 5.025184145779349e-05, + -0.018888581544160843, + 0.03896655514836311, + -6.366754678310826e-05, + -0.0305197574198246, + -0.02635541372001171, + 0.033198174089193344, + -0.05458349362015724, + 0.05759964883327484, + 4.2280807974748313e-05, + -0.11151527613401413, + -3.662232484202832e-05, + 0.020513087511062622, + -0.05141099542379379, + 0.07365782558917999, + 0.024018950760364532, + -5.0344086048426107e-05, + -0.09080703556537628, + -0.0007432051352225244, + -0.007074395194649696, + -0.003914040047675371, + 6.333445344353095e-05, + 2.993214911839459e-05, + 2.4309782020282e-05, + 1.7277367078349926e-05, + -8.570309546485078e-06, + -0.026591796427965164 + ], + [ + -0.6330482959747314, + -0.32221120595932007, + -4.59016928289202e-06, + -4.294110112823546e-05, + 0.10744490474462509, + -0.020747598260641098, + 1.60250419867225e-05, + -0.013621357269585133, + -0.03382641077041626, + -0.16371199488639832, + 0.07638383656740189, + -0.07720658928155899, + -7.601392280776054e-05, + 0.14683467149734497, + 5.96834797761403e-05, + 0.14281515777111053, + 0.049410752952098846, + -0.30182844400405884, + -0.3717176616191864, + 8.130358764901757e-05, + -0.16011588275432587, + 0.0014387584524229169, + -0.06794456392526627, + -0.037415191531181335, + 8.47614646772854e-05, + -7.443592039635405e-05, + 3.582351200748235e-05, + -2.62288776866626e-05, + -4.084203737875214e-06, + 0.0428403876721859 + ], + [ + -0.152847558259964, + -0.15310251712799072, + -8.902082481654361e-05, + -4.52451131423004e-05, + 0.014658422209322453, + 0.28663739562034607, + -4.81323786516441e-06, + -0.1757870763540268, + 0.03198644146323204, + -0.07880756258964539, + 0.06819391995668411, + -0.2767678499221802, + -1.9319708371767774e-05, + 0.030104363337159157, + -2.8547827241709456e-05, + -0.11126092076301575, + 0.8706058859825134, + 0.15536656975746155, + -0.24976618587970734, + 2.7629526812233962e-05, + 0.20156703889369965, + -0.0035819450858980417, + 0.09473411738872528, + -0.03741946071386337, + -3.139999171253294e-05, + 8.786057151155546e-05, + 7.64898068155162e-05, + -4.1786275687627494e-05, + 5.072505882708356e-05, + 0.17339420318603516 + ] + ], + "b1": [ + -0.2202903926372528, + -0.19966638088226318, + -0.29291394352912903, + 0.2784007787704468, + 1.114177942276001, + 0.42879536747932434, + 0.16518251597881317, + 0.5123059153556824, + -1.690555453300476, + 0.48297303915023804, + -0.07636553049087524, + -0.5670318603515625, + -0.002066113753244281, + 0.023889515548944473, + 2.8910934925079346, + 0.16293974220752716 + ], + "w2": [ + -0.28682073950767517, + -0.7433742880821228, + 0.23186810314655304, + 0.31032678484916687, + -0.7010297179222107, + 0.7156338691711426, + 0.2737733721733093, + 0.404502272605896, + 1.053452968597412, + -0.8269684314727783, + 0.22637619078159332, + -0.566148579120636, + -0.34994953870773315, + 0.2673782706260681, + -0.8537940382957458, + 0.7767978310585022 + ], + "b2": -0.06936526298522949, + "mu": [ + -5.207204418747822, + -0.8536666018084688, + 0.0, + 0.0, + 0.0635267134105244, + 0.9945879062842657, + 0.0, + 0.032019877875803515, + 0.0027261661653605884, + 0.06369772752421862, + 0.019103282465017555, + 0.005905016749323489, + 0.0, + 0.4640593720764131, + 0.0, + 0.8441457845020974, + 0.2873592521701261, + -3.607677970246196, + -0.6229429937976536, + 0.0, + 0.7253767015522542, + 0.00035208788113513134, + 0.0016799621757019123, + 0.0005029826873359019, + 0.0, + 0.0, + 1.0, + 0.0, + 3.0, + 0.007786171999959761 + ], + "sd": [ + 1.767742572327731, + 0.5699703093068407, + 1e-06, + 1e-06, + 0.24390891314296623, + 0.07336858792101188, + 1e-06, + 0.1760538480219903, + 0.05214248236672571, + 0.2442146913286182, + 0.13688908225735466, + 0.07661788799808258, + 1e-06, + 0.2879000830067093, + 1e-06, + 0.3627180784639491, + 0.24511581048928208, + 1.9138403327338624, + 0.5696832006509231, + 1e-06, + 0.19638058877378506, + 0.018761701353063934, + 0.04095389858839651, + 0.022422634546833062, + 1e-06, + 1e-06, + 1e-06, + 1e-06, + 1e-06, + 0.08789609386499421 + ], + "val_rerank_cv2": 55.79 +} \ No newline at end of file diff --git a/models/hi/params.json.gz b/models/hi/params.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..30e0f89d51fc518e6a210e488cb7a27f04e55f27 --- /dev/null +++ b/models/hi/params.json.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e42f8aec0204bca50891244478d94ac3ba1801f8cc774c475b84c5581377c73 +size 6367 diff --git a/models/hi/sentence/nlm.q4 b/models/hi/sentence/nlm.q4 new file mode 100644 index 0000000000000000000000000000000000000000..8a6d3c127175dc7b9de3e153194c0a056df22a5d --- /dev/null +++ b/models/hi/sentence/nlm.q4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c90f300c0114354fcf0c5a12300b9ba98ce71b4580b54f0b870a9395e869227 +size 1862360 diff --git a/models/hi/sentence/nlm.q8 b/models/hi/sentence/nlm.q8 new file mode 100644 index 0000000000000000000000000000000000000000..252dd69db6ab624bf6d79744fdcf08534b1f8b34 --- /dev/null +++ b/models/hi/sentence/nlm.q8 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8f3850506d93f3b0531beb37362cf1ad7c02a9e1d8c81ebc128efee9e44c17e +size 3239640 diff --git a/models/hi/sentence/nlm.q8.gz b/models/hi/sentence/nlm.q8.gz new file mode 100644 index 0000000000000000000000000000000000000000..19b089846a5d79b607a0c76b195defb9bb067c18 --- /dev/null +++ b/models/hi/sentence/nlm.q8.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7face18513d342c59b005f3ea57a0e86310dc863778a828e7743cea0d5c68526 +size 2921848 diff --git a/models/hi/xlit.q4 b/models/hi/xlit.q4 new file mode 100644 index 0000000000000000000000000000000000000000..daf7a81f7aee37515a8e315e6973bdcaf5c6d177 --- /dev/null +++ b/models/hi/xlit.q4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:652eafe0c1e2b46fec85663825621284f0a5911f2d7cc6e13b1182ce47cd9803 +size 588891 diff --git a/models/hi/xlit.q4.gz b/models/hi/xlit.q4.gz new file mode 100644 index 0000000000000000000000000000000000000000..20b3867ce80694d6ba052cce77cc48b6fb3b62e3 --- /dev/null +++ b/models/hi/xlit.q4.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:191eb28f75f29c45ec29f044031a6ae5aedd6a80edc3f3a9d1ae78a53c04fb9d +size 543838 diff --git a/models/kn/dict.tsv.gz b/models/kn/dict.tsv.gz new file mode 100644 index 0000000000000000000000000000000000000000..165eeeb0712d80c391fbc9de16915eb51365cca6 --- /dev/null +++ b/models/kn/dict.tsv.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97035264d30eddb5810467dc5bbf088614221ea6ba2c3fff349c6bbd196bb644 +size 2684083 diff --git a/models/kn/inventory.json b/models/kn/inventory.json new file mode 100644 index 0000000000000000000000000000000000000000..92e1c17ea640256882198a9ffdd17a36d01d4832 --- /dev/null +++ b/models/kn/inventory.json @@ -0,0 +1,8 @@ +{ + "parameters": 988928, + "model_raw": 588904, + "model_gz9": 539543, + "dict_raw": 16786145, + "dict_gz9": 2684083, + "dict_words": 500000 +} diff --git a/models/kn/params.json b/models/kn/params.json new file mode 100644 index 0000000000000000000000000000000000000000..85397fcd68394a7982b7b2e5ac0b47610f5d08aa --- /dev/null +++ b/models/kn/params.json @@ -0,0 +1,58 @@ +{ + "format": "aksharalp-multiindic/1", + "language": "kn", + "beam": 8, + "nbest": 8, + "model": "xlit.q4", + "dictionary": "dict.tsv.gz", + "dictionary_words": 500000, + "dictionary_total": 23038668, + "unk": -16.952684623026045, + "reranker": { + "type": "linear", + "features": [ + "sc", + "scn", + "inlex", + "uni", + "free0", + "rank", + "sc_rel", + "uni_rel" + ], + "w": [ + 0.7762413620948792, + 0.47590601444244385, + 1.8370015621185303, + 0.37460821866989136, + -0.14645907282829285, + -0.39526867866516113, + 0.7561540603637695, + 0.40543532371520996 + ], + "mu": [ + -5.910224747519752, + -0.7570656360672512, + 0.21765999600123598, + -2.8589205075172495, + 0.12688805278368503, + 0.43655597360815745, + -4.8503386403114055, + -0.3713378327997356 + ], + "sd": [ + 2.385781122067686, + 0.5672953341562836, + 0.4126559674266211, + 5.516374375872727, + 0.3328485249185093, + 0.2863515606523184, + 2.4442090407344486, + 1.2445700746967023 + ], + "fit_split": "all Aksharantar VALID", + "fit_n": 6981, + "l2": 0.001, + "seed": 0 + } +} diff --git a/models/kn/training_report.json b/models/kn/training_report.json new file mode 100644 index 0000000000000000000000000000000000000000..9b79b941047baa27add63e9656e4490751f78509 --- /dev/null +++ b/models/kn/training_report.json @@ -0,0 +1,29 @@ +{ + "pairs": 3084994, + "counts": { + "aks-Dakshina": 43373, + "aks-Wikidata": 178, + "aks-Samanantar": 36482, + "aks-IndicCorp": 2826694, + "ie": 160660, + "dup_ie": 83571, + "heldout_drop_ie": 1642, + "ie_words": 90000, + "dup_ei": 96771, + "ei": 17607, + "heldout_drop_ei": 100, + "ei_words": 60000 + }, + "unique_natives": 2904007, + "unique_romans": 2864227, + "overlap_valid_test_natives": 0, + "overlap_valid_test_romans": 0, + "heldout_natives_total": 11062, + "heldout_romans_total": 18017, + "margins": { + "ie": 0.6, + "ei": 0.35, + "maxv_ie": 4, + "maxv_ei": 2 + } +} \ No newline at end of file diff --git a/models/kn/xlit.q4 b/models/kn/xlit.q4 new file mode 100644 index 0000000000000000000000000000000000000000..88876381536300aa0a88fa0d2ae51c7a7c877f27 --- /dev/null +++ b/models/kn/xlit.q4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3788fd545c297febb7dbbdbdd780e831042395b4ec99260b95c963607afc8664 +size 588904 diff --git a/models/kn/xlit.q4.gz b/models/kn/xlit.q4.gz new file mode 100644 index 0000000000000000000000000000000000000000..7a752439570f4978e3444bccf8142083b2da5ca4 --- /dev/null +++ b/models/kn/xlit.q4.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bedc5a698dc5c83a964f03586bc0ded67e88fa0b644cdd82fafc21f1e50925d9 +size 539543 diff --git a/models/ta/dict.tsv.gz b/models/ta/dict.tsv.gz new file mode 100644 index 0000000000000000000000000000000000000000..ae77671c9ec167586df5599eaa761aad6689a0f0 --- /dev/null +++ b/models/ta/dict.tsv.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d14f514020e7737144c22991e9324a29734b5ab203bef3b68ec6d9f51e7cddb +size 2588634 diff --git a/models/ta/inventory.json b/models/ta/inventory.json new file mode 100644 index 0000000000000000000000000000000000000000..d1c7ebf1a03ea8113b99bc49d7fe00a208c5a8b8 --- /dev/null +++ b/models/ta/inventory.json @@ -0,0 +1,8 @@ +{ + "parameters": 988928, + "model_raw": 588904, + "model_gz9": 540818, + "dict_raw": 18105290, + "dict_gz9": 2588634, + "dict_words": 500000 +} diff --git a/models/ta/params.json b/models/ta/params.json new file mode 100644 index 0000000000000000000000000000000000000000..b5c315b71943c7ceb2f06a40334e2f260dcef676 --- /dev/null +++ b/models/ta/params.json @@ -0,0 +1,58 @@ +{ + "format": "aksharalp-multiindic/1", + "language": "ta", + "beam": 8, + "nbest": 8, + "model": "xlit.q4", + "dictionary": "dict.tsv.gz", + "dictionary_words": 500000, + "dictionary_total": 20232595, + "unk": -16.82280552518196, + "reranker": { + "type": "linear", + "features": [ + "sc", + "scn", + "inlex", + "uni", + "free0", + "rank", + "sc_rel", + "uni_rel" + ], + "w": [ + 0.9623245596885681, + 0.5414344072341919, + 1.828786015510559, + 0.003193225245922804, + -0.12186560034751892, + -0.003971157129853964, + 0.9330659508705139, + 0.5227358937263489 + ], + "mu": [ + -6.01308456526665, + -0.7381095081752549, + 0.22912272701362366, + -2.9371551977749273, + 0.1254346462976686, + 0.43728267685116573, + -4.8824671233512555, + -0.38838364800108277 + ], + "sd": [ + 2.449121527790252, + 0.5435378258591739, + 0.42026937018709404, + 5.483689004938199, + 0.3312121045929875, + 0.28639834850311835, + 2.5225466225846818, + 1.2477123699412511 + ], + "fit_split": "all Aksharantar VALID", + "fit_n": 8802, + "l2": 0.001, + "seed": 0 + } +} diff --git a/models/ta/training_report.json b/models/ta/training_report.json new file mode 100644 index 0000000000000000000000000000000000000000..c5c5ed1f1ec7fdeedfe7dac1df6c015b30d4c1c3 --- /dev/null +++ b/models/ta/training_report.json @@ -0,0 +1,32 @@ +{ + "pairs": 3452431, + "counts": { + "aks-Dakshina": 60210, + "aks-AK-Freq": 776, + "aks-Existing": 1545, + "aks-Wikidata": 273, + "aks-Samanantar": 34447, + "aks-IndicCorp": 3133649, + "ie": 204261, + "heldout_drop_ie": 1379, + "dup_ie": 90268, + "bad_roman_ie": 10, + "ie_words": 90000, + "dup_ei": 96489, + "ei": 17270, + "heldout_drop_ei": 133, + "ei_words": 60000 + }, + "unique_natives": 3200785, + "unique_romans": 3322500, + "overlap_valid_test_natives": 0, + "overlap_valid_test_romans": 0, + "heldout_natives_total": 10964, + "heldout_romans_total": 20104, + "margins": { + "ie": 0.6, + "ei": 0.35, + "maxv_ie": 4, + "maxv_ei": 2 + } +} \ No newline at end of file diff --git a/models/ta/xlit.q4 b/models/ta/xlit.q4 new file mode 100644 index 0000000000000000000000000000000000000000..afb1ab62499c49fdd51f44392258396b25bc83b7 --- /dev/null +++ b/models/ta/xlit.q4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5dd185a033db79835fede66ab41a25f5897f24ad5d7c8ecc9864ca6ec167fdc0 +size 588904 diff --git a/models/ta/xlit.q4.gz b/models/ta/xlit.q4.gz new file mode 100644 index 0000000000000000000000000000000000000000..062286784c2f07426275d8715b4f697bdaefd084 --- /dev/null +++ b/models/ta/xlit.q4.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c15be889468520e6d5ee8dfbc4c77cad781a79f454d4681026fc082ae217afc2 +size 540818 diff --git a/models/te/dict.tsv.gz b/models/te/dict.tsv.gz new file mode 100644 index 0000000000000000000000000000000000000000..792aa78806f1f5f7169e3ea3a385e32ba9ce452a --- /dev/null +++ b/models/te/dict.tsv.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20612a52ccf39c816d3068e78e9e9a3aa9aed9156f10de45ebbd543363943640 +size 2680527 diff --git a/models/te/inventory.json b/models/te/inventory.json new file mode 100644 index 0000000000000000000000000000000000000000..b2d26a986c963d604b53110d9e00caa0de2c429f --- /dev/null +++ b/models/te/inventory.json @@ -0,0 +1,8 @@ +{ + "parameters": 988928, + "model_raw": 588904, + "model_gz9": 541242, + "dict_raw": 15989177, + "dict_gz9": 2680527, + "dict_words": 500000 +} diff --git a/models/te/params.json b/models/te/params.json new file mode 100644 index 0000000000000000000000000000000000000000..8dc45c28c6a29132d2c3a3cdb2842702dc524afa --- /dev/null +++ b/models/te/params.json @@ -0,0 +1,58 @@ +{ + "format": "aksharalp-multiindic/1", + "language": "te", + "beam": 8, + "nbest": 8, + "model": "xlit.q4", + "dictionary": "dict.tsv.gz", + "dictionary_words": 500000, + "dictionary_total": 24117810, + "unk": -16.998461171144285, + "reranker": { + "type": "linear", + "features": [ + "sc", + "scn", + "inlex", + "uni", + "free0", + "rank", + "sc_rel", + "uni_rel" + ], + "w": [ + 0.8893349170684814, + 0.42266765236854553, + 1.647674560546875, + 0.01816709153354168, + -0.09264160692691803, + -0.17859779298305511, + 0.8579440116882324, + 0.4983164668083191 + ], + "mu": [ + -5.577303872867701, + -0.7360878454967777, + 0.22874146800712822, + -3.057939543170047, + 0.12881207760330857, + 0.43559396119834565, + -4.512450547449157, + -0.4069890521202824 + ], + "sd": [ + 2.345458571867553, + 0.5172883363068768, + 0.4200233908567531, + 5.716765533673338, + 0.33499283014943054, + 0.28628677366288935, + 2.4278868542564216, + 1.326799761380314 + ], + "fit_split": "all Aksharantar VALID", + "fit_n": 7662, + "l2": 0.001, + "seed": 0 + } +} diff --git a/models/te/training_report.json b/models/te/training_report.json new file mode 100644 index 0000000000000000000000000000000000000000..80d22974aea3bceb89cfcdfbdc55d50593ca5ba2 --- /dev/null +++ b/models/te/training_report.json @@ -0,0 +1,30 @@ +{ + "pairs": 2875167, + "counts": { + "aks-Dakshina": 50927, + "aks-Existing": 24043, + "aks-Wikidata": 26, + "aks-Samanantar": 42413, + "aks-IndicCorp": 2312138, + "ie": 419006, + "dup_ie": 159572, + "heldout_drop_ie": 3317, + "ie_words": 185770, + "dup_ei": 144002, + "ei": 26614, + "heldout_drop_ei": 153, + "ei_words": 90000 + }, + "unique_natives": 2437907, + "unique_romans": 2637382, + "overlap_valid_test_natives": 0, + "overlap_valid_test_romans": 0, + "heldout_natives_total": 11204, + "heldout_romans_total": 17503, + "margins": { + "ie": 0.6, + "ei": 0.35, + "maxv_ie": 4, + "maxv_ei": 2 + } +} \ No newline at end of file diff --git a/models/te/xlit.q4 b/models/te/xlit.q4 new file mode 100644 index 0000000000000000000000000000000000000000..88ab1d67a8208c6f392ed683d5b26f852c451cf5 --- /dev/null +++ b/models/te/xlit.q4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aea094e46a3c61eb9487d1aaba3824d3d9dc60f3f1cf6b6f99cadf04fd4318f9 +size 588904 diff --git a/models/te/xlit.q4.gz b/models/te/xlit.q4.gz new file mode 100644 index 0000000000000000000000000000000000000000..c2b10d676ddc2a88c4f997f6e4ec81132a03baa9 --- /dev/null +++ b/models/te/xlit.q4.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3513df30b5d3bfa07085baa19f0c9c1076e85268a5df88fcc4b492630b1b4eb2 +size 541242 diff --git a/multiindic.mjs b/multiindic.mjs new file mode 100644 index 0000000000000000000000000000000000000000..ec76560ea765ac0359207bdcd568f173290df1d7 --- /dev/null +++ b/multiindic.mjs @@ -0,0 +1,69 @@ +/* AksharaLP MultiIndic runtime. Apache-2.0 code; selected non-Hindi weights are CC-BY-NC-4.0. See NOTICE. */ +import {BhashaCore, XlitModel, parseWeights, TOKEN_RE} from './aksharalp.mjs'; +const norm=s=>s.replace(/[\u200c\u200d]/g,'').normalize('NFC'); +const protectedTokens=/(https?:\/\/\S+|www\.\S+|[\w.+-]+@[\w.-]+\.[A-Za-z]+|[@#][\p{L}\p{N}_]+)|[A-Za-z]+/gu; +async function decompress(b) { + if (typeof process !== 'undefined' && process.versions?.node) {const {gunzipSync}=await import('node:zlib');const x=gunzipSync(new Uint8Array(b));return x.buffer.slice(x.byteOffset,x.byteOffset+x.byteLength);} + if (!globalThis.DecompressionStream) throw new Error('This browser needs DecompressionStream (Chrome/Edge 80+, Firefox 113+, Safari 16.4+).'); + return new Response(new Blob([b]).stream().pipeThrough(new DecompressionStream('gzip'))).arrayBuffer(); +} +function rerank(cands,lex,p) { + const rr=p.reranker, unis=cands.map(([s])=>lex.get(s)), mu=Math.max(...unis.filter(x=>x!==undefined),-Infinity), msc=Math.max(...cands.map(c=>c[1])); + return cands.map(([text,sc],i)=>{const u=unis[i],inside=+(u!==undefined),f=[sc,sc/(text.length+1),inside,inside?u:0,+(i===0),i/cands.length,sc-msc,inside?u-mu:0]; + const score=f.reduce((a,x,j)=>a+(x-rr.mu[j])/rr.sd[j]*rr.w[j],0);return {text,score};}).sort((a,b)=>b.score-a.score); +} +export class MultiIndic { + static async load(base='./', {language='hi',sentence=false,fetch:fetchImpl=globalThis.fetch,onProgress}={}) { + const e=new MultiIndic();e.base=base.replace(/\/$/,'');e.fetch=fetchImpl;e.onProgress=onProgress;e.cache=new Map();e.pending=new Map();e.sentenceMode=false;await e.setLanguage(language);if(sentence)await e.setSentence(true);return e; + } + async bytes(path) {const r=await this.fetch(`${this.base}/${path}`);if(!r.ok)throw new Error(`Could not load ${path}: HTTP ${r.status}`);let b=await r.arrayBuffer();this.onProgress?.(path,b.byteLength);return path.endsWith('.gz')?decompress(b):b;} + async compressedFetch(url) {const path=url.slice(this.base.length+1),b=await this.bytes(path+'.gz');return new Response(b);} + async setLanguage(language) { + if(!['hi','bn','te','kn','ta'].includes(language))throw new Error('Unsupported language'); + if(!this.cache.has(language)) { + if(!this.pending.has(language))this.pending.set(language,(async()=>{ + if(language==='hi'){const hi=await BhashaCore.load(`${this.base}/models/hi`,{fetch:u=>this.compressedFetch(u),cache:false,dict:true});if(!hi.hasDict)throw new Error(hi.dictError||'Hindi dictionary failed to load');return {hi};} + const prefix=`models/${language}/`,[mb,pb,lb]=await Promise.all([this.bytes(prefix+'xlit.q4.gz'),this.bytes(prefix+'params.json'),this.bytes(prefix+'dict.tsv.gz')]); + const p=JSON.parse(new TextDecoder().decode(pb)),lex=new Map(); + for(const line of new TextDecoder().decode(lb).trim().split('\n')){const j=line.lastIndexOf('\t');lex.set(line.slice(0,j),Math.log(Number(line.slice(j+1))/p.dictionary_total));} + return {model:new XlitModel(parseWeights(mb)),p,lex,memo:new Map()}; + })()); + this.cache.set(language,await this.pending.get(language)); + } + this.language=language;this.active=this.cache.get(language);this.sentenceMode=false;if(this.active.hi)this.active.hi.sentenceMode=false;return this; + } + async setSentence(enabled=true) { + if(this.language!=='hi'&&enabled)throw new Error('Sentence mode is Hindi-only'); + if(enabled&&!this.active.hi.nlm) {const [{attachSentence},bytes]=await Promise.all([import('./sent.mjs'),this.bytes('models/hi/sentence/nlm.q8.gz')]);attachSentence(this.active.hi,bytes);} + this.sentenceMode=enabled;if(this.active.hi)this.active.hi.sentenceMode=enabled;return this; + } + word(word,k=8) { + if(!/^[A-Za-z]+$/.test(word))return [{text:word,score:0}]; + const a=this.active;if(a.hi)return a.hi.word(word,k); + word=word.toLowerCase();if(!a.memo.has(word)){ + const ids=[...word.slice(0,31)].map(c=>c.charCodeAt(0)-97+4).concat([2]),enc=a.model.encode(ids),seen=new Set(),cands=[]; + for(const [s,sc] of a.model.beam(enc,ids.length,{beam:8,nbest:8})){const t=norm(s);if(t&&!seen.has(t)){seen.add(t);cands.push([t,sc]);}} + if(a.memo.size>1000)a.memo.clear();a.memo.set(word,rerank(cands,a.lex,a.p)); + } + return a.memo.get(word).slice(0,k); + } + transliterate(text,{k=8,overrides={}}={}) { + const t0=performance.now(); + if(this.active.hi){ + // Plain reference conversion, without the older UI's brand/acronym/punctuation heuristics. + const h=this.active.hi,spans=[...text.matchAll(TOKEN_RE)].map(m=>[m.index,m.index+m[0].length,m[0]]); + const slots=spans.map(([a,b,t])=>overrides[a]!==undefined?['fix',overrides[a]]:/^[A-Za-z]+$/.test(t)?['x',t]:['fix',t]); + const r=h.decodeSlots(slots,this.sentenceMode);if(r.out.at(-1)==='.')r.out[r.out.length-1]='\u0964'; + let output='',pos=0;const tokens=[]; + spans.forEach(([a,b,t],i)=>{output+=text.slice(pos,a)+r.out[i];pos=b;tokens.push({text:t,start:a,end:b,output:r.out[i],candidates:slots[i][0]==='x'?h.word(t,k):[]});}); + return {output:output+text.slice(pos),tokens,ms:performance.now()-t0,sentence:null,guard:r.guard||{ok:true,words:0,outside:0}}; + } + const tokens=[];const output=text.replace(protectedTokens,(word,protectedWord,offset)=>{ + if(protectedWord)return word;const candidates=this.word(word,k),out=overrides[offset]??candidates[0]?.text??word; + tokens.push({text:word,start:offset,end:offset+word.length,output:out,candidates});return out;}); + return {output,tokens,ms:performance.now()-t0,sentence:null}; + } + clearCache(){if(this.active.memo)this.active.memo.clear();if(this.active.hi)this.active.hi.cache.clear();} +} +export async function loadFromDir(dir,opt={}) {const fs=await import('node:fs/promises');return MultiIndic.load(dir,{...opt,fetch:async u=>{try{return new Response(await fs.readFile(u));}catch{return new Response(null,{status:404});}}});} +export default MultiIndic; diff --git a/nlm.mjs b/nlm.mjs new file mode 100644 index 0000000000000000000000000000000000000000..e39eaff7aa734fed3e1c1a68fb99f14f1d95e8ea --- /dev/null +++ b/nlm.mjs @@ -0,0 +1,126 @@ +// AksharaLP v3 tiny sentence LM (JS port of bhc/nlm.py): Viterbi unigram-piece tokenizer + 2-layer LSTMP + tied embedding. +// File: u32 header length | JSON {cfg, bits, G, tensors:[[name, shape, kind, off, len]], vocab:[[piece, score]]} | data (4-byte aligned). +const SP = "\u2581"; +function f16(h) { + const s = h & 0x8000 ? -1 : 1, e = (h >> 10) & 0x1f, f = h & 0x3ff; + if (e === 0) return s * f * 2 ** -24; if (e === 31) return f ? NaN : s * Infinity; return s * (1 + f / 1024) * 2 ** (e - 15); +} +function dq(data, off, [r, c], kind, G) { + const out = new Float32Array(r * c), ng = Math.ceil(c / G), dv = new DataView(data.buffer, data.byteOffset); + let q; + if (kind === "q8") { q = new Int8Array(data.buffer, data.byteOffset + off, r * c); off += r * c; } + else { const nb = (r * c + 1) >> 1; q = new Int8Array(nb * 2); for (let i = 0; i < nb; i++) { const u = data[off + i]; q[2 * i] = (u >> 4) - 8; q[2 * i + 1] = (u & 15) - 8; } off += nb; } + off += (4 - (off % 4)) % 4; + for (let i = 0; i < r; i++) for (let g = 0; g < ng; g++) { + const sc = f16(dv.getUint16(off + 2 * (i * ng + g), true)), e = Math.min(c, (g + 1) * G); + for (let j = g * G; j < e; j++) out[i * c + j] = q[i * c + j] * sc; + } + return out; +} +export class Tok { + constructor(vocab) { + this.score = new Map(); this.id = new Map(); this.maxlen = 0; let mn = Infinity; + vocab.forEach(([p, s], i) => { this.score.set(p, s); this.id.set(p, i); this.maxlen = Math.max(this.maxlen, [...p].length); mn = Math.min(mn, s); }); + this.unk = this.id.get(""); this.eos = this.id.get(""); this.unkScore = mn - 10; this.cache = new Map(); + } + encodeWord(w) { + let r = this.cache.get(w); if (r) return r; + if (this.id.has(w) && w.startsWith("<")) { r = [this.id.get(w)]; this.cache.set(w, r); return r; } + const s = [...(SP + w)], n = s.length, best = new Float64Array(n + 1).fill(-1e30), back = new Array(n + 1).fill(null); best[0] = 0; + for (let i = 0; i < n; i++) { + if (best[i] <= -1e29) continue; let hit = false; + for (let j = i + 1; j <= Math.min(n, i + this.maxlen); j++) { + const p = s.slice(i, j).join(""), sc = this.score.get(p); + if (sc !== undefined && p !== "" && p !== "" && p !== "" && p !== "") { + hit = true; if (best[i] + sc > best[j]) { best[j] = best[i] + sc; back[j] = [i, this.id.get(p)]; } + } + } + if (!hit || back[i + 1] === null) if (best[i] + this.unkScore > best[i + 1]) { best[i + 1] = best[i] + this.unkScore; back[i + 1] = [i, this.unk]; } + } + const out = []; for (let j = n; j > 0;) { const [i, pid] = back[j]; out.push(pid); j = i; } + r = out.reverse(); if (this.cache.size < 200000) this.cache.set(w, r); return r; + } +} +export class NLM { + constructor(buf, norm) { + const b = Uint8Array.from(buf instanceof ArrayBuffer ? new Uint8Array(buf) : buf), n = new DataView(b.buffer, b.byteOffset).getUint32(0, true); + const meta = JSON.parse(new TextDecoder().decode(b.subarray(4, 4 + n))), data = b.subarray(4 + n), W = {}; + for (const [k, shape, kind, off] of meta.tensors) { + const cnt = shape.reduce((a, x) => a * x, 1); + W[k] = kind === "f32" ? new Float32Array(Uint8Array.prototype.slice.call(data, off, off + 4 * cnt).buffer) : dq(data, off, shape, kind, meta.G); + } + this.cfg = meta.cfg; this.W = W; this.tok = new Tok(meta.vocab); this.L = meta.cfg.layers; this.norm = norm || meta.cfg.norm || "exact"; + this.E = meta.cfg.emb; this.H = meta.cfg.hid; this.V = meta.vocab.length; this.emb = W["emb.weight"]; this.ob = W["out_bias"]; this._init = null; + } + static async load(bytes, norm) { return new NLM(bytes, norm); } + // state = {h: [Float32Array(E) x L], c: [Float32Array(H) x L]} + _gates(l, x, h, out) { // out[4H] = Wih x + Whh h + b_ih + b_hh (x may be null -> skip input part) + const W = this.W, E = this.E, H4 = 4 * this.H, wih = W[`lstm.weight_ih_l${l}`], whh = W[`lstm.weight_hh_l${l}`], bi = W[`lstm.bias_ih_l${l}`], bh = W[`lstm.bias_hh_l${l}`]; + for (let r = 0, o = 0; r < H4; r++, o += E) { + let a0 = 0, a1 = 0, a2 = 0, a3 = 0, i = 0; + if (x && h) for (; i + 3 < E; i += 4) { + a0 += wih[o + i] * x[i] + whh[o + i] * h[i]; a1 += wih[o + i + 1] * x[i + 1] + whh[o + i + 1] * h[i + 1]; + a2 += wih[o + i + 2] * x[i + 2] + whh[o + i + 2] * h[i + 2]; a3 += wih[o + i + 3] * x[i + 3] + whh[o + i + 3] * h[i + 3]; + } else if (x) for (; i + 3 < E; i += 4) { a0 += wih[o + i] * x[i]; a1 += wih[o + i + 1] * x[i + 1]; a2 += wih[o + i + 2] * x[i + 2]; a3 += wih[o + i + 3] * x[i + 3]; } + else for (; i + 3 < E; i += 4) { a0 += whh[o + i] * h[i]; a1 += whh[o + i + 1] * h[i + 1]; a2 += whh[o + i + 2] * h[i + 2]; a3 += whh[o + i + 3] * h[i + 3]; } + let a = out[r] + ((a0 + a1) + (a2 + a3)); + for (; i < E; i++) { if (x) a += wih[o + i] * x[i]; if (h) a += whh[o + i] * h[i]; } + if (x) a += bi[r] + bh[r]; + out[r] = a; + } + return out; + } + _cell(l, g, c) { // -> {h, c} + const H = this.H, E = this.E, whr = this.W[`lstm.weight_hr_l${l}`], cn = new Float32Array(H), m = new Float32Array(H), h = new Float32Array(E); + const sg = (v) => 1 / (1 + Math.exp(-v)); + for (let j = 0; j < H; j++) { cn[j] = sg(g[H + j]) * c[j] + sg(g[j]) * Math.tanh(g[2 * H + j]); m[j] = sg(g[3 * H + j]) * Math.tanh(cn[j]); } + for (let r = 0, o = 0; r < E; r++, o += H) { + let a0 = 0, a1 = 0, a2 = 0, a3 = 0, j = 0; + for (; j + 3 < H; j += 4) { a0 += whr[o + j] * m[j]; a1 += whr[o + j + 1] * m[j + 1]; a2 += whr[o + j + 2] * m[j + 2]; a3 += whr[o + j + 3] * m[j + 3]; } + let s = (a0 + a1) + (a2 + a3); for (; j < H; j++) s += whr[o + j] * m[j]; h[r] = s; + } + return { h, c: cn }; + } + _xg0(id) { // cached layer-0 input projection per piece + this._xc = this._xc || new Map(); let g = this._xc.get(id); if (g) return g; + g = this._gates(0, this.emb.subarray(id * this.E, (id + 1) * this.E), null, new Float32Array(4 * this.H)); + if (this._xc.size > 4000) this._xc.clear(); this._xc.set(id, g); return g; + } + step(st, id, hh0) { // hh0: optional precomputed Whh0 h0 + const hs = [], cs = []; let x = null; + for (let l = 0; l < this.L; l++) { + let g; + if (l === 0) { g = Float32Array.from(this._xg0(id)); const r = hh0 || this._gates(0, null, st.h[0], new Float32Array(4 * this.H)); for (let i = 0; i < g.length; i++) g[i] += r[i]; } + else g = this._gates(l, x, st.h[l], new Float32Array(4 * this.H)); + const o = this._cell(l, g, st.c[l]); hs.push(o.h); cs.push(o.c); x = o.h; + } + return { h: hs, c: cs }; + } + logZ(st) { + if (this.norm !== "exact") return 0; + const top = st.h[this.L - 1], E = this.E, lg = new Float64Array(this.V); let m = -Infinity; + for (let v = 0; v < this.V; v++) { let s = this.ob[v]; const o = v * E; for (let i = 0; i < E; i++) s += this.emb[o + i] * top[i]; lg[v] = s; if (s > m) m = s; } + let z = 0; for (let v = 0; v < this.V; v++) z += Math.exp(lg[v] - m); return m + Math.log(z); + } + logit(st, id) { const top = st.h[this.L - 1], E = this.E, o = id * E; let s = this.ob[id]; for (let i = 0; i < E; i++) s += this.emb[o + i] * top[i]; return s; } + initState() { + if (!this._init) { const z = { h: Array.from({ length: this.L }, () => new Float32Array(this.E)), c: Array.from({ length: this.L }, () => new Float32Array(this.H)) }; this._init = this.step(z, this.tok.eos); } + return this._init; + } + /** states[B], words[K] -> {lp[B][K], ns[B][K]} (same math as bhc/nlm.py score_words) */ + scoreWords(states, words) { + const seqs = words.map((w) => this.tok.encodeWord(w)), lp = [], ns = []; + for (const st of states) { + const z = this.logZ(st), hh0 = this._gates(0, null, st.h[0], new Float32Array(4 * this.H)), row = [], nrow = []; + for (const q of seqs) { + let s = this.logit(st, q[0]) - z, cur = this.step(st, q[0], hh0); + for (let t = 1; t < q.length; t++) { s += this.logit(cur, q[t]) - this.logZ(cur); cur = this.step(cur, q[t]); } + row.push(s); nrow.push(cur); + } + lp.push(row); ns.push(nrow); + } + return { lp, ns }; + } + eosLogp(st) { return this.logit(st, this.tok.eos) - this.logZ(st); } + sentLogp(words) { let st = this.initState(), tot = 0; for (const w of words) { const { lp, ns } = this.scoreWords([st], [w]); tot += lp[0][0]; st = ns[0][0]; } return tot + this.eosLogp(st); } +} diff --git a/python/bhc/__init__.py b/python/bhc/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/python/bhc/agree.py b/python/bhc/agree.py new file mode 100644 index 0000000000000000000000000000000000000000..81f1617fbe5079b719a5c58061df0b9e74cf08e1 --- /dev/null +++ b/python/bhc/agree.py @@ -0,0 +1,55 @@ +"""AksharaLP v3 agreement / grammar-consistency features (identical rules in release/space/js/aksharalp.mjs). +Scored incrementally when word i is appended to a hypothesis; returns (pos, neg) counts: + 1. participle -> auxiliary agreement (gender/number): रहा था / रही थी / रहे थे ok; रहा थे / रहे हूँ / रही था violations. + 2. subject pronoun -> auxiliary person/number in the same clause: मैं..हूँ, हम..हैं, तुम..हो, आप..हैं ok; मैं..है, तुम..हूँ violations. +Only uses the words already chosen from per-token candidate lists; it never adds words.""" +AUX = {"था": "ms", "थी": "fs", "थे": "mp", "थीं": "fp", "हूँ": "1s", "हूं": "1s", "है": "3s", "हैं": "pl", "हो": "2p"} +PART = {} +for w, g in (("रहा", "ms"), ("रही", "fs"), ("रहे", "mp"), ("चुका", "ms"), ("चुकी", "fs"), ("चुके", "mp"), ("सका", "ms"), ("सकी", "fs"), ("सके", "mp"), + ("गया", "ms"), ("गई", "fs"), ("गयी", "fs"), ("गए", "mp"), ("गये", "mp"), ("हुआ", "ms"), ("हुई", "fs"), ("हुए", "mp"), + ("लिया", "ms"), ("ली", "fs"), ("लिए", "mp"), ("लिये", "mp"), ("दिया", "ms"), ("दी", "fs"), ("दिए", "mp"), ("दिये", "mp"), + ("किया", "ms"), ("की", "fs"), ("किए", "mp"), ("किये", "mp"), ("आया", "ms"), ("आई", "fs"), ("आए", "mp"), ("आये", "mp")): + PART[w] = g +SUBJ = {"मैं": "1s", "मै": "1s", "हम": "pl", "तुम": "2p", "आप": "hon", "तू": "2s"} +BOUND = {"कि", "और", "पर", "लेकिन", "तो", "जब", "क्योंकि", "अगर", "या", "मगर", "जो", ",", ".", "?", "!", "।"} +# participle class x auxiliary class -> violation? +BAD_PA = {("ms", "fs"), ("ms", "mp"), ("ms", "fp"), ("ms", "pl"), ("fs", "ms"), ("fs", "mp"), ("mp", "ms"), ("mp", "fs"), ("mp", "1s"), ("mp", "3s")} +# subject x auxiliary -> +1 ok / -1 violation (absent = neutral) +SA = {("1s", "1s"): 1, ("1s", "3s"): -1, ("1s", "pl"): -1, ("1s", "2p"): -1, + ("pl", "pl"): 1, ("pl", "1s"): -1, ("pl", "3s"): -1, + ("2p", "2p"): 1, ("2p", "1s"): -1, ("2p", "3s"): -1, + ("hon", "pl"): 1, ("hon", "1s"): -1, ("hon", "3s"): -1, + ("2s", "3s"): 1, ("2s", "1s"): -1} + + +def _part_class(w): + g = PART.get(w) + if g: return g + if len(w) > 2 and w.endswith("ता"): return "ms" + if len(w) > 2 and w.endswith("ती"): return "fs" + if len(w) > 2 and w.endswith("ते"): return "mp" + return None + + +def agree_feats(words, i): + """contribution of words[i] given words[:i]; i=None -> 0 (no end-of-sentence term).""" + if i is None or i <= 0: return 0, 0 + w = words[i]; a = AUX.get(w) + if a is None: return 0, 0 + pos = neg = 0 + pc = _part_class(words[i - 1]) + if pc is not None: + if (pc, a) in BAD_PA: neg += 1 + else: pos += 1 + for j in range(i - 1, max(-1, i - 9), -1): # nearest subject pronoun in the clause + u = words[j] + if u in BOUND: break + s = SUBJ.get(u) + if s: + v = SA.get((s, a), 0); pos += v > 0; neg += v < 0 + break + return pos, neg + + +def agree_score(words, i, wp=1.0, wn=1.0): + p, n = agree_feats(words, i); return wp * p - wn * n diff --git a/python/bhc/casual.py b/python/bhc/casual.py new file mode 100644 index 0000000000000000000000000000000000000000..d17c8a745447cefdfaa071902195c1d4ad7a64bf --- /dev/null +++ b/python/bhc/casual.py @@ -0,0 +1,174 @@ +"""Casual / chat-romanization keys (AksharaLP v2). + +Two lossy keys are computed for a roman token and for a Devanagari word; a +casual spelling matches the intended word when the keys collide. + K1: consonant classes (aspiration, nukta, sibilants merged), vowel classes + (length merged, ai->e, au->o), every 'a'/schwa removed except word + initial, nasal dropped, repeats collapsed. + K2: K1 with all vowels removed (chat consonant-only spellings: bht, thk). +The index is built at load time from the lexicon (0 bytes stored). +Same algorithm is ported 1:1 to JS (release/runtime) - keep in sync. +""" +import re + +ASP = set("kgjtdpbc") +# Devanagari -> key class (nukta ignored except ड़/ढ़ -> R) +_DC = {} +for chars, k in [("कख", "k"), ("गघ", "g"), ("चछ", "c"), ("जझ", "j"), ("टठतथ", "t"), + ("डढदध", "d"), ("णनऩ", "n"), ("पफ", "p"), ("बभ", "b"), ("म", "m"), + ("य", "y"), ("रऱ", "r"), ("लळ", "l"), ("व", "v"), ("शषस", "s"), ("ह", "h")]: + for c in chars: _DC[c] = k +for c, k in zip("\u0958\u0959\u095a\u095b\u095c\u095d\u095e\u095f", "kkgjRRpy"): _DC[c] = k +_DV = {} +for chars, k in [("अआा", "a"), ("इईिी", "i"), ("उऊुू", "u"), ("एऐेैॅऍ", "e"), ("ओऔऑोौॉ", "o"), ("ऋृ", "r")]: + for c in chars: _DV[c] = k +VOW = set("aeiou") + + +def _fin(s): + # drop non-initial a, collapse repeats + out = [] + for i, ch in enumerate(s): + if ch == "a" and i > 0: continue + if out and out[-1] == ch: continue + out.append(ch) + return "".join(out) + + +def k2_of(k1): + s = k1[0] + "".join(c for c in k1[1:] if c not in VOW) if k1 else "" + if s and s[0] in VOW: s = s[1:] or s + out = [] + for ch in s: + if out and out[-1] == ch: continue + out.append(ch) + return "".join(out) + + +def dev_keys(w): + """K1 keys of a Devanagari word (2 if it has ड़/ढ़: as d and as r).""" + raw = [] + for i, ch in enumerate(w): + if ch == "\u093c": + if raw and raw[-1] == "d" and i > 0 and w[i - 1] in "डढ": raw[-1] = "R" + continue + if ch in _DC: raw.append(_DC[ch]) + elif ch in _DV: raw.append(_DV[ch]) + s = "".join(raw) + if not s: return [] + ks = {_fin(s.replace("R", "d")), _fin(s.replace("R", "r"))} + return sorted(k for k in ks if k) + + +def _norm(t): + t = t.lower() + t = re.sub(r"[^a-z]", "", t) + return t + + +def rom_keys(t, cap=12): + """K1 keys of a roman token, expanded over ambiguous casual rules.""" + t = _norm(t) + if not t: return [], [] + t = re.sub(r"(.)\1{2,}", r"\1", t) # emphasis repeats + # deterministic letter merges + t = t.replace("chh", "c").replace("cch", "c").replace("ch", "c").replace("sh", "s") + t = t.replace("x", "ks").replace("q", "k").replace("z", "j").replace("f", "p").replace("w", "v") + # (unit list) each position: list of alternatives + alts = [] + i = 0; L = len(t) + while i < L: + c = t[i]; nx = t[i + 1] if i + 1 < L else "" + prev = t[i - 1] if i > 0 else "" + if c == "h" and prev in ASP and i > 0: + alts.append(("", "h")); i += 1; continue # aspiration / separate h + if nx and c in "ae" and nx in "iy" and (i + 2 >= L or t[i + 2] not in VOW): + if c == "a" and nx == "i": alts.append(("e", "ai")); i += 2; continue + if c == "a" and nx == "y" and i + 2 >= L: alts.append(("e",)); i += 2; continue + if c == "e" and nx == "y" and i + 2 >= L: alts.append(("e",)); i += 2; continue + if nx and c in "ao" and nx in "uvw" and (i + 2 >= L or t[i + 2] not in VOW) and c + nx != "av": + alts.append(("o", c + "u")); i += 2; continue + if c == "o" and nx == "u": alts.append(("o", "u")); i += 2; continue + if c == "e" and nx == "e": alts.append(("i",)); i += 2; continue + if c == "o" and nx == "o": alts.append(("u", "o")); i += 2; continue + if c == "e" and nx == "i" and (i + 2 >= L or t[i + 2] not in VOW): alts.append(("e",)); i += 2; continue + if c == "e" and nx == "h" and i + 2 < L and t[i + 2] not in VOW and i > 0: alts.append(("e", "")); i += 1; continue + if nx and c == "i" and nx in "aeou" and i > 0: alts.append(("y", "i")); i += 1; continue + if c == "y" and i == L - 1 and prev and prev not in VOW: alts.append(("e", "i")); i += 1; continue + if c == "n" and prev in VOW and i > 0 and (i == L - 1 or (nx and nx not in VOW and nx != "y")): + alts.append(("n", "")); i += 1; continue + if c in "iu" and prev == "r" and i >= 2 and t[i - 2] not in VOW: + alts.append((c, "")); i += 1; continue + alts.append((c,)); i += 1 + outs = [""] + for a in alts: + nxt = [] + for o in outs: + for x in a: nxt.append(o + x) + outs = list(dict.fromkeys(nxt))[:64] + k1 = []; seen = set() + for o in outs: + k = _fin(o) + if k and k not in seen: seen.add(k); k1.append(k) + k1 = k1[:cap] + k2 = list(dict.fromkeys(k2_of(k) for k in k1 if k2_of(k))) + return k1, k2 + + +# Irregular chat abbreviations (not derivable by rules). Values: candidate natives in prior order. +ABBR = { + "h": ["है", "हैं"], "hy": ["है"], "hn": ["हाँ", "हैं"], "hm": ["हम", "हम्म"], + "k": ["के", "की", "ओके"], "ki": ["की", "कि"], "n": ["ना", "न"], "ni": ["नहीं", "नी"], "nai": ["नहीं", "नई"], + "nhi": ["नहीं"], "nhe": ["नहीं"], "nahi": ["नहीं"], "nai": ["नहीं", "नई"], + "kr": ["कर"], "krna": ["करना"], "kro": ["करो"], "tm": ["तुम"], "tu": ["तू"], "mai": ["मैं", "में"], + "me": ["में", "मैं"], "m": ["मैं", "में"], "thk": ["ठीक"], "tk": ["ठीक", "तक"], + "yr": ["यार"], "yrr": ["यार"], "plz": ["प्लीज़"], "pls": ["प्लीज़"], "plzz": ["प्लीज़"], + "tq": ["थैंक यू"], "ty": ["थैंक यू"], "thx": ["थैंक्स"], "thnx": ["थैंक्स"], "thnks": ["थैंक्स"], + "gn": ["गुड नाइट"], "gm": ["गुड मॉर्निंग"], "ok": ["ओके"], "okk": ["ओके"], "kk": ["ओके"], + "sry": ["सॉरी"], "bro": ["ब्रो"], "msg": ["मैसेज"], "bday": ["बर्थडे"], "clg": ["कॉलेज"], + "ofc": ["ऑफ़िस"], "pic": ["पिक"], "bhai": ["भाई"], "bht": ["बहुत"], "bhot": ["बहुत"], "bohot": ["बहुत"], + "pta": ["पता"], "kb": ["कब"], "jb": ["जब"], "sb": ["सब"], "mtlb": ["मतलब"], "kch": ["कुछ"], "kuch": ["कुछ"], +} + + +class CasualIndex: + """K1/K2 -> lexicon ids sorted by frequency (top `top_n` lexicon words only).""" + + def __init__(self, words, uni=None, top_n=300000, per_key=8): + order = range(len(words)) + if uni is not None: + u = uni if callable(uni) else uni.__getitem__ + order = sorted(order, key=lambda i: (-u(i), i)) + self.k1 = {}; self.k2 = {} + for r, i in enumerate(order): + if r >= top_n: break + for k in dev_keys(words[i]): + b = self.k1.setdefault(k, []) + if len(b) < per_key and i not in b: b.append(i) + k2 = k2_of(k) + if k2: + b2 = self.k2.setdefault(k2, []) + if len(b2) < per_key and i not in b2: b2.append(i) + + def lookup(self, roman, n1=6, n2=4): + k1, k2 = rom_keys(roman) + r1 = []; r2 = [] + for k in k1: + for i in self.k1.get(k, ()): + if i not in r1: r1.append(i) + for k in k2: + for i in self.k2.get(k, ()): + if i not in r2: r2.append(i) + return r1[:n1 * 2], r2[:n2 * 2] + + +def squeeze_in(t, mode=0): + """Emphasis-repeat normalization of the model input. mode 0: off; 1: 3+ -> 1; 2: 3+ -> 2 for a/e/o, 1 otherwise.""" + if not mode: return t + if mode == 1: return re.sub(r"([a-z])\1{2,}", r"\1", t) + return re.sub(r"([a-z])\1{2,}", lambda m: m.group(1) * (2 if m.group(1) in "aeo" else 1), t) + + +def squeeze(t): + """Normalize emphasis repeats (3+ same letters -> keep 1, e.g. pleaseee->please).""" + return re.sub(r"([a-zA-Z])\1{2,}", r"\1", t) diff --git a/python/bhc/lexlm.py b/python/bhc/lexlm.py new file mode 100644 index 0000000000000000000000000000000000000000..5cfbf6be85ccf56f3b0b0bc122f98a6e269a8192 --- /dev/null +++ b/python/bhc/lexlm.py @@ -0,0 +1,189 @@ +"""Compact Devanagari lexicon + pruned backoff word-bigram LM (BhashaCore Track B/C assets), format v2. + +Word ids: words sorted by (unigram bucket = round(-2*ln count) i.e. frequency class, then alphabetical) so +ids are roughly frequency-ordered (small ids for frequent words -> small LM deltas), while the list is still +front-coded within each bucket. Unigram ln-prob is per bucket (implicit; no per-word bytes). +lex.bin : header | per-bucket (n_words varint, ln-prob f16) | front-coded list (1 byte/char = codepoint-0x0900, + last suffix byte has bit 7 set; 0x7F = empty suffix) +lm.bin : header | successor count per history (varint; history N = sentence start) | delta-coded successor + ids (varint) | 4-bit codebook ln p(w|v), 16 centroids f32 | 4-bit codebook ln backoff for histories + with successors (16 centroids) +""" +import gzip, math, struct +import numpy as np + +HK = 1 << 21 # in-memory bigram key = history * HK + successor (lexicons up to 2M words; file format unchanged) + + +def varints(vals): + out = bytearray() + for v in vals: + v = int(v) + while v >= 0x80: out.append((v & 0x7F) | 0x80); v >>= 7 + out.append(v) + return bytes(out) + + +def read_varints(b, n, pos=0): + out = [0] * n; i = 0 + while i < n: + v = sh = 0 + while True: + c = b[pos]; pos += 1; v |= (c & 0x7F) << sh; sh += 7 + if c < 0x80: break + out[i] = v; i += 1 + return out, pos + + +def kmeans1d(x, k=16, it=30): + x = np.asarray(x, np.float64); c = np.quantile(x, (np.arange(k) + 0.5) / k) + for _ in range(it): + a = np.abs(x[:, None] - c[None]).argmin(1) + for j in range(k): + if (a == j).any(): c[j] = x[a == j].mean() + c = np.sort(c); a = np.abs(x[:, None] - c[None]).argmin(1) if len(x) < 2_000_000 else None + return c.astype(np.float32), a.astype(np.uint8) + + +def pack4(a): + a = np.r_[a, np.zeros(len(a) % 2, np.uint8)].astype(np.uint8); return (a[0::2] * 16 + a[1::2]).astype(np.uint8).tobytes() + + +def unpack4(b, n): + a = np.frombuffer(b, np.uint8); return np.stack([a >> 4, a & 15], 1).reshape(-1)[:n] + + +def build(words_by_freq, counts_by_freq, bigram_pairs, bigram_counts, n_lex, maxbi, D=0.75, cmin=2, K=262144, alpha=False): + """words_by_freq[i] has count id i+2 in bigram_pairs (0=boundary, 1=unk).""" + N = n_lex; ws = words_by_freq[:N]; cs = counts_by_freq[:N] + bk = [-int(round(2 * math.log(c))) for c in cs] + if alpha: # format v4: one global alphabetical front-coded list + 16-level (4-bit) uniform log-frequency band per word + order = sorted(range(N), key=lambda i: ws[i]) + else: + order = sorted(range(N), key=lambda i: (bk[i], ws[i])) + nid = np.empty(N + 2, np.int64); nid[0] = N; nid[1] = -1 + for a, i in enumerate(order): nid[i + 2] = a + tot = float(sum(cs)) * 1.02 # ~2% mass reserved for OOV words + buckets = [] + for i in order: + if not buckets or buckets[-1][0] != bk[i]: buckets.append([bk[i], 0, 0.0]) + buckets[-1][1] += 1; buckets[-1][2] += cs[i] + b_lp = [math.log(s / n / tot) for _, n, s in buckets] + unk_lp = math.log(0.02 / 50000) + puni = np.empty(N + 2); puni[0] = math.log(0.04); puni[1] = unk_lp; j = 0 + if alpha: + lc = np.log(np.array(cs, np.float64) / tot); NB = 16 # 16 uniform log-frequency bands (4 bits/word) + ucb = np.linspace(lc.min(), lc.max(), NB).astype(np.float32) + ua = np.abs(lc[:, None] - ucb[None].astype(np.float64)).argmin(1).astype(np.uint8) + puni[2:] = ucb[ua] + else: + for (_, n, _), lp in zip(buckets, b_lp): + for _ in range(n): puni[[order[j] + 2]] = lp; j += 1 + v = bigram_pairs // K; w = bigram_pairs % K; c = bigram_counts.astype(np.float64) + inlex = (v - 2 < N) & (w - 2 < N) + hist_tot = np.bincount(np.where(v - 2 < N, v, 1), weights=c, minlength=N + 2) + keep = inlex & (v != 1) & (w != 1) & (c >= cmin) + pk_all = np.log(np.maximum(c - D, 0.1) / np.maximum(hist_tot[np.minimum(v, N + 1)], 1)) + gain = np.where(keep, c * (pk_all - puni[np.minimum(w, N + 1)]), -np.inf) + if keep.sum() > maxbi: + thr = np.partition(gain, len(gain) - maxbi)[len(gain) - maxbi]; keep &= gain >= thr + kv, kw, pk = v[keep], w[keep], pk_all[keep] + av = nid[kv]; aw = nid[kw] # history boundary 0 -> N ; successor boundary 0 -> N (= ) + o = np.lexsort((aw, av)); av, aw, pk = av[o], aw[o], pk[o] + cb, qa = kmeans1d(pk); pkd = cb[qa] + cnt = np.bincount(av, minlength=N + 1) + first = np.r_[True, av[1:] != av[:-1]]; delta = np.where(first, aw, aw - np.r_[0, aw[:-1]] - 1) + # backoff for histories with successors (dequantised probs; unigram of successors in new-id space) + pu_new = np.empty(N + 1); pu_new[nid[np.arange(2, N + 2)]] = puni[2:]; pu_new[N] = puni[0] + s_bi = np.bincount(av, weights=np.exp(pkd), minlength=N + 1); s_un = np.bincount(av, weights=np.exp(pu_new[aw]), minlength=N + 1) + hs = np.nonzero(cnt)[0] + bow = np.log(np.maximum(1 - s_bi[hs], 1e-4) / np.maximum(1 - s_un[hs], 1e-4)) + cbb, qb = kmeans1d(bow) + fc = bytearray(); prev = b"" + for i in order: + bb = bytes(ord(ch) - 0x0900 for ch in ws[i]); k = 0 + while k < min(len(bb), len(prev), 127) and bb[k] == prev[k]: k += 1 + suf = bytearray(bb[k:]) or bytearray([0x7F]); suf[-1] |= 0x80; fc.append(k); fc += suf; prev = bb + bh = varints([n for _, n, _ in buckets]) + np.array(b_lp, np.float16).tobytes() + if alpha: # v4: prefix-length stream and suffix stream stored separately (compresses ~8% better) + P = bytearray(); S = bytearray(); prev = b"" + for i in order: + bb = bytes(ord(ch) - 0x0900 for ch in ws[i]); k = 0 + while k < min(len(bb), len(prev), 127) and bb[k] == prev[k]: k += 1 + suf = bytearray(bb[k:]) or bytearray([0x7F]); suf[-1] |= 0x80; P.append(k); S += suf; prev = bb + lex = b"LX4\0" + struct.pack(".""" + h = self.N if prev is None else prev + if wid is not None: + p = self.bi.get(h * HK + wid) + if p is not None: return p + return self.bow.get(h, 0.0) + self.unigram(wid) + + +def write_assets(out_dir, n_lex, maxbi): + import os + root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + rows = [l.rstrip("\n").split("\t") for l in open(f"{root}/data/processed/trackB/hi_wordfreq.tsv", encoding="utf-8")] + d = np.load(f"{root}/data/processed/trackB/lm/bigrams.npz") + lex, lm, st = build([r[0] for r in rows], [int(r[1]) for r in rows], d["pairs"], d["counts"], n_lex, maxbi) + os.makedirs(out_dir, exist_ok=True) + open(f"{out_dir}/lex.bin", "wb").write(lex); open(f"{out_dir}/lm.bin", "wb").write(lm) + st.update(lex_bytes=len(lex), lex_gz=len(gzip.compress(lex, 9)), lm_bytes=len(lm), lm_gz=len(gzip.compress(lm, 9))) + return st diff --git a/python/bhc/lextrie.py b/python/bhc/lextrie.py new file mode 100644 index 0000000000000000000000000000000000000000..73a6611d256a16ddfd7ca6e6240ddf70d80535c5 --- /dev/null +++ b/python/bhc/lextrie.py @@ -0,0 +1,38 @@ +"""Memory-light lexicon trie for constrained beam search over large (0.5-2M word) lexicons. +A node is a (lo, hi, depth) range of the sorted word list; children are found with bisect and cached lazily, so +only nodes actually visited by the beam are ever materialised. Same interface as xlit_model.Trie (kids[n], final[n]).""" +import bisect +from bhc.xlit_model import T2I, TGT_VOCAB + + +class _Kids: + def __init__(s, t): s.t = t + def __getitem__(s, n): return s.t._children(n) + + +class _Final: + def __init__(s, t): s.t = t + def __getitem__(s, n): + lo, hi, d = s.t.nodes[n]; return len(s.t.w[lo]) == d + + +class LazyTrie: + def __init__(s, words): + s.w = sorted(w for w in set(words) if w and all(c in T2I for c in w)) + s.nodes = [(0, len(s.w), 0)]; s.ids = {s.nodes[0]: 0}; s.cache = {} + s.kids = _Kids(s); s.final = _Final(s) + + def _children(s, n): + c = s.cache.get(n) + if c is not None: return c + lo, hi, d = s.nodes[n]; w = s.w; out = {} + pos = lo + if pos < hi and len(w[pos]) == d: pos += 1 # the word equal to the prefix sorts first + while pos < hi: + ch = w[pos][d]; pre = w[pos][:d + 1] + end = bisect.bisect_left(w, pre[:-1] + chr(ord(ch) + 1), pos, hi) + key = (pos, end, d + 1); nid = s.ids.get(key) + if nid is None: nid = len(s.nodes); s.nodes.append(key); s.ids[key] = nid + out[T2I[ch]] = nid; pos = end + if len(s.cache) > 400000: s.cache.clear() + s.cache[n] = out; return out diff --git a/python/bhc/nlm.py b/python/bhc/nlm.py new file mode 100644 index 0000000000000000000000000000000000000000..fd7aba52961893461188ba38c4b1e2209d9f902c --- /dev/null +++ b/python/bhc/nlm.py @@ -0,0 +1,187 @@ +"""AksharaLP v3 tiny sentence LM: Devanagari subword (unigram-LM pieces, Viterbi segmentation reimplemented here so Python and +JS tokenize identically) + 2-layer LSTM with projection (LSTMP) + tied input/output embedding, trained with a self-normalisation +penalty (alpha * logZ^2) so the runtime may skip the softmax normaliser (norm='self') or compute it exactly (norm='exact'). +File format (same low-bit scheme as bhc/xlit_q.py): u32 header length | JSON header {cfg, bits, G, tensors, vocab:[[piece, score]...]} | data. +Sentence = '' w1 w2 ... wn '' ; digits runs are the word ''.""" +import io, json, math +import numpy as np + +SP = "\u2581" + + +class Tok: + """Viterbi over piece log-probs (identical to sentencepiece unigram encode for single whitespace-free words, no sampling).""" + def __init__(self, vocab): + self.pieces = [p for p, _ in vocab]; self.score = {p: s for p, s in vocab}; self.id = {p: i for i, p in enumerate(self.pieces)} + self.maxlen = max(len(p) for p in self.pieces); self.unk = self.id[""]; self.eos = self.id[""] + self.unk_score = min(s for _, s in vocab) - 10.0; self.cache = {} + + def encode_word(self, w): + r = self.cache.get(w) + if r is not None: return r + if w in self.id and w.startswith("<"): r = [self.id[w]]; self.cache[w] = r; return r + s = SP + w; n = len(s); best = [-1e30] * (n + 1); back = [None] * (n + 1); best[0] = 0.0 + for i in range(n): + if best[i] <= -1e29: continue + hit = False + for j in range(i + 1, min(n, i + self.maxlen) + 1): + p = s[i:j]; sc = self.score.get(p) + if sc is not None and p not in ("", "", "", ""): + hit = True + if best[i] + sc > best[j]: best[j] = best[i] + sc; back[j] = (i, self.id[p]) + if not hit or (back[i + 1] is None): # single unknown character -> + if best[i] + self.unk_score > best[i + 1]: best[i + 1] = best[i] + self.unk_score; back[i + 1] = (i, self.unk) + out = []; j = n + while j > 0: i, pid = back[j]; out.append(pid); j = i + r = out[::-1] + if len(self.cache) < 200000: self.cache[w] = r + return r + + +def _dq(data, off, shape, kind, G): + r, c = shape; ng = (c + G - 1) // G + if kind == "q8": + q = np.frombuffer(data, np.int8, r * c, off).astype(np.float32).reshape(r, c); off += r * c + else: + nb = (r * c + 1) // 2; u = np.frombuffer(data, np.uint8, nb, off); off += nb + q = np.empty(nb * 2, np.int8); q[0::2] = (u >> 4).astype(np.int8) - 8; q[1::2] = (u & 15).astype(np.int8) - 8 + q = q[:r * c].astype(np.float32).reshape(r, c) + off += (-off) % 4 + sc = np.frombuffer(data, np.float16, r * ng, off).astype(np.float32).reshape(r, ng) + return q * np.repeat(sc, G, 1)[:, :c] + + +class NLM: + """numpy float32 inference. state = (h[L,E], c[L,H]) per hypothesis, stacked into arrays for batching.""" + def __init__(self, cfg, W, vocab, norm="exact"): + self.cfg = cfg; self.W = W; self.tok = Tok(vocab); self.L = cfg["layers"]; self.norm = norm + self.emb = W["emb.weight"]; self.ob = W["out_bias"] + self._init = None; L = self.L + self._wihT = [np.ascontiguousarray(W[f"lstm.weight_ih_l{l}"].T) for l in range(L)]; self._wih0T = self._wihT[0] + self._whhT = [np.ascontiguousarray(W[f"lstm.weight_hh_l{l}"].T) for l in range(L)] + self._whrT = [np.ascontiguousarray(W[f"lstm.weight_hr_l{l}"].T) for l in range(L)] + self._b = [W[f"lstm.bias_ih_l{l}"] + W[f"lstm.bias_hh_l{l}"] for l in range(L)]; self._embT = np.ascontiguousarray(self.emb.T) + + @staticmethod + def load(path, norm=None): + b = open(path, "rb").read(); n = int.from_bytes(b[:4], "little"); meta = json.loads(b[4:4 + n]); data = b[4 + n:] + W = {} + for k, shape, kind, off, ln in meta["tensors"]: + if kind == "f16": + cnt = int(np.prod(shape)); W[k] = np.frombuffer(data, np.float16, cnt, off).astype(np.float32).reshape(shape) + elif kind == "f32": + cnt = int(np.prod(shape)); W[k] = np.frombuffer(data, np.float32, cnt, off).reshape(shape).copy() + else: W[k] = _dq(data, off, shape, kind, meta["G"]) + return NLM(meta["cfg"], W, meta["vocab"], norm or meta["cfg"].get("norm", "exact")) + + def _step(self, h, c, ids, g0=None): + """h: [L,B,E], c: [L,B,H], ids: [B] -> new h, c. g0: optional precomputed layer-0 gate pre-activations [B,4H].""" + hs = []; cs = [] + if g0 is None: + u, inv = np.unique(ids, return_inverse=True) # layer-0 input projection once per distinct piece + g0 = (self.emb[u] @ self._wih0T + self._b[0])[inv] + h[0] @ self._whhT[0] + for l in range(self.L): + g = g0 if l == 0 else x @ self._wihT[l] + h[l] @ self._whhT[l] + self._b[l] + H = g.shape[1] // 4 + i_ = 1 / (1 + np.exp(-g[:, :H])); f_ = 1 / (1 + np.exp(-g[:, H:2 * H])); g_ = np.tanh(g[:, 2 * H:3 * H]); o_ = 1 / (1 + np.exp(-g[:, 3 * H:])) + cn = f_ * c[l] + i_ * g_; hn = (o_ * np.tanh(cn)) @ self._whrT[l] + hs.append(hn); cs.append(cn); x = hn + return np.stack(hs), np.stack(cs) + + def _logp(self, h, ids): + """log P(ids[b] | state b) using the top-layer output h[-1]: [B]""" + top = h[-1]; z = (top * self.emb[ids]).sum(1) + self.ob[ids] + if self.norm == "exact": + lg = top @ self._embT + self.ob; m = lg.max(1); z = z - (m + np.log(np.exp(lg - m[:, None]).sum(1))) + return z + + def init_state(self): + if self._init is None: + E = self.emb.shape[1]; H = self.W["lstm.weight_hh_l0"].shape[1] * 0 + self.W["lstm.weight_ih_l0"].shape[0] // 4 + h = np.zeros((self.L, 1, E), np.float32); c = np.zeros((self.L, 1, H), np.float32) + h, c = self._step(h, c, np.array([self.tok.eos])); self._init = (h[:, 0], c[:, 0]) + return self._init + + def score_words(self, states, words): + """states: list of B states; words: list of K Devanagari words -> (lp[B][K], new_states[B][K]). + Exact chain rule over each word's pieces from each state, batched over all (b,k) pairs. First piece: one softmax + normaliser per beam state and the layer-0 recurrent term once per beam (broadcast over the K candidates).""" + B, K = len(states), len(words); seqs = [self.tok.encode_word(w) for w in words] + h = np.stack([s[0] for s in states], 1); c = np.stack([s[1] for s in states], 1) # [L,B,E], [L,B,H] + id0 = np.array([q[0] for q in seqs]); top = h[-1] + lp = top @ self.emb[id0].T + self.ob[id0][None, :] # [B,K] + if self.norm == "exact": + lg = top @ self._embT + self.ob; m = lg.max(1); lp = lp - (m + np.log(np.exp(lg - m[:, None]).sum(1)))[:, None] + g0 = ((self.emb[id0] @ self._wih0T + self._b[0])[None, :, :] + (h[0] @ self._whhT[0])[:, None, :]).reshape(B * K, -1) + hb, cb = self._step(np.repeat(h, K, 1), np.repeat(c, K, 1), None, g0=g0) # pair j = b*K + k + flat = lp.reshape(-1).astype(np.float32); maxp = max(len(q) for q in seqs) + for t in range(1, maxp): + ks = [k for k in range(K) if len(seqs[k]) > t] + idx = (np.arange(B)[:, None] * K + np.array(ks)[None, :]).reshape(-1) + ids = np.tile(np.array([seqs[k][t] for k in ks]), B) + flat[idx] += self._logp(hb[:, idx], ids) + hn, cn = self._step(hb[:, idx], cb[:, idx], ids); hb[:, idx] = hn; cb[:, idx] = cn + ns = [[(hb[:, b * K + k], cb[:, b * K + k]) for k in range(K)] for b in range(B)] + return flat.reshape(B, K).tolist(), ns + + def advance_many(self, states, words): + lp, ns = self.score_words(states, words); return ns + + def eos_logp(self, state): + h = state[0][:, None, :] + return float(self._logp(h, np.array([self.tok.eos]))[0]) + + def sent_logp(self, words): + st = self.init_state(); tot = 0.0 + for w in words: + lp, ns = self.score_words([st], [w]); tot += lp[0][0]; st = ns[0][0] + return tot + self.eos_logp(st) + + +# ---------------- torch model + export (training side) +def torch_model(cfg): + import torch, torch.nn as nn + + class LSTMP_LM(nn.Module): + def __init__(s, cfg): + super().__init__(); s.cfg = cfg; V, E, H, L = cfg["vocab"], cfg["emb"], cfg["hid"], cfg["layers"] + s.emb = nn.Embedding(V, E); nn.init.normal_(s.emb.weight, 0, E ** -0.5) + s.lstm = nn.LSTM(E, H, num_layers=L, proj_size=E, batch_first=True, dropout=cfg.get("dropout", 0.0)) + s.out_bias = nn.Parameter(torch.zeros(V)); s.drop = nn.Dropout(cfg.get("dropout", 0.0)) + + def forward(s, x, state=None): + h, st = s.lstm(s.drop(s.emb(x)), state) + return s.drop(h) @ s.emb.weight.T + s.out_bias, st + return LSTMP_LM(cfg) + + +def _q(v, bits, G): + """symmetric b-bit ints, one fp16 scale per G consecutive inputs per row, MSE-optimal clipping for 4-bit (same as bhc/xlit_q.py).""" + r, c = v.shape; pad = (-c) % G; vp = np.pad(v, ((0, 0), (0, pad))).reshape(r, -1, G) + L = 2 ** (bits - 1) - 1; base = np.abs(vp).max(2, keepdims=True) / L; base[base == 0] = 1 + best_err = None + for al in ((1.0,) if bits >= 8 else (1.0, 0.95, 0.9, 0.85, 0.8, 0.75, 0.7, 0.65)): + s_ = (base * al).astype(np.float16).astype(np.float32); q_ = np.clip(np.round(vp / s_), -L, L) + e = ((q_ * s_ - vp) ** 2).sum(2, keepdims=True) + if best_err is None: best_err, sc, q = e, s_, q_ + else: m = e < best_err; best_err = np.where(m, e, best_err); sc = np.where(m, s_, sc); q = np.where(m, q_, q) + return q.astype(np.int8).reshape(r, -1)[:, :c], sc[..., 0].astype(np.float16) + + +def export(model, vocab, path, bits=4, G=32, emb_bits=8, norm="self"): + """bits/emb_bits in {4, 8} or 0 (= float32, exact reference).""" + sd = model.state_dict(); cfg = dict(model.cfg); cfg["norm"] = norm + meta = {"cfg": cfg, "bits": bits, "G": G, "tensors": [], "vocab": vocab}; buf = io.BytesIO() + for k, v in sd.items(): + v = v.float().cpu().numpy(); buf.write(b"\0" * ((-buf.tell()) % 4)) # 4-byte aligned tensors (JS typed arrays) + b_ = emb_bits if k.startswith("emb") else bits + if v.ndim == 2 and b_: + q, sc = _q(v, b_, G); flat = q.reshape(-1) + if b_ == 4: + u = (flat + 8).astype(np.uint8); u = np.r_[u, np.zeros(len(u) % 2, np.uint8)]; data = (u[0::2] << 4 | u[1::2]).astype(np.uint8).tobytes() + else: data = flat.astype(np.int8).tobytes() + meta["tensors"].append([k, list(v.shape), "q%d" % b_, buf.tell(), len(data)]); buf.write(data); buf.write(b"\0" * ((-buf.tell()) % 4)); buf.write(sc.tobytes()) + else: + meta["tensors"].append([k, list(v.shape), "f32", buf.tell(), 0]); buf.write(v.astype(np.float32).tobytes()) + hdr = json.dumps(meta, separators=(",", ":"), ensure_ascii=False).encode(); hdr += b" " * ((-len(hdr) - 4) % 4) + with open(path, "wb") as f: f.write(len(hdr).to_bytes(4, "little")); f.write(hdr); f.write(buf.getvalue()) diff --git a/python/bhc/skel.py b/python/bhc/skel.py new file mode 100644 index 0000000000000000000000000000000000000000..14b14443bb5888e40e61c37eafb992d7abf6f081 --- /dev/null +++ b/python/bhc/skel.py @@ -0,0 +1,107 @@ +"""Consonant-skeleton index over the lexicon: a storage-free 'romanization memory'. +Both the Devanagari lexicon word and the romanized input are reduced to a coarse consonant-class string +(vowels, y, aspiration, nukta, halant dropped; n/m/anusvara merged; repeats collapsed). The index is built at load time +from lex.bin (no extra bytes shipped). At query time, lexicon words sharing the input's skeleton (top-K by frequency) +become candidates and are scored by the model (teacher-forced). The JS runtime (release/space/js/aksharalp.mjs) mirrors +these tables exactly -- keep them in sync.""" +DEV = {} +for chars, c in (("कखक़ख़", "K"), ("गघग़", "G"), ("चछ", "C"), ("जझज़", "J"), ("टठतथ", "T"), ("डढदधड़ढ़", "D"), + ("ङञणनऩमंँ", "N"), ("पफफ़", "P"), ("बभ", "B"), ("रऱऋृ", "R"), ("लळ", "L"), ("व", "V"), ("शषस", "S"), + ("हः", "H")): + for ch in chars: + if ch != "\u093c": DEV[ch] = c +DEV.pop("\u093c", None) # nukta: dropped (NFC keeps क़ etc. decomposed) +ROM1 = dict(k="K", q="K", g="G", j="J", z="J", t="T", d="D", n="N", m="N", p="P", f="P", b="B", r="R", l="L", v="V", w="V", + s="S", h="H") +ASP = set("kgcjtdpbsz") # consonant + h -> same class (h dropped); 'sh' -> S, 'ch' -> C + + +def dev_skel(w): + out = [] + for ch in w: + c = DEV.get(ch) + if c and (not out or out[-1] != c): out.append(c) + return "".join(out) + + +def rom_skels(w, maxk=4): + """Skeletons of a roman word, ambiguity expanded (c -> K|S, ch -> C|K, tion -> SN|TN), at most maxk.""" + w = w.lower(); outs = [""]; i = 0; n = len(w) + while i < n: + ch = w[i]; step = 1 + if ch in "ts" and w.startswith("ion", i + 1): + opts = ["SN", "TN"] if ch == "t" else ["SN"]; step = 4 + elif ch == "c": + nx = w[i + 1] if i + 1 < n else "" + if nx == "h": + opts = ["C", "K"]; step = 2 + while i + step < n and w[i + step] in "ch": step += 1 # chh, chch + elif nx in ("e", "i", "y"): opts = ["S", "K"] + elif nx == "k": opts = ["K"]; step = 2 + else: opts = ["K", "S"] + elif ch == "x": opts = ["KS"] + elif ch in ASP and i + 1 < n and w[i + 1] == "h": + opts = [ROM1.get(ch, "")]; step = 2 + while i + step < n and w[i + step] == "h": step += 1 + else: opts = [ROM1.get(ch, "")] + new = [] + for o in outs: + for p in opts: + s = o + for c in p: + if not s or s[-1] != c: s += c + new.append(s) + outs = list(dict.fromkeys(new))[:maxk]; i += step + return outs + + +class SkelIndex: + def __init__(self, words, uni): + """words: lexicon list; uni(i) -> unigram log-prob. Buckets keep ids sorted by frequency (desc).""" + b = {} + for i, w in enumerate(words): + k = dev_skel(w) + if k: b.setdefault(k, []).append(i) + for k, v in b.items(): v.sort(key=lambda i: (-uni(i), i)) + self.b = b + + def lookup(self, roman, topk=24): + seen = []; S = set() + for k in rom_skels(roman): + for i in self.b.get(k, ())[:topk]: + if i not in S: S.add(i); seen.append(i) + return seen + + +# ---- cheap rule romanizer + edit distance prefilter (rank skeleton-bucket words before model scoring) +_C = dict(zip("कखगघङचछजझञटठडढणतथदधनपफबभमयरलवशषसहळ", + "k kh g gh n ch chh j jh n t th d dh n t th d dh n p ph b bh m y r l v sh sh s h l".split())) +_V = dict(zip("अआइईउऊऋएऐओऔऑ", "a aa i ee u oo ri e ai o au o".split())) +_M = dict(zip("ािीुूृेैोौॉॅ", "a i ee u oo ri e ai o au o e".split())) + + +def dev_rom(w): + out = []; L = len(w) + for j, ch in enumerate(w): + if ch in _C: + out.append(_C[ch]); nx = w[j + 1] if j + 1 < L else "" + if nx == "\u093c": nx = w[j + 2] if j + 2 < L else "" + if nx and nx not in _M and nx != "\u094d" and j + 1 < L - 1: out.append("a") + elif ch in _V: out.append(_V[ch]) + elif ch in _M: out.append(_M[ch]) + elif ch in "ंँ": out.append("n") + elif ch == "ः": out.append("h") + elif ch == "\u093c" and out: + out[-1] = {"k": "q", "kh": "kh", "g": "g", "j": "z", "ph": "f", "d": "r", "dh": "rh"}.get(out[-1], out[-1]) + return "".join(out) + + +def lev(a, b): + if len(a) < len(b): a, b = b, a + prev = list(range(len(b) + 1)) + for i, ca in enumerate(a, 1): + cur = [i] + for j, cb in enumerate(b, 1): + cur.append(min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (ca != cb))) + prev = cur + return prev[-1] diff --git a/python/bhc/text.py b/python/bhc/text.py new file mode 100644 index 0000000000000000000000000000000000000000..6a6e65fd205c9d0f918b332aee37a79107f06953 --- /dev/null +++ b/python/bhc/text.py @@ -0,0 +1,125 @@ +"""Text normalisation, Unicode validation and tokenisation shared by data prep, baselines and eval.""" +import re +import unicodedata + +ZW = "\u200c\u200d\u200b\ufeff\u00ad" # ZWNJ, ZWJ, ZWSP, BOM, soft hyphen +_ZW_RE = re.compile(f"[{ZW}]") +_WS_RE = re.compile(r"\s+") + +# Devanagari character classes +CONS = set(chr(c) for c in range(0x0915, 0x093A)) | set(chr(c) for c in range(0x0958, 0x0960)) | set("ॹॺॻॼॽॾॿ") +INDEP_V = set(chr(c) for c in range(0x0904, 0x0915)) | set("ॠॡॲॳॴॵॶॷ") +MATRA = set(chr(c) for c in range(0x093E, 0x094D)) | set("ॢॣॏॕॖॗ\u093a\u093b\u094e") +VIRAMA = "\u094d" +NUKTA = "\u093c" +NASAL = set("\u0901\u0902") # chandrabindu, anusvara +VISARGA = "\u0903" +DEVA_DIGITS = "०१२३४५६७८९" +DEVA_RANGE = re.compile(r"[\u0900-\u097F]") +DANDA = "।॥" + + +def nfc(s: str) -> str: + return unicodedata.normalize("NFC", s) + + +def is_malformed(s: str) -> bool: + """True if the string contains characters we treat as malformed/garbage: + replacement char, private use, surrogates, unassigned, or control chars other than tab/newline.""" + for ch in s: + if ch == "\ufffd": + return True + cat = unicodedata.category(ch) + if cat in ("Co", "Cs", "Cn"): + return True + if cat == "Cc" and ch not in "\t\n": + return True + return False + + +def clean(s: str) -> str: + """NFC + strip zero-width/format chars + collapse whitespace.""" + s = nfc(s) + s = _ZW_RE.sub("", s) + s = "".join(ch for ch in s if unicodedata.category(ch) != "Cf") + return _WS_RE.sub(" ", s).strip() + + +def valid_devanagari_word(w: str) -> bool: + """Orthographic sanity check for a single Devanagari word (letters only). + Rejects: non-Devanagari chars, leading combining marks, matra/virama/nukta after a non-consonant, + two dependent vowel signs in a row, nukta not on a consonant, dangling virama+matra.""" + if not w: + return False + prev = None + for i, ch in enumerate(w): + if not ("\u0900" <= ch <= "\u097F"): + return False + if ch in DEVA_DIGITS or ch in DANDA: + return False + if ch in MATRA or ch == VIRAMA or ch == NUKTA: + if prev is None: + return False + if ch == NUKTA and prev not in CONS: + return False + if ch in MATRA and not (prev in CONS or prev == NUKTA): + return False + if ch == VIRAMA and not (prev in CONS or prev == NUKTA): + return False + if ch in NASAL or ch == VISARGA: + if prev is None or prev in NASAL or prev == VIRAMA: + return False + prev = ch + return True + + +def valid_roman_word(w: str) -> bool: + return bool(re.fullmatch(r"[a-z]+", w)) + + +# --- tokenisation ----------------------------------------------------------- +# Words (Latin or Devanagari letters incl. marks), numbers, URLs/emails/handles, punctuation. +TOKEN_RE = re.compile( + r"(https?://\S+|www\.\S+|[\w.+-]+@[\w-]+\.[\w.]+|[@#][\w_]+" + r"|[A-Za-z]+(?:'[A-Za-z]+)?" + r"|[\u0900-\u0963\u0971-\u097F]+" + r"|[0-9\u0966-\u096F]+(?:[.,:][0-9\u0966-\u096F]+)*" + r"|[\u0964\u0965]|[^\sA-Za-z0-9\u0900-\u097F])" +) + + +def tokenize(s: str): + return TOKEN_RE.findall(s) + + +PUNCT_RE = re.compile(r"[^\w\u0900-\u097F]+", re.UNICODE) + + +# --- scoring normalisation -------------------------------------------------- +_DIGIT_TR = str.maketrans(DEVA_DIGITS, "0123456789") + + +def norm_for_score(s: str) -> str: + """'norm' scoring view: NFC, no zero-width chars, Devanagari digits -> ASCII, punctuation removed + (incl. danda), whitespace collapsed. Spelling is otherwise untouched (strict on letters).""" + s = clean(s).translate(_DIGIT_TR) + s = re.sub(r"[^\w\s\u0900-\u097F]|[\u0964\u0965]", " ", s) + return _WS_RE.sub(" ", s).strip() + + +_FOLD_MAP = { + "\u0901": "\u0902", # chandrabindu -> anusvara (हूँ ~ हूं) + NUKTA: "", # drop nukta (ज़ ~ ज) + "\u0945": "\u0947", # candra e (ॅ) -> e + "\u0949": "\u094b", # candra o (ॉ) -> o (ऑफिस ~ ओफिस) + "\u0911": "\u0913", # independent candra O -> O + "\u090d": "\u090f", +} + + +def fold(s: str) -> str: + """'fold' scoring view (variant-tolerant, reported separately): norm_for_score + nukta dropped, + chandrabindu->anusvara, candra vowels -> plain. Never used as the primary metric.""" + s = unicodedata.normalize("NFD", norm_for_score(s)) # decompose precomposed nukta letters + s = "".join(_FOLD_MAP.get(ch, ch) for ch in s) + return unicodedata.normalize("NFC", s) diff --git a/python/bhc/translit2.py b/python/bhc/translit2.py new file mode 100644 index 0000000000000000000000000000000000000000..bb8d7c5d07efa5bf4e2dc29f204622d00d679745 --- /dev/null +++ b/python/bhc/translit2.py @@ -0,0 +1,182 @@ +"""BhashaCore Track B v2 (commercially-usable data only): distilled char Transformer (4/8-bit weights) -> +candidates (free beam + lexicon-constrained beam + lexicon-gated ऑ/ः/nukta variants + letter-name acronym) +-> linear reranker (weights fit on Aksharantar VALID only) for word mode; Viterbi with the IndicCorp bigram LM +for sentence mode. Assets dir: xlit.q4|xlit.q8|xlit.int8, lex.bin, lm.bin, params.json.""" +import json, math, os, re, sys +import torch +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")); sys.path.insert(0, ROOT) +from bhc.xlit_model import beam_search_fast as beam_search, score_pairs +from bhc.xlit_q import load_any +from bhc.lexlm import LexLM +from bhc.lextrie import LazyTrie +from bhc.text import TOKEN_RE +from bhc.skel import SkelIndex, rom_skels, dev_skel, dev_rom, lev +from bhc.casual import CasualIndex, ABBR, squeeze_in + +LATIN = re.compile(r"[A-Za-z]+$") +LETTER = dict(a="ए", b="बी", c="सी", d="डी", e="ई", f="एफ", g="जी", h="एच", i="आई", j="जे", k="के", l="एल", m="एम", n="एन", + o="ओ", p="पी", q="क्यू", r="आर", s="एस", t="टी", u="यू", v="वी", w="डब्ल्यू", x="एक्स", y="वाई", z="जेड") +FEATS = ["sc", "scn", "inlex", "uni", "free0", "infree", "incon", "aw", "visarga", "nukta", "chandra", "acro", "variant", "frank", + "skel", "skmatch", "lev", "sc_rel", "scn_rel", "uni_rel", "lenr", "visr", "hfin", "acro_nv", + "ck1", "ck2", "ckr", "abbr", "abbr_r", "novow"] # v2 casual features (append-only; older params use the first len(mu)) +VIS_RE = re.compile(r"[aeiou]h$") +VAR_SWAPS = [("ॉ", "ो"), ("ो", "ॉ"), ("ा", "ॉ"), ("ॉ", "ा"), ("ः", ""), ("फ़", "फ"), ("फ", "फ़"), ("ज़", "ज"), ("ज", "ज़"), ("ँ", "ं"), ("ं", "ँ")] +DEFAULTS = dict(beam=8, nbest=8, cbeam=8, variants=True, acro=True, w=None, mu=None, sd=None, + w_lm=0.8, oov_pen_s=4.0, w_ch_s=1.0, use_lm=True, + skel=False, skel_k=200, skel_m=16, skel_wu=0.03, visr=False, acro_v=False, + casual=False, cas_top=300000, cas_n1=6, cas_n2=4, abbr=False, squeeze=0, k1gate=99, k2gate=99) # skel/visr on via params.json (v3 assets) + + +class BhcTranslit2: + def __init__(self, asset_dir, threads=1, use_dict=True, **over): + """asset_dir: flat dir (model + params.json [+ lex.bin + lm.bin]) or a release bundle: core (model + params.json) + with the dictionary pack in asset_dir/dict/ (lex.bin, lm.bin, params.json overriding the core params). + use_dict=False (or no lexicon present) -> core-only mode: free beam + rule candidates, core reranker.""" + torch.set_num_threads(threads); self.dir = asset_dir; self.p = dict(DEFAULTS) + if os.path.exists(f"{asset_dir}/params.json"): self.p.update(json.load(open(f"{asset_dir}/params.json"))) + pack = f"{asset_dir}/dict"; self.dict_dir = None + if use_dict and os.path.exists(f"{pack}/lex.bin"): + self.dict_dir = pack + if os.path.exists(f"{pack}/params.json"): self.p.update(json.load(open(f"{pack}/params.json"))) + self.p.update(over) + mf = [f for f in ("xlit.q4", "xlit.q8", "xlit.int8", "model.pt") if os.path.exists(f"{asset_dir}/{f}")] + self.model_file = f"{asset_dir}/{self.p.get('model_file', mf[0])}"; self.model = load_any(self.model_file) + ld = self.dict_dir or asset_dir; lm, lx = f"{ld}/lm.bin", f"{ld}/lex.bin" + if os.path.exists(lx) and use_dict: self.lm = LexLM(lx, lm if os.path.exists(lm) and self.p["use_lm"] else None) + else: self.lm = LexLM(None) # core-only: no dictionary pack -> no constrained beam / skeleton index / variants + if not self.lm.N: self.p.update(cbeam=0, skel=False) + self.trie = LazyTrie(self.lm.words) if self.p["cbeam"] else None + self.skix = SkelIndex(self.lm.words, self.lm.unigram) if self.p.get("skel") else None + self.cix = CasualIndex(self.lm.words, self.lm.unigram, self.p["cas_top"]) if self.p.get("casual") and self.lm.N else None + self.name = "bhc-xlit2" + + # ---------------- candidates with features + def cand_feats(self, words): + p = self.p; lw = [squeeze_in(w.lower(), p["squeeze"]) for w in words]; todo = sorted(set(lw)); n = len(todo) + if p["cbeam"]: + r = beam_search(self.model, todo + todo, beam=p["beam"], nbest=p["nbest"], trie=self.trie, constrain=[False] * n + [True] * n) + free, con = r[:n], r[n:] + else: + free = beam_search(self.model, todo, beam=p["beam"], nbest=p["nbest"]); con = [[] for _ in todo] + res = {}; need = [] + for w, f, c in zip(todo, free, con): + d = {} + for i, (s, sc) in enumerate(f): + if s: d[s] = dict(sc=sc, infree=1, frank=i, free0=int(i == 0)) + for s, sc in c: + if s: + e = d.setdefault(s, dict(sc=sc, infree=0, frank=p["nbest"], free0=0)); e["incon"] = 1 + if p["variants"]: + for s in sorted(d, key=lambda x: -d[x]["sc"])[:4]: + for a, b in VAR_SWAPS: + if a in s: + v = s.replace(a, b) + if v not in d and v in self.lm.id: d[v] = dict(sc=None, infree=0, frank=p["nbest"], free0=0, variant=1); need.append((w, v)) + if p["acro"] and (re.fullmatch(r"[b-df-hj-np-tv-z]{2,7}", w) or (p.get("acro_v") and 2 <= len(w) <= 4)): + v = "".join(LETTER[ch] for ch in w) + if v not in d: d[v] = dict(sc=None, infree=0, frank=p["nbest"], free0=0, acro=1); need.append((w, v)) + else: d[v]["acro"] = 1 + if p.get("visr") and VIS_RE.search(w): # Aksharantar convention: literal word-final h -> visarga + for s in sorted((x for x in d if d[x]["sc"] is not None), key=lambda x: -d[x]["sc"])[:2]: # beam entries only + v = s[:-1] + "ः" if s.endswith("ह") else (s + "ः" if not s.endswith("ः") else None) + if v and v not in d: d[v] = dict(sc=None, infree=0, frank=p["nbest"], free0=0, visr=1); need.append((w, v)) + if self.skix is not None: + ids = self.skix.lookup(w, p["skel_k"]); L = len(w) + 1 + ids = sorted(ids, key=lambda i: lev(w, dev_rom(self.lm.words[i])) / L - p["skel_wu"] * self.lm.unigram(i))[:p["skel_m"]] + for i in ids: + v = self.lm.words[i] + if v not in d: d[v] = dict(sc=None, infree=0, frank=p["nbest"], free0=0, skel=1); need.append((w, v)) + else: d[v]["skel"] = 1 + if self.cix is not None: + r1, r2 = self.cix.lookup(w, p["cas_n1"], p["cas_n2"]); nv = sum(ch in "aeiou" for ch in w) + ab = set(ABBR.get(w, ())) if p.get("abbr") else set(); s1 = {self.lm.words[i] for i in r1[:p["cas_n1"]]} + for key, ids, n_ in (("ck1", r1, p["cas_n1"]), ("ck2", r2, p["cas_n2"])): + for rk, i in enumerate(ids[:n_]): + v = self.lm.words[i] + if v not in d: + if v not in ab and ((v in s1 and nv > p["k1gate"]) or (v not in s1 and nv > p["k2gate"])): continue # vowel gate + d[v] = dict(sc=None, infree=0, frank=p["nbest"], free0=0); need.append((w, v)) + d[v][key] = 1; d[v]["ckr"] = min(d[v].get("ckr", 1.0), rk / n_) + if p.get("abbr") and w in ABBR: + for rk, v in enumerate(ABBR[w]): + if v not in d: d[v] = dict(sc=None, infree=0, frank=p["nbest"], free0=0); need.append((w, v)) + d[v]["abbr"] = 1; d[v]["abbr_r"] = rk + res[w] = d + if need: + for (w, v), sc in zip(need, score_pairs(self.model, [x[0] for x in need], [x[1] for x in need])): res[w][v]["sc"] = sc + out = []; memo = {} + for w in lw: + if w in memo: out.append(memo[w]); continue + rows = []; sks = set(rom_skels(w)); L = len(w) + 1; vis = bool(VIS_RE.search(w)) + for s, e in res[w].items(): + wid = self.lm.id.get(s) + fv = [e["sc"], e["sc"] / (len(s) + 1), int(wid is not None), self.lm.unigram(wid) if wid is not None else 0.0, + e.get("free0", 0), e.get("infree", 0), e.get("incon", 0), int("ॉ" in s or "ऑ" in s), int("ः" in s), + int("़" in s), int("ँ" in s), e.get("acro", 0), e.get("variant", 0), e.get("frank", p["nbest"]) / p["nbest"], + e.get("skel", 0), int(dev_skel(s) in sks), lev(w, dev_rom(s)) / L, 0.0, 0.0, 0.0, len(s) / L, + e.get("visr", 0), int(vis and s.endswith("ः")), int(e.get("acro", 0) and not re.search("[aeiou]", w)), + e.get("ck1", 0), e.get("ck2", 0), e.get("ckr", 1.0), e.get("abbr", 0), e.get("abbr_r", 3), int(not re.search("[aeiouy]", w))] + rows.append((s, fv, wid)) + if rows: + msc = max(r[1][0] for r in rows); mscn = max(r[1][1] for r in rows) + mu = max((r[1][3] for r in rows if r[1][2]), default=0.0) + for _, fv, _ in rows: fv[17] = fv[0] - msc; fv[18] = fv[1] - mscn; fv[19] = fv[3] - mu if fv[2] else 0.0 + memo[w] = rows; out.append(rows) + return out + + def score(self, fv): + p = self.p + if not p.get("w1") and p["w"] is None: return fv[0] + 0.2 * (fv[3] if fv[2] else -25.0) # untuned fallback + z = [(x - m) / s for x, m, s in zip(fv, p["mu"], p["sd"])] # zip truncates: old 24-feature params ignore v2 features + if p.get("w1"): # 1-hidden-layer tanh MLP reranker (fit on VALID only): w2 . tanh(W1 z + b1) + b2 + return sum(v * math.tanh(sum(a * b for a, b in zip(row, z)) + c) for v, row, c in zip(p["w2"], p["w1"], p["b1"])) + p.get("b2", 0.0) + return sum(w * x for w, x in zip(p["w"], z)) + + def translit_words(self, words, k=8): + out = [] + for w, rows in zip(words, self.cand_feats(words)): + if not LATIN.match(w) or not rows: out.append([w]); continue + out.append([s for s, _, _ in sorted(rows, key=lambda r: -self.score(r[1]))[:k]]) + return out + + # ---------------- sentence mode: Viterbi over candidates with the bigram LM + def decode_slots(self, slots): + xs = [t for kind, t in slots if kind == "x"]; cl = iter(self.cand_feats(xs)) if xs else iter(()) + p = self.p; lm = self.lm; lat = [] + for kind, t in slots: + if kind == "x": + rows = next(cl); sc = [(s, self.score(fv), wid) for s, fv, wid in rows] + if sc: + mx = max(x[1] for x in sc); lse = mx + math.log(sum(math.exp(x[1] - mx) for x in sc)) + sc = sorted(((s, p["w_ch_s"] * (v - lse), wid) for s, v, wid in sc), key=lambda x: -x[1])[:12] + lat.append(sc or [(t, 0.0, None)]) + else: + lat.append([(t, 0.0, lm.id.get(t, -1) if re.match(r"[\u0900-\u0963\u0971-\u097F]+$", t) else -2)]) + if not p["use_lm"] or not lm.bi: + return [max(o, key=lambda x: x[1])[0] for o in lat] + beams = [(0.0, None, [])] + for opts in lat: + if opts[0][2] == -2 and len(opts) == 1: + beams = [(s, h, ch + [opts[0][0]]) for s, h, ch in beams]; continue + best = {} + for s, h, ch in beams: + for cand, csc, wid in opts: + w = None if wid in (None, -1) else wid + tot = s + csc + p["w_lm"] * (lm.logp(h, w) - (p["oov_pen_s"] if w is None else 0.0)) + if cand not in best or tot > best[cand][0]: best[cand] = (tot, w, ch + [cand]) + beams = sorted(best.values(), key=lambda x: -x[0])[:16] + return max(beams, key=lambda b: b[0] + p["w_lm"] * lm.logp(b[1], lm.N))[2] + + def convert(self, text, policy=None): + spans = [(m.start(), m.end(), m.group()) for m in TOKEN_RE.finditer(text)] + slots = [("x", t) if LATIN.match(t) else ("fix", t) for _, _, t in spans] + out = self.decode_slots(slots) + if out and out[-1] == ".": out[-1] = "।" + s, pos = [], 0 + for (a, b, _), o in zip(spans, out): s.append(text[pos:a]); s.append(o); pos = b + return "".join(s) + text[pos:] + + def asset_files(self): + fs = [self.model_file, f"{self.dir}/lex.bin", f"{self.dir}/params.json"] + if self.p["use_lm"] and os.path.exists(f"{self.dir}/lm.bin"): fs.append(f"{self.dir}/lm.bin") + return [f for f in fs if os.path.exists(f)] diff --git a/python/bhc/translit3.py b/python/bhc/translit3.py new file mode 100644 index 0000000000000000000000000000000000000000..363103e2a6a1d2259e44abbe954e56c20c99cf23 --- /dev/null +++ b/python/bhc/translit3.py @@ -0,0 +1,86 @@ +"""AksharaLP v3 runtime (Python reference): v2 candidates + reranker (BhcTranslit2), then sentence-level rescoring of the +candidate lattice with (a) the v2 bigram LM, (b) a tiny neural sentence LM over Devanagari subwords (bhc/nlm.py) and +(c) agreement/grammar consistency features (bhc/agree.py). HARD guardrail: the search only ever picks a word from the +candidate list produced for that typed roman token (lattice), never anything else. +Lattice slot = list of (cand, channel_logsoftmax, wid); fixed slots (punctuation, digits, Devanagari) pass through.""" +import math, os, re, json +from bhc.translit2 import BhcTranslit2, LATIN +from bhc.text import TOKEN_RE + +DEV_RE = re.compile(r"[\u0900-\u0963\u0971-\u097F]+$") +NUM_RE = re.compile(r"[0-9\u0966-\u096F]+$") +V3DEF = dict(lat_k=12, beam_s=16, w_nlm=0.0, w_agr=0.0, w_agn=0.0, nlm_file="nlm.q8", nlm_margin=99.0, nlm_k=12, rec_n=3) + + +class BhcTranslit3(BhcTranslit2): + def __init__(self, asset_dir, threads=1, use_dict=True, nlm=None, **over): + super().__init__(asset_dir, threads=threads, use_dict=use_dict, **{k: v for k, v in over.items() if k not in V3DEF}) + for k, v in V3DEF.items(): self.p.setdefault(k, v) + self.p.update({k: v for k, v in over.items() if k in V3DEF}) + self.nlm = nlm + if self.nlm is None: + d = self.dict_dir or asset_dir; f = f"{d}/{self.p['nlm_file']}" + if os.path.exists(f): + from bhc.nlm import NLM + self.nlm = NLM.load(f) + self.name = "bhc-xlit3" + + def build_lattice(self, slots, k=None): + k = k or self.p["lat_k"]; xs = [t for kind, t in slots if kind == "x"] + cl = iter(self.cand_feats(xs)) if xs else iter(()); lat = [] + for kind, t in slots: + if kind == "x": + rows = next(cl); sc = [(s, self.score(fv), wid) for s, fv, wid in rows] + if sc: + mx = max(x[1] for x in sc); lse = mx + math.log(sum(math.exp(x[1] - mx) for x in sc)) + sc = sorted(((s, v - lse, wid) for s, v, wid in sc), key=lambda x: -x[1])[:k] + lat.append(("x", sc or [(t, 0.0, None)])) + else: + lat.append(("fix", [(t, 0.0, self.lm.id.get(t, -1) if DEV_RE.match(t) else -2)])) + return lat + + def search(self, lat): + """Beam search over the lattice. score = sum w_ch_s*channel + w_lm*bigram(-oov) + w_nlm*nlm_logp + w_agr*agree.""" + from bhc.agree import agree_feats + p = self.p; lm = self.lm; use_bi = p["use_lm"] and lm.bi; nlm = self.nlm if p["w_nlm"] else None + # beam item: (score, prev_wid, words(list), nlm_state) + beams = [(0.0, None, [], nlm.init_state() if nlm else None)] + for kind, opts in lat: + if kind == "fix": + t, _, wid = opts[0] + if wid == -2: # punctuation / digits / latin: no LM context change except for numbers in the NLM + if nlm and NUM_RE.match(t): + st = nlm.advance_many([b[3] for b in beams], [""]) + beams = [(s, h, ch + [t], st[i][0]) for i, (s, h, ch, _) in enumerate(beams)] + else: + beams = [(s, h, ch + [t], ns) for s, h, ch, ns in beams] + continue + opts = [(t, 0.0, wid)] + opts = opts[:p["lat_k"]] + if nlm: + cand = [o for o in opts if o[1] >= opts[0][1] - p["nlm_margin"]][:p["nlm_k"]] + lp, nst = nlm.score_words([b[3] for b in beams], [c[0] for c in cand]) # lp[i][j], nst[i][j] + else: + cand = opts + best = {} + for i, (s, h, ch, ns) in enumerate(beams): + for j, (c, csc, wid) in enumerate(cand): + w = None if wid in (None, -1) else wid + tot = s + p["w_ch_s"] * csc + if use_bi: tot += p["w_lm"] * (lm.logp(h, w) - (p["oov_pen_s"] if w is None else 0.0)) + if nlm: tot += p["w_nlm"] * lp[i][j] + words = ch + [c] + if p["w_agr"] or p["w_agn"]: + ap_, an_ = agree_feats(words, len(words) - 1); tot += p["w_agr"] * ap_ - p["w_agn"] * an_ + key = c if not (nlm or p["w_agr"] or p["w_agn"]) else tuple(words[-p["rec_n"]:]) # v2-exact recombination for the bigram-only case + if key not in best or tot > best[key][0]: best[key] = (tot, w, words, nst[i][j] if nlm else None) + beams = sorted(best.values(), key=lambda x: -x[0])[:p["beam_s"]] + def final(b): + f = b[0] + if use_bi: f += p["w_lm"] * lm.logp(b[1], lm.N) + if nlm: f += p["w_nlm"] * nlm.eos_logp(b[3]) + return f + return max(beams, key=final)[2] + + def decode_slots(self, slots): + return self.search(self.build_lattice(slots)) diff --git a/python/bhc/xlit_model.py b/python/bhc/xlit_model.py new file mode 100644 index 0000000000000000000000000000000000000000..44a0cf0dfbfb3980769070678fa35c51eebdc6d7 --- /dev/null +++ b/python/bhc/xlit_model.py @@ -0,0 +1,359 @@ +"""BhashaCore Track B candidate generator: tiny character-level Transformer (Roman -> Devanagari). + +Pre-LN encoder/decoder, learned positions, tied decoder input/output embeddings. Inference = batched beam +search over all words of a sentence at once, with an incremental (cached) decoder, optionally constrained to a +Devanagari lexicon trie. Weights are stored as INT8 (per-output-row symmetric scales) + fp16 for small vectors +and de-quantised to fp32 at load time (compute is fp32; storage is INT8). +""" +import io, gzip, json, math, os +import numpy as np +import torch, torch.nn as nn, torch.nn.functional as F + +ROMAN = "abcdefghijklmnopqrstuvwxyz" +PAD, BOS, EOS, UNK = 0, 1, 2, 3 +SPECIAL = ["", "", "", ""] +# Target-script blocks, one full Unicode block per language (hi exclusive), same policy as Hindi: keep the whole +# block rather than only the observed characters, because the cost is one embedding row per unused code point. +TGT_BLOCK = { + "hi": (0x0900, 0x0980), # Devanagari + "bn": (0x0980, 0x0A00), # Bengali + "ta": (0x0B80, 0x0C00), # Tamil + "te": (0x0C00, 0x0C80), # Telugu + "kn": (0x0C80, 0x0D00), # Kannada +} +# Characters observed in the language's Aksharantar data that fall OUTSIDE its block (checked over train+valid+test +# by research/multi/check_blocks.py; only Bengali needed one: the Devanagari danda U+0964, 5 occurrences). +TGT_EXTRA = {"hi": [], "bn": ["\u0964"], "te": [], "kn": [], "ta": []} +DEVA_CHARS = [chr(c) for c in range(0x0900, 0x0980)] # full block (128), most unused but cheap +SRC_VOCAB = ["", "", "", ""] + list(ROMAN) +S2I = {c: i for i, c in enumerate(SRC_VOCAB)} +TGT_LANG = "hi" # the language TGT_VOCAB/T2I currently hold +TGT_VOCAB = SPECIAL + DEVA_CHARS +T2I = {c: i for i, c in enumerate(TGT_VOCAB)} +MAXLEN = 32 + + +def tgt_chars(lang="hi"): + """Target vocabulary characters for `lang`: its full Unicode block + any observed char outside it. 'hi' returns + exactly the historical Devanagari list, so Hindi vocabularies, checkpoints and releases are unaffected.""" + lo, hi = TGT_BLOCK[lang] + return [chr(c) for c in range(lo, hi)] + list(TGT_EXTRA.get(lang, ())) + + +def set_vocab(lang="hi"): + """Switch the module-level target vocabulary to `lang` (default restores Hindi). enc_tgt/dec_tgt/beam_search read + TGT_VOCAB/T2I at call time, so build the model AFTER calling this; models are per-language and one process should + hold one language at a time. Returns the new TGT_VOCAB.""" + global TGT_LANG, TGT_VOCAB, T2I + if lang not in TGT_BLOCK: raise ValueError(f"no target block for {lang!r}; known: {sorted(TGT_BLOCK)}") + TGT_LANG = lang; TGT_VOCAB = SPECIAL + tgt_chars(lang) + T2I = {c: i for i, c in enumerate(TGT_VOCAB)} + return TGT_VOCAB + + +def enc_src(w): return [S2I.get(c, UNK) for c in w.lower()[:MAXLEN - 1]] + [EOS] +def enc_tgt(w): return [T2I.get(c, UNK) for c in w[:MAXLEN - 2]] + [EOS] +def dec_tgt(ids): return "".join(TGT_VOCAB[i] for i in ids if i > UNK) + + +class MHA(nn.Module): + def __init__(s, d, h): + super().__init__(); s.h = h; s.dk = d // h + s.q = nn.Linear(d, d); s.kv = nn.Linear(d, 2 * d); s.o = nn.Linear(d, d) + + def forward(s, x, mem, mask=None, cache=None): + B, T, D = x.shape + q = s.q(x).view(B, T, s.h, s.dk).transpose(1, 2) + if cache is not None and "k" in cache and cache.get("static"): + k, v = cache["k"], cache["v"] + else: + k, v = s.kv(mem).view(B, -1, 2, s.h, s.dk).permute(2, 0, 3, 1, 4) + if cache is not None: + if "k" in cache: # self-attn incremental + k = torch.cat([cache["k"], k], 2); v = torch.cat([cache["v"], v], 2) + cache["k"], cache["v"] = k, v + if mask is not None and mask.dtype != q.dtype: mask = mask.to(q.dtype) + a = F.scaled_dot_product_attention(q, k, v, attn_mask=mask) + return s.o(a.transpose(1, 2).reshape(B, T, D)) + + +class EncLayer(nn.Module): + def __init__(s, d, h, ff, p): + super().__init__(); s.n1 = nn.LayerNorm(d); s.att = MHA(d, h); s.n2 = nn.LayerNorm(d) + s.ff = nn.Sequential(nn.Linear(d, ff), nn.GELU(), nn.Linear(ff, d)); s.dr = nn.Dropout(p) + + def forward(s, x, mask): + y = s.n1(x); x = x + s.dr(s.att(y, y, mask)); return x + s.dr(s.ff(s.n2(x))) + + +class DecLayer(nn.Module): + def __init__(s, d, h, ff, p): + super().__init__(); s.n1 = nn.LayerNorm(d); s.sa = MHA(d, h); s.n2 = nn.LayerNorm(d); s.ca = MHA(d, h) + s.n3 = nn.LayerNorm(d); s.ff = nn.Sequential(nn.Linear(d, ff), nn.GELU(), nn.Linear(ff, d)); s.dr = nn.Dropout(p) + + def forward(s, x, mem, smask, mmask, cache=None): + y = s.n1(x); x = x + s.dr(s.sa(y, y, smask, None if cache is None else cache["sa"])) + x = x + s.dr(s.ca(s.n2(x), mem, mmask, None if cache is None else cache["ca"])) + return x + s.dr(s.ff(s.n3(x))) + + +class XlitTransformer(nn.Module): + def __init__(s, d=128, h=4, enc=3, dec=2, ff=384, p=0.1, vocab=None): + """vocab: target script code ('bn', 'te', 'kn', 'ta'); None/'hi' keeps the historical Devanagari vocabulary and + leaves cfg without a "vocab" key, so every existing Hindi checkpoint and INT8 blob loads unchanged.""" + super().__init__() + if vocab not in (None, "hi"): set_vocab(vocab) + s.cfg = dict(d=d, h=h, enc=enc, dec=dec, ff=ff, p=p) + if vocab is not None: s.cfg["vocab"] = vocab + s.semb = nn.Embedding(len(SRC_VOCAB), d, padding_idx=PAD); s.temb = nn.Embedding(len(TGT_VOCAB), d, padding_idx=PAD) + s.spos = nn.Embedding(MAXLEN, d); s.tpos = nn.Embedding(MAXLEN, d) + s.enc = nn.ModuleList(EncLayer(d, h, ff, p) for _ in range(enc)); s.dec = nn.ModuleList(DecLayer(d, h, ff, p) for _ in range(dec)) + s.en = nn.LayerNorm(d); s.dn = nn.LayerNorm(d); s.scale = math.sqrt(d) + for e in (s.semb, s.temb): nn.init.normal_(e.weight, 0, d ** -0.5) + nn.init.normal_(s.spos.weight, 0, 0.02); nn.init.normal_(s.tpos.weight, 0, 0.02) + + def encode(s, src): + B, S = src.shape; pad = src == PAD + x = s.semb(src) * s.scale + s.spos(torch.arange(S, device=src.device))[None] + m = torch.zeros(B, 1, 1, S, device=src.device).masked_fill(pad[:, None, None, :], float("-inf")) + for l in s.enc: x = l(x, m) + return s.en(x), m + + def logits(s, x): return x @ s.temb.weight.t() + + def forward(s, src, tgt_in): + mem, mm = s.encode(src); B, T = tgt_in.shape + x = s.temb(tgt_in) * s.scale + s.tpos(torch.arange(T, device=tgt_in.device))[None] + causal = torch.full((T, T), float("-inf"), device=tgt_in.device).triu(1) + for l in s.dec: x = l(x, mem, causal, mm) + return s.logits(s.dn(x)) + + @torch.no_grad() + def step(s, tok, pos, mem, mm, caches): + x = s.temb(tok) * s.scale + s.tpos.weight[pos][None, None] + for l, c in zip(s.dec, caches): x = l(x, mem, None, mm, c) + return F.log_softmax(s.logits(s.dn(x))[:, -1], -1) + + def n_params(s): return sum(p.numel() for p in s.parameters()) + + +# ------------------------------------------------------------------ lexicon trie (for constrained decoding) +class Trie: + """Character trie over a word list, stored as flat arrays; node 0 = root. child(node, tok) lookup via dict.""" + def __init__(s, words): + s.kids = [{}]; s.final = [False] + for w in words: + n = 0 + for c in w: + t = T2I.get(c) + if t is None: break + nx = s.kids[n].get(t) + if nx is None: + nx = len(s.kids); s.kids[n][t] = nx; s.kids.append({}); s.final.append(False) + n = nx + else: + s.final[n] = True + + +# ------------------------------------------------------------------ beam search +def ensure_vocab(model): + """Point the module target vocabulary at the model's own script when the model declares one (cfg["vocab"]). + Models without the key (every Hindi checkpoint, INT8 blob and the shared-vocab research probes) are left alone.""" + v = getattr(model, "cfg", {}).get("vocab") + if v and v != TGT_LANG: set_vocab(v) + if model.temb.num_embeddings != len(TGT_VOCAB): + raise RuntimeError(f"vocab mismatch: model has {model.temb.num_embeddings} target symbols, module has {len(TGT_VOCAB)}") + return model + + +@torch.no_grad() +def beam_search(model, words, beam=8, nbest=8, trie=None, maxlen=None, constrain=None): + """Return per word a list of (devanagari, logprob) best first. All words decoded in one batch. + trie: constrain outputs to lexicon words (EOS allowed only at final nodes); constrain[i] selects which rows + are constrained (default: all rows when a trie is given).""" + if not words: return [] + ensure_vocab(model) + src = [enc_src(w) for w in words]; S = max(map(len, src)) + src = torch.tensor([x + [PAD] * (S - len(x)) for x in src]) + mem, mm = model.encode(src); W = len(words); K = beam + mem = mem.repeat_interleave(K, 0); mm = mm.repeat_interleave(K, 0) + caches = [{"sa": {}, "ca": {"static": False}} for _ in model.dec] + # first step: only beam 0 alive + score = torch.full((W, K), float("-inf")); score[:, 0] = 0 + tok = torch.full((W * K, 1), BOS); seqs = [[[] for _ in range(K)] for _ in range(W)] + if trie and constrain is None: constrain = [True] * len(words) + nodes = [[0 if constrain[i] else None] * K for i in range(W)] if trie else None + done = [[] for _ in range(W)]; V = len(TGT_VOCAB) + L = maxlen or min(MAXLEN - 1, S + 8) + for t in range(L): + lp = model.step(tok, t, mem, mm, caches) + if t == 0: + for c in caches: c["ca"]["static"] = True + lp = lp.view(W, K, V) + if trie: + mask = torch.zeros((W, K, V)) + for i in range(W): + if not constrain[i]: continue + mask[i] = float("-inf") + for k in range(K): + n = nodes[i][k] + if n is None: continue + ks = list(trie.kids[n].keys()) + if ks: mask[i, k, ks] = 0 + if trie.final[n]: mask[i, k, EOS] = 0 + lp = lp + mask + lp[:, :, PAD] = float("-inf"); lp[:, :, BOS] = float("-inf"); lp[:, :, UNK] = float("-inf") + tot = (score[:, :, None] + lp).view(W, -1) + top, idx = tot.topk(2 * K, -1) + new_score = torch.full((W, K), float("-inf")); reorder = torch.zeros(W, K, dtype=torch.long); new_tok = torch.full((W, K), EOS) + new_seqs = [[[] for _ in range(K)] for _ in range(W)]; new_nodes = [[None] * K for _ in range(W)] if trie else None + alive_any = False + for i in range(W): + j = 0 + for sc, ix in zip(top[i].tolist(), idx[i].tolist()): + if sc == float("-inf"): break + b, v = divmod(ix, V) + if v == EOS: + if len(done[i]) < nbest: done[i].append((dec_tgt(seqs[i][b]), sc)) + continue + if j < K: + new_score[i, j] = sc; reorder[i, j] = i * K + b; new_tok[i, j] = v + new_seqs[i][j] = seqs[i][b] + [v] + if trie: new_nodes[i][j] = trie.kids[nodes[i][b]][v] if constrain[i] else None + j += 1 + # stop this word if enough finished and best alive can't beat worst finished + if len(done[i]) >= nbest and (j == 0 or new_score[i, 0].item() < min(x[1] for x in done[i])): + new_score[i] = float("-inf") + elif j: alive_any = True + if not alive_any: break + flat = reorder.view(-1) + for c in caches: + c["sa"]["k"] = c["sa"]["k"][flat]; c["sa"]["v"] = c["sa"]["v"][flat] + score = new_score; seqs = new_seqs; nodes = new_nodes + tok = torch.cat([tok[flat], new_tok.view(-1, 1)], 1)[:, -1:] + out = [] + for i in range(W): + d = {} + for s_, sc in done[i]: + if s_ not in d or sc > d[s_]: d[s_] = sc + out.append(sorted(d.items(), key=lambda x: -x[1])[:nbest]) + return out + + +# ------------------------------------------------------------------ INT8 export / load +def export_int8(model, path): + """Linear/embedding matrices -> int8 with per-row fp16 scale; vectors (bias, LN) -> fp16. Single .npz-like blob.""" + sd = model.state_dict(); meta = {"cfg": model.cfg, "tensors": []}; buf = io.BytesIO() + for k, v in sd.items(): + v = v.float().numpy() + if v.ndim == 2: + sc = np.abs(v).max(1, keepdims=True) / 127.0; sc[sc == 0] = 1 + q = np.clip(np.round(v / sc), -127, 127).astype(np.int8) + meta["tensors"].append([k, list(v.shape), "i8", buf.tell()]); buf.write(q.tobytes()); buf.write(sc.astype(np.float16).tobytes()) + else: + meta["tensors"].append([k, list(v.shape), "f16", buf.tell()]); buf.write(v.astype(np.float16).tobytes()) + hdr = json.dumps(meta, separators=(",", ":")).encode() + with open(path, "wb") as f: + f.write(len(hdr).to_bytes(4, "little")); f.write(hdr); f.write(buf.getvalue()) + + +def load_int8(path): + b = open(path, "rb").read(); n = int.from_bytes(b[:4], "little"); meta = json.loads(b[4:4 + n]); data = b[4 + n:] + m = XlitTransformer(**meta["cfg"]); sd = {} + for k, shape, kind, off in meta["tensors"]: + cnt = int(np.prod(shape)) + if kind == "i8": + q = np.frombuffer(data, np.int8, cnt, off).reshape(shape).astype(np.float32) + sc = np.frombuffer(data, np.float16, shape[0], off + cnt).astype(np.float32)[:, None] + sd[k] = torch.from_numpy(q * sc) + else: + sd[k] = torch.from_numpy(np.frombuffer(data, np.float16, cnt, off).astype(np.float32).reshape(shape)) + m.load_state_dict(sd); m.eval(); return m + + +@torch.no_grad() +def score_pairs(model, srcs, tgts): + """Teacher-forced log P(tgt | src) for each pair (batched).""" + if not srcs: return [] + ensure_vocab(model) + s = [enc_src(w) for w in srcs]; t = [enc_tgt(w) for w in tgts]; S = max(map(len, s)); T = max(map(len, t)) + src = torch.tensor([x + [PAD] * (S - len(x)) for x in s]); tin = torch.tensor([[BOS] + x[:-1] + [PAD] * (T - len(x)) for x in t]) + tout = torch.tensor([x + [PAD] * (T - len(x)) for x in t]) + lp = F.log_softmax(model(src, tin), -1).gather(-1, tout[..., None])[..., 0] + return (lp * (tout != PAD)).sum(-1).tolist() + + +@torch.no_grad() +def beam_search_fast(model, words, beam=8, nbest=8, trie=None, maxlen=None, constrain=None): + """Same search as beam_search (identical results), with the per-candidate bookkeeping vectorised: + hypotheses kept as a token-history tensor; only EOS hits and trie-node updates touch Python.""" + if not words: return [] + ensure_vocab(model) + src = [enc_src(w) for w in words]; S = max(map(len, src)) + src = torch.tensor([x + [PAD] * (S - len(x)) for x in src]) + mem, mm = model.encode(src); W = len(words); K = beam; V = len(TGT_VOCAB) + mem = mem.repeat_interleave(K, 0); mm = mm.repeat_interleave(K, 0) + caches = [{"sa": {}, "ca": {"static": False}} for _ in model.dec] + score = torch.full((W, K), float("-inf")); score[:, 0] = 0 + tok = torch.full((W * K, 1), BOS); hist = torch.zeros((W * K, 0), dtype=torch.long) + if trie and constrain is None: constrain = [True] * W + crow = [i for i in range(W) if trie and constrain[i]] + nodes = {i: [0] * K for i in crow} + if trie and not hasattr(trie, "_allow"): trie._allow = {} + done = [[] for _ in range(W)]; alive = [True] * W + L = maxlen or min(MAXLEN - 1, S + 8); NEG = float("-inf") + for t in range(L): + lp = model.step(tok, t, mem, mm, caches) + if t == 0: + for c in caches: c["ca"]["static"] = True + lp = lp.view(W, K, V) + if crow: + mask = torch.full((len(crow), K, V), NEG) + for j, i in enumerate(crow): + for k in range(K): + n = nodes[i][k] + if n is None: continue + a = trie._allow.get(n) + if a is None: + a = list(trie.kids[n].keys()) + ([EOS] if trie.final[n] else []); a = torch.tensor(a, dtype=torch.long); trie._allow[n] = a + if len(a): mask[j, k, a] = 0 + lp[crow] = lp[crow] + mask + lp[:, :, PAD] = NEG; lp[:, :, BOS] = NEG; lp[:, :, UNK] = NEG + tot = (score[:, :, None] + lp).view(W, -1) + top, idx = tot.topk(2 * K, -1) + b = idx // V; v = idx % V; fin = torch.isfinite(top) + eos = (v == EOS) & fin + if eos.any(): + for r, c in eos.nonzero().tolist(): + if alive[r] and len(done[r]) < nbest: + done[r].append((dec_tgt(hist[r * K + b[r, c]].tolist()), top[r, c].item())) + ne = (v != EOS) & fin; rank = ne.long().cumsum(1) - 1; keep = ne & (rank < K) + rr, cc = keep.nonzero(as_tuple=True); kk = rank[rr, cc] + new_score = torch.full((W, K), NEG); new_score[rr, kk] = top[rr, cc] + reorder = (torch.arange(W)[:, None] * K).repeat(1, K); reorder[rr, kk] = rr * K + b[rr, cc] + new_tok = torch.full((W, K), EOS); new_tok[rr, kk] = v[rr, cc] + if crow: + nn_ = {i: [None] * K for i in crow} + bl, vl, kl, rl = b[rr, cc].tolist(), v[rr, cc].tolist(), kk.tolist(), rr.tolist() + for r, bb, vv, k in zip(rl, bl, vl, kl): + if r in nn_: nn_[r][k] = trie.kids[nodes[r][bb]][vv] + nodes = nn_ + nkeep = keep.sum(1).tolist(); best0 = new_score[:, 0].tolist(); any_alive = False + for i in range(W): + if not alive[i]: new_score[i] = NEG; continue + if len(done[i]) >= nbest and (nkeep[i] == 0 or best0[i] < min(x[1] for x in done[i])): new_score[i] = NEG; alive[i] = False + elif nkeep[i]: any_alive = True + else: alive[i] = False + if not any_alive: break + flat = reorder.view(-1) + for c in caches: + c["sa"]["k"] = c["sa"]["k"][flat]; c["sa"]["v"] = c["sa"]["v"][flat] + hist = torch.cat([hist[flat], new_tok.view(-1, 1)], 1) + score = new_score; tok = new_tok.view(-1, 1) + out = [] + for i in range(W): + d = {} + for s_, sc in done[i]: + if s_ not in d or sc > d[s_]: d[s_] = sc + out.append(sorted(d.items(), key=lambda x: -x[1])[:nbest]) + return out diff --git a/python/bhc/xlit_q.py b/python/bhc/xlit_q.py new file mode 100644 index 0000000000000000000000000000000000000000..a74f7de9de14820a1ae3bd9b5d133c4e67107cf6 --- /dev/null +++ b/python/bhc/xlit_q.py @@ -0,0 +1,63 @@ +"""Low-bit weight storage for the char Transformer: matrices -> b-bit (4 or 8) symmetric integers with one fp16 +scale per group of G consecutive input weights (per output row); vectors -> fp16. De-quantised to fp32 at load.""" +import io, json +import numpy as np, torch +from bhc.xlit_model import XlitTransformer + + +def _q(v, bits, G): + r, c = v.shape; pad = (-c) % G; vp = np.pad(v, ((0, 0), (0, pad))).reshape(r, -1, G) + L = 2 ** (bits - 1) - 1; base = np.abs(vp).max(2, keepdims=True) / L; base[base == 0] = 1 + best_err = None + for al in ((1.0,) if bits >= 8 else (1.0, 0.95, 0.9, 0.85, 0.8, 0.75, 0.7, 0.65)): # MSE-optimal clipping per group + s_ = (base * al).astype(np.float16).astype(np.float32); q_ = np.clip(np.round(vp / s_), -L, L) + e = ((q_ * s_ - vp) ** 2).sum(2, keepdims=True) + if best_err is None: best_err, sc, q = e, s_, q_ + else: m = e < best_err; best_err = np.where(m, e, best_err); sc = np.where(m, s_, sc); q = np.where(m, q_, q) + q = q.astype(np.int8) + return q.reshape(r, -1)[:, :c], sc[..., 0].astype(np.float16) + + +def export_q(model, path, bits=4, G=32, emb_bits=8): + """bits for Linear weights; emb_bits for the (small, outlier-prone) embedding/position tables.""" + sd = model.state_dict(); meta = {"cfg": model.cfg, "bits": bits, "G": G, "tensors": []}; buf = io.BytesIO() + for k, v in sd.items(): + v = v.float().cpu().numpy() + if v.ndim == 2: + b_ = emb_bits if ("emb" in k or "pos" in k) else bits + q, sc = _q(v, b_, G); flat = q.reshape(-1) + if b_ == 4: + u = (flat + 8).astype(np.uint8); u = np.r_[u, np.zeros(len(u) % 2, np.uint8)]; data = (u[0::2] << 4 | u[1::2]).astype(np.uint8).tobytes() + else: data = flat.astype(np.int8).tobytes() + meta["tensors"].append([k, list(v.shape), "q%d" % b_, buf.tell(), len(data)]); buf.write(data); buf.write(sc.tobytes()) + else: + meta["tensors"].append([k, list(v.shape), "f16", buf.tell(), 0]); buf.write(v.astype(np.float16).tobytes()) + hdr = json.dumps(meta, separators=(",", ":")).encode() + with open(path, "wb") as f: f.write(len(hdr).to_bytes(4, "little")); f.write(hdr); f.write(buf.getvalue()) + + +def load_q(path): + b = open(path, "rb").read(); n = int.from_bytes(b[:4], "little"); meta = json.loads(b[4:4 + n]); data = b[4 + n:] + bits, G = meta["bits"], meta["G"]; m = XlitTransformer(**meta["cfg"]); sd = {} + for k, shape, kind, off, ln in meta["tensors"]: + cnt = int(np.prod(shape)) + if kind.startswith("q"): + bits = int(kind[1:]) if len(kind) > 1 else meta["bits"] + raw = np.frombuffer(data, np.uint8 if bits == 4 else np.int8, ln, off) + if bits == 4: q = (np.stack([raw >> 4, raw & 15], 1).reshape(-1)[:cnt].astype(np.int16) - 8) + else: q = raw.astype(np.int16) + r, c = shape; ng = (c + G - 1) // G + sc = np.frombuffer(data, np.float16, r * ng, off + ln).astype(np.float32).reshape(r, ng) + w = q.reshape(r, c).astype(np.float32) * np.repeat(sc, G, 1)[:, :c] + sd[k] = torch.from_numpy(w) + else: + sd[k] = torch.from_numpy(np.frombuffer(data, np.float16, cnt, off).astype(np.float32).reshape(shape).copy()) + m.load_state_dict(sd); m.eval(); return m + + +def load_any(path): + if path.endswith(".int8"): + from bhc.xlit_model import load_int8; return load_int8(path) + if path.endswith(".pt"): + ck = torch.load(path, weights_only=False, map_location="cpu"); m = XlitTransformer(**ck["cfg"]); m.load_state_dict(ck["model"]); m.eval(); return m + return load_q(path) diff --git a/python/gguf_read.py b/python/gguf_read.py new file mode 100644 index 0000000000000000000000000000000000000000..b4fcc2503e032288110de803263bf33b2bea5d4b --- /dev/null +++ b/python/gguf_read.py @@ -0,0 +1,383 @@ +"""Minimal, dependency-light reader for the GGUF files shipped with AksharaLP-MultiIndic-8M. + +GGUF here is used as what it is: a portable, single-file tensor container -- a header, typed +key/value metadata, a tensor table, and aligned little-endian tensor data. It is NOT a +llama.cpp-loadable model: AksharaLP is a custom character-level encoder-decoder transformer with a +bespoke decoding stack (lexicon-trie candidate beam, hand-built candidate features, gates, MLP +reranker), and llama.cpp has no such architecture. This reader is the decoder-side tool: it returns +the weights our reference implementation (python/bhc/xlit_*.py) expects. + +Only numpy is required. No torch, no llama.cpp, no gguf/gguf-py. + + python gguf_read.py aksharalp-1m-v2-f16.gguf # summary + python gguf_read.py --json aksharalp-1m-v2-q4.gguf # metadata + tensor table + + import gguf_read + w = gguf_read.load("aksharalp-1m-v2-f16.gguf") # -> Weights(cfg, state_dict, metadata) + w.state_dict["semb.weight"].shape # (30, 128) float32 + +Two container layouts are understood, both GGUF v3: + + * aksharalp-1m-v2-q4.gguf one I8 tensor, ``aksharalp.q4.blob``: the byte-exact xlit.q4 file + (4-byte header length, JSON tensor table, then the payload). Decoded + here exactly as bhc/xlit_q.py:load_q does. + * aksharalp-1m-v2-f16.gguf every weight under its state_dict name at F16. +""" +import json +import struct +import sys + +import numpy as np + +MAGIC = b"GGUF" +SUPPORTED_VERSIONS = (3,) + +# GGUF metadata value types +GGUF_UINT8, GGUF_INT8, GGUF_UINT16, GGUF_INT16 = 0, 1, 2, 3 +GGUF_UINT32, GGUF_INT32, GGUF_FLOAT32, GGUF_BOOL = 4, 5, 6, 7 +GGUF_STRING, GGUF_ARRAY, GGUF_UINT64, GGUF_INT64, GGUF_FLOAT64 = 8, 9, 10, 11, 12 + +# GGML tensor types we can read: F32, F16, I8 (the three this model uses) +_GGML_F32, _GGML_F16, _GGML_I8 = 0, 1, 24 +_TENSOR_DTYPES = {_GGML_F32: np.dtype(" len(self.buf): + raise GGUFError("truncated file: wanted %d bytes at %d, file is %d" % (n, self.pos, len(self.buf))) + b = self.buf[self.pos:self.pos + n] + self.pos += n + return b + + def unpack(self, fmt): + n = struct.calcsize(fmt) + return struct.unpack(fmt, self.take(n))[0] + + def string(self): + """GGUF strings: u64 length + UTF-8 bytes (not NUL-terminated).""" + n = self.unpack("Q") + return self.take(n).decode("utf-8") + + +class Tensor: + """One tensor: name, logical shape (GGML order reversed back to row-major), dtype, raw bytes.""" + + __slots__ = ("name", "shape", "type_id", "offset", "data") + + def __init__(self, name, shape, type_id, offset, data): + self.name, self.shape, self.type_id, self.offset, self.data = name, shape, type_id, offset, data + + @property + def dtype(self): + return _TENSOR_DTYPES[self.type_id] + + @property + def type_name(self): + return _TENSOR_NAMES[self.type_id] + + def array(self): + return np.frombuffer(self.data, dtype=self.dtype).reshape(self.shape) + + def __repr__(self): + return "" % (self.name, self.type_name, tuple(self.shape)) + + +class GGUF: + """Parsed GGUF file: header fields, typed metadata, tensor table, tensor data.""" + + def __init__(self, path): + self.path = str(path) + with open(self.path, "rb") as f: + self.buf = f.read() + self._parse() + + # ---------------------------------------------------------------- parsing + def _parse(self): + c = _Cursor(self.buf) + self.magic = c.take(4) + if self.magic != MAGIC: + raise GGUFError("%s: not a GGUF file (magic %r)" % (self.path, self.magic)) + self.version = c.unpack("I") + if self.version not in SUPPORTED_VERSIONS: + raise GGUFError("%s: GGUF version %d not supported (this reader knows %s)" + % (self.path, self.version, SUPPORTED_VERSIONS)) + self.tensor_count = c.unpack("Q") + self.kv_count = c.unpack("Q") + + self.metadata = {} + for _ in range(self.kv_count): + key = c.string() + self.metadata[key] = self._value(c) + + self.alignment = int(self.metadata.get("general.alignment", 32)) + if self.alignment <= 0 or self.alignment & (self.alignment - 1): + raise GGUFError("%s: general.alignment %d is not a power of two" % (self.path, self.alignment)) + + infos = [] + for _ in range(self.tensor_count): + name = c.string() + n_dims = c.unpack("I") + if n_dims > 8: + raise GGUFError("%s: tensor %s declares %d dims" % (self.path, name, n_dims)) + # GGML stores dims fastest-varying first; reverse to get the row-major shape. + dims = [c.unpack("Q") for _ in range(n_dims)] + type_id = c.unpack("I") + offset = c.unpack("Q") + infos.append((name, list(reversed(dims)), type_id, offset)) + + pad = (-c.pos) % self.alignment + self.data_start = c.pos + pad + if self.data_start > len(self.buf): + raise GGUFError("%s: tensor info block overruns the file" % self.path) + + self.tensors = {} + self.order = [] + for name, shape, type_id, offset in infos: + if type_id not in _TENSOR_DTYPES: + raise GGUFError("%s: tensor %s has GGML type %d, which this reader does not decode " + "(only F32/F16/I8 are used by these files)" % (self.path, name, type_id)) + if name in self.tensors: + raise GGUFError("%s: duplicate tensor name %r" % (self.path, name)) + numel = int(np.prod(shape, dtype=np.int64)) if shape else 1 + nbytes = numel * _TENSOR_DTYPES[type_id].itemsize + start = self.data_start + offset + end = start + nbytes + if start < self.data_start or end > len(self.buf): + raise GGUFError("%s: tensor %s spans [%d, %d) outside the data section [%d, %d)" + % (self.path, name, start, end, self.data_start, len(self.buf))) + if offset % self.alignment: + raise GGUFError("%s: tensor %s offset %d is not aligned to %d" + % (self.path, name, offset, self.alignment)) + self.tensors[name] = Tensor(name, shape, type_id, offset, self.buf[start:end]) + self.order.append(name) + + def _value(self, c, vtype=None): + if vtype is None: + vtype = c.unpack("I") + if vtype == GGUF_STRING: + return c.string() + if vtype == GGUF_ARRAY: + sub = c.unpack("I") + n = c.unpack("Q") + return [self._value(c, sub) for _ in range(n)] + if vtype in _KV_FMT: + return c.unpack(_KV_FMT[vtype]) + raise GGUFError("%s: unknown metadata value type %d" % (self.path, vtype)) + + # ---------------------------------------------------------------- checks + def validate(self): + """Structural checks; returns a list of human-readable results, raises GGUFError on failure.""" + notes = [] + if len(self.tensors) != self.tensor_count: + raise GGUFError("tensor count mismatch: header says %d, parsed %d" % (self.tensor_count, len(self.tensors))) + notes.append("magic GGUF, version %d" % self.version) + notes.append("%d tensors, %d metadata keys" % (self.tensor_count, len(self.metadata))) + notes.append("alignment %d, data section at byte %d" % (self.alignment, self.data_start)) + declared = self.metadata.get("general.architecture") + if declared is None: + raise GGUFError("general.architecture is missing") + for name in self.order: + t = self.tensors[name] + if t.offset + len(t.data) > len(self.buf) - self.data_start: + raise GGUFError("tensor %s overruns the data section" % name) + notes.append("all tensor offsets aligned and inside the data section") + arch = self.metadata.get("general.architecture") + notes.append("general.architecture = %r (custom; not a llama.cpp architecture)" % arch) + return notes + + # ---------------------------------------------------------------- weights + def state_dict(self): + """Return {state_dict name: float32 ndarray} for the reference XlitTransformer.""" + cfg, sd = self._weights() + return cfg, sd + + def _weights(self): + if "aksharalp.nlm.blob" in self.tensors: + return _weights_from_nlm_blob(bytes(self.tensors["aksharalp.nlm.blob"].data)) + if _BLOB_TENSOR in self.tensors: + return _weights_from_q4_blob(bytes(self.tensors[_BLOB_TENSOR].data)) + cfg = _cfg_from_metadata(self.metadata) + sd = {} + for name in self.order: + if name.startswith(RERANKER_PREFIX): + continue # reranker matrices are carried separately; see reranker() + a = self.tensors[name].array() + sd[name] = np.ascontiguousarray(a, dtype=np.float32) + return cfg, sd + + def reranker(self): + """{('core'|'pack', 'w1'|'b1'|'w2'|'b2'): float32 ndarray} from the F16 variant, else {}.""" + out = {} + for name in self.order: + if not name.startswith(RERANKER_PREFIX): + continue + _, part, param = name.split(".") + out[(part, param)] = np.ascontiguousarray(self.tensors[name].array(), dtype=np.float32) + return out + + +_BLOB_TENSOR = "aksharalp.q4.blob" +RERANKER_PREFIX = "reranker." + + +def _cfg_from_metadata(md): + if "aksharalp.config_json" in md: return json.loads(md["aksharalp.config_json"]) + def g(key, default=None): + v = md.get("aksharalp." + key, default) + if v is None: + raise GGUFError("metadata is missing aksharalp.%s" % key) + return v + cfg = {"d": int(g("d_model")), "h": int(g("n_heads")), "enc": int(g("encoder_layers")), + "dec": int(g("decoder_layers")), "ff": int(g("ff_dim")), "p": float(g("dropout", 0.1))} + return cfg + + +def _weights_from_q4_blob(blob): + """Decode the carried xlit.q4 bytes exactly as python/bhc/xlit_q.py:load_q does.""" + n = int.from_bytes(blob[:4], "little") + meta = json.loads(blob[4:4 + n]) + data = blob[4 + n:] + bits_default, G = int(meta["bits"]), int(meta["G"]) + cfg = dict(meta["cfg"]) + sd = {} + for k, shape, kind, off, ln in meta["tensors"]: + cnt = int(np.prod(shape)) + if kind.startswith("q"): + bits = int(kind[1:]) if len(kind) > 1 else bits_default + if bits == 4: + raw = np.frombuffer(data, np.uint8, ln, off) + q = np.stack([raw >> 4, raw & 15], 1).reshape(-1)[:cnt].astype(np.int16) - 8 + elif bits == 8: + q = np.frombuffer(data, np.int8, ln, off).astype(np.int16) + else: + raise GGUFError("unsupported q width %d for %s" % (bits, k)) + r, c = shape + ng = (c + G - 1) // G + sc = np.frombuffer(data, np.float16, r * ng, off + ln).astype(np.float32).reshape(r, ng) + w = q.reshape(r, c).astype(np.float32) * np.repeat(sc, G, 1)[:, :c] + sd[k] = w + elif kind == "f16": + sd[k] = np.frombuffer(data, np.float16, cnt, off).astype(np.float32).reshape(shape).copy() + else: + raise GGUFError("unsupported tensor kind %r for %s" % (kind, k)) + return cfg, sd + + +class Weights: + """Weights + config, in the shape the reference implementation consumes. + + ``state_dict`` maps state_dict names (as in python/bhc/xlit_q.py:load_q) to float32 ndarrays; + ``cfg`` is the XlitTransformer config. To build the torch model the reference uses: + + import torch + from bhc.xlit_model import XlitTransformer + m = XlitTransformer(**w.cfg) + m.load_state_dict({k: torch.from_numpy(v) for k, v in w.state_dict.items()}) + m.eval() + """ + + def __init__(self, cfg, state_dict, metadata=None, source=None, reranker=None): + self.cfg, self.state_dict, self.metadata, self.source = cfg, state_dict, metadata or {}, source + self.reranker = reranker or {} + + @property + def n_params(self): + return int(sum(v.size for v in self.state_dict.values())) + + def __repr__(self): + return "" % (self.source, len(self.state_dict), self.n_params) + + +def load(path): + """Parse either GGUF variant and return Weights (cfg + state_dict of float32 arrays).""" + g = GGUF(path) + cfg, sd = g.state_dict() + return Weights(cfg, sd, g.metadata, g.path, g.reranker()) + + +def tensors(path): + """Yield (name, shape, type_name, float32 array) for every tensor in the file.""" + g = GGUF(path) + for name in g.order: + t = g.tensors[name] + yield name, tuple(t.shape), t.type_name, t.array() + + +def q4_blob(path): + """Bytes of the carried xlit.q4 file from the compact GGUF (byte-identical to the original).""" + g = GGUF(path) + if _BLOB_TENSOR not in g.tensors: + raise GGUFError("%s carries no %s tensor (that is the F16 variant)" % (path, _BLOB_TENSOR)) + return bytes(g.tensors[_BLOB_TENSOR].data) + + +def main(argv): + args = [a for a in argv[1:] if not a.startswith("-")] + flags = {a for a in argv[1:] if a.startswith("-")} + if not args: + print(__doc__) + return 2 + g = GGUF(args[0]) + if "--json" in flags: + print(json.dumps({"version": g.version, "tensors": g.tensor_count, + "metadata": g.metadata, + "tensor_table": [{"name": n, "shape": g.tensors[n].shape, + "type": g.tensors[n].type_name, + "offset": g.tensors[n].offset} for n in g.order]}, + indent=1, ensure_ascii=False)) + return 0 + for line in g.validate(): + print(line) + print("general.name = %r" % g.metadata.get("general.name")) + print("-- tensors --") + for n in g.order: + t = g.tensors[n] + print(" %-28s %-4s %-18s %d B" % (n, t.type_name, tuple(t.shape), len(t.data))) + w = load(args[0]) + print("-- weights --") + print("cfg = %s" % (w.cfg,)) + print("%d tensors, %d parameters" % (len(w.state_dict), w.n_params)) + return 0 + + + + + +def _weights_from_nlm_blob(blob): + n=int.from_bytes(blob[:4],"little");md=json.loads(blob[4:4+n]);data=blob[4+n:];W={};G=md["G"] + for k,shape,kind,off,ln in md["tensors"]: + count=int(np.prod(shape)) + if kind in ("f16","f32"): + W[k]=np.frombuffer(data,np.float16 if kind=="f16" else np.float32,count,off).astype(np.float32).reshape(shape).copy() + else: + r,c=shape;ng=(c+G-1)//G + if kind=="q8":q=np.frombuffer(data,np.int8,r*c,off).astype(np.float32).reshape(r,c);end=off+r*c + else: + nb=(r*c+1)//2;u=np.frombuffer(data,np.uint8,nb,off);q=(np.stack([u>>4,u&15],1).reshape(-1)[:r*c].astype(np.int16)-8).astype(np.float32).reshape(r,c);end=off+nb + end+=(-end)%4;sc=np.frombuffer(data,np.float16,r*ng,end).astype(np.float32).reshape(r,ng);W[k]=q*np.repeat(sc,G,1)[:,:c] + return md["cfg"],W + +def load_word_model(path): + import torch + from bhc.xlit_model import XlitTransformer, set_vocab + w=load(path);set_vocab(w.cfg.get("vocab","hi"));m=XlitTransformer(**w.cfg);m.load_state_dict({k:torch.from_numpy(v.copy()) for k,v in w.state_dict.items()});return m.eval() + +def load_sentence_lm(path): + from bhc.nlm import NLM + w=load(path);vocab=json.loads(w.metadata["aksharalp.tokenizer_json"]) + return NLM(w.cfg,w.state_dict,vocab,w.cfg.get("norm","self")) + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/python/multiindic.py b/python/multiindic.py new file mode 100644 index 0000000000000000000000000000000000000000..6fd90649b78d0fbd9a1dd8b826fd95cbc3e24c01 --- /dev/null +++ b/python/multiindic.py @@ -0,0 +1,69 @@ +"""AksharaLP MultiIndic reference. Code Apache-2.0; see NOTICE for component weight licences.""" +from pathlib import Path +import json,gzip,math,re,unicodedata +import numpy as np +import torch +from bhc.xlit_q import load_any +from bhc.xlit_model import beam_search_fast, set_vocab + +def norm(s):return unicodedata.normalize('NFC',s.replace('\u200c','').replace('\u200d','')) +V3=dict(w_nlm=.4,w_lm=.25,w_ch_s=1.5,w_agn=2,w_agr=0,nlm_margin=2,nlm_k=6,beam_s=8,lat_k=12,rec_n=3,use_lm=True,oov_pen_s=4.) +class MultiIndic: + def __init__(self,root,language='hi',sentence=False,threads=1): + self.root=Path(root);self.threads=threads;torch.set_num_threads(threads);self.cache={};self.set_language(language) + if sentence:self.set_sentence(True) + def set_language(self,language): + if language not in ['hi','bn','te','kn','ta']:raise ValueError('Unsupported language') + p=self.root/'models'/language;set_vocab(language) + if language not in self.cache: + if language=='hi': + from bhc.translit2 import BhcTranslit2 + self.cache[language]=dict(hi=BhcTranslit2(str(p),threads=self.threads)) + else: + cfg=json.loads((p/'params.json').read_text());lex={} + for line in gzip.decompress((p/'dict.tsv.gz').read_bytes()).decode().splitlines(): + w,c=line.rsplit('\t',1);lex[w]=math.log(int(c)/cfg['dictionary_total']) + self.cache[language]=dict(model=load_any(str(p/'xlit.q4')),p=cfg,lex=lex) + self.language=language;self.active=self.cache[language];self.sentence=False;return self + def set_sentence(self,enabled=True): + if enabled and self.language!='hi':raise ValueError('Sentence mode is Hindi-only') + if enabled and 'sent' not in self.active: + from bhc.nlm import NLM + from bhc.translit3 import BhcTranslit3 + p=self.root/'models/hi';nlm=NLM.load(str(p/'sentence/nlm.q8'));self.active['sent']=BhcTranslit3(str(p),threads=self.threads,nlm=nlm,**V3) + self.sentence=enabled;return self + @torch.inference_mode() + def word(self,word,k=8): + if not re.fullmatch('[A-Za-z]+',word):return [word] + if self.language=='hi':return self.active['hi'].translit_words([word],k=k)[0] + a=self.active;c=[];seen=set() + for s,sc in beam_search_fast(a['model'],[word.lower()],beam=8,nbest=8)[0]: + s=norm(s) + if s and s not in seen:seen.add(s);c.append((s,sc)) + if not c:return [] + lex=a['lex'];r=a['p']['reranker'];mu=max((lex[s] for s,_ in c if s in lex),default=0);msc=max(sc for _,sc in c);out=[] + for i,(s,sc) in enumerate(c): + u=lex.get(s);inside=u is not None;f=[sc,sc/(len(s)+1),int(inside),u if inside else 0,int(i==0),i/len(c),sc-msc,u-mu if inside else 0] + score=sum((x-m)/sd*w for x,m,sd,w in zip(f,r['mu'],r['sd'],r['w']));out.append((s,score)) + return [s for s,_ in sorted(out,key=lambda v:-v[1])[:k]] + def transliterate(self,text): + if self.language=='hi': + if self.sentence:return self.active['sent'].convert(text) + old=self.active['hi'].p['use_lm'];self.active['hi'].p['use_lm']=False + try:return self.active['hi'].convert(text) + finally:self.active['hi'].p['use_lm']=old + return re.sub(r'(https?://\S+|www\.\S+|[\w.+-]+@[\w.-]+\.[A-Za-z]+|[@#][\w]+)|[A-Za-z]+',lambda m:m[0] if m[1] else (self.word(m[0],1)or[m[0]])[0],text) + + def load_word_gguf(self,path): + from gguf_read import load_word_model + m=load_word_model(path) + if m.cfg.get('vocab','hi')!=self.language:raise ValueError('GGUF language does not match selected language') + if self.language=='hi': + self.active['hi'].model=m + if 'sent' in self.active:self.active['sent'].model=m + else:self.active['model']=m + return self + def load_sentence_gguf(self,path): + if self.language!='hi':raise ValueError('Sentence mode is Hindi-only') + from gguf_read import load_sentence_lm + self.set_sentence(True);self.active['sent'].nlm=load_sentence_lm(path);return self diff --git a/python/requirements.txt b/python/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..3b7480fbd11f4b81a4a8d670cf9bb1e7ed3cdd98 --- /dev/null +++ b/python/requirements.txt @@ -0,0 +1,2 @@ +numpy +torch diff --git a/sent.mjs b/sent.mjs new file mode 100644 index 0000000000000000000000000000000000000000..e533f1a8947eaca13228b56d004e1f9eb8a86eef --- /dev/null +++ b/sent.mjs @@ -0,0 +1,72 @@ +// AksharaLP sentence mode (v3): word-mode candidate generation from the v2 package, then a tiny on-device Hindi neural +// language model re-scores whole sentences by choosing one candidate per word jointly (search3). +// HARD GUARDRAIL: the search only ever returns a word that is in that slot's own candidate list; fixed slots pass through. +// Every result carries guard: {ok, words, outside}, so a caller can audit the guardrail at runtime. +import { NLM } from "./nlm.mjs"; +import { search3 } from "./v3.mjs"; + +const RE_DEVA = /^[\u0900-\u0963\u0971-\u097F]+$/; + +/** Chosen on validation/dev data only (PROGRESS.md '2026-11-01 07:05–07:30 IST — v3 FINAL'): Kaggle 'S' LM, q8, norm self. */ +export const V3SET = { + w_nlm: 0.4, w_lm: 0.25, w_ch_s: 1.5, w_agn: 2, w_agr: 0, nlm_margin: 2, nlm_k: 6, beam_s: 8, + lat_k: 12, rec_n: 3, use_lm: true, oov_pen_s: 4.0, nlm_norm: "self", +}; + +/** + * Attach sentence mode to a loaded BhashaCore instance. + * `bytes` is the nlm.q8 file. Word mode keeps working: engine.sentenceMode toggles between the two decoders. + */ +export function attachSentence(engine, bytes) { + const buf = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes); + const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); + const n = dv.getUint32(0, true); + const meta = JSON.parse(new TextDecoder().decode(buf.subarray(4, 4 + n))); + const nParams = meta.tensors.reduce((a, t) => a + t[1].reduce((x, y) => x * y, 1), 0); + const nlm = new NLM(buf, V3SET.nlm_norm); + + const wordSlot = engine.decodeSlots.bind(engine); + /** v3 build_lattice (bhc/translit3.py) + search3 (release/space/js/v3.mjs) */ + const sentSlot = (slots, context = true) => { + const p = { ...engine.p, ...V3SET }; + const lat = []; + for (const [kind, t] of slots) { + if (kind === "x") { + let sc = engine.candFeats(t).map(([s, fv, wid]) => [s, engine.score(fv), wid]); + if (sc.length) { + const mx = Math.max(...sc.map((x) => x[1])), lse = mx + Math.log(sc.reduce((a, x) => a + Math.exp(x[1] - mx), 0)); + sc = sc.map(([s, v, wid]) => [s, v - lse, wid]).sort((a, b) => b[1] - a[1]).slice(0, p.lat_k); + } + lat.push(["x", sc.length ? sc : [[t, 0, null]]]); + } else lat.push(["fix", [[t, 0, RE_DEVA.test(t) ? (engine.lm.id(t) ?? -1) : -2]]]); + } + const post = lat.map(([, opts]) => opts.map(([s, v]) => [s, v])); // per-slot candidate list (for the UI chips) + let out, guard; + if (!context) { // context off: top-ranked candidate per slot (= word mode) + out = lat.map((o) => o.reduce((a, b) => (b[1] > a[1] ? b : a))[0]); + guard = { ok: true, words: 0, outside: 0 }; + } else { + out = search3(lat, p, engine.lm, nlm); + let words = 0, outside = 0; + out.forEach((w, i) => { if (lat[i][0] !== "x") return; words++; if (!lat[i][1].some((o) => o[0] === w)) outside++; }); + guard = { ok: outside === 0, words, outside }; + } + engine._lastGuard = guard; + return { out, lat: post, guard }; + }; + + engine.nlm = nlm; + engine.v3 = { params: { ...V3SET }, params_count: nParams, cfg: meta.cfg, vocab: meta.vocab.length, bytes: buf.byteLength }; + engine.sentenceMode = true; + engine._lastGuard = null; + engine.decodeSlots = (slots, context) => (engine.sentenceMode ? sentSlot(slots, context) : wordSlot(slots, context)); + + const base = engine.transliterate.bind(engine); + engine.transliterate = function (text, opt) { + this._lastGuard = null; + const r = base(text, opt); + r.guard = this._lastGuard || { ok: true, words: 0, outside: 0 }; + return r; + }; + return { nlm, info: engine.v3 }; +} diff --git a/tools/build_gguf.py b/tools/build_gguf.py new file mode 100644 index 0000000000000000000000000000000000000000..8d0afca87227f4cae1b0904a7ad5746f1718b165 --- /dev/null +++ b/tools/build_gguf.py @@ -0,0 +1,30 @@ +"""Reuses v2 GGUF writer. Custom architectures; no llama.cpp support.""" +from pathlib import Path +import sys,json,gzip,hashlib,math +import numpy as np +from gguf_format import Writer,GGML_I8,GGML_F16,dequantise +B=Path(__file__).resolve().parents[1];sys.path.insert(0,str(B/'python')) +from bhc.nlm import NLM +from gguf_read import GGUF +O=B/'gguf';O.mkdir(exist_ok=True);report={} +def common(cfg,name,component,lic,params): + w=Writer();w.string('general.architecture','aksharalp' if component=='word' else 'aksharalp-lstmp');w.string('general.name',name);w.string('general.author','Sourojit Dhua');w.string('general.license',lic);w.uint32('general.alignment',32);w.string('aksharalp.config_json',json.dumps(cfg,separators=(',',':')));w.string('aksharalp.component',component);w.uint32('aksharalp.parameter_count',params);w.boolean('aksharalp.requires_bundled_decoder',True);w.boolean('aksharalp.runs_in_llama_cpp',False);return w +for la in ['hi','bn','te','kn','ta']: + p=B/'models'/la;blob=(p/'xlit.q4').read_bytes();n=int.from_bytes(blob[:4],'little');meta=json.loads(blob[4:4+n]);data=blob[4+n:];np_=sum(math.prod(x[1]) for x in meta['tensors']);params=(p/'params.json').read_text();lic='apache-2.0' if la=='hi' else 'cc-by-nc-4.0' + for variant in ['q4','f16']: + w=common(meta['cfg'],f'AksharaLP-MultiIndic-8M {la} word {variant}','word',lic,np_);w.string('aksharalp.language',la);w.string('aksharalp.params_json',params) + if la=='hi':w.string('aksharalp.pack_params_json',(p/'dict/params.json').read_text()) + if variant=='q4': + w.string('aksharalp.storage','byte-exact shipped xlit.q4 as I8 blob; mixed q4/q8/f16, not GGML Q4_0');w.string('aksharalp.q4.sha256',hashlib.sha256(blob).hexdigest());t=[('aksharalp.q4.blob',np.frombuffer(blob,np.int8),GGML_I8)] + else: + w.string('aksharalp.storage','F16 materialization of shipped quantized weights; original reranker JSON is retained at exact precision in metadata, external dictionaries required');t=[(k,a.astype(np.float16),GGML_F16) for k,sh,a in dequantise(meta,data)] + dest=O/f'aksharalp-{la}-word-{variant}.gguf';w.write(dest,t);GGUF(dest).validate();report[dest.name]=dict(raw=dest.stat().st_size,gzip9=len(gzip.compress(dest.read_bytes(),9,mtime=0)),sha256=hashlib.sha256(dest.read_bytes()).hexdigest()) +p=B/'models/hi/sentence/nlm.q8';blob=p.read_bytes();n=int.from_bytes(blob[:4],'little');meta=json.loads(blob[4:4+n]);lm=NLM.load(p) +for variant in ['q8','q4','f16']: + w=common(meta['cfg'],f'AksharaLP-MultiIndic-8M hi sentence {variant}','sentence','apache-2.0',2770752);w.string('aksharalp.language','hi');w.string('aksharalp.tokenizer_json',json.dumps(meta['vocab'],ensure_ascii=False,separators=(',',':'))) + if variant=='f16': + w.string('aksharalp.storage','F16 materialization of shipped q8 sentence LM');t=[(k,a.astype(np.float16),GGML_F16) for k,a in lm.W.items()] + else: + bb=blob if variant=='q8' else (B/'models/hi/sentence/nlm.q4').read_bytes();w.string('aksharalp.storage',f'byte-exact existing nlm.{variant} file as I8 blob; custom grouped low-bit storage');w.string('aksharalp.source.sha256',hashlib.sha256(bb).hexdigest());w.string('aksharalp.variant_note','q8 is the shipped test-measured default; q4 is validation-parity-only, no test accuracy claim');t=[('aksharalp.nlm.blob',np.frombuffer(bb,np.int8),GGML_I8)] + dest=O/f'aksharalp-hi-sentence-{variant}.gguf';w.write(dest,t);GGUF(dest).validate();report[dest.name]=dict(raw=dest.stat().st_size,gzip9=len(gzip.compress(dest.read_bytes(),9,mtime=0)),sha256=hashlib.sha256(dest.read_bytes()).hexdigest()) +(B/'evaluation').mkdir(exist_ok=True);(B/'evaluation/gguf_sizes.json').write_text(json.dumps(report,indent=2)+'\n');print(json.dumps(report,indent=2)) diff --git a/tools/gguf_format.py b/tools/gguf_format.py new file mode 100644 index 0000000000000000000000000000000000000000..2b75606eb262a88b2c8411dc39fc5bc536c76e8a --- /dev/null +++ b/tools/gguf_format.py @@ -0,0 +1,141 @@ +"""GGUF v3 writer and q4 tensor decoder reused from the v2 release.""" +import argparse +import hashlib +import json +import os +import struct +import sys + +import numpy as np + +MAGIC = b"GGUF" +VERSION = 3 +ALIGNMENT = 32 + +# GGUF metadata value types +T_UINT8, T_INT8, T_UINT16, T_INT16 = 0, 1, 2, 3 +T_UINT32, T_INT32, T_FLOAT32, T_BOOL = 4, 5, 6, 7 +T_STRING, T_ARRAY, T_UINT64 = 8, 9, 10 +# GGML tensor types +GGML_F16, GGML_I8 = 1, 24 + + +# ---------------------------------------------------------------------------- q4 decoding (mirrors bhc/xlit_q.py) +def read_q4(path): + blob = open(path, "rb").read() + n = int.from_bytes(blob[:4], "little") + header = blob[4:4 + n] + meta = json.loads(header) + return blob, header, meta, blob[4 + n:] + + +def dequantise(meta, data): + """Dequantise every tensor of an xlit.q4 payload to float32 (same arithmetic as bhc/xlit_q.py).""" + bits_default, G = int(meta["bits"]), int(meta["G"]) + out = [] + for k, shape, kind, off, ln in meta["tensors"]: + cnt = int(np.prod(shape)) + if kind.startswith("q"): + bits = int(kind[1:]) if len(kind) > 1 else bits_default + if bits == 4: + raw = np.frombuffer(data, np.uint8, ln, off) + q = np.stack([raw >> 4, raw & 15], 1).reshape(-1)[:cnt].astype(np.int16) - 8 + else: + q = np.frombuffer(data, np.int8, ln, off).astype(np.int16) + r, c = shape + ng = (c + G - 1) // G + sc = np.frombuffer(data, np.float16, r * ng, off + ln).astype(np.float32).reshape(r, ng) + w = q.reshape(r, c).astype(np.float32) * np.repeat(sc, G, 1)[:, :c] + else: + w = np.frombuffer(data, np.float16, cnt, off).astype(np.float32).reshape(shape).copy() + out.append((k, list(shape), w)) + return out + + +# ---------------------------------------------------------------------------- GGUF writer (spec-conformant, v3) +class Writer: + def __init__(self): + self.kv = [] + + # -- typed metadata helpers + def add(self, key, vtype, value): + self.kv.append((key, vtype, value)) + return self + + def string(self, k, v): return self.add(k, T_STRING, v) + def uint32(self, k, v): return self.add(k, T_UINT32, int(v)) + def uint64(self, k, v): return self.add(k, T_UINT64, int(v)) + def float32(self, k, v): return self.add(k, T_FLOAT32, float(v)) + def boolean(self, k, v): return self.add(k, T_BOOL, bool(v)) + def strings(self, k, v): return self.add(k, T_ARRAY, (T_STRING, list(v))) + + @staticmethod + def _kv_bytes(key, vtype, value): + out = bytearray() + + def s(x): + b = x.encode("utf-8") + return struct.pack(" Object.prototype.hasOwnProperty.call(o, k); +function partClass(w) { + if (own(PART, w)) return PART[w]; const n = [...w].length; + if (n > 2 && w.endsWith("ता")) return "ms"; if (n > 2 && w.endsWith("ती")) return "fs"; if (n > 2 && w.endsWith("ते")) return "mp"; return null; +} +export function agreeFeats(words, i) { + if (i === null || i <= 0) return [0, 0]; + const a = own(AUX, words[i]) ? AUX[words[i]] : null; if (a === null) return [0, 0]; + let pos = 0, neg = 0; const pc = partClass(words[i - 1]); + if (pc !== null) { if (BAD_PA.has(pc + "|" + a)) neg++; else pos++; } + for (let j = i - 1; j > Math.max(-1, i - 9); j--) { + const u = words[j]; if (BOUND.has(u)) break; + if (own(SUBJ, u)) { const v = SA[SUBJ[u] + "|" + a] || 0; pos += v > 0; neg += v < 0; break; } + } + return [pos, neg]; +} +const NUM_RE = /^[0-9\u0966-\u096F]+$/; +export const V3DEF = { lat_k: 12, beam_s: 16, w_nlm: 0, w_agr: 0, w_agn: 0, nlm_margin: 99, nlm_k: 12, rec_n: 3 }; +/** p: v2 params (w_lm, w_ch_s, oov_pen_s, use_lm) + V3DEF keys; lm: LexLM (logp(h, w), N, hasLM); nlm: NLM or null */ +export function search3(lat, p, lm, nlm) { + const useBi = p.use_lm && lm && lm.hasLM; if (!p.w_nlm) nlm = null; + const agr = p.w_agr || p.w_agn; + let beams = [[0, null, [], nlm ? nlm.initState() : null]]; + for (let [kind, opts] of lat) { + if (kind === "fix") { + const [t, , wid] = opts[0]; + if (wid === -2) { + if (nlm && NUM_RE.test(t)) { const { ns } = nlm.scoreWords(beams.map((b) => b[3]), [""]); beams = beams.map(([s, h, ch], i) => [s, h, ch.concat([t]), ns[i][0]]); } + else beams = beams.map(([s, h, ch, st]) => [s, h, ch.concat([t]), st]); + continue; + } + opts = [[t, 0, wid]]; + } + opts = opts.slice(0, p.lat_k); let cand = opts, lp = null, nst = null; + if (nlm) { cand = opts.filter((o) => o[1] >= opts[0][1] - p.nlm_margin).slice(0, p.nlm_k); ({ lp, ns: nst } = nlm.scoreWords(beams.map((b) => b[3]), cand.map((c) => c[0]))); } + const best = new Map(); + beams.forEach(([s, h, ch], i) => cand.forEach(([c, csc, wid], j) => { + const w = wid === null || wid === undefined || wid === -1 ? null : wid; + let tot = s + p.w_ch_s * csc; + if (useBi) tot += p.w_lm * (lm.logp(h, w) - (w === null ? p.oov_pen_s : 0)); + if (nlm) tot += p.w_nlm * lp[i][j]; + const words = ch.concat([c]); + if (agr) { const [ap, an] = agreeFeats(words, words.length - 1); tot += p.w_agr * ap - p.w_agn * an; } + const key = !(nlm || agr) ? c : words.slice(-p.rec_n).join("\u0001"); + const cur = best.get(key); + if (cur === undefined || tot > cur[0]) best.set(key, [tot, w, words, nlm ? nst[i][j] : null]); + })); + beams = [...best.values()].sort((a, b) => b[0] - a[0]).slice(0, p.beam_s); + } + const fin = (b) => b[0] + (useBi ? p.w_lm * lm.logp(b[1], lm.N) : 0) + (nlm ? p.w_nlm * nlm.eosLogp(b[3]) : 0); + let bb = beams[0], bv = -Infinity; for (const b of beams) { const v = fin(b); if (v > bv) { bv = v; bb = b; } } + return bb[2]; +}