Full Code of ali1234/vhs-teletext for AI

master f470629a3d5d cached
78 files
341.5 KB
98.6k tokens
667 symbols
1 requests
Download .txt
Showing preview only (362K chars total). Download the full file or copy to clipboard to get everything.
Repository: ali1234/vhs-teletext
Branch: master
Commit: f470629a3d5d
Files: 78
Total size: 341.5 KB

Directory structure:
gitextract_t4lypv78/

├── .coveragerc
├── .gitignore
├── HOW_IT_WORKS.md
├── LICENSE
├── README.md
├── TRAINING.md
├── examples/
│   ├── filter
│   ├── maze
│   ├── service
│   ├── template.t42
│   ├── terminal
│   ├── tti
│   └── video
├── misc/
│   ├── teletext-noscanlines.css
│   └── teletext.css
├── setup.py
├── teletext/
│   ├── __init__.py
│   ├── __main__.py
│   ├── celp.py
│   ├── charset.py
│   ├── cli/
│   │   ├── __init__.py
│   │   ├── celp.py
│   │   ├── clihelpers.py
│   │   ├── teletext.py
│   │   ├── training.py
│   │   └── vbi.py
│   ├── coding.py
│   ├── elements.py
│   ├── file.py
│   ├── finders.py
│   ├── gui/
│   │   ├── __init__.py
│   │   ├── classify.py
│   │   ├── decoder.py
│   │   ├── decoder.qml
│   │   ├── editor.py
│   │   ├── editor.ui
│   │   ├── qthelpers.py
│   │   ├── service.py
│   │   └── vbiplot.py
│   ├── image.py
│   ├── interactive.py
│   ├── mp.py
│   ├── packet.py
│   ├── parser.py
│   ├── pipeline.py
│   ├── printer.py
│   ├── service.py
│   ├── servicedir.py
│   ├── sigint.py
│   ├── spellcheck.py
│   ├── stats.py
│   ├── subpage.py
│   ├── ts.py
│   └── vbi/
│       ├── __init__.py
│       ├── clustering.py
│       ├── config.py
│       ├── line.py
│       ├── pattern.py
│       ├── patterncuda.py
│       ├── patternopencl.py
│       ├── training.py
│       └── viewer.py
└── tests/
    ├── __init__.py
    ├── test_cli.py
    ├── test_coding.py
    ├── test_elements.py
    ├── test_file.py
    ├── test_mp.py
    ├── test_packet.py
    ├── test_sigint.py
    ├── test_spellcheck.py
    ├── test_stats.py
    ├── test_subpage.py
    └── vbi/
        ├── __init__.py
        ├── test_line.py
        ├── test_patterncuda.py
        ├── test_patternopencl.py
        └── test_training.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .coveragerc
================================================
[run]
source = teletext


================================================
FILE: .gitignore
================================================
/.idea
/.coverage
/venv
/*.vbi
*.pyc
/build
/dist
/MANIFEST
/*.egg-info
/vbi
/teletext/vbi/data/*-*



================================================
FILE: HOW_IT_WORKS.md
================================================
HOW IT WORKS
------------

Teletext is encoded as a non-return-to-zero signal with two levels representing
one and zero. This is a fancy way of saying that a line of teletext data is
a sequence of black and white "pixels" in the TV signal. Of course, since the
signal is analogue there are no individual pixels, the signal is continuous.
But you can imagine that there are pixels in the idealized "perfect" signal.

The problem of decoding teletext from a VHS recording is that VHS bandwidth is
lower than teletext bandwidth. This means that the signal is effectively low
pass filtered, which in terms of an image is equivalent to gaussian blurring.

There are methods for reversing gaussian blur, but they are designed to work
with general image data. In the case of teletext we only have black or white
levels, so these methods are not optimal. We can exploit the limitations on
the input in order to get a better result. We can also exploit information
about the protocol to further improve efficiency and accuracy.

When the black and white signal is blurred, the individual pixels are blurred 
in to each other. This makes the signal unreadable using normal methods, because
instead of a clean sequence like "1010" you something close to "0.5 0.5 0.5 0.5".
But all is not lost, because a sequence like "1111" or "0000" will be the same
after blurring. So if you see a signal like "0.5 0.7 1.0 1.0" you can guess that
the original was probably "0 1 1 1" or "0 0 1 1".  

There are 45 bytes in each teletext line, so the space of possible guesses is
2^(45*8) which is a very big number, which makes trying every guess completely
impractical. However there are ways to reduce this number:

FOUR RULES
----------

1. Nearly all bytes have a parity bit which means there are only 128 possible
combinations instead of 256.

2. Some bytes are hamming encoded. These have even fewer possible combinations.

3. The first three bytes in the signal are always the same. We can use this
to find the start of the signal in the sample data (it moves a bit in each
line, but the width is always the same.)

4. The protocol itself defines rules about which bytes are allowed in which
positions, reducing the problem space further.


TRAINING
--------

A known signal is recorded to a tape using a Raspberry Pi with rpi-teletext.
This signal is played back into the computer, which builds a table of convolved
-> original sequences.


DECONVOLUTION
-------------

The convolved training data can be compared to recorded tapes in order to determine
what the data originally was. The line is first resampled to 1 sample per "bit". 
Then it is divided into "bytes". Each one is compared against the training
tables, including a few bits before and after. The closest match is the most
likely original signal.

This algorithm can be performed in parallel using CUDA or OpenCL. This allows
deconvolution to run in near realtime with a GTX 780.

See TRAINING.md for more.


SQUASHING
---------

The algorithm outputs lots of teletext packets, but they will still not be
perfect (even though they may be valid, they aren't necessarily correct.)

Since the teletext pages are broadcast on a loop, any recording of more than a
few minutes will have multiple copies of every packet. This means, if two packets
are received that only differ at a couple of bytes, they can be assumed to be the same.

The stream is first "paginated", ie split in to subpages.

All versions of the same subpage are compared, and for each byte, the most
frequent decoding is used so for example if you had these inputs:

HELLO
HELLP
MELLO

Then the result "HELLO" would be decoded, since those are the most frequent bytes
in this position. For this to work well, you need a lot of copies of every packet.


================================================
FILE: LICENSE
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU General Public License is a free, copyleft license for
software and other kinds of works.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.  We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors.  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights.  Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received.  You must make sure that they, too, receive
or can get the source code.  And you must show them these terms so they
know their rights.

  Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.

  For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software.  For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.

  Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so.  This is fundamentally incompatible with the aim of
protecting users' freedom to change the software.  The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable.  Therefore, we
have designed this version of the GPL to prohibit the practice for those
products.  If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.

  Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary.  To prevent this, the GPL assures that
patents cannot be used to render the program non-free.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Use with the GNU Affero General Public License.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:

    <program>  Copyright (C) <year>  <name of author>
    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.

  The GNU General Public License does not permit incorporating your program
into proprietary programs.  If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.  But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.


================================================
FILE: README.md
================================================
This is a suite of tools for processing teletext signals recorded on VHS, as
well as tools for processing teletext packet streams. The software has only
been tested with bt8x8 capture hardware, but should work with any VBI capture
hardware with appropriate configuration.

This is the second rewrite of the original software. The old versions are
still available in the `v1` and `v2` branches of this repo, or from the
releases page.

You can see my collection of pages recovered with this software at:

https://al.zerostem.io/~al/teletext/

And more at:

http://www.teletextarchive.com

And:

http://archive.teletextart.co.uk/

INSTALLATION
------------

In order to use CUDA decoding you need to use the Nvidia proprietary driver.

To install with optional dependencies run:

    pip3 install -e .[CUDA,spellcheck,viewer]

If CUDA or pyenchant are not available for your platform simply omit them
from the install command.

In order to use OpenCL you need to install pyopencl and the appropriate
opencl runtime for your card.  Then run the deconvolve command with the '-O'
option.

In order for the output to be rendered correctly you need to use a specific
font and terminal:

    sudo apt-get install tv-fonts rxvt-unicode

Then enable bitmap fonts in your X server:

    cd /etc/fonts/conf.d
    sudo rm 70-no-bitmaps.conf
    sudo ln -s ../conf.avail/70-yes-bitmaps.conf .

After doing this you may need to rehash:

    xset fp rehash

Finally open a terminal with the required font:

    urxvt -fg white -bg black -fn teletext -fb teletext -geometry 41x25 +sb &


USAGE
-----

First capture VBI from VHS:

    teletext record -d /dev/vbi0 > capture.vbi

Deconvolve the recording:

    teletext deconvolve capture.vbi > stream.t42

Examine the headers to find services on the tape:

    teletext filter -r 0 stream.t42

Split capture into services:

    teletext filter --start <N> --stop <N> stream.t42 > stream-1.t42

Display all copies of a page in a stream:

    teletext filter stream.t42 -p 100

Squash duplicate subpages, which reduces errors:

    teletext squash stream.t42 > output.t42

Generate HTML pages from a stream:

    teletext html output/ stream.t42 

Interactively view the pages in a t42 stream:

    teletext service stream.t42 | teletext interactive

In the interactive viewer you can type page numbers, or '.' for hold.

Run each command with '--help' for a complete list of options.


================================================
FILE: TRAINING.md
================================================
Training
--------

1. Record the training signal to a tape:

```
teletext training | raspi-teletext -
```

2. Record it back to `training.vbi`.

3. Run the following script to process the training file into patterns:

```
#!/bin/sh
SPLITS=$(mktemp -d -p .)
teletext training split $SPLITS training.vbi
teletext training squash $SPLITS training.dat
teletext training build -m full -b 4 20 training.dat full.dat
teletext training build -m parity -b 6 20 training.dat parity.dat
teletext training build -m hamming -b 1 20 training.dat hamming.dat
cp full.dat parity.dat hamming.dat ~/Source/vhs-teletext/teletext/vbi/data/
echo $SPLITS
```

Theory
------

The idea behind training is to record a known teletext signal on to
tape and then play it back into the computer in the same way as you
would when recovering a tape. Then the original and observed signal
can be compared to build a database of patterns.

To make sure we can identify the degraded training packets, each one
has an ID and checksum. These are encoded so that each bit of data is
three bits wide in the output. This makes recovery of the original
trivial. There are also fixed bytes which can be used to help with
alignment.

We want to fit the most possible patterns into the least possible
tape. A De Bruijn sequence is used to do this. This is defined as the
shortest possible sequence which contains every sequence of input
characters (0 and 1 in this case) up to length N.

We use the De Bruijn sequence [2, 24]. This means it contains every
possible sequence of 1 or 0 of length 24 bits, which is about 16
million patterns. The ID field stores an offset into this sequence.

When recording it is possible for a run of whole frames to be lost,
so we do not simply display the whole De Bruijn sequence from start
to finish. Instead, for each packet, we add a prime number to the
offset and modulo the sequence length. This way every part of the
sequence is shown multiple times, and even a long run of frame drops
is unlikely to cause total loss of any part of the pattern.

After recording the signal back into the computer it is sliced into
patterns representing 24 bits of data. For a specific 24 bit pattern
there will be multiple slices in the signal. An average is taken of
every occurence and saved along with the original data it represents.
This is the intermediate training data.

Finally the pattern files are built. A pattern is describe like this:

 1. Number of bits to match before.
 2. Set of possible bytes to match.
 3. Number of bits to match after.

So for example, the parity data file is like this:

 build_pattern(args.parity, 'parity.dat', 4, 18, parity_set)

 Means: 

 1. Match 4 bits before.
 2. Match any byte with odd parity. (128 possibilities/7 bits)
 3. Match 3 bits after.

giving 14 bits total, or 16384 patterns.

To build the pattern data the intermediate data is processed and any
pattern which matches the criteria is added into a list. Then the
average for each list is taken. That is the final pattern we will 
match against.





================================================
FILE: examples/filter
================================================
#!/usr/bin/env python3

# An example of making a customized filter in Python.

# This is almost a direct copy-paste of the filter subcommand
# tweaked to run standalone, and with an extra filter that
# removes non-page packets.

import click

from teletext.cli.clihelpers import packetreader, packetwriter, paginated
from teletext import pipeline


@click.command()
@packetwriter
@paginated()
@click.option('--pagecount', 'n', type=int, default=0, help='Stop after n pages. 0 = no limit. Implies -P.')
@click.option('-k', '--keep-empty', is_flag=True, help='Keep empty packets in the output.')
@packetreader()
def filter(packets, pages, subpages, paginate, n, keep_empty):

    """Demultiplex and display t42 packet streams."""

    if n:
        paginate = True

    if not keep_empty:
        packets = (p for p in packets if not p.is_padding())

    # customize the filtering:
    packets = (p for p in packets if p.mrag.row not in [29, 30, 31])

    if paginate:
        for pn, pl in enumerate(pipeline.paginate(packets, pages=pages, subpages=subpages), start=1):
            yield from pl
            if pn == n:
                return
    else:
        yield from packets


if __name__ == '__main__':
    filter()


================================================
FILE: examples/maze
================================================
#!/usr/bin/env python

import random

import click
import numpy as np
from PIL import Image, ImageDraw

from teletext.cli.clihelpers import packetwriter
from teletext.service import Service
from teletext.subpage import Subpage


class Maze:
    directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]

    def __init__(self, width=8, height=6):
        self.width = width
        self.height = height
        self.data = np.zeros((2*height-1, 2*width-1), dtype=np.uint8)
        self.data[::2, ::2] = 1
        self.connect(0, 0, 0, 1)
        self.connect(self.width-1, self.height-1, 0, -1)
        self.generate(self.width//2, self.height//2, {(0, 0), (self.width-1, self.height-1)})

    def valid(self, px, py):
        return 0 <= px < self.width and 0 <= py < self.height

    def connect(self, px, py, dx, dy):
        self.data[(2*py)+dy, (2*px)+dx] = 1

    def generate(self, x, y, visited):
        visited.add((x, y))
        for d in random.sample(self.directions, 4):
            print(x, y, d)
            nx, ny = x+d[0], y+d[1]
            if self.valid(nx, ny) and (nx, ny) not in visited:
                self.connect(x, y, *d)
                self.generate(nx, ny, visited)

    def connections(self, px, py, dx, dy):
        ldx, ldy = -dy, dx
        rdx, rdy = dy, -dx
        left = []
        right = []
        while True:
            if self.valid(px+ldx, py+ldy):
                left.append(self.data[(2*py)+ldy, (2*px)+ldx])
            else:
                left.append(0)
            if self.valid(px+rdx, py+rdy):
                right.append(self.data[(2*py)+rdy, (2*px)+rdx])
            else:
                right.append(0)
            if self.valid(px+dx, py+dy) and self.data[(2*py)+dy, (2*px)+dx]:
                px += dx
                py += dy
            else:
                break
        return left, right

    def bitmap(self, left, right):
        w, h = 39*2, 23*3
        hh = h - 1
        ww = w - 1
        bitmap = Image.new("1", (w, h))

        def ox(o, l):
            o = min(o, (ww//2)-4)
            return o + 4 if l else ww-o-4

        def oy(o, t):
            o = min(o, (hh//2))
            return o if t else hh-o

        def draw_p(x1, y1, x2, y2, l):
            x1 = ox(x1, l)
            x2 = ox(x2, l)
            for t in (True, False):
                y1 = oy(y1, t)
                y2 = oy(y2, t)
                draw.line((x1, y1, x2, y2), fill=1)

        def draw_h(x1, x2, y, l):
            return draw_p(x1, y, x2, y, l)

        def draw_d(xy1, xy2, l):
            return draw_p(xy1, xy1, xy2, xy2, l)

        def draw_v(xy, l):
            x = ox(xy, l)
            draw.line((x, oy(xy, True), x, oy(xy, False)), fill=1)

        def draw_side(n, o1, o2, o3, l, p):
            if p:
                draw_h(o2, o3, o3, l)
                draw_v(o2, l)
                if n+1 < len(left):
                    draw_v(o3, l)
            else:
                draw_d(o2, o3, l)
            draw_d(o1, o2, l)

        def calc_o(n):
            return (n * 22) - 16 - ((n*(n+1)*2))

        draw = ImageDraw.Draw(bitmap)
        for n, (l, r) in enumerate(zip(left, right)):
            o1 = calc_o(n)
            o2 = o1 + max(0, 6 - n)
            o3 = max(o1, calc_o(n+1))
            draw_side(n, o1, o2, o3, True, l)
            draw_side(n, o1, o2, o3, False, r)

        o = calc_o(len(left))
        draw_h(o, ww, o, True)
        draw_h(o, ww, o, False)
        if not left[-1]:
            draw_v(o, True)
        if not right[-1]:
            draw_v(o, False)

        return np.array(bitmap)

    def view_to_mrag(self, px, py, dx, dy):
        return self.directions.index((dx, dy)) + 2, py + (px << 4)

    def view(self, px, py, dx, dy):
        m, p = self.view_to_mrag(px, py, dx, dy)
        page = Subpage(prefill=True, magazine=m)
        page.header.page = p
        page.header.control = 0
        left, right = self.connections(px, py, dx, dy)

        page.displayable.place_bitmap(np.array(self.bitmap(left[:5], right[:5])))

        # cheat mode / debug dump
        # put revealo maps on every wall
        if True and len(left) == 1:
            x = (38 - self.data.shape[1])//2
            y = (23 - self.data.shape[0])//2
            for n, r in enumerate(self.data[::-1]):
                page.displayable.place_string('\x07\x18' + ''.join('.' if p else ' ' for p in r) + '\x17', x=x, y=y+n)
            dn = ['^', '>', 'v', '<'][self.directions.index((dx, dy))]
            page.displayable.place_string(dn, x=px*2+x+2, y=self.data.shape[0]+y-1-(2*py))

        # create fastext links
        # TODO: add some helpers to make this easier
        page.displayable.place_string('\x01TurnLeft\x02StepForward\x03TurnRight\x06StepBack', y=23)

        page.init_packet(27, magazine=m)
        page.packet(27).fastext.dc = 0
        page.packet(27).fastext.control = 0xf

        lm, lp = self.view_to_mrag(px, py, -dy, dx)
        page.packet(27).fastext.links[0].magazine = lm
        page.packet(27).fastext.links[0].page = lp

        lm, lp = self.view_to_mrag(px, py, dy, -dx)
        page.packet(27).fastext.links[2].magazine = lm
        page.packet(27).fastext.links[2].page = lp

        if self.valid(px+dx, py+dy) and self.data[(2*py)+dy, (2*px)+dx]:
            lm, lp = self.view_to_mrag(px+dx, py+dy, dx, dy)
        else:
            lm, lp = m, p
        page.packet(27).fastext.links[1].magazine = lm
        page.packet(27).fastext.links[1].page = lp

        if self.valid(px-dx, py-dy)and self.data[(2*py)-dy, (2*px)-dx]:
            lm, lp = self.view_to_mrag(px-dx, py-dy, dx, dy)
        else:
            lm, lp = m, p
        page.packet(27).fastext.links[3].magazine = lm
        page.packet(27).fastext.links[3].page = lp

        return page

    def map(self):
        page = Subpage(prefill=True, magazine=1)
        page.header.page = 0
        page.header.control = 0
        page.displayable.place_string("\x0d      You are trapped in a maze!       ", y=2)
        page.displayable.place_string("\x0d   Use the fastext buttons to move.    ", y=4)
        page.displayable.place_string("\x0d       Press reveal for a hint:        ", y=6)

        page.displayable.place_string('\x12\x18\x24', x=18+(self.data.shape[1]//4), y=8)
        page.displayable.place_bitmap(self.data[::-1], x=19-(self.data.shape[1]//4), y=9, conceal=True)
        page.displayable.place_string('\x11\x18\x21', x=17-(self.data.shape[1]//4), y=10+(self.data.shape[0]//3))

        page.displayable.place_string("\x0d              Good luck!               ", y=14)
        page.displayable.place_string("\x0d       Press any button to begin.      ", y=16)
        page.displayable.place_string("\x01  Begin  \x02  Begin  \x03  Begin  \x06  Begin  ", y=23)

        page.init_packet(27, magazine=1)
        page.packet(27).fastext.dc = 0
        page.packet(27).fastext.control = 0xf
        for n in range(4):
            page.packet(27).fastext.links[n].magazine = 2
            page.packet(27).fastext.links[n].page = 0
        return page

    def service(self):
        service = Service(replace_headers=True, title="Maze!")
        service.insert_page(self.map())
        for y in range(self.height):
            for x in range(self.width):
                for d in self.directions:
                    service.insert_page(self.view(x, y, *d))
        return service

@click.command()
@packetwriter
def maze():
    return Maze().service()

if __name__ == '__main__':
    maze()


================================================
FILE: examples/service
================================================
#!/usr/bin/env python3

# An example of generating a service from scratch.

import sys

from teletext.service import Service
from teletext.subpage import Subpage

# Create a subpage
subpage = Subpage(prefill=True)

# Fill with clock cracker
subpage.displayable[:,0::2] = 0xfe
subpage.displayable[:,1::2] = 0x7f
subpage.displayable[:,0] = 0x20

# Put a message in the middle
subpage.displayable.place_string('Hello World', 15, 11)

# Create the service
service = Service(replace_headers=True)

# Add the subpage to the service.
service.magazines[1].pages[0].subpages[0] = subpage

# Set magazine name and number
service.magazines[1].title = 'Example '

# Broadcast it forever
while True:
    # Stream some packets
    for packet in service.packets(32):
        sys.stdout.buffer.write(packet.bytes)

    # Modify the service if required


================================================
FILE: examples/terminal
================================================
#!/usr/bin/env python3

import datetime
import os
import pty
import select
import shlex
import time

import click
import pyte

from teletext.cli.clihelpers import packetwriter
from teletext.subpage import Subpage


@click.command()
@click.argument("command", type=str)
@packetwriter
def terminal(command):
    """
    Runs a command in a simulated terminal and outputs a packet stream on page 100.
    """
    columns, lines = 40, 24

    # run the command
    p_pid, p_fd = pty.fork()
    if p_pid == 0:  # Child.
        argv = shlex.split(command)
        env = dict(TERM="linux", LC_ALL="en_GB.UTF-8",
                   COLUMNS=str(columns), LINES=str(lines))
        os.execvpe(argv[0], argv, env)

    # set up virtual terminal
    screen = pyte.Screen(columns, lines)
    screen.set_mode(pyte.modes.LNM)
    screen.write_process_input = lambda data: p_fd.write(data.encode())
    stream = pyte.ByteStream()
    stream.attach(screen)

    # set up page
    page = Subpage(prefill=True, magazine=1)
    page.header.control = 1<<4
    page.header.page = 0
    # init fastext so we can use row 24
    page.init_packet(27, magazine=1)
    page.packet(27).fastext.dc = 0
    page.packet(27).fastext.control = 0xf

    # generation loop
    prev_refresh = time.time()
    try:
        while True:
            r, w, x = select.select([p_fd], [], [], 1.0)
            if p_fd in r:
                stream.feed(os.read(p_fd, 65536))

            dt = datetime.datetime.now().strftime(" %a %d %b\x03%H:%M/%S")
            page.header.displayable.place_string('%-12s' % (command[:12]) + dt)

            for y in screen.dirty:
                line = screen.buffer[y]
                data = ''.join(char.data for char in (line[x] for x in range(screen.columns)))
                page.packet(y+1).displayable.place_string(data)

            now = time.time()
            elapsed = now - prev_refresh
            if elapsed > 1.0:
                prev_refresh = now
                send_lines = range(lines)
            else:
                send_lines = screen.dirty

            yield page.packet(0)
            for y in send_lines:
                yield page.packet(y+1)
            yield page.packet(27)

            screen.dirty.clear()
    except OSError:
        pass


if __name__ == '__main__':
    terminal()


================================================
FILE: examples/tti
================================================
#!/usr/bin/env python3

import json
import re
import requests
from PIL import Image
import numpy as np

from teletext.subpage import Subpage


def get_sensors():
    url = "http://hms.nottinghack.org.uk/api/spaceapi"
    dat = json.loads(requests.get(url).text)
    return {s['location']: s["value"] for s in dat['sensors']['temperature']}


def colour(t):
    if t < 10:
        return '\x06'
    elif t < 20:
        return '\x02'
    elif t < 30:
        return '\x04'
    else:
        return '\x01'

def clean_name(s):
    s = s.split('/')[0].replace('-LLAP', '').replace('G5-', '')
    return re.sub(r'([^\s-])([A-Z])', r'\1 \2', s)

def tti():
    with open('template.t42', 'rb') as f:
        page = Subpage.from_file(f)

    page.header.page = 0
    page.header.control = 0

    layout = (
        ('Airlock', 'ComfyArea-LLAP'),
        ('Studio-LLAP', 'CraftRoom-LLAP'),
        ('Kitchen-LLAP', 'Workshop-LLAP'),
        ('ClassRoom', 'G5-BlueRoom-LLAP'),
    )

    i = Image.open("sensors.png").convert(mode="1")
    page.displayable.place_bitmap(np.array(i), 16, 2, 0x13)

    sensors = get_sensors()
    for n, l in enumerate(layout):
        clean_names = (clean_name(s)[:10] + ':' for s in l)
        string = '  \x07'.join(f'{c:11s}{colour(sensors[s])}{sensors[s]:4.1f}C' for c, s in zip(clean_names, l))
        page.displayable.place_string('\x0d'+string, 1, (3*n)+8) # 0d = double height

    page.displayable.place_string('\x02\x1d\x04Temperature readings from the space', 0, 21)
    page.displayable.place_string('\x02\x1d\x04See P123 for info about the heating', 0, 22)
    page.displayable.place_string('\x01Main Index   \x02News    \x03Events    \x06Tools', 0, 23)

    with open('P100.tti', 'wb') as f:
        f.write(page.to_tti())

    print("https://zxnet.co.uk/teletext/editor/#" + page.url)

if __name__ == '__main__':
    tti()


================================================
FILE: examples/video
================================================
#!/usr/bin/env python3

import datetime
import socket

import click
import cv2
import srt

from teletext.cli.clihelpers import packetwriter
from teletext.subpage import Subpage


@click.command()
@click.argument('videofile', type=click.Path(readable=True))
@click.option('-s', '--subs', type=click.File('r'), help="Subtitle file (srt)")
@click.option('--start', type=int, default=0, help="Start at frame N")
@click.option('--end', type=int, default=None, help="End at frame N")
@packetwriter
def video(videofile, subs, start, end):
    """
    Converts a video into a page stream. Also supports SRT subtitles.
    Can set a start and end position in frames. Also works with images
    because OpenCV treats them as videos with one frame.
    """
    hostname = socket.gethostname()[:12]

    # Set up page
    page = Subpage(prefill=True, magazine=1)
    page.header.control = 1<<4
    page.header.page = 0
    # init fastext so we can use row 24
    page.init_packet(27, magazine=1)
    page.packet(27).fastext.dc = 0
    page.packet(27).fastext.control = 0xf

    # Creating a VideoCapture object to read the video
    cap = cv2.VideoCapture(videofile)
    cap.set(cv2.CAP_PROP_POS_FRAMES, start)
    fps = cap.get(cv2.CAP_PROP_FPS)
    frameno = start

    sub = None
    if subs:
        subs = srt.parse(subs.read())
        sub = next(subs)

    # Loop until the end of the video
    while (cap.isOpened() and (end is None or frameno <= end)):
        # Capture frame-by-frame
        ret, frame = cap.read()
        if not ret: # eof
            break

        frameno += 1

        frame = cv2.resize(frame, (39*2, 24*3), fx = 0, fy = 0,
                             interpolation = cv2.INTER_CUBIC)

        # conversion of BGR to grayscale is necessary to apply this operation
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

        # adaptive thresholding to use different threshold
        # values on different regions of the frame.
        thresha = cv2.adaptiveThreshold(gray, 1, cv2.ADAPTIVE_THRESH_MEAN_C,
                                               cv2.THRESH_BINARY, 3, 2)
        threshb = cv2.threshold(gray, 16, 255, cv2.THRESH_BINARY)[1]
        thresh = (thresha * threshb) > 0

        dt = datetime.datetime.now().strftime(" %a %d %b\x03%H:%M/%S")
        page.header.displayable.place_string('%-12s' % (hostname) + dt)
        page.displayable.place_bitmap(thresh)

        if sub:
            seconds = datetime.timedelta(seconds = frameno / fps)
            try:
                while seconds > sub.end:
                    sub = next(subs)
                if sub and seconds >= sub.start:
                    x = sub.content.split('\n')
                    w = len(max(x, key=len))
                    o = max(min(38 - w, 2), 0)
                    for n, l in enumerate(x):
                        page.packet(n+21).displayable.place_string(('\x06' + l + '\x17')[:(40-o)], x=o)
            except StopIteration:
                sub = None

        yield from page.packets

    # release the video capture object
    cap.release()

if __name__ == '__main__':
    video()


================================================
FILE: misc/teletext-noscanlines.css
================================================
body {background: black;}

a {color: inherit; text-decoration: none;}
a:hover {color: orange; } a:active {color: red;}
:focus {outline: 0;}
@font-face {font-family:teletext2; src:url('teletext2.ttf')}
@font-face {font-family:teletext4; src:url('teletext4.ttf')}

.subpage {
    position:relative; top:0; left:0;
    float:left; white-space: pre; color:white; background:black;
    font-family: teletext2; font-size:30px; line-height: 100%;
    border: solid black 10px;
    border-bottom: solid black 20px;
    text-shadow: 0 0 0.05em;
}

.subpage{
    filter:blur(0.5px) brightness(120%);
}

.row{
    display: flex;
}

.subpage:after{
    content: "";
    display: block;
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    z-index:10;
    pointer-events:none;
}

.f0{color:black;} .f1{color:red;} .f2{color:#00ff00;} .f3{color:yellow;}
.f4{color:blue;} .f5{color:magenta;} .f6{color:cyan;} .f7{color:white;}

.b0{background:black;} .b1{background:red;} .b2{background:#00ff00;} .b3{background:yellow;}
.b4{background:blue;} .b5{background:magenta;} .b6{background:cyan;} .b7{background:white;}

.dh{font-family: teletext4; font-size:200%; line-height:100%;}

.fl{text-decoration: blink}


================================================
FILE: misc/teletext.css
================================================
@import url("teletext-noscanlines.css");

/* scanlines */

.subpage {
    /* This gradient looks worse but can be rendered at 2 (physical) pixels. */
    --gradient-small: repeating-linear-gradient(
                            to top,
                            rgba(0,0,0,0.8) 0px,
                            transparent 1px,
                            transparent 2px,
                            rgba(0,0,0,0.8) 3px
                        );
    
    /* This gradient looks better but can't be rendered at 2 (physical) pixels. */
    --gradient-big: repeating-linear-gradient(
                        to top,
                        rgba(0,0,0,1),
                        transparent 25%,
                        transparent 75%,
                        rgba(0,0,0,1) 100%
                    );
}


/* Small size: 245px x 250px, 1 x 1 scaling */
@media (max-width: 660px) {
    .subpage { font-size:10px; }
    .subpage:after { background-image: none; }
}

/* Medium size: 490px x 500px, 2 x 2 scaling, scanlines possible */
@media (min-width: 660px) and (max-width: 980px) {
    .subpage { font-size:20px; }
    .subpage:after{
        background-size: 2px 2px;
        background-image: var(--gradient-small);
    }
    /* Use the nicer gradient on hidpi/retina displays. */
    /* This is only necessary when 2x2 scaling. */
    @media (min-resolution: 2dppx) {
        .subpage:after{
            background-image: var(--gradient-big);
        }
    }
}

/* Large size: 735 x 750px, 3 x 3 scaling, scanlines possible */
@media (min-width: 980px) {
    .subpage { font-size:30px; }
    .subpage:after{
        /* At 3x3 scaling and above, the big gradient looks better. */
        background-image: var(--gradient-big);
        background-size: 3px 3px;
    }
}


================================================
FILE: setup.py
================================================
from setuptools import setup

setup(
    name='teletext',
    version='3.1.99',
    author='Alistair Buxton',
    author_email='a.j.buxton@gmail.com',
    url='http://github.com/ali1234/vhs-teletext',
    packages=['teletext', 'teletext.vbi', 'teletext.cli', 'teletext.gui'],
    package_data={
        'teletext.vbi': [
            'data/debruijn.dat',
            'data/vhs/parity.dat',
            'data/vhs/hamming.dat',
            'data/vhs/full.dat',
            'data/betamax/parity.dat',
            'data/betamax/hamming.dat',
            'data/betamax/full.dat'
        ],
        'teletext.gui': [
            'decoder.qml',
            'editor.ui',
        ]
    },
    entry_points={
        'console_scripts': [
            'teletext = teletext.cli.teletext:teletext',
        ],
        'gui_scripts': [
            'ttviewer = teletext.gui.editor:main',
        ],
    },
    install_requires=[
        'numpy<2', 'scipy', 'matplotlib', 'click', 'tqdm',  'pyzmq', 'watchdog', 'pyserial',
        'windows-curses;platform_system=="Windows"',
    ],
    extras_require={
        'spellcheck': ['pyenchant'],
        'CUDA': ['pycuda'],
        'OpenCL': ['pyopencl'],
        'viewer': ['PyOpenGL'],
        'profiler': ['plop'],
        'qt': ['PyQt5'],
        'audio': ['spectrum', 'miniaudio'],
    }
)


================================================
FILE: teletext/__init__.py
================================================


================================================
FILE: teletext/__main__.py
================================================
from teletext.cli.teletext import teletext


if __name__ == '__main__':
    teletext()


================================================
FILE: teletext/celp.py
================================================
import numpy as np
import matplotlib.pyplot as plt
from spectrum import lsf2poly
import numpy as np
from scipy.signal import lfilter
from collections import deque
from tqdm import tqdm

hamming7_dec = np.array([
    [ 0x00, 0x10, 0x10, 0x14, 0x10, 0x11, 0x18, 0x12 ],
    [ 0x10, 0x11, 0x13, 0x19, 0x11, 0x01, 0x15, 0x11 ],
    [ 0x10, 0x1a, 0x13, 0x12, 0x16, 0x12, 0x12, 0x02 ],
    [ 0x13, 0x17, 0x03, 0x13, 0x1b, 0x11, 0x13, 0x12 ],
    [ 0x10, 0x14, 0x14, 0x04, 0x16, 0x1c, 0x15, 0x14 ],
    [ 0x1d, 0x17, 0x15, 0x14, 0x15, 0x11, 0x05, 0x15 ],
    [ 0x16, 0x17, 0x1e, 0x14, 0x06, 0x16, 0x16, 0x12 ],
    [ 0x17, 0x07, 0x13, 0x17, 0x16, 0x17, 0x15, 0x1f ],
    [ 0x10, 0x1a, 0x18, 0x19, 0x18, 0x1c, 0x08, 0x18 ],
    [ 0x1d, 0x19, 0x19, 0x09, 0x1b, 0x11, 0x18, 0x19 ],
    [ 0x1a, 0x0a, 0x1e, 0x1a, 0x1b, 0x1a, 0x18, 0x12 ],
    [ 0x1b, 0x1a, 0x13, 0x19, 0x0b, 0x1b, 0x1b, 0x1f ],
    [ 0x1d, 0x1c, 0x1e, 0x14, 0x1c, 0x0c, 0x18, 0x1c ],
    [ 0x0d, 0x1d, 0x1d, 0x19, 0x1d, 0x1c, 0x15, 0x1f ],
    [ 0x1e, 0x1a, 0x0e, 0x1e, 0x16, 0x1c, 0x1e, 0x1f ],
    [ 0x1d, 0x17, 0x1e, 0x1f, 0x1b, 0x1f, 0x1f, 0x0f ],
], dtype=np.uint8)

class LtpCodebook:
    def __init__(self, subframe_length):
        #self.buffer = np.zeros((147, subframe_length), dtype=np.double)
        #self.pos = 0
        self.buffer = deque(maxlen=147)

    def insert(self, subframe):
        self.buffer.extendleft(subframe)

    def get(self, lag):
        try:
            return self.buffer[lag + 20]
        except IndexError:
            return 0


class CELPStats:
    def __init__(self, decoder):
        self.decoder = decoder

    def __str__(self):
        result = f', L:{self.decoder.lsf_error:.0f}%, VG:{self.decoder.vector_gain_error:.0f}%'
        # reset the error counters
        self.decoder.lsf_errors = 0
        self.decoder.vector_gain_errors = 0
        self.decoder.subframes = 0
        return result


class CELPDecoder:
    widths = np.array([
        0,
        3, 4, 4, 4, 4, 4, 4, 4, 3, 3, # 37 bytes - 10 x LPC params of (unknown?) variable size
        5, 5, 5, 5,             # 4x5 = 20 bytes - pitch gain (LTP gain)
        5, 5, 5, 5,             # 4x5 = 20 bytes - vector gain
        7, 7, 7, 7,             # 4x7 = 28 bytes - pitch index (LTP lag)
        8, 8, 8, 8,             # 4x8 = 32 bytes - vector index
        3, 3, 3, 3,             # 4x3 = 12 bytes - error correction for vector gains?
        3,                      # 3 bytes - always zero (except for recovery errors)
    ])
    offsets = np.cumsum(widths)


    lsf_vector_quantizers = {
        # Source Reliant Error Control For Low Bit Rate Speech Communications, Ong, p103
        'ong': np.array([
            [ 178,  218,  236,  267,  293,  332,  378,  420,    0,    0,    0,    0,    0,    0,    0,    0,],
            [ 210,  235,  265,  295,  325,  360,  400,  440,  480,  520,  560,  610,  670,  740,  810,  880,],
            [ 420,  460,  500,  540,  585,  640,  705,  775,  850,  950, 1050, 1150, 1250, 1350, 1450, 1550,],
            [ 752,  844,  910,  968, 1016, 1064, 1110, 1155, 1202, 1249, 1295, 1349, 1409, 1498, 1616, 1808,],
            [1041, 1174, 1274, 1340, 1407, 1466, 1514, 1559, 1611, 1658, 1714, 1773, 1834, 1906, 2008, 2166,],
            [1438, 1583, 1671, 1740, 1804, 1855, 1905, 1947, 1988, 2034, 2081, 2135, 2193, 2267, 2369, 2476,],
            [2005, 2115, 2176, 2222, 2260, 2297, 2333, 2365, 2394, 2427, 2463, 2501, 2551, 2625, 2728, 2851,],
            [2286, 2410, 2480, 2528, 2574, 2613, 2650, 2689, 2723, 2758, 2790, 2830, 2879, 2957, 3049, 3197,],
            [2775, 2908, 3000, 3086, 3159, 3234, 3331, 3453,    0,    0,    0,    0,    0,    0,    0,    0,],
            [3150, 3272, 3354, 3415, 3473, 3531, 3580, 3676,    0,    0,    0,    0,    0,    0,    0,    0,],
        ]),

        # Speech Coding in Private and Broadcast Networks, Suddle, p121
        # note the transposition of first 8 values in column 7/8
        'suddle': np.array([
            [ 143,  182,  214,  246,  284,  329,  389,  475,    0,    0,    0,    0,    0,    0,    0,    0,],
            [ 211,  252,  285,  317,  349,  383,  419,  458,  503,  554,  608,  665,  731,  809,  912, 1072,],
            [ 402,  470,  522,  571,  621,  671,  724,  778,  835,  902,  979, 1065, 1147, 1241, 1357, 1517,],
            [ 617,  732,  819,  885,  944, 1001, 1060, 1121, 1186, 1260, 1342, 1425, 1514, 1613, 1723, 1885,],
            [ 981, 1081, 1172, 1254, 1329, 1403, 1473, 1539, 1609, 1679, 1753, 1826, 1908, 1998, 2106, 2236,],
            [1334, 1446, 1539, 1626, 1697, 1763, 1828, 1890, 1954, 2019, 2087, 2160, 2238, 2328, 2420, 2526,],
            [1830, 1959, 2056, 2134, 2198, 2254, 2303, 2349, 2397, 2448, 2500, 2560, 2632, 2715, 2823, 2966,],
            [2247, 2361, 2434, 2496, 2550, 2600, 2647, 2694, 2742, 2791, 2846, 2904, 2966, 3049, 3155, 3256,],
            [2347, 2481, 2583, 2674, 2767, 2874, 3005, 3202,    0,    0,    0,    0,    0,    0,    0,    0,],
            [3140, 3246, 3326, 3395, 3458, 3524, 3601, 3709,    0,    0,    0,    0,    0,    0,    0,    0,],
        ]),
    }

    lsf_weights = np.array([1/8, 3/8, 5/8, 7/8])

    vec_gain_quantizers = {
        # Suddle, chapter 7
        'audetel': np.array([
            -1996, -1306, -990, -780, -628, -510, -418, -336,
            -268, -204, -148, -96, -54, -20, -6, -2,
            2, 6, 20, 54, 96, 148, 204, 268,
            336, 418, 510, 628, 780, 990, 1306, 1996,
        ]),

        # Suddle, chapter 5
        'unknown': np.array([
            -1100,  -850,  -650,  -510,  -415,  -335,  -275,  -220,
             -175,  -135,   -98,   -65,   -35,   -12,    -3,    -1,
                1,     3,    12,    35,    65,    98,   135,   175,
              220,   275,   335,   415,   510,   650,   850,  1100,
        ]),
    }

    ltp_gain_quantization = np.array([
        -0.993, -0.831, -0.693, -0.555, -0.414, -0.229,    0.0,  0.193,
         0.255,  0.368,  0.457,  0.531,  0.601,  0.653,  0.702,  0.745,
         0.780,  0.816,  0.850,  0.881,  0.915,  0.948,  0.983,  1.020,
         1.062,  1.117,  1.193,  1.289,  1.394,  1.540,  1.765,  1.991,
    ])

    def __init__(self, lsf_lut='suddle', vec_gain_lut='audetel', sample_rate=8000):
        self.lsf_lut = lsf_lut
        self.lsf_vector_quantization = self.lsf_vector_quantizers[lsf_lut]
        self.vec_gain_quantization = self.vec_gain_quantizers[vec_gain_lut]
        self.sample_rate = sample_rate
        self.subframe_length = sample_rate // 200
        self.pos = 0
        self.vector_gain_errors = 0
        self.lsf_errors = 0
        self.subframes = 0

    @property
    def lsf_error(self):
        return 100.0 * self.lsf_errors / self.subframes

    @property
    def vector_gain_error(self):
        return 100.0 * self.vector_gain_errors / self.subframes

    def stats(self):
        return CELPStats(self)

    def vector_parity(self, data, parity):
        hamm = hamming7_dec[data>>1, parity]
        self.vector_gain_errors += np.sum(hamm >> 4)
        return ((hamm&0xf)<<1)|(data&1)

    def decode_params(self, raw_frame):
        """Extracts the parameters from the raw packet according to offsets and widths."""
        bits = np.unpackbits(raw_frame, bitorder='little')
        decoded_frame = np.empty((30, ), dtype=np.uint8)
        for n in range(len(self.offsets)-2):
            slice = bits[self.offsets[n]:self.offsets[n+1]]
            decoded_frame[n] = np.packbits(slice, bitorder='little')
        lsf = self.lsf_vector_quantization[np.arange(10), decoded_frame[:10]]
        if np.any(np.diff(lsf) < 0):
            self.lsf_errors += 4
        pitch_gain = self.ltp_gain_quantization[decoded_frame[10:14]]
        #vector_gain = self.vec_gain_quantization[decoded_frame[14:18]]  # no hamming correction
        vector_gain = self.vec_gain_quantization[self.vector_parity(decoded_frame[14:18], decoded_frame[26:30])]
        pitch_idx = decoded_frame[18:22]
        vector_idx = decoded_frame[22:26]
        self.subframes += 4
        return lsf, pitch_gain, vector_gain, pitch_idx, vector_idx

    # wave we're going to play through the filter
    wave = np.random.uniform(-1, 1, size=(8000, ))

    def apply_lpc_filter(self, lsf, signal):
        """Convert line spectrum frequencies to a filter and apply to the signal."""
        a = lsf2poly(sorted(lsf * 2 * np.pi / self.sample_rate))
        result, self.last_z = lfilter([1], a, signal, zi=self.last_z)
        return result

    def generate_audio(self, raw_frame):
        """Generate an audio frame from a raw frame."""
        lsf, pitch_gain, vector_gain, pitch_idx, vector_idx = self.decode_params(raw_frame)

        # interpolate the LSFs
        if self.last_lsf is None:
            sub_lsf = np.repeat(lsf, 4).reshape(10, 4).T
        else:
            sub_lsf = (self.last_lsf[np.newaxis, :] * self.lsf_weights[::-1, np.newaxis]) + (lsf[np.newaxis, :] * self.lsf_weights[:, np.newaxis])

        frame = np.empty((self.subframe_length * 4,), dtype=np.double)
        subframe_buf = np.empty((self.subframe_length), dtype=np.double)
        for subframe in range(4):
            for n in range(self.subframe_length):
                self.pos += 1
                subframe_buf[n] = (self.wave[self.pos % self.sample_rate] * vector_gain[subframe]) + (self.ltp_codebook.get(pitch_idx[subframe] - n) * pitch_gain[subframe])
            self.ltp_codebook.insert(subframe_buf)
            p = subframe * self.subframe_length
            frame[p:p + self.subframe_length] = self.apply_lpc_filter(sub_lsf[subframe], subframe_buf)

        self.last_lsf = lsf
        return np.clip(frame * 0.5, -32767, 32767).astype(np.int16)

    def decode_packet_stream(self, packets, frame=None):
        """Decode an entire packet stream, yielding audio frames."""
        self.last_lsf = None
        self.last_z = np.zeros((10, ))
        self.ltp_codebook = LtpCodebook(self.subframe_length)
        for p in packets:
            if frame is None or frame == 0:
                yield self.generate_audio(p._array[4:23])
            if frame is None or frame == 1:
                yield self.generate_audio(p._array[23:42])

    def stream_pcm(self, packets, frame, device):
        src = self.decode_packet_stream(packets, frame)
        buf = []
        buflen = 0
        required_samples = yield b""  # generator initialization
        for frame in src:
            buf.append(frame)
            buflen += frame.shape[0]
            if buflen >= required_samples:
                tmp = np.concatenate(buf)
                buf = [tmp[required_samples:]]
                buflen = buf[0].shape[0]
                required_samples = yield tmp[:required_samples].tobytes()
        device.__running = False

    def play(self, packets, frame=None):
        """Play a packet stream."""
        import miniaudio
        import time

        with miniaudio.PlaybackDevice(output_format=miniaudio.SampleFormat.SIGNED16,
                                      nchannels=1, sample_rate=self.sample_rate,
                                      buffersize_msec=500) as device:
            device.__running = True
            stream = self.stream_pcm(packets, frame, device)
            next(stream)  # start the generator
            device.start(stream)
            while device.__running:
                time.sleep(0.1)

    def convert(self, output, packets, frame=None):
        import wave
        wf = wave.open(output, 'wb')
        wf.setnchannels(1)
        wf.setsampwidth(2)
        wf.setframerate(8000)
        for frame in self.decode_packet_stream(packets, frame):
            wf.writeframes(frame.tobytes())
        wf.close()

    @classmethod
    def plot(cls, packets):
        """Plot statistics of the raw packets."""
        datas = []
        for p in packets:
            datas.append(p._array[4:])
        datas = np.concatenate(datas)

        data = np.unpackbits(datas.reshape(-1, 2, 19), bitorder='little').reshape(-1, 2, 152)
        d1 = np.sum(data, axis=0)
        p = np.arange(152)

        fig, ax = plt.subplots(6, 2)

        # plot the bit counts (top plots)
        for n in range(cls.offsets.shape[0]-1):
            s = slice(cls.offsets[n], cls.offsets[n + 1])
            ax[0][0].bar(p[s], d1[0][s], 0.8)
            ax[0][1].bar(p[s], d1[1][s], 0.8)

        # plot vector and pitch parameters over time
        for x in range(2):
            frame = data[:, x, :]
            for y, o in enumerate([10, 14, 18, 22], start=2):
                bits = frame[:,cls.offsets[o]:cls.offsets[o+4]].reshape(-1, cls.widths[o+1])
                a = np.packbits(bits, axis=-1, bitorder='little').flatten()
                ax[y][x].plot(a[:10000], linewidth=0.1)
                if y == 3:
                    hm = frame[:, cls.offsets[26]:cls.offsets[30]].reshape(-1, cls.widths[27])
                    hm = np.packbits(hm, axis=-1, bitorder='little').flatten()
                    for ham in range(8):
                        h = np.histogram(a[np.where(hm == ham)]>>1, np.arange(17))
                        ax[1][x].bar(np.arange(16), h[0])

        fig.tight_layout()
        plt.show()


================================================
FILE: teletext/charset.py
================================================
#	Name:   Map from Teletext G0 character set to Unicode
#	Date:   2018 April 20
#	Author: Rebecca Bettencourt <support@kreativekorp.com>

g0 = {'default': {
    0x20: chr(0x0020),  # SPACE
    0x21: chr(0x0021),  # EXCLAMATION MARK
    0x22: chr(0x0022),  # QUOTATION MARK
    0x23: chr(0x00A3),  # POUND SIGN
    0x24: chr(0x0024),  # DOLLAR SIGN
    0x25: chr(0x0025),  # PERCENT SIGN
    0x26: chr(0x0026),  # AMPERSAND
    0x27: chr(0x0027),  # APOSTROPHE
    0x28: chr(0x0028),  # LEFT PARENTHESIS
    0x29: chr(0x0029),  # RIGHT PARENTHESIS
    0x2A: chr(0x002A),  # ASTERISK
    0x2B: chr(0x002B),  # PLUS SIGN
    0x2C: chr(0x002C),  # COMMA
    0x2D: chr(0x002D),  # HYPHEN-MINUS
    0x2E: chr(0x002E),  # FULL STOP
    0x2F: chr(0x002F),  # SOLIDUS
    0x30: chr(0x0030),  # DIGIT ZERO
    0x31: chr(0x0031),  # DIGIT ONE
    0x32: chr(0x0032),  # DIGIT TWO
    0x33: chr(0x0033),  # DIGIT THREE
    0x34: chr(0x0034),  # DIGIT FOUR
    0x35: chr(0x0035),  # DIGIT FIVE
    0x36: chr(0x0036),  # DIGIT SIX
    0x37: chr(0x0037),  # DIGIT SEVEN
    0x38: chr(0x0038),  # DIGIT EIGHT
    0x39: chr(0x0039),  # DIGIT NINE
    0x3A: chr(0x003A),  # COLON
    0x3B: chr(0x003B),  # SEMICOLON
    0x3C: chr(0x003C),  # LESS-THAN SIGN
    0x3D: chr(0x003D),  # EQUALS SIGN
    0x3E: chr(0x003E),  # GREATER-THAN SIGN
    0x3F: chr(0x003F),  # QUESTION MARK
    0x40: chr(0x0040),  # COMMERCIAL AT
    0x41: chr(0x0041),  # LATIN CAPITAL LETTER A
    0x42: chr(0x0042),  # LATIN CAPITAL LETTER B
    0x43: chr(0x0043),  # LATIN CAPITAL LETTER C
    0x44: chr(0x0044),  # LATIN CAPITAL LETTER D
    0x45: chr(0x0045),  # LATIN CAPITAL LETTER E
    0x46: chr(0x0046),  # LATIN CAPITAL LETTER F
    0x47: chr(0x0047),  # LATIN CAPITAL LETTER G
    0x48: chr(0x0048),  # LATIN CAPITAL LETTER H
    0x49: chr(0x0049),  # LATIN CAPITAL LETTER I
    0x4A: chr(0x004A),  # LATIN CAPITAL LETTER J
    0x4B: chr(0x004B),  # LATIN CAPITAL LETTER K
    0x4C: chr(0x004C),  # LATIN CAPITAL LETTER L
    0x4D: chr(0x004D),  # LATIN CAPITAL LETTER M
    0x4E: chr(0x004E),  # LATIN CAPITAL LETTER N
    0x4F: chr(0x004F),  # LATIN CAPITAL LETTER O
    0x50: chr(0x0050),  # LATIN CAPITAL LETTER P
    0x51: chr(0x0051),  # LATIN CAPITAL LETTER Q
    0x52: chr(0x0052),  # LATIN CAPITAL LETTER R
    0x53: chr(0x0053),  # LATIN CAPITAL LETTER S
    0x54: chr(0x0054),  # LATIN CAPITAL LETTER T
    0x55: chr(0x0055),  # LATIN CAPITAL LETTER U
    0x56: chr(0x0056),  # LATIN CAPITAL LETTER V
    0x57: chr(0x0057),  # LATIN CAPITAL LETTER W
    0x58: chr(0x0058),  # LATIN CAPITAL LETTER X
    0x59: chr(0x0059),  # LATIN CAPITAL LETTER Y
    0x5A: chr(0x005A),  # LATIN CAPITAL LETTER Z
    0x5B: chr(0x2190),  # LEFTWARDS ARROW
    0x5C: chr(0x00BD),  # VULGAR FRACTION ONE HALF
    0x5D: chr(0x2192),  # RIGHTWARDS ARROW
    0x5E: chr(0x2191),  # UPWARDS ARROW
    0x5F: chr(0x0023),  # NUMBER SIGN
    #0x60: chr(0x2500),  # BOX DRAWINGS LIGHT HORIZONTAL
    0x60: chr(0x2014),  # EM DASH
    0x61: chr(0x0061),  # LATIN SMALL LETTER A
    0x62: chr(0x0062),  # LATIN SMALL LETTER B
    0x63: chr(0x0063),  # LATIN SMALL LETTER C
    0x64: chr(0x0064),  # LATIN SMALL LETTER D
    0x65: chr(0x0065),  # LATIN SMALL LETTER E
    0x66: chr(0x0066),  # LATIN SMALL LETTER F
    0x67: chr(0x0067),  # LATIN SMALL LETTER G
    0x68: chr(0x0068),  # LATIN SMALL LETTER H
    0x69: chr(0x0069),  # LATIN SMALL LETTER I
    0x6A: chr(0x006A),  # LATIN SMALL LETTER J
    0x6B: chr(0x006B),  # LATIN SMALL LETTER K
    0x6C: chr(0x006C),  # LATIN SMALL LETTER L
    0x6D: chr(0x006D),  # LATIN SMALL LETTER M
    0x6E: chr(0x006E),  # LATIN SMALL LETTER N
    0x6F: chr(0x006F),  # LATIN SMALL LETTER O
    0x70: chr(0x0070),  # LATIN SMALL LETTER P
    0x71: chr(0x0071),  # LATIN SMALL LETTER Q
    0x72: chr(0x0072),  # LATIN SMALL LETTER R
    0x73: chr(0x0073),  # LATIN SMALL LETTER S
    0x74: chr(0x0074),  # LATIN SMALL LETTER T
    0x75: chr(0x0075),  # LATIN SMALL LETTER U
    0x76: chr(0x0076),  # LATIN SMALL LETTER V
    0x77: chr(0x0077),  # LATIN SMALL LETTER W
    0x78: chr(0x0078),  # LATIN SMALL LETTER X
    0x79: chr(0x0079),  # LATIN SMALL LETTER Y
    0x7A: chr(0x007A),  # LATIN SMALL LETTER Z
    0x7B: chr(0x00BC),  # VULGAR FRACTION ONE QUARTER
    0x7C: chr(0x2016),  # DOUBLE VERTICAL LINE
    0x7D: chr(0x00BE),  # VULGAR FRACTION THREE QUARTERS
    0x7E: chr(0x00F7),  # DIVISION SIGN
    0x7F: chr(0x25A0),  # BLACK SQUARE
}, 'cyr': {
    0x20: chr(0x0020),  # SPACE
    0x21: chr(0x0021),  # EXCLAMATION MARK
    0x22: chr(0x0022),  # QUOTATION MARK
    0x23: chr(0x00A3),  # POUND SIGN
    0x24: chr(0x0024),  # DOLLAR SIGN
    0x25: chr(0x0025),  # PERCENT SIGN
    0x26: chr(0x044B),  # CYRILLIC SMALL LETTER YERU
    0x27: chr(0x0027),  # APOSTROPHE
    0x28: chr(0x0028),  # LEFT PARENTHESIS
    0x29: chr(0x0029),  # RIGHT PARENTHESIS
    0x2A: chr(0x002A),  # ASTERISK
    0x2B: chr(0x002B),  # PLUS SIGN
    0x2C: chr(0x002C),  # COMMA
    0x2D: chr(0x002D),  # HYPHEN-MINUS
    0x2E: chr(0x002E),  # FULL STOP
    0x2F: chr(0x002F),  # SOLIDUS
    0x30: chr(0x0030),  # DIGIT ZERO
    0x31: chr(0x0031),  # DIGIT ONE
    0x32: chr(0x0032),  # DIGIT TWO
    0x33: chr(0x0033),  # DIGIT THREE
    0x34: chr(0x0034),  # DIGIT FOUR
    0x35: chr(0x0035),  # DIGIT FIVE
    0x36: chr(0x0036),  # DIGIT SIX
    0x37: chr(0x0037),  # DIGIT SEVEN
    0x38: chr(0x0038),  # DIGIT EIGHT
    0x39: chr(0x0039),  # DIGIT NINE
    0x3A: chr(0x003A),  # COLON
    0x3B: chr(0x003B),  # SEMICOLON
    0x3C: chr(0x003C),  # LESS-THAN SIGN
    0x3D: chr(0x003D),  # EQUALS SIGN
    0x3E: chr(0x003E),  # GREATER-THAN SIGN
    0x3F: chr(0x003F),  # QUESTION MARK
    0x40: chr(0x042E),  # CYRILLIC CAPITAL LETTER YU
    0x41: chr(0x0410),  # CYRILLIC CAPITAL LETTER A
    0x42: chr(0x0411),  # CYRILLIC CAPITAL LETTER BE
    0x43: chr(0x0426),  # CYRILLIC CAPITAL LETTER TSE
    0x44: chr(0x0414),  # CYRILLIC CAPITAL LETTER DE
    0x45: chr(0x0415),  # CYRILLIC CAPITAL LETTER IE
    0x46: chr(0x0424),  # CYRILLIC CAPITAL LETTER EF
    0x47: chr(0x0413),  # CYRILLIC CAPITAL LETTER GHE
    0x48: chr(0x0425),  # CYRILLIC CAPITAL LETTER HA
    0x49: chr(0x0418),  # CYRILLIC CAPITAL LETTER I
    0x4A: chr(0x0419),  # CYRILLIC CAPITAL LETTER SHORT I
    0x4B: chr(0x041A),  # CYRILLIC CAPITAL LETTER KA
    0x4C: chr(0x041B),  # CYRILLIC CAPITAL LETTER EL
    0x4D: chr(0x041C),  # CYRILLIC CAPITAL LETTER EМ
    0x4E: chr(0x041D),  # CYRILLIC CAPITAL LETTER EN
    0x4F: chr(0x041E),  # CYRILLIC CAPITAL LETTER O
    0x50: chr(0x041F),  # CYRILLIC CAPITAL LETTER PE
    0x51: chr(0x042F),  # CYRILLIC CAPITAL LETTER YA
    0x52: chr(0x0420),  # CYRILLIC CAPITAL LETTER ER
    0x53: chr(0x0421),  # CYRILLIC CAPITAL LETTER ES
    0x54: chr(0x0422),  # CYRILLIC CAPITAL LETTER TE
    0x55: chr(0x0423),  # CYRILLIC CAPITAL LETTER U
    0x56: chr(0x0416),  # CYRILLIC CAPITAL LETTER ZHE
    0x57: chr(0x0412),  # CYRILLIC CAPITAL LETTER BE
    0x58: chr(0x042C),  # CYRILLIC CAPITAL LETTER SOFT SIGN
    0x59: chr(0x042A),  # CYRILLIC CAPITAL LETTER HARD SIGN
    0x5A: chr(0x0417),  # CYRILLIC CAPITAL LETTER ZE
    0x5B: chr(0x0428),  # CYRILLIC CAPITAL LETTER SHA
    0x5C: chr(0x042D),  # CYRILLIC CAPITAL LETTER E
    0x5D: chr(0x0429),  # CYRILLIC CAPITAL LETTER SHCHA
    0x5E: chr(0x0427),  # CYRILLIC CAPITAL LETTER CHA
    0x5F: chr(0x042B),  # CYRILLIC CAPITAL LETTER YERU
    0x60: chr(0x044E),  # CYRILLIC SMALL LETTER YU
    0x61: chr(0x0430),  # CYRILLIC SMALL LETTER A
    0x62: chr(0x0431),  # CYRILLIC SMALL LETTER BE
    0x63: chr(0x0446),  # CYRILLIC SMALL LETTER TSE
    0x64: chr(0x0434),  # CYRILLIC SMALL LETTER DE
    0x65: chr(0x0435),  # CYRILLIC SMALL LETTER IE
    0x66: chr(0x0444),  # CYRILLIC SMALL LETTER EF
    0x67: chr(0x0433),  # CYRILLIC SMALL LETTER GHE
    0x68: chr(0x0445),  # CYRILLIC SMALL LETTER HA
    0x69: chr(0x0438),  # CYRILLIC SMALL LETTER I
    0x6A: chr(0x0439),  # CYRILLIC SMALL LETTER SHORT I
    0x6B: chr(0x043A),  # CYRILLIC SMALL LETTER KA
    0x6C: chr(0x043B),  # CYRILLIC SMALL LETTER EL
    0x6D: chr(0x043C),  # CYRILLIC SMALL LETTER EM
    0x6E: chr(0x043D),  # CYRILLIC SMALL LETTER EN
    0x6F: chr(0x043E),  # CYRILLIC SMALL LETTER O
    0x70: chr(0x043F),  # CYRILLIC SMALL LETTER PE
    0x71: chr(0x044F),  # CYRILLIC SMALL LETTER YA
    0x72: chr(0x0440),  # CYRILLIC SMALL LETTER ER
    0x73: chr(0x0441),  # CYRILLIC SMALL LETTER ES
    0x74: chr(0x0442),  # CYRILLIC SMALL LETTER TE
    0x75: chr(0x0443),  # CYRILLIC SMALL LETTER U
    0x76: chr(0x0436),  # CYRILLIC SMALL LETTER ZHE
    0x77: chr(0x0432),  # CYRILLIC SMALL LETTER BE
    0x78: chr(0x044C),  # CYRILLIC SMALL LETTER SOFT SIGN
    0x79: chr(0x044A),  # CYRILLIC SMALL LETTER HARD SIGN
    0x7A: chr(0x0437),  # CYRILLIC SMALL LETTER ZE
    0x7B: chr(0x0448),  # CYRILLIC SMALL LETTER SHA
    0x7C: chr(0x044D),  # CYRILLIC SMALL LETTER E
    0x7D: chr(0x0449),  # CYRILLIC SMALL LETTER SHCHA
    0x7E: chr(0x0447),  # CYRILLIC SMALL LETTER CHE
    0x7F: chr(0x25A0),  # BLACK SQUARE

    # Swedish national subset (replaces characters 0x40, 0x5B-0x5F, 0x60, 0x7B-0x7E)
}, 'swe': {
    0x20: chr(0x0020),  # SPACE
    0x21: chr(0x0021),  # EXCLAMATION MARK
    0x22: chr(0x0022),  # QUOTATION MARK
    0x23: chr(0x00A3),  # POUND SIGN
    0x24: chr(0x0024),  # DOLLAR SIGN
    0x25: chr(0x0025),  # PERCENT SIGN
    0x26: chr(0x0026),  # AMPERSAND
    0x27: chr(0x0027),  # APOSTROPHE
    0x28: chr(0x0028),  # LEFT PARENTHESIS
    0x29: chr(0x0029),  # RIGHT PARENTHESIS
    0x2A: chr(0x002A),  # ASTERISK
    0x2B: chr(0x002B),  # PLUS SIGN
    0x2C: chr(0x002C),  # COMMA
    0x2D: chr(0x002D),  # HYPHEN-MINUS
    0x2E: chr(0x002E),  # FULL STOP
    0x2F: chr(0x002F),  # SOLIDUS
    0x30: chr(0x0030),  # DIGIT ZERO
    0x31: chr(0x0031),  # DIGIT ONE
    0x32: chr(0x0032),  # DIGIT TWO
    0x33: chr(0x0033),  # DIGIT THREE
    0x34: chr(0x0034),  # DIGIT FOUR
    0x35: chr(0x0035),  # DIGIT FIVE
    0x36: chr(0x0036),  # DIGIT SIX
    0x37: chr(0x0037),  # DIGIT SEVEN
    0x38: chr(0x0038),  # DIGIT EIGHT
    0x39: chr(0x0039),  # DIGIT NINE
    0x3A: chr(0x003A),  # COLON
    0x3B: chr(0x003B),  # SEMICOLON
    0x3C: chr(0x003C),  # LESS-THAN SIGN
    0x3D: chr(0x003D),  # EQUALS SIGN
    0x3E: chr(0x003E),  # GREATER-THAN SIGN
    0x3F: chr(0x003F),  # QUESTION MARK
    0x40: chr(0x00C9),  # CAPITAL E-ACUTE
    0x41: chr(0x0041),  # LATIN CAPITAL LETTER A
    0x42: chr(0x0042),  # LATIN CAPITAL LETTER B
    0x43: chr(0x0043),  # LATIN CAPITAL LETTER C
    0x44: chr(0x0044),  # LATIN CAPITAL LETTER D
    0x45: chr(0x0045),  # LATIN CAPITAL LETTER E
    0x46: chr(0x0046),  # LATIN CAPITAL LETTER F
    0x47: chr(0x0047),  # LATIN CAPITAL LETTER G
    0x48: chr(0x0048),  # LATIN CAPITAL LETTER H
    0x49: chr(0x0049),  # LATIN CAPITAL LETTER I
    0x4A: chr(0x004A),  # LATIN CAPITAL LETTER J
    0x4B: chr(0x004B),  # LATIN CAPITAL LETTER K
    0x4C: chr(0x004C),  # LATIN CAPITAL LETTER L
    0x4D: chr(0x004D),  # LATIN CAPITAL LETTER M
    0x4E: chr(0x004E),  # LATIN CAPITAL LETTER N
    0x4F: chr(0x004F),  # LATIN CAPITAL LETTER O
    0x50: chr(0x0050),  # LATIN CAPITAL LETTER P
    0x51: chr(0x0051),  # LATIN CAPITAL LETTER Q
    0x52: chr(0x0052),  # LATIN CAPITAL LETTER R
    0x53: chr(0x0053),  # LATIN CAPITAL LETTER S
    0x54: chr(0x0054),  # LATIN CAPITAL LETTER T
    0x55: chr(0x0055),  # LATIN CAPITAL LETTER U
    0x56: chr(0x0056),  # LATIN CAPITAL LETTER V
    0x57: chr(0x0057),  # LATIN CAPITAL LETTER W
    0x58: chr(0x0058),  # LATIN CAPITAL LETTER X
    0x59: chr(0x0059),  # LATIN CAPITAL LETTER Y
    0x5A: chr(0x005A),  # LATIN CAPITAL LETTER Z
    0x5B: chr(0x00C4),  # LATIN CAPITAL A WITH DIAERESIS
    0x5C: chr(0x00D6),  # LATIN CAPITAL O WITH DIAERESIS 
    0x5D: chr(0x00C5),  # LATIN CAPITAL A WITH OVERRING
    0x5E: chr(0x00DC),  # LATIN CAPITAL U WITH DIAERESIS
    0x5F: chr(0x005F),  # UNDERSCORE
    #0x60: chr(0x2500),  # BOX DRAWINGS LIGHT HORIZONTAL
    0x60: chr(0x00E9),  # LOWER-CASE E-ACUTE
    0x61: chr(0x0061),  # LATIN SMALL LETTER A
    0x62: chr(0x0062),  # LATIN SMALL LETTER B
    0x63: chr(0x0063),  # LATIN SMALL LETTER C
    0x64: chr(0x0064),  # LATIN SMALL LETTER D
    0x65: chr(0x0065),  # LATIN SMALL LETTER E
    0x66: chr(0x0066),  # LATIN SMALL LETTER F
    0x67: chr(0x0067),  # LATIN SMALL LETTER G
    0x68: chr(0x0068),  # LATIN SMALL LETTER H
    0x69: chr(0x0069),  # LATIN SMALL LETTER I
    0x6A: chr(0x006A),  # LATIN SMALL LETTER J
    0x6B: chr(0x006B),  # LATIN SMALL LETTER K
    0x6C: chr(0x006C),  # LATIN SMALL LETTER L
    0x6D: chr(0x006D),  # LATIN SMALL LETTER M
    0x6E: chr(0x006E),  # LATIN SMALL LETTER N
    0x6F: chr(0x006F),  # LATIN SMALL LETTER O
    0x70: chr(0x0070),  # LATIN SMALL LETTER P
    0x71: chr(0x0071),  # LATIN SMALL LETTER Q
    0x72: chr(0x0072),  # LATIN SMALL LETTER R
    0x73: chr(0x0073),  # LATIN SMALL LETTER S
    0x74: chr(0x0074),  # LATIN SMALL LETTER T
    0x75: chr(0x0075),  # LATIN SMALL LETTER U
    0x76: chr(0x0076),  # LATIN SMALL LETTER V
    0x77: chr(0x0077),  # LATIN SMALL LETTER W
    0x78: chr(0x0078),  # LATIN SMALL LETTER X
    0x79: chr(0x0079),  # LATIN SMALL LETTER Y
    0x7A: chr(0x007A),  # LATIN SMALL LETTER Z
    0x7B: chr(0x00E4),  # LATIN SMALL A WITH DIAERESIS
    0x7C: chr(0x00F6),  # LATIN SMALL O WITH DIAERESIS
    0x7D: chr(0x00E5),  # LATIN SMALL A WITH OVERRING
    0x7E: chr(0x00FC),  # LATIN SMALL U WITH DIAERESIS
    0x7F: chr(0x25A0),  # BLACK SQUARE

}}

#       Name:   Map from Teletext G1 character set to Unicode
#       Date:   2018 April 20
#       Author: Rebecca Bettencourt <support@kreativekorp.com>

g1 = {
    0x20: chr(0x00A0), # NO-BREAK SPACE; unification of EMPTY BLOCK SEXTANT
    0x21: chr(0x1FB00), # BLOCK SEXTANT-1
    0x22: chr(0x1FB01), # BLOCK SEXTANT-2
    0x23: chr(0x1FB02), # BLOCK SEXTANT-12
    0x24: chr(0x1FB03), # BLOCK SEXTANT-3
    0x25: chr(0x1FB04), # BLOCK SEXTANT-13
    0x26: chr(0x1FB05), # BLOCK SEXTANT-23
    0x27: chr(0x1FB06), # BLOCK SEXTANT-123
    0x28: chr(0x1FB07), # BLOCK SEXTANT-4
    0x29: chr(0x1FB08), # BLOCK SEXTANT-14
    0x2A: chr(0x1FB09), # BLOCK SEXTANT-24
    0x2B: chr(0x1FB0A), # BLOCK SEXTANT-124
    0x2C: chr(0x1FB0B), # BLOCK SEXTANT-34
    0x2D: chr(0x1FB0C), # BLOCK SEXTANT-134
    0x2E: chr(0x1FB0D), # BLOCK SEXTANT-234
    0x2F: chr(0x1FB0E), # BLOCK SEXTANT-1234
    0x30: chr(0x1FB0F), # BLOCK SEXTANT-5
    0x31: chr(0x1FB10), # BLOCK SEXTANT-15
    0x32: chr(0x1FB11), # BLOCK SEXTANT-25
    0x33: chr(0x1FB12), # BLOCK SEXTANT-125
    0x34: chr(0x1FB13), # BLOCK SEXTANT-35
    0x35: chr(0x258C), # LEFT HALF BLOCK; unification of BLOCK SEXTANT-135
    0x36: chr(0x1FB14), # BLOCK SEXTANT-235
    0x37: chr(0x1FB15), # BLOCK SEXTANT-1235
    0x38: chr(0x1FB16), # BLOCK SEXTANT-45
    0x39: chr(0x1FB17), # BLOCK SEXTANT-145
    0x3A: chr(0x1FB18), # BLOCK SEXTANT-245
    0x3B: chr(0x1FB19), # BLOCK SEXTANT-1245
    0x3C: chr(0x1FB1A), # BLOCK SEXTANT-345
    0x3D: chr(0x1FB1B), # BLOCK SEXTANT-1345
    0x3E: chr(0x1FB1C), # BLOCK SEXTANT-2345
    0x3F: chr(0x1FB1D), # BLOCK SEXTANT-12345
    0x40: chr(0x0040), # COMMERCIAL AT
    0x41: chr(0x0041), # LATIN CAPITAL LETTER A
    0x42: chr(0x0042), # LATIN CAPITAL LETTER B
    0x43: chr(0x0043), # LATIN CAPITAL LETTER C
    0x44: chr(0x0044), # LATIN CAPITAL LETTER D
    0x45: chr(0x0045), # LATIN CAPITAL LETTER E
    0x46: chr(0x0046), # LATIN CAPITAL LETTER F
    0x47: chr(0x0047), # LATIN CAPITAL LETTER G
    0x48: chr(0x0048), # LATIN CAPITAL LETTER H
    0x49: chr(0x0049), # LATIN CAPITAL LETTER I
    0x4A: chr(0x004A), # LATIN CAPITAL LETTER J
    0x4B: chr(0x004B), # LATIN CAPITAL LETTER K
    0x4C: chr(0x004C), # LATIN CAPITAL LETTER L
    0x4D: chr(0x004D), # LATIN CAPITAL LETTER M
    0x4E: chr(0x004E), # LATIN CAPITAL LETTER N
    0x4F: chr(0x004F), # LATIN CAPITAL LETTER O
    0x50: chr(0x0050), # LATIN CAPITAL LETTER P
    0x51: chr(0x0051), # LATIN CAPITAL LETTER Q
    0x52: chr(0x0052), # LATIN CAPITAL LETTER R
    0x53: chr(0x0053), # LATIN CAPITAL LETTER S
    0x54: chr(0x0054), # LATIN CAPITAL LETTER T
    0x55: chr(0x0055), # LATIN CAPITAL LETTER U
    0x56: chr(0x0056), # LATIN CAPITAL LETTER V
    0x57: chr(0x0057), # LATIN CAPITAL LETTER W
    0x58: chr(0x0058), # LATIN CAPITAL LETTER X
    0x59: chr(0x0059), # LATIN CAPITAL LETTER Y
    0x5A: chr(0x005A), # LATIN CAPITAL LETTER Z
    0x5B: chr(0x2190), # LEFTWARDS ARROW
    0x5C: chr(0x00BD), # VULGAR FRACTION ONE HALF
    0x5D: chr(0x2192), # RIGHTWARDS ARROW
    0x5E: chr(0x2191), # UPWARDS ARROW
    0x5F: chr(0x0023), # NUMBER SIGN
    0x60: chr(0x1FB1E), # BLOCK SEXTANT-6
    0x61: chr(0x1FB1F), # BLOCK SEXTANT-16
    0x62: chr(0x1FB20), # BLOCK SEXTANT-26
    0x63: chr(0x1FB21), # BLOCK SEXTANT-126
    0x64: chr(0x1FB22), # BLOCK SEXTANT-36
    0x65: chr(0x1FB23), # BLOCK SEXTANT-136
    0x66: chr(0x1FB24), # BLOCK SEXTANT-236
    0x67: chr(0x1FB25), # BLOCK SEXTANT-1236
    0x68: chr(0x1FB26), # BLOCK SEXTANT-46
    0x69: chr(0x1FB27), # BLOCK SEXTANT-146
    0x6A: chr(0x2590), # RIGHT HALF BLOCK; unification of BLOCK SEXTANT-246
    0x6B: chr(0x1FB28), # BLOCK SEXTANT-1246
    0x6C: chr(0x1FB29), # BLOCK SEXTANT-346
    0x6D: chr(0x1FB2A), # BLOCK SEXTANT-1346
    0x6E: chr(0x1FB2B), # BLOCK SEXTANT-2346
    0x6F: chr(0x1FB2C), # BLOCK SEXTANT-12346
    0x70: chr(0x1FB2D), # BLOCK SEXTANT-56
    0x71: chr(0x1FB2E), # BLOCK SEXTANT-156
    0x72: chr(0x1FB2F), # BLOCK SEXTANT-256
    0x73: chr(0x1FB30), # BLOCK SEXTANT-1256
    0x74: chr(0x1FB31), # BLOCK SEXTANT-356
    0x75: chr(0x1FB32), # BLOCK SEXTANT-1356
    0x76: chr(0x1FB33), # BLOCK SEXTANT-2356
    0x77: chr(0x1FB34), # BLOCK SEXTANT-12356
    0x78: chr(0x1FB35), # BLOCK SEXTANT-456
    0x79: chr(0x1FB36), # BLOCK SEXTANT-1456
    0x7A: chr(0x1FB37), # BLOCK SEXTANT-2456
    0x7B: chr(0x1FB38), # BLOCK SEXTANT-12456
    0x7C: chr(0x1FB39), # BLOCK SEXTANT-3456
    0x7D: chr(0x1FB3A), # BLOCK SEXTANT-13456
    0x7E: chr(0x1FB3B), # BLOCK SEXTANT-23456
    0x7F: chr(0x2588), # FULL BLOCK; unification of BLOCK SEXTANT-123456
}


#       Name:   Map from Teletext G2 character set to Unicode
#       Date:   2018 April 20
#       Author: Rebecca Bettencourt <support@kreativekorp.com>

g2 = {
    0x20: chr(0x0020), # SPACE
    0x21: chr(0x00A1), # INVERTED EXCLAMATION MARK
    0x22: chr(0x00A2), # CENT SIGN
    0x23: chr(0x00A3), # POUND SIGN
    0x24: chr(0x0024), # DOLLAR SIGN
    0x25: chr(0x00A5), # YEN SIGN
    0x26: chr(0x0023), # NUMBER SIGN
    0x27: chr(0x00A7), # SECTION SIGN
    0x28: chr(0x00A4), # CURRENCY SIGN
    0x29: chr(0x2018), # LEFT SINGLE QUOTATION MARK
    0x2A: chr(0x201C), # LEFT DOUBLE QUOTATION MARK
    0x2B: chr(0x00AB), # LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
    0x2C: chr(0x2190), # LEFTWARDS ARROW
    0x2D: chr(0x2191), # UPWARDS ARROW
    0x2E: chr(0x2192), # RIGHTWARDS ARROW
    0x2F: chr(0x2193), # DOWNWARDS ARROW
    0x30: chr(0x00B0), # DEGREE SIGN
    0x31: chr(0x00B1), # PLUS-MINUS SIGN
    0x32: chr(0x00B2), # SUPERSCRIPT TWO
    0x33: chr(0x00B3), # SUPERSCRIPT THREE
    0x34: chr(0x00D7), # MULTIPLICATION SIGN
    0x35: chr(0x00B5), # MICRO SIGN
    0x36: chr(0x00B6), # PILCROW SIGN
    0x37: chr(0x00B7), # MIDDLE DOT
    0x38: chr(0x00F7), # DIVISION SIGN
    0x39: chr(0x2019), # RIGHT SINGLE QUOTATION MARK
    0x3A: chr(0x201D), # RIGHT DOUBLE QUOTATION MARK
    0x3B: chr(0x00BB), # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
    0x3C: chr(0x00BC), # VULGAR FRACTION ONE QUARTER
    0x3D: chr(0x00BD), # VULGAR FRACTION ONE HALF
    0x3E: chr(0x00BE), # VULGAR FRACTION THREE QUARTERS
    0x3F: chr(0x00BF), # INVERTED QUESTION MARK
    0x40: chr(0x00A0), # NO-BREAK SPACE
    0x41: chr(0x02CB), # MODIFIER LETTER GRAVE ACCENT
    0x42: chr(0x02CA), # MODIFIER LETTER ACUTE ACCENT
    0x43: chr(0x02C6), # MODIFIER LETTER CIRCUMFLEX ACCENT
    0x44: chr(0x02DC), # SMALL TILDE
    0x45: chr(0x02C9), # MODIFIER LETTER MACRON
    0x46: chr(0x02D8), # BREVE
    0x47: chr(0x02D9), # DOT ABOVE
    0x48: chr(0x00A8), # DIAERESIS
    0x49: chr(0x02CC), # MODIFIER LETTER LOW VERTICAL LINE
    0x4A: chr(0x02DA), # RING ABOVE
    0x4B: chr(0x00B8), # CEDILLA
    0x4C: chr(0x005F), # LOW LINE
    0x4D: chr(0x02DD), # DOUBLE ACUTE ACCENT
    0x4E: chr(0x02DB), # OGONEK
    0x4F: chr(0x02C7), # CARON
    0x50: chr(0x2500), # BOX DRAWINGS LIGHT HORIZONTAL
    0x51: chr(0x00B9), # SUPERSCRIPT ONE
    0x52: chr(0x00AE), # REGISTERED SIGN
    0x53: chr(0x00A9), # COPYRIGHT SIGN
    0x54: chr(0x2122), # TRADE MARK SIGN
    0x55: chr(0x266A), # EIGHTH NOTE
    0x56: chr(0x20A0), # EURO-CURRENCY SIGN
    0x57: chr(0x2030), # PER MILLE SIGN
    0x58: chr(0x03B1), # GREEK SMALL LETTER ALPHA
    0x5C: chr(0x215B), # VULGAR FRACTION ONE EIGHTH
    0x5D: chr(0x215C), # VULGAR FRACTION THREE EIGHTHS
    0x5E: chr(0x215D), # VULGAR FRACTION FIVE EIGHTHS
    0x5F: chr(0x215E), # VULGAR FRACTION SEVEN EIGHTHS
    0x60: chr(0x03A9), # GREEK CAPITAL LETTER OMEGA
    0x61: chr(0x00C6), # LATIN CAPITAL LETTER AE
    0x62: chr(0x00D0), # LATIN CAPITAL LETTER ETH
    0x63: chr(0x00AA), # FEMININE ORDINAL INDICATOR
    0x64: chr(0x0126), # LATIN CAPITAL LETTER H WITH STROKE
    0x66: chr(0x0132), # LATIN CAPITAL LIGATURE IJ
    0x67: chr(0x013F), # LATIN CAPITAL LETTER L WITH MIDDLE DOT
    0x68: chr(0x0141), # LATIN CAPITAL LETTER L WITH STROKE
    0x69: chr(0x00D8), # LATIN CAPITAL LETTER O WITH STROKE
    0x6A: chr(0x0152), # LATIN CAPITAL LIGATURE OE
    0x6B: chr(0x00BA), # MASCULINE ORDINAL INDICATOR
    0x6C: chr(0x00DE), # LATIN CAPITAL LETTER THORN
    0x6D: chr(0x0166), # LATIN CAPITAL LETTER T WITH STROKE
    0x6E: chr(0x014A), # LATIN CAPITAL LETTER ENG
    0x6F: chr(0x0149), # LATIN SMALL LETTER N PRECEDED BY APOSTROPHE
    0x70: chr(0x0138), # LATIN SMALL LETTER KRA
    0x71: chr(0x00E6), # LATIN SMALL LETTER AE
    0x72: chr(0x0111), # LATIN SMALL LETTER D WITH STROKE
    0x73: chr(0x00F0), # LATIN SMALL LETTER ETH
    0x74: chr(0x0127), # LATIN SMALL LETTER H WITH STROKE
    0x75: chr(0x0131), # LATIN SMALL LETTER DOTLESS I
    0x76: chr(0x0133), # LATIN SMALL LIGATURE IJ
    0x77: chr(0x0140), # LATIN SMALL LETTER L WITH MIDDLE DOT
    0x78: chr(0x0142), # LATIN SMALL LETTER L WITH STROKE
    0x79: chr(0x00F8), # LATIN SMALL LETTER O WITH STROKE
    0x7A: chr(0x0153), # LATIN SMALL LIGATURE OE
    0x7B: chr(0x00DF), # LATIN SMALL LETTER SHARP S
    0x7C: chr(0x00FE), # LATIN SMALL LETTER THORN
    0x7D: chr(0x0167), # LATIN SMALL LETTER T WITH STROKE
    0x7E: chr(0x014B), # LATIN SMALL LETTER ENG
    0x7F: chr(0x25A0), # BLACK SQUARE
}


#       Name:   Map from Teletext G3 character set to Unicode
#       Date:   2018 April 20
#       Author: Rebecca Bettencourt <support@kreativekorp.com>

g3 = {
    0x20: chr(0x1FB3C), # LOWER LEFT BLOCK DIAGONAL LOWER MIDDLE LEFT TO LOWER CENTRE
    0x21: chr(0x1FB3D), # LOWER LEFT BLOCK DIAGONAL LOWER MIDDLE LEFT TO LOWER RIGHT
    0x22: chr(0x1FB3E), # LOWER LEFT BLOCK DIAGONAL UPPER MIDDLE LEFT TO LOWER CENTRE
    0x23: chr(0x1FB3F), # LOWER LEFT BLOCK DIAGONAL UPPER MIDDLE LEFT TO LOWER RIGHT
    0x24: chr(0x1FB40), # LOWER LEFT BLOCK DIAGONAL UPPER LEFT TO LOWER CENTRE
    0x25: chr(0x25E3), # BLACK LOWER LEFT TRIANGLE
    0x26: chr(0x1FB41), # LOWER RIGHT BLOCK DIAGONAL UPPER MIDDLE LEFT TO UPPER CENTRE
    0x27: chr(0x1FB42), # LOWER RIGHT BLOCK DIAGONAL UPPER MIDDLE LEFT TO UPPER RIGHT
    0x28: chr(0x1FB43), # LOWER RIGHT BLOCK DIAGONAL LOWER MIDDLE LEFT TO UPPER CENTRE
    0x29: chr(0x1FB44), # LOWER RIGHT BLOCK DIAGONAL LOWER MIDDLE LEFT TO UPPER RIGHT
    0x2A: chr(0x1FB45), # LOWER RIGHT BLOCK DIAGONAL LOWER LEFT TO UPPER CENTRE
    0x2B: chr(0x1FB46), # LOWER RIGHT BLOCK DIAGONAL LOWER MIDDLE LEFT TO UPPER MIDDLE RIGHT
    0x2C: chr(0x1FB68), # UPPER AND RIGHT AND LOWER TRIANGULAR THREE QUARTERS BLOCK
    0x2D: chr(0x1FB69), # LEFT AND LOWER AND RIGHT TRIANGULAR THREE QUARTERS BLOCK
    0x2E: chr(0x1FB70), # VERTICAL ONE EIGHTH BLOCK-2
    0x2F: chr(0x2592), # MEDIUM SHADE
    0x30: chr(0x1FB47), # LOWER RIGHT BLOCK DIAGONAL LOWER CENTRE TO LOWER MIDDLE RIGHT
    0x31: chr(0x1FB48), # LOWER RIGHT BLOCK DIAGONAL LOWER LEFT TO LOWER MIDDLE RIGHT
    0x32: chr(0x1FB49), # LOWER RIGHT BLOCK DIAGONAL LOWER CENTRE TO UPPER MIDDLE RIGHT
    0x33: chr(0x1FB4A), # LOWER RIGHT BLOCK DIAGONAL LOWER LEFT TO UPPER MIDDLE RIGHT
    0x34: chr(0x1FB4B), # LOWER RIGHT BLOCK DIAGONAL LOWER CENTRE TO UPPER RIGHT
    0x35: chr(0x25E2), # BLACK LOWER RIGHT TRIANGLE
    0x36: chr(0x1FB4C), # LOWER LEFT BLOCK DIAGONAL UPPER CENTRE TO UPPER MIDDLE RIGHT
    0x37: chr(0x1FB4D), # LOWER LEFT BLOCK DIAGONAL UPPER LEFT TO UPPER MIDDLE RIGHT
    0x38: chr(0x1FB4E), # LOWER LEFT BLOCK DIAGONAL UPPER CENTRE TO LOWER MIDDLE RIGHT
    0x39: chr(0x1FB4F), # LOWER LEFT BLOCK DIAGONAL UPPER LEFT TO LOWER MIDDLE RIGHT
    0x3A: chr(0x1FB50), # LOWER LEFT BLOCK DIAGONAL UPPER CENTRE TO LOWER RIGHT
    0x3B: chr(0x1FB51), # LOWER LEFT BLOCK DIAGONAL UPPER MIDDLE LEFT TO LOWER MIDDLE RIGHT
    0x3C: chr(0x1FB6A), # UPPER AND LEFT AND LOWER TRIANGULAR THREE QUARTERS BLOCK
    0x3D: chr(0x1FB6B), # LEFT AND UPPER AND RIGHT TRIANGULAR THREE QUARTERS BLOCK
    0x3E: chr(0x1FB75), # VERTICAL ONE EIGHTH BLOCK-7
    0x3F: chr(0x2588), # FULL BLOCK
    0x40: chr(0x2537), # BOX DRAWINGS UP LIGHT AND HORIZONTAL HEAVY
    0x41: chr(0x252F), # BOX DRAWINGS DOWN LIGHT AND HORIZONTAL HEAVY
    0x42: chr(0x251D), # BOX DRAWINGS VERTICAL LIGHT AND RIGHT HEAVY
    0x43: chr(0x2525), # BOX DRAWINGS VERTICAL LIGHT AND LEFT HEAVY
    0x44: chr(0x1FBA4), # BOX DRAWINGS LIGHT DIAGONAL UPPER CENTRE TO MIDDLE LEFT TO LOWER CENTRE
    0x45: chr(0x1FBA5), # BOX DRAWINGS LIGHT DIAGONAL UPPER CENTRE TO MIDDLE RIGHT TO LOWER CENTRE
    0x46: chr(0x1FBA6), # BOX DRAWINGS LIGHT DIAGONAL MIDDLE LEFT TO LOWER CENTRE TO MIDDLE RIGHT
    0x47: chr(0x1FBA7), # BOX DRAWINGS LIGHT DIAGONAL MIDDLE LEFT TO UPPER CENTRE TO MIDDLE RIGHT
    0x48: chr(0x1FBA0), # BOX DRAWINGS LIGHT DIAGONAL UPPER CENTRE TO MIDDLE LEFT
    0x49: chr(0x1FBA1), # BOX DRAWINGS LIGHT DIAGONAL UPPER CENTRE TO MIDDLE RIGHT
    0x4A: chr(0x1FBA2), # BOX DRAWINGS LIGHT DIAGONAL MIDDLE LEFT TO LOWER CENTRE
    0x4B: chr(0x1FBA3), # BOX DRAWINGS LIGHT DIAGONAL MIDDLE RIGHT TO LOWER CENTRE
    0x4C: chr(0x253F), # BOX DRAWINGS VERTICAL LIGHT AND HORIZONTAL HEAVY
    0x4D: chr(0x2022), # BULLET
    0x4E: chr(0x25CF), # BLACK CIRCLE
    0x4F: chr(0x25CB), # WHITE CIRCLE
    0x50: chr(0x2502), # BOX DRAWINGS LIGHT VERTICAL
    0x51: chr(0x2500), # BOX DRAWINGS LIGHT HORIZONTAL
    0x52: chr(0x250C), # BOX DRAWINGS LIGHT DOWN AND RIGHT
    0x53: chr(0x2510), # BOX DRAWINGS LIGHT DOWN AND LEFT
    0x54: chr(0x2514), # BOX DRAWINGS LIGHT UP AND RIGHT
    0x55: chr(0x2518), # BOX DRAWINGS LIGHT UP AND LEFT
    0x56: chr(0x251C), # BOX DRAWINGS LIGHT VERTICAL AND RIGHT
    0x57: chr(0x2524), # BOX DRAWINGS LIGHT VERTICAL AND LEFT
    0x58: chr(0x252C), # BOX DRAWINGS LIGHT DOWN AND HORIZONTAL
    0x59: chr(0x2534), # BOX DRAWINGS LIGHT UP AND HORIZONTAL
    0x5A: chr(0x253C), # BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL
    0x5B: chr(0x2B62), # RIGHTWARDS TRIANGLE-HEADED ARROW
    0x5C: chr(0x2B60), # LEFTWARDS TRIANGLE-HEADED ARROW
    0x5D: chr(0x2B61), # UPWARDS TRIANGLE-HEADED ARROW
    0x5E: chr(0x2B63), # DOWNWARDS TRIANGLE-HEADED ARROW
    0x5F: chr(0x00A0), # NO-BREAK SPACE
    0x60: chr(0x1FB52), # UPPER RIGHT BLOCK DIAGONAL LOWER MIDDLE LEFT TO LOWER CENTRE
    0x61: chr(0x1FB53), # UPPER RIGHT BLOCK DIAGONAL LOWER MIDDLE LEFT TO LOWER RIGHT
    0x62: chr(0x1FB54), # UPPER RIGHT BLOCK DIAGONAL UPPER MIDDLE LEFT TO LOWER CENTRE
    0x63: chr(0x1FB55), # UPPER RIGHT BLOCK DIAGONAL UPPER MIDDLE LEFT TO LOWER RIGHT
    0x64: chr(0x1FB56), # UPPER RIGHT BLOCK DIAGONAL UPPER LEFT TO LOWER CENTRE
    0x65: chr(0x25E5), # BLACK UPPER RIGHT TRIANGLE
    0x66: chr(0x1FB57), # UPPER LEFT BLOCK DIAGONAL UPPER MIDDLE LEFT TO UPPER CENTRE
    0x67: chr(0x1FB58), # UPPER LEFT BLOCK DIAGONAL UPPER MIDDLE LEFT TO UPPER RIGHT
    0x68: chr(0x1FB59), # UPPER LEFT BLOCK DIAGONAL LOWER MIDDLE LEFT TO UPPER CENTRE
    0x69: chr(0x1FB5A), # UPPER LEFT BLOCK DIAGONAL LOWER MIDDLE LEFT TO UPPER RIGHT
    0x6A: chr(0x1FB5B), # UPPER LEFT BLOCK DIAGONAL LOWER LEFT TO UPPER CENTRE
    0x6B: chr(0x1FB5C), # UPPER LEFT BLOCK DIAGONAL LOWER MIDDLE LEFT TO UPPER MIDDLE RIGHT
    0x6C: chr(0x1FB6C), # LEFT TRIANGULAR ONE QUARTER BLOCK
    0x6D: chr(0x1FB6D), # UPPER TRIANGULAR ONE QUARTER BLOCK
    0x70: chr(0x1FB5D), # UPPER LEFT BLOCK DIAGONAL LOWER CENTRE TO LOWER MIDDLE RIGHT
    0x71: chr(0x1FB5E), # UPPER LEFT BLOCK DIAGONAL LOWER LEFT TO LOWER MIDDLE RIGHT
    0x72: chr(0x1FB5F), # UPPER LEFT BLOCK DIAGONAL LOWER CENTRE TO UPPER MIDDLE RIGHT
    0x73: chr(0x1FB60), # UPPER LEFT BLOCK DIAGONAL LOWER LEFT TO UPPER MIDDLE RIGHT
    0x74: chr(0x1FB61), # UPPER LEFT BLOCK DIAGONAL LOWER CENTRE TO UPPER RIGHT
    0x75: chr(0x25E4), # BLACK UPPER LEFT TRIANGLE
    0x76: chr(0x1FB62), # UPPER RIGHT BLOCK DIAGONAL UPPER CENTRE TO UPPER MIDDLE RIGHT
    0x77: chr(0x1FB63), # UPPER RIGHT BLOCK DIAGONAL UPPER LEFT TO UPPER MIDDLE RIGHT
    0x78: chr(0x1FB64), # UPPER RIGHT BLOCK DIAGONAL UPPER CENTRE TO LOWER MIDDLE RIGHT
    0x79: chr(0x1FB65), # UPPER RIGHT BLOCK DIAGONAL UPPER LEFT TO LOWER MIDDLE RIGHT
    0x7A: chr(0x1FB66), # UPPER RIGHT BLOCK DIAGONAL UPPER CENTRE TO LOWER RIGHT
    0x7B: chr(0x1FB67), # UPPER RIGHT BLOCK DIAGONAL UPPER MIDDLE LEFT TO LOWER MIDDLE RIGHT
    0x7C: chr(0x1FB6E), # RIGHT TRIANGULAR ONE QUARTER BLOCK
    0x7D: chr(0x1FB6F), # LOWER TRIANGULAR ONE QUARTER BLOCK
}


================================================
FILE: teletext/cli/__init__.py
================================================


================================================
FILE: teletext/cli/celp.py
================================================
import click

from teletext.cli.clihelpers import packetreader
from teletext.celp import CELPDecoder

@click.group()
def celp():
    """Tools for analysing CELP audio packets."""
    pass


@celp.command()
@click.option('-f', '--frame', type=int, default=None, help='Frame selection.')
@click.option('-o', '--output', type=click.File('wb'), help='Write audio to WAV file.')
@click.option('-l', '--lsf-lut', type=click.Choice(CELPDecoder.lsf_vector_quantizers.keys()), default='suddle', help='LSF vector look-up table.')
@click.option('-g', '--gain-lut', type=click.Choice(CELPDecoder.vec_gain_quantizers.keys()), default='audetel', help='LSF vector look-up table.')
@packetreader(filtered='data', mag_hist=None, row_hist=None, err_hist=None, pass_progress=True)
def play(progress, frame, output, lsf_lut, gain_lut, packets):
    """Play data from CELP packets. Warning: Will make a horrible noise."""
    dec = CELPDecoder(lsf_lut=lsf_lut, vec_gain_lut=gain_lut)
    progress.postfix.append(dec.stats())
    if output is not None:
        dec.convert(output, packets, frame=frame)
    else:
        dec.play(packets, frame=frame)


@celp.command()
@packetreader(filtered='data')
def plot(packets):
    """Plot data from CELP packets. Experimental code."""
    CELPDecoder.plot(packets)


================================================
FILE: teletext/cli/clihelpers.py
================================================
import cProfile
import os
import stat
from functools import wraps

import click
from tqdm import tqdm

from teletext import pipeline
from teletext.packet import Packet
from teletext.stats import StatsList, MagHistogram, RowHistogram, ErrorHistogram
from teletext.file import FileChunker
from teletext.vbi.config import Config

try:
    import plop.collector as plop
except ImportError:
    plop = None


class BasedIntType(click.ParamType):
    name = "integer"

    def convert(self, value, param, ctx):
        if isinstance(value, int):
            return value

        try:
            if value[:2].lower() == "0x":
                return int(value[2:], 16)
            return int(value, 10)
        except ValueError:
            self.fail(f"{value!r} is not a valid integer", param, ctx)

BasedInt = BasedIntType()

def dcnparams(f):
    return click.option('-d', '--dcn', 'dcn', type=int, required=True, help='Data channel to read from.')(f)


def filterparams(enabled=True):
    def fp(f):
        if enabled:
            for d in [
                click.option('-m', '--mag', 'mags', type=int, multiple=True, default=range(9), help='Limit output to specific magazines. Can be specified multiple times.'),
                click.option('-r', '--row', 'rows', type=int, multiple=True, default=range(32), help='Limit output to specific rows. Can be specified multiple times.'),
            ][::-1]:
                f = d(f)
        return f
    return fp

def progressparams(progress=True, mag_hist=False, row_hist=False, err_hist=False):
    def p(f):
        if err_hist is not None:
            f = click.option('--err-hist/--no-err-hist', default=err_hist, help='Display error distribution.')(f)
        if row_hist is not None:
            f = click.option('--row-hist/--no-row-hist', default=row_hist, help='Display row histogram.')(f)
        if mag_hist is not None:
            f = click.option('--mag-hist/--no-mag-hist', default=mag_hist, help='Display magazine histogram.')(f)
        if progress is not None:
            f = click.option('--progress/--no-progress', default=progress, help='Display progress bar.')(f)
        return f
    return p


def carduser(extended=False):
    def c(f):
        if extended:
            for d in [
                click.option('--sample-rate', type=float, default=None, help='Override capture card sample rate (Hz).'),
                click.option('--sample-rate-adjust', type=float, default=0, help='Adjustment to default capture card sample rate (Hz).'),
                click.option('--extra-roll', type=int, default=0, help='Shift line by N samples after locking to the packet.'),
                click.option('--line-start-range', type=(int, int), default=(None, None), help='Override capture card line start offset.'),
            ][::-1]:
                f = d(f)

        @click.option('-c', '--card', type=click.Choice(list(Config.cards.keys())), default='bt8x8', help='Capture device type. Default: bt8x8.')
        @click.option('--line-length', type=int, default=None, help='Override capture card samples per line.')
        @wraps(f)
        def wrapper(card, line_length=None, sample_rate=None, sample_rate_adjust=0, line_start_range=None, extra_roll=0, *args, **kwargs):
            if line_start_range == (None, None):
                line_start_range = None
            config = Config(card=card, line_length=line_length, sample_rate=sample_rate, sample_rate_adjust=sample_rate_adjust, line_start_range=line_start_range, extra_roll=extra_roll)
            return f(config=config, *args,**kwargs)
        return wrapper
    return c


def chunkreader(loop=False, dup_stdin=False):
    def cr(f):
        @click.argument('input', type=click.File('rb'), default='-')
        @click.option('--start', type=int, default=0, help='Start at the Nth line of the input file.')
        @click.option('--stop', type=int, default=None, help='Stop before the Nth line of the input file.')
        @click.option('--step', type=int, default=1, help='Process every Nth line from the input file.')
        @click.option('--limit', type=int, default=None, help='Stop after processing N lines from the input file.')
        @wraps(f)
        def wrapper(input, start, stop, step, limit, *args, **kwargs):

            if input.isatty():
                raise click.UsageError('No input file and stdin is a tty - exiting.', )

            if 'progress' in kwargs and kwargs['progress'] is None:
                if hasattr(input, 'fileno') and stat.S_ISFIFO(os.fstat(input.fileno()).st_mode):
                    kwargs['progress'] = False

            chunker = lambda size, flines=16, frange=range(0, 16): FileChunker(input, size, start, stop, step, limit, flines, frange, loop=loop, dup_stdin=dup_stdin)

            return f(chunker=chunker, *args, **kwargs)
        return wrapper
    return cr

def packetreader(filtered=True, progress=True, mag_hist=False, row_hist=False, err_hist=False, pass_progress=False, loop=False, dup_stdin=False):
    if filtered == 'data':
        filterdec = dcnparams
    else:
        filterdec = filterparams(filtered)

    def pr(f):
        @chunkreader(loop=loop, dup_stdin=dup_stdin)
        @click.option('--wst', is_flag=True, default=False, help='Input is 43 bytes per packet (WST capture card format.)')
        @click.option('--ts', type=BasedInt, default=None, help='Input is MPEG transport stream. (Specify PID to extract.)')
        @filterdec
        @progressparams(progress=(progress and not loop), mag_hist=mag_hist, row_hist=row_hist, err_hist=err_hist)
        @wraps(f)
        def wrapper(chunker, wst, ts, progress, *args, **kwargs):

            if wst and (ts is not None):
                raise click.UsageError('--wst and --ts can not be specified at the same time.')

            if wst:
                chunks = chunker(43)
                chunks = ((c[0],c[1][:42]) for c in chunks if c[1][0] != 0)
            elif ts is not None:
                from teletext.ts import pidextract
                chunks = chunker(188)
                chunks = pidextract(chunks, ts)
            else:
                chunks = chunker(42)

            if progress is None:
                progress = True

            mag_hist = kwargs.pop('mag_hist', None)
            row_hist = kwargs.pop('row_hist', None)
            err_hist = kwargs.pop('err_hist', None)

            if progress:
                chunks = tqdm(chunks, unit='P', dynamic_ncols=True)
                if pass_progress or any((mag_hist, row_hist, err_hist)):
                    chunks.postfix = StatsList()

            packets = (Packet(data, number) for number, data in chunks)
            if 'mags' in kwargs and 'rows' in kwargs:
                mags = kwargs.pop('mags')
                rows = kwargs.pop('rows')
                packets = (p for p in packets if p.mrag.magazine in mags and p.mrag.row in rows)

            elif 'dcn' in kwargs:
                dcn = kwargs.pop('dcn')
                mags = (dcn & 0x7,)
                rows = (30 + (dcn>>3),)
                packets = (p for p in packets if p.mrag.magazine in mags and p.mrag.row in rows)

            if progress:
                if mag_hist:
                    packets = MagHistogram(packets)
                    chunks.postfix.append(packets)
                if row_hist:
                    packets = RowHistogram(packets)
                    chunks.postfix.append(packets)
                if err_hist:
                    packets = ErrorHistogram(packets)
                    chunks.postfix.append(packets)

            if pass_progress:
                return f(progress=chunks, packets=packets, *args, **kwargs)
            else:
                return f(packets=packets, *args, **kwargs)

        return wrapper

    return pr


def paginated(always=False, filtered=True):
    def _paginated(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            paginate = always or kwargs['paginate']

            if filtered:
                pages = kwargs['pages']
                if pages is None or len(pages) == 0:
                    pages = range(0x900)
                else:
                    pages = {int(x, 16) for x in pages}
                    paginate = True
                kwargs['pages'] = pages

                subpages = kwargs['subpages']
                if subpages is None or len(subpages) == 0:
                    subpages = range(0x3f80)
                else:
                    subpages = {int(x, 16) for x in subpages}
                    paginate = True
                kwargs['subpages'] = subpages

            if paginate and 0 not in kwargs['rows']:
                raise click.BadArgumentUsage("Can't paginate when row 0 is filtered.")

            if not always:
                kwargs['paginate'] = paginate

            return f(*args, **kwargs)

        if filtered:
            wrapper = click.option('-s', '--subpage', 'subpages', type=str, multiple=True,
                      help='Limit output to specific subpage. Can be specified multiple times.')(wrapper)
            wrapper = click.option('-p', '--page', 'pages', type=str, multiple=True,
                      help='Limit output to specific page. Can be specified multiple times.')(wrapper)
        if not always:
            wrapper = click.option('-P', '--paginate', is_flag=True, help='Sort rows into contiguous pages.')(wrapper)

        return wrapper
    return _paginated


def packetwriter(f):
    @click.option(
        '-o', '--output', type=(click.Choice(['auto', 'text', 'ansi', 'debug', 'bar', 'bytes', 'hex', 'vbi']), click.File('wb')),
        multiple=True, default=[('auto', '-')]
    )
    @wraps(f)
    def wrapper(output, *args, **kwargs):

        if 'progress' in kwargs and kwargs['progress'] is None:
            for attr, o in output:
                if o.isatty():
                    kwargs['progress'] = False

        packets = f(*args, **kwargs)

        for attr, o in output:
            packets = pipeline.to_file(packets, o, attr)

        for p in packets:
            pass

    return wrapper


================================================
FILE: teletext/cli/teletext.py
================================================
import itertools
import multiprocessing
import os
import pathlib
import platform

import sys
from collections import defaultdict

import click
from tqdm import tqdm

from teletext.charset import g0
from teletext.cli.clihelpers import packetreader, packetwriter, paginated, \
    progressparams, filterparams, carduser, chunkreader
from teletext.file import FileChunker
from teletext.mp import itermap
from teletext.packet import Packet, np
from teletext.stats import StatsList, MagHistogram, RowHistogram, Rejects, ErrorHistogram
from teletext.subpage import Subpage
from teletext import pipeline
from teletext.cli.training import training
from teletext.cli.vbi import vbi
from teletext.vbi.config import Config


if os.name == 'nt' and platform.release() == '10' and platform.version() >= '10.0.14393':
    # Fix ANSI color in Windows 10 version 10.0.14393 (Windows Anniversary Update)
    import ctypes
    kernel32 = ctypes.windll.kernel32
    kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)


@click.group(invoke_without_command=True, no_args_is_help=True)
@click.option('-u', '--unicode', is_flag=True, help='Use experimental Unicode 13.0 Terminal graphics.')
@click.version_option()
@click.help_option()
@click.option('--help-all', is_flag=True, help='Show help for all subcommands.')
@click.pass_context
def teletext(ctx, unicode, help_all):
    """Teletext stream processing toolkit."""
    if help_all:
        print(teletext.get_help(ctx))

        def help_recurse(group, ctx):
            for scmd in group.list_commands(ctx):
                cmd = group.get_command(ctx, scmd)
                nctx = click.Context(cmd, ctx, scmd)
                if isinstance(cmd, click.Group):
                    help_recurse(cmd, nctx)
                else:
                    click.echo()
                    click.echo(cmd.get_help(nctx))

        help_recurse(teletext, ctx)

    if unicode:
        from teletext import parser
        parser._unicode13 = True


teletext.add_command(training)
teletext.add_command(vbi)

try:
    from teletext.cli.celp import celp
    teletext.add_command(celp)
except ImportError:
    pass


@teletext.command()
@packetwriter
@paginated()
@click.option('--pagecount', 'n', type=int, default=0, help='Stop after n pages. 0 = no limit. Implies -P.')
@click.option('-k', '--keep-empty', is_flag=True, help='Keep empty packets in the output.')
@packetreader()
def filter(packets, pages, subpages, paginate, n, keep_empty):

    """Demultiplex and display t42 packet streams."""

    if n:
        paginate = True

    if not keep_empty:
        packets = (p for p in packets if not p.is_padding())

    if paginate:
        for pn, pl in enumerate(pipeline.paginate(packets, pages=pages, subpages=subpages), start=1):
            yield from pl
            if pn == n:
                return
    else:
        yield from packets


@teletext.command()
@packetwriter
@paginated()
@click.argument('regex', type=str)
@click.option('-v', is_flag=True, help='Invert matches.')
@click.option('-i', is_flag=True, help='Ignore case.')
@click.option('--pagecount', 'n', type=int, default=0, help='Stop after n pages. 0 = no limit. Implies -P.')
@click.option('-k', '--keep-empty', is_flag=True, help='Keep empty packets in the output.')
@packetreader()
def grep(packets, pages, subpages, paginate, regex, v, i, n, keep_empty):

    """Filter packets with a regular expression."""

    import re

    pattern = re.compile(regex.encode('ascii'), re.IGNORECASE if i else 0)

    if n:
        paginate = True

    if not keep_empty:
        packets = (p for p in packets if not p.is_padding())

    if paginate:
        for pn, pl in enumerate(pipeline.paginate(packets, pages=pages, subpages=subpages), start=1):
            for p in pl:
                if bool(v) != bool(re.search(pattern, p.to_bytes_no_parity())):
                    yield from pl
                    if pn == n:
                        return
    else:
        for p in packets:
            if bool(v) != bool(re.search(pattern, p.to_bytes_no_parity())):
                yield p


@teletext.command(name='list')
@click.option('-c', '--count', is_flag=True, help='Show counts of each entry.')
@click.option('-s', '--subpages', is_flag=True, help='Also list subpages.')
@paginated(always=True, filtered=False)
@packetreader()
@progressparams(progress=True, mag_hist=True)
def _list(packets, count, subpages):

    """List pages present in a t42 stream."""

    import textwrap

    packets = (p for p in packets if not p.is_padding())

    seen = {}
    try:
        for pl in pipeline.paginate(packets):
            s = Subpage.from_packets(pl)
            identifier = f'{s.mrag.magazine}{s.header.page:02x}'
            if subpages:
                identifier += f':{s.header.subpage:04x}'
            if identifier in seen:
                seen[identifier]+=1
            else:
                seen[identifier]=1
    except KeyboardInterrupt:
        print('\n')
    finally:
        if count:
            maxdigits = len(str(max(seen.values())))
            formatstr="{page}/{count:0" + str(maxdigits) +"}"
        else:
            formatstr="{page}"
        seen = list(map(lambda e: formatstr.format(page = e[0], count = e[1]), seen.items()))
        print('\n'.join(textwrap.wrap(' '.join(sorted(seen)))))


@teletext.command()
@click.argument('pattern')
@paginated(always=True)
@packetreader()
def split(packets, pattern, pages, subpages):

    """Split a t42 stream in to multiple files."""

    packets = (p for p in packets if not p.is_padding())
    counts = defaultdict(int)

    for pl in pipeline.paginate(packets, pages=pages, subpages=subpages):
        subpage = Subpage.from_packets(pl)
        m = subpage.mrag.magazine
        p = subpage.header.page
        s = subpage.header.subpage
        c = counts[(m,p,s)]
        counts[(m,p,s)] += 1
        f = pathlib.Path(pattern.format(m=m, p=f'{p:02x}', s=f'{s:04x}', c=f'{c:04d}'))
        f.parent.mkdir(parents=True, exist_ok=True)
        with f.open('ab') as ff:
            ff.write(b''.join(p.bytes for p in pl))


@teletext.command()
@click.argument('a', type=click.File('rb'))
@click.argument('b', type=click.File('rb'))
@filterparams()
def diff(a, b, mags, rows):
    """Show side by side difference of two t42 streams."""
    for chunka, chunkb in zip(FileChunker(a, 42), FileChunker(b, 42)):
        pa = Packet(chunka[1], chunka[0])
        pb = Packet(chunkb[1], chunkb[0])
        if (pa.mrag.row in rows and pa.mrag.magazine in mags) or (pb.mrag.row in rows and pa.mrag.magazine in mags):
            if any(pa[:] != pb[:]):
                print(pa.to_ansi(), pb.to_ansi())


@teletext.command()
@packetwriter
@packetreader()
def finders(packets):

    """Apply finders to fix up common packets."""

    for p in packets:
        if p.type == 'header':
            p.header.apply_finders()
        yield p


@teletext.command()
@packetreader(filtered=False)
@click.option('-l', '--lines', type=int, default=32, help='Number of recorded lines per frame.')
@click.option('-f', '--frames', type=int, default=250, help='Number of frames to squash.')
def scan(packets, lines, frames):

    """Filter a t42 stream down to headers and bsdp, with squashing."""

    from teletext.pipeline import packet_squash, bsdp_squash_format1, bsdp_squash_format2
    bars = '_:|I'

    while True:
        actives = np.zeros((lines,), dtype=np.uint32)
        headers = [[], [], [], [], [], [], [], [], []]
        service = [[], []]
        start = None
        try:
            for i in range(frames):
                for n, p in enumerate(itertools.islice(packets, lines)):
                    if start is None:
                        start = p._number
                    if not p.is_padding():
                        if p.type == 'header':
                            p.header.apply_finders()
                        actives[n] += 1
                        if p.mrag.row == 0:
                            headers[p.mrag.magazine].append(p)
                        elif p.mrag.row == 30 and p.mrag.magazine == 8:
                            if p.broadcast.dc in [0, 1]:
                                service[0].append(p)
                            elif p.broadcast.dc in [2, 3]:
                                service[1].append(p)

        except StopIteration:
            pass
        if start is None:
            return
        active_group = 1*(actives>0) + 1*(actives>(frames/2)) + 1*(actives==frames)
        print(f'{start:8d}', '['+''.join(bars[a] for a in active_group)+']', end=' ')
        for h in headers:
            if h:
                print(packet_squash(h).header.displayable.to_ansi(), end=' ')
                break
        for s in service:
            if s:
                print(packet_squash(s).broadcast.displayable.to_ansi(), end=' ')
                break
        if service[0]:
            print(bsdp_squash_format1(service[0]), end=' ')
        if service[1]:
            print(bsdp_squash_format2(service[1]), end=' ')
        print()


@teletext.command()
@click.option('-d', '--min-duplicates', type=int, default=3, help='Only squash and output subpages with at least N duplicates.')
@click.option('-t', '--threshold', type=int, default=-1, help='Max difference for squashing.')
@click.option('-i', '--ignore-empty', is_flag=True, default=False, help='Ignore the emptiest duplicate packets instead of the earliest.')
@packetwriter
@paginated(always=True)
@packetreader()
def squash(packets, min_duplicates, threshold, pages, subpages, ignore_empty):

    """Reduce errors in t42 stream by using frequency analysis."""

    packets = (p for p in packets if not p.is_padding())
    for sp in pipeline.subpage_squash(
            pipeline.paginate(packets, pages=pages, subpages=subpages),
            min_duplicates=min_duplicates, ignore_empty=ignore_empty,
            threshold=threshold
    ):
        yield from sp.packets


@teletext.command()
@click.option('-l', '--language', default='en_GB', help='Language. Default: en_GB')
@click.option('-b', '--both', is_flag=True, help='Show packet before and after corrections.')
@click.option('-t', '--threads', type=int, default=multiprocessing.cpu_count(), help='Number of threads.')
@packetwriter
@packetreader()
def spellcheck(packets, language, both, threads):

    """Spell check a t42 stream."""

    try:
        from teletext.spellcheck import spellcheck_packets
    except ModuleNotFoundError as e:
        if e.name == 'enchant':
            raise click.UsageError(f'{e.msg}. PyEnchant is not installed. Spelling checker is not available.')
        else:
            raise e
    else:
        if both:
            packets, orig_packets = itertools.tee(packets, 2)
            packets = itermap(spellcheck_packets, packets, threads, language=language)
            try:
                while True:
                    yield next(orig_packets)
                    yield next(packets)
            except StopIteration:
                pass
        else:
            yield from itermap(spellcheck_packets, packets, threads, language=language)


@teletext.command()
@click.option('-r', '--replace_headers', 'replace_headers', is_flag=True, default=False, help='Replace headers with a live clock.')
@click.option('-t', '--title', 'title', type=str, default="Teletext ", help='Replace header title field with this string.')
@packetwriter
@paginated(always=True, filtered=False)
@packetreader()
def service(packets, replace_headers, title):

    """Build a service carousel from a t42 stream."""

    from teletext.service import Service
    return Service.from_packets((p for p in packets if  not p.is_padding()), replace_headers, title)


@teletext.command()
@click.option('-r', '--replace_headers', 'replace_headers', is_flag=True, default=False, help='Replace headers with a live clock.')
@click.option('-t', '--title', 'title', type=str, default=None, help='Replace header title field with this string.')
@packetwriter
@click.argument('directory', type=click.Path(exists=True, readable=True, file_okay=False, dir_okay=True))
def servicedir(directory, replace_headers, title):
    """Build a service from a directory of t42 files."""

    from teletext.servicedir import ServiceDir
    with ServiceDir(directory, replace_headers, title) as s:
        yield from s


@teletext.command()
@click.option('-i', '--initial_page', 'initial_page', type=str, default='100', help='Initial page.')
@packetreader(loop=True, dup_stdin=True)
def interactive(packets, initial_page):

    """Interactive teletext emulator."""

    from teletext import interactive
    interactive.main(packets, int(initial_page, 16))


@teletext.command()
@packetreader(loop=True)
@click.option('-p', '--port', type=str, default=None)
def serial(packets, port):

    """Write escaped packets to serial inserter."""

    import serial.tools.list_ports
    import time

    if port is None:
        for comport in serial.tools.list_ports.comports():
            if comport.vid == 0x2e8a and (comport.pid == 0x000a or comport.pid == 0x0009):
                port = comport.device

    if port is None:
        raise click.UsageError('No serial inserter found. Specify the path with -p')

    port = serial.Serial(port, timeout=3, rtscts=True)

    for p in packets:
        buf = p.bytes
        buf = buf.replace(b'\xfe', b'\xfe\x00')
        buf = buf.replace(b'\xff', b'\xfe\x01')
        buf = b'\xff' + buf
        port.write(buf)


@teletext.command()
@click.option('-e', '--editor', type=str, default='https://zxnet.co.uk/teletext/editor/#',
              show_default=True, help='Teletext editor URL.')
@paginated(always=True)
@packetreader()
def urls(packets, editor, pages, subpages):

    """Paginate a t42 stream and print edit.tf URLs."""

    packets = (p for p in packets if  not p.is_padding())
    subpages = (Subpage.from_packets(pl) for pl in pipeline.paginate(packets, pages=pages, subpages=subpages))

    for s in subpages:
        print(f'{editor}{s.url}')

@teletext.command()
@click.argument('outdir', type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True), required=True)
@click.option('-f', '--font', type=click.File('rb'), help='PCF font for rendering.')
@paginated(always=True)
@packetreader()
def images(packets, outdir, font, pages, subpages):

    """Generate images for the input stream."""

    try:
        from teletext.image import subpage_to_image, load_glyphs
    except ModuleNotFoundError as e:
        if e.name == 'PIL':
            raise click.UsageError(
                f'{e.msg}. PIL is not installed. Image generation is not available.')
        else:
            raise e

    from teletext.service import Service

    glyphs = load_glyphs(font)

    packets = (p for p in packets if  not p.is_padding())
    svc = Service.from_packets(p for p in packets if not p.is_padding())

    subpages = tqdm(list(svc.all_subpages), unit="subpage")
    for s in subpages:
        image = subpage_to_image(s, glyphs)
        filename = f'P{s.mrag.magazine}{s.header.page:02x}-{s.header.subpage:04x}.png'
        subpages.set_description(filename, refresh=False)
        if image._flash_used:
            opts = {
                'save_all': True,
                'append_images': [subpage_to_image(s, glyphs, flash_off=True)],
                'duration': 500,
                'loop': 0,
                'disposal': 2,
            }
        else:
            opts = {}
        image.save(pathlib.Path(outdir) / filename, **opts)
        if image._missing_glyphs:
            missing = ', '.join(f'{repr(c)} {hex(ord(c))}' for c in image._missing_glyphs)
            print(f'{filename} missing characters: {missing}')


@teletext.command()
@click.argument('outdir', type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True), required=True)
@click.option('-t', '--template', type=click.File('r'), default=None, help='HTML template.')
@click.option('--localcodepage', type=click.Choice(g0.keys()), default=None, help='Select codepage for Local Code of Practice')
@paginated(always=True, filtered=False)
@packetreader()
def html(packets, outdir, template, localcodepage):

    """Generate HTML files from the input stream."""

    from teletext.service import Service

    if template is not None:
        template = template.read()

    svc = Service.from_packets(p for p in packets if not p.is_padding())
    svc.to_html(outdir, template, localcodepage)


@teletext.command()
@click.argument('output', type=click.File('wb'), default='-')
@click.option('-d', '--device', type=click.File('rb'), default='/dev/vbi0', help='Capture device.')
@carduser()
def record(output, device, config):

    """Record VBI samples from a capture device."""

    import struct
    import sys

    if output.name.startswith('/dev/vbi'):
        raise click.UsageError(f'Refusing to write output to VBI device. Did you mean -d?')

    chunks = FileChunker(device, config.line_length*config.field_lines*2)
    bar = tqdm(chunks, unit=' Frames')

    prev_seq = None
    dropped = 0

    try:
        for n, chunk in bar:
            output.write(chunk)
            if config.card == 'bt8x8':
                seq, = struct.unpack('<I', chunk[-4:])
                if prev_seq is not None and seq != (prev_seq + 1):
                   dropped += 1
                   sys.stderr.write('Frame drop? %d\n' % dropped)
                prev_seq = seq

    except KeyboardInterrupt:
        pass


@teletext.command()
@click.option('-p', '--pause', is_flag=True, help='Start the viewer paused.')
@click.option('-f', '--tape-format', type=click.Choice(Config.tape_formats), default='vhs', help='Source VCR format.')
@click.option('-n', '--n-lines', type=int, default=None, help='Number of lines to display. Overrides card config.')
@carduser(extended=True)
@chunkreader()
def vbiview(chunker, config, pause, tape_format, n_lines):

    """Display raw VBI samples with OpenGL."""

    try:
        from teletext.vbi.viewer import VBIViewer
    except ModuleNotFoundError as e:
        if e.name.startswith('OpenGL'):
            raise click.UsageError(f'{e.msg}. PyOpenGL is not installed. VBI viewer is not available.')
        else:
            raise e
    else:
        from teletext.vbi.line import Line

        Line.configure(config, force_cpu=True, tape_format=tape_format)

        if n_lines is not None:
            chunks = chunker(config.line_bytes, n_lines, range(n_lines))
        else:
            chunks = chunker(config.line_bytes, config.field_lines, config.field_range)

        lines = (Line(chunk, number) for number, chunk in chunks)

        VBIViewer(lines, config, pause=pause, nlines=n_lines)


@teletext.command()
@click.option('-M', '--mode', type=click.Choice(['deconvolve', 'slice']), default='deconvolve', help='Deconvolution mode.')
@click.option('-8', '--eight-bit', is_flag=True, help='Treat rows 1-25 as 8-bit data without parity check.')
@click.option('-f', '--tape-format', type=click.Choice(Config.tape_formats), default='vhs', help='Source VCR format.')
@click.option('-C', '--force-cpu', is_flag=True, help='Disable GPU even if it is available.')
@click.option('-O', '--prefer-opencl', is_flag=True, default=False, help='Use OpenCL even if CUDA is available.')
@click.option('-t', '--threads', type=int, default=multiprocessing.cpu_count(), help='Number of threads.')
@click.option('-k', '--keep-empty', is_flag=True, help='Insert empty packets in the output when line could not be deconvolved.')
@carduser(extended=True)
@packetwriter
@chunkreader()
@filterparams()
@paginated()
@progressparams(progress=True, mag_hist=True)
@click.option('--rejects/--no-rejects', default=True, help='Display percentage of lines rejected.')
def deconvolve(chunker, mags, rows, pages, subpages, paginate, config, mode, eight_bit, force_cpu, prefer_opencl, threads, keep_empty, progress, mag_hist, row_hist, err_hist, rejects, tape_format):

    """Deconvolve raw VBI samples into Teletext packets."""

    if keep_empty and paginate:
        raise click.UsageError("Can't keep empty packets when paginating.")

    from teletext.vbi.line import process_lines

    if force_cpu:
        sys.stderr.write('GPU disabled by user request.\n')

    chunks = chunker(config.line_bytes, config.field_lines, config.field_range)

    if progress:
        chunks = tqdm(chunks, unit='L', dynamic_ncols=True)
        if any((mag_hist, row_hist, rejects)):
            chunks.postfix = StatsList()

    packets = itermap(process_lines, chunks, threads,
                      mode=mode, config=config,
                      force_cpu=force_cpu, prefer_opencl=prefer_opencl,
                      mags=mags, rows=rows,
                      tape_format=tape_format,
                      eight_bit=eight_bit)

    if progress and rejects:
        packets = Rejects(packets)
        chunks.postfix.append(packets)

    if keep_empty:
        packets = (p if isinstance(p, Packet) else Packet() for p in packets)
    else:
        packets = (p for p in packets if isinstance(p, Packet))

    if progress and mag_hist:
        packets = MagHistogram(packets)
        chunks.postfix.append(packets)
    if progress and row_hist:
        packets = RowHistogram(packets)
        chunks.postfix.append(packets)
    if progress and err_hist:
        packets = ErrorHistogram(packets)
        chunks.postfix.append(packets)

    if paginate:
        for p in pipeline.paginate(packets, pages=pages, subpages=subpages):
            yield from p
    else:
        yield from packets


================================================
FILE: teletext/cli/training.py
================================================
import multiprocessing
import os

import click
from tqdm import tqdm

from teletext.cli.clihelpers import carduser, chunkreader
from teletext.file import FileChunker
from teletext.mp import itermap
from teletext.packet import Packet, np
from teletext.stats import StatsList, Rejects


@click.group()
def training():
    """Training and calibration tools."""
    pass


@training.command()
@click.argument('output', type=click.File('wb'), default='-')
def generate(output):
    """Generate training samples for raspi-teletext."""
    from teletext.vbi.training import PatternGenerator
    PatternGenerator().to_file(output)


@training.command()
@click.argument('outdir', type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True), required=True)
@click.option('-t', '--threads', type=int, default=multiprocessing.cpu_count(), help='Number of threads.')
@carduser(extended=True)
@chunkreader()
@click.option('--progress/--no-progress', default=True, help='Display progress bar.')
@click.option('--rejects/--no-rejects', default=True, help='Display percentage of lines rejected.')
def split(chunker, outdir, config, threads, progress, rejects):
    """Split training recording into intermediate bins."""
    from teletext.vbi.training import process_training, split

    chunks = chunker(config.line_length * np.dtype(config.dtype).itemsize, config.field_lines, config.field_range)

    if progress:
        chunks = tqdm(chunks, unit='L', dynamic_ncols=True)

    results = itermap(process_training, chunks, threads, config=config)

    if progress and rejects:
        results = Rejects(results)
        chunks.postfix = StatsList()
        chunks.postfix.append(results)

    results = (r for r in results if isinstance(r, tuple))

    files = [open(os.path.join(outdir, f'training.{n:02x}.dat'), 'wb') for n in range(256)]

    split(results, files)


@training.command(name='squash')
@click.argument('indir', type=click.Path(exists=True, file_okay=False, dir_okay=True), required=True)
@click.argument('output', type=click.File('wb'), default='-')
def training_squash(output, indir):
    """Squash the intermediate bins into a single file."""
    from teletext.vbi.training import squash
    squash(output, indir)


@training.command()
@chunkreader()
def showbin(chunker):
    """Visually display an intermediate training bin."""
    import numpy as np

    bars = ' ▁▂▃▄▅▆▇█'
    bits = ' █'

    chunks = chunker(27)

    for n, chunk in chunks:
        arr = np.frombuffer(chunk, dtype=np.uint8)
        bi = ''.join(bits[n] for n in np.unpackbits(arr[:3][::-1])[::-1])
        by = ''.join(bars[n] for n in arr[3:]>>5)
        print(f'[{bi}] [{by}]')


@training.command()
@click.argument('input', type=click.File('rb'), required=True)
@click.argument('output', type=click.File('wb'), required=True)
@click.option('-m', '--mode', type=click.Choice(['full', 'parity', 'hamming']), default='full')
@click.option('-b', '--bits', type=(int, int), default=(3, 21))
def build(input, output, mode, bits):
    """Build pattern tables."""
    from teletext.coding import parity_encode, hamming8_enc
    from teletext.vbi.pattern import build_pattern

    if mode == 'parity':
        pattern_set = set(parity_encode(range(0x80)))
    elif mode == 'hamming':
        pattern_set = set(hamming8_enc)
    else:
        pattern_set = range(256)

    chunks = FileChunker(input, 27)
    chunks = tqdm(chunks, unit='P', dynamic_ncols=True)

    build_pattern(chunks, output, *bits, pattern_set)


@training.command()
@click.option('-f', '--tape-format', type=click.Choice(['vhs', 'betamax', 'grundig_2x4']), default='vhs', help='Source VCR format.')
def similarities(tape_format):
    from teletext.vbi.pattern import Pattern

    pattern = Pattern(os.path.dirname(__file__) + '/vbi/data-' + tape_format + '/parity.dat')

    print(pattern.similarities())


@training.command()
@click.option('-t', '--threads', type=int, default=multiprocessing.cpu_count(), help='Number of threads.')
@carduser(extended=True)
@chunkreader()
@click.option('--progress/--no-progress', default=True, help='Display progress bar.')
@click.option('--rejects/--no-rejects', default=True, help='Display percentage of lines rejected.')
def crifc(chunker, config, threads, progress, rejects):
    """Split training recording into intermediate bins."""
    from teletext.vbi.training import process_crifc

    chunks = chunker(config.line_length * np.dtype(config.dtype).itemsize, config.field_lines, config.field_range)

    if progress:
        chunks = tqdm(chunks, unit='L', dynamic_ncols=True)

    process_crifc(chunks, config=config)


================================================
FILE: teletext/cli/vbi.py
================================================
import click
import pathlib
import numpy as np
from tqdm import tqdm

from teletext.cli.clihelpers import carduser, chunkreader

@click.group()
def vbi():
    """Tools for analysing raw VBI samples."""
    pass


@vbi.command()
@click.argument('output', type=click.Path(writable=True))
@click.option('-d', '--diff', is_flag=True, help='User first differential of samples.')
@click.option('-s', '--show', is_flag=True, help='Show image when complete.')
@click.option('-n', '--n-lines', type=int, default=None, help='Number of lines to display. Overrides card config.')
@carduser(extended=True)
@chunkreader()
def histogram(output, diff, show, chunker, config, n_lines):
    from PIL import Image
    import colorsys

    n_lines = n_lines or len(list(config.field_range))*2
    line_length = config.line_length - (1 if diff else 0)
    result = np.zeros((n_lines, 256, line_length), dtype=np.uint32)
    sel = np.arange(line_length)
    chunks = chunker(config.line_length * np.dtype(config.dtype).itemsize, config.field_lines, config.field_range)
    chunks = tqdm(chunks, unit='L', dynamic_ncols=True)
    for n, d in chunks:
        l = np.frombuffer(d, dtype=config.dtype) >> ((np.dtype(config.dtype).itemsize - 1) * 8)
        if diff:
            l = np.diff(l) + 128
        result[n%n_lines, l, sel] += 1

    for i in range(n_lines):
        for j in range(line_length):
            result[i,:,j] = 255*result[i,:,j]/np.max(result[i,:,j])

    # flip vertically
    result = result[:,::-1,:].reshape(-1, line_length)

    palette = np.zeros((256, 3), dtype=np.uint8)
    palette[0] = [0, 0, 0]
    for c in range(1, 256):
        palette[c] = [n * 255 for n in colorsys.hsv_to_rgb(c/1025, 1, 1)]

    rgb = palette[result]
    rgb[0::256, :] += 100
    rgb[0::32, :] = np.maximum(rgb[0::32, :], 32)

    i = Image.fromarray(rgb)
    if show:
        i.show()
    i.convert('RGB').save(output)


@vbi.command()
@carduser(extended=True)
@chunkreader()
def plot(chunker, config):
    from teletext.gui.vbiplot import vbiplot
    vbiplot(chunker, config)


@vbi.command()
@carduser(extended=True)
@click.argument('input', type=click.Path(readable=True), required=True)
@click.argument('sampledir', type=click.Path(writable=True), required=True)
@click.option('-a', '--auto', is_flag=True)
def classifygui(input, sampledir, auto, config):
    from teletext.gui.classify import classify_gui
    classify_gui(input, sampledir, auto, config)


@vbi.command()
@carduser()
@chunkreader()
@click.argument('output', type=click.File('wb'))
@click.option('--progress/--no-progress', default=True, help='Display progress bar.')
def copy(chunker, config, progress, output):
    """Copy input to output"""
    chunks = chunker(config.line_length * np.dtype(config.dtype).itemsize, config.field_lines, config.field_range)
    if progress:
        chunks = tqdm(chunks, unit='L', dynamic_ncols=True)
    for n, c in chunks:
        output.write(c)


@vbi.command()
@carduser()
@chunkreader()
@click.argument('output', type=click.Path(), required=True)
@click.option('--progress/--no-progress', default=True, help='Display progress bar.')
def linesplit(chunker, config, progress, output):
    """Split VBI file into one file per line"""
    chunks = chunker(config.line_length * np.dtype(config.dtype).itemsize, config.field_lines, config.field_range)
    if progress:
        chunks = tqdm(chunks, unit='L', dynamic_ncols=True)
    output = pathlib.Path(output)
    output.mkdir(parents=True, exist_ok=True)
    files = [(output / f'{n:02x}.vbi').open("wb") for n in range(config.frame_lines)]
    for number, chunk in chunks:
        files[number % config.frame_lines].write(chunk)


@vbi.command()
@carduser()
@chunkreader()
@click.argument('output', type=click.Path(), required=True)
@click.option('--progress/--no-progress', default=True, help='Display progress bar.')
@click.option('--prefix', type=str, default="", help='Prefix for cluster file names.')
def cluster(chunker, config, progress, output, prefix):
    """Split VBI file into clusters of similar lines"""
    import teletext.vbi.clustering
    chunks = chunker(config.line_bytes, config.field_lines, config.field_range)
    if progress:
        chunks = tqdm(chunks, unit='L', dynamic_ncols=True)
    output = pathlib.Path(output)
    output.mkdir(parents=True, exist_ok=True)
    teletext.vbi.clustering.batch_cluster(chunks, output, prefix, config.field_lines * 2)


@vbi.command()
@carduser()
@click.argument('map', type=click.File('rb'), required=True)
@click.argument('output', type=click.File('wb'), required=True)
def rendermap(config, map, output):
    """Render cluster map to image"""
    import teletext.vbi.clustering
    teletext.vbi.clustering.rendermap(config, map, output)


================================================
FILE: teletext/coding.py
================================================
# * Copyright 2016 Alistair Buxton <a.j.buxton@gmail.com>
# *
# * License: This program is free software; you can redistribute it and/or
# * modify it under the terms of the GNU General Public License as published
# * by the Free Software Foundation; either version 3 of the License, or (at
# * your option) any later version. This program is distributed in the hope
# * that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# * GNU General Public License for more details.


"""Byte coding and error protection

Odd parity:
The high bit of each byte is set such that there are an odd number of bits in the byte.
Single bit errors can be detected.

Hamming 8/4:
P1 D1 P2 D2 P3 D3 P4 D4 (Transmission order, LSB first.)
Single bit errors can be identified and corrected. Double bit errors can be detected.

Hamming 24/16:
P1 P2 D1 P3 D2 D3 D4 P4  D5 D6 D7 D8 D9 D10 D11 P5  D12 D13 D14 D15 D16 D17 D18 P6
Single bit errors can be identified and corrected. Double bit errors
can be detected.

"""
import numpy as np


def thue_morse(n, even=True):
    arr = np.array([even], dtype=bool)
    for i in range(0, n):
        arr = np.append(arr,~arr)
    return arr

# hamming 8/4 encoding look up table
hamming8_enc = np.array([
    0x15, 0x02, 0x49, 0x5e, 0x64, 0x73, 0x38, 0x2f, 0xd0, 0xc7, 0x8c, 0x9b, 0xa1, 0xb6, 0xfd, 0xea,
], dtype=np.uint8)
hamming8_enc.flags.writeable = False

# hamming 8/4 correctable errors occur when the input has even parity
hamming8_cor = thue_morse(8, even=True)
hamming8_cor.flags.writeable = False

# hamming 8/4 uncorrectable errors always have odd parity,
# but so do valid bytes.
hamming8_unc = thue_morse(8, even=False)
hamming8_unc[hamming8_enc] = False
hamming8_unc.flags.writeable = False

# hamming 8/4 decoding lookup table
hamming8_dec = np.array([
    0x1, 0xf, 0x1, 0x1, 0xf, 0x0, 0x1, 0xf, 0xf, 0x2, 0x1, 0xf, 0xa, 0xf, 0xf, 0x7,
    0xf, 0x0, 0x1, 0xf, 0x0, 0x0, 0xf, 0x0, 0x6, 0xf, 0xf, 0xb, 0xf, 0x0, 0x3, 0xf,
    0xf, 0xc, 0x1, 0xf, 0x4, 0xf, 0xf, 0x7, 0x6, 0xf, 0xf, 0x7, 0xf, 0x7, 0x7, 0x7,
    0x6, 0xf, 0xf, 0x5, 0xf, 0x0, 0xd, 0xf, 0x6, 0x6, 0x6, 0xf, 0x6, 0xf, 0xf, 0x7,
    0xf, 0x2, 0x1, 0xf, 0x4, 0xf, 0xf, 0x9, 0x2, 0x2, 0xf, 0x2, 0xf, 0x2, 0x3, 0xf,
    0x8, 0xf, 0xf, 0x5, 0xf, 0x0, 0x3, 0xf, 0xf, 0x2, 0x3, 0xf, 0x3, 0xf, 0x3, 0x3,
    0x4, 0xf, 0xf, 0x5, 0x4, 0x4, 0x4, 0xf, 0xf, 0x2, 0xf, 0xf, 0x4, 0xf, 0xf, 0x7,
    0xf, 0x5, 0x5, 0x5, 0x4, 0xf, 0xf, 0x5, 0x6, 0xf, 0xf, 0x5, 0xf, 0xe, 0x3, 0xf,
    0xf, 0xc, 0x1, 0xf, 0xa, 0xf, 0xf, 0x9, 0xa, 0xf, 0xf, 0xb, 0xa, 0xa, 0xa, 0xf,
    0x8, 0xf, 0xf, 0xb, 0xf, 0x0, 0xd, 0xf, 0xf, 0xb, 0xb, 0xb, 0xa, 0xf, 0xf, 0xb,
    0xc, 0xc, 0xf, 0xc, 0xf, 0xc, 0xd, 0xf, 0xf, 0xc, 0xf, 0xf, 0xa, 0xf, 0xf, 0x7,
    0xf, 0xc, 0xd, 0xf, 0xd, 0xf, 0xd, 0xd, 0x6, 0xf, 0xf, 0xb, 0xf, 0xe, 0xd, 0xf,
    0x8, 0xf, 0xf, 0x9, 0xf, 0x9, 0x9, 0x9, 0xf, 0x2, 0xf, 0xf, 0xa, 0xf, 0xf, 0x9,
    0x8, 0x8, 0x8, 0xf, 0x8, 0xf, 0xf, 0x9, 0x8, 0xf, 0xf, 0xb, 0xf, 0xe, 0x3, 0xf,
    0xf, 0xc, 0xf, 0xf, 0x4, 0xf, 0xf, 0x9, 0xf, 0xf, 0xf, 0xf, 0xf, 0xe, 0xf, 0xf,
    0x8, 0xf, 0xf, 0x5, 0xf, 0xe, 0xd, 0xf, 0xf, 0xe, 0xf, 0xf, 0xe, 0xe, 0xf, 0xe,
], dtype=np.uint8)
hamming8_dec.flags.writeable = False

# odd parity bits
parity_tab = thue_morse(7, even=True) * 0x80
parity_tab.flags.writeable = False

# bit reverse
reverse8_tab = np.packbits(np.unpackbits(np.array(range(256), dtype=np.uint8)).reshape((-1, 8))[:,::-1])
reverse8_tab.flags.writeable = False


def hamming8_encode(a):
    return hamming8_enc[a]


def hamming8_decode(a):
    return hamming8_dec[a]


def hamming16_encode(a):
    return np.ravel(np.column_stack((
        hamming8_enc[a & 0xf],
        hamming8_enc[a >> 4],
    )))


def hamming16_decode(a):
    if len(a) == 2:
        return hamming8_dec[a[0]] | (hamming8_dec[a[1]] << 4)
    else:
        return hamming8_dec[a[0::2]] | (hamming8_dec[a[1::2]] << 4)


def hamming8_correctable_errors(a):
    return hamming8_cor[a]


def hamming8_uncorrectable_errors(a):
    return hamming8_unc[a]


def hamming8_errors(a):
    return (2 * hamming8_unc[a]) + hamming8_cor[a]


def parity_encode(a):
    return a | parity_tab[a]


def parity_decode(a):
    return a & 0x7f


def parity_errors(a):
    return parity_tab[a&0x7f] != a&0x80


def bcd8_decode(a):
    tens = ((a >> 4) - 1) & 0xf
    units = ((a & 0xf) - 1) & 0xf
    return (tens * 10) + units


def bcd8_encode(a):
    tens = ((a / 10) + 1) & 0xf
    units = ((a % 10) + 1) & 0xf
    return (tens << 4) | units


def byte_reverse(a):
    return reverse8_tab[a]


def crc(n, c):
    for b in range(7, -1, -1):
        r = ((n >> b) & 1) ^ ((c >> 6) & 1) ^ ((c >> 8) & 1) ^ ((c >> 11) & 1) ^ ((c >> 15) & 1)
        c = r | ((c & 0x7FFF) << 1)
    return c


================================================
FILE: teletext/elements.py
================================================
import datetime

from .printer import PrinterANSI
from .coding import *

from . import finders


class Element(object):

    def __init__(self, shape, array=None):
        if array is None:
            self._array = np.zeros(shape, dtype=np.uint8)
        elif type(array) == bytes:
            self._array = np.frombuffer(array, dtype=np.uint8).copy()
        else:
            self._array = array

        if self._array.shape != shape:
            raise IndexError('Element got wrong shaped data.')

    def __getitem__(self, item):
        return self._array[item]

    def __setitem__(self, item, value):
        self._array[item] = value

    def __repr__(self):
        return f'{self.__class__.__name__}({repr(self._array)})'

    @property
    def bytes(self):
        return self._array.tobytes()

    @property
    def sevenbit(self):
        return np.packbits(np.unpackbits(self._array).reshape(-1, 8)[:, 1:].flatten()).tobytes()

    @sevenbit.setter
    def sevenbit(self, b):
        a = np.frombuffer(b, dtype=np.uint8)
        s = self._array.size * 7
        u = np.unpackbits(a)[:s].reshape(-1, 7)
        self._array[:] = np.packbits(u, axis=-1).reshape(self._array.shape) >> 1

    @property
    def errors(self):
        raise NotImplementedError


class ElementParity(Element):

    @property
    def errors(self):
        return parity_errors(self._array)


class ElementHamming(Element):

    @property
    def errors(self):
        return hamming8_errors(self._array)


class Mrag(ElementHamming):

    def __init__(self, array=None):
        super().__init__((2,), array)

    @property
    def magazine(self):
        magazine = hamming8_decode(self._array[0]) & 0x7
        return magazine or 8

    @property
    def row(self):
        return hamming16_decode(self._array[:2]) >> 3

    @magazine.setter
    def magazine(self, magazine):
        if magazine < 0 or magazine > 8:
            raise ValueError('Magazine numbers must be between 0 and 8.')
        self._array[0] = hamming8_encode((magazine & 0x7) | ((self.row&0x1) << 3))

    @row.setter
    def row(self, row):
        if row < 0 or row > 31:
            raise ValueError('Row numbers must be between 0 and 31.')
        self._array[0] = hamming8_encode((self.magazine & 0x7) | ((row & 0x1) << 3))
        self._array[1] = hamming8_encode(row >> 1)

    def __str__(self):
        return f'{self.magazine} {self.row} {self.errors}'


class Displayable(ElementParity):

    def place_string(self, string, x=0, y=None):
        if isinstance(string, str):
            string = string.encode('ascii')
        a = np.frombuffer(string, dtype=np.uint8)
        if y is None:
            self._array[x:x+a.shape[0]] = parity_encode(a)
        else:
            self._array[y, x:x + a.shape[0]] = parity_encode(a)

    def place_bitmap(self, bitmap, x=1, y=0, colour=0x17, conceal=False):
        yp, xp = bitmap.shape
        yr = yp % 3
        xr = xp % 2
        if yr or xr:
            bitmap = np.pad(bitmap, ((0, (3-yr)%3), (0, (2-xr)%2)))
            yp, xp = bitmap.shape
        yc = yp // 3
        xc = xp // 2
        mosaic = bitmap.reshape(yc, 3, xc, 2).swapaxes(1, 2).reshape(yc, xc, 6)
        mosaic = np.packbits(mosaic, axis=2, bitorder="little").reshape(yc, xc)
        mosaic = mosaic + ((mosaic >= 0x20) * 0x20) + 0x20
        if conceal:
            self._array[y:y+yc, x-2] = parity_encode(colour)
            self._array[y:y+yc, x-1] = parity_encode(0x18)
        else:
            self._array[y:y+yc, x-1] = parity_encode(colour)
        self._array[y:y+yc, x:x+xc] = parity_encode(mosaic)

    def to_ansi(self, colour=True):
        if len(self._array.shape) == 1:
            return str(PrinterANSI(self._array, colour))
        else:
            return '\n'.join(
                [str(PrinterANSI(a, colour)) for a in self._array]
            )

    def _tti_escape(self, array):
        return ''.join(f'\x1b{chr(x | 0x40)}' if 0 <= x <= 0x1f else chr(x) for x in (array & 0x7f))

    def to_tti(self):
        if len(self._array.shape) == 1:
            return self._tti_escape(self._array)
        else:
            return [self._tti_escape(a) for a in self._array]

    @property
    def bytes_no_parity(self):
        return (self._array & 0x7f).tobytes()


class Page(Element):

    @property
    def page(self):
        return hamming16_decode(self._array[:2])

    @page.setter
    def page(self, page):
        if page < 0 or page > 0xff:
            raise ValueError('Page numbers must be between 0 and 0xff.')
        self._array[:2] = hamming16_encode(page)

    @property
    def errors(self):
        e = np.zeros_like(self._array)
        e[:2] = hamming8_errors(self._array[:2])
        return e


class Header(Page):

    def __init__(self, array):
        super().__init__((40,), array)

    @property
    def subpage(self):
        values = hamming16_decode(self._array[2:6])
        return (values[0] & 0x7f) | ((values[1] & 0x3f) <<8)

    @property
    def control(self):
        values = hamming16_decode(self._array[2:8])
        return (values[0] >> 7) | ((values[1] >> 5) & 0x6) | (values[2] << 3)

    @property
    def displayable(self):
        return Displayable((32,), self._array[8:])

    @property
    def codepage(self):
        return (self.control >> 8) & 0x7

    @property
    def newsflash(self):
        return bool(self.control & 0x2)

    @property
    def subtitle(self):
        return bool(self.control & 0x4)

    @property
    def supress_header(self):
        return bool(self.control & 0x8)

    @subpage.setter
    def subpage(self, subpage):
        if subpage < 0 or subpage > 0x3f7f:
            raise ValueError('Subpage numbers must be between 0 and 0x3f7f.')
        control = self.control
        self._array[2:6] = hamming16_encode(np.array([
            (subpage & 0x7f) | ((control & 1) << 7),
            (subpage >> 8) | ((control & 6) << 6),
        ], dtype=np.uint8))

    @control.setter
    def control(self, control):
        if control < 0 or control > 2047:
            raise ValueError('Control bits must be between 0 and 2047.')
        subpage = self.subpage
        self._array[3] = hamming8_encode(((subpage >> 4) & 0x7) | ((control & 1) << 3))
        self._array[5] = hamming8_encode(((subpage >> 12) & 0x3) | ((control & 6) << 1))
        self._array[6:8] = hamming16_encode(control >> 3)

    def to_ansi(self, colour=True):
        return f'{self.page:02x} {self.displayable.to_ansi(colour)}'

    def apply_finders(self):
        ranks = [(f.match(self.displayable[:]),f) for f in finders.HeaderFinders]
        ranks.sort(reverse=True, key=lambda x: x[0])
        if ranks[0][0] > 20:
            self.finder = ranks[0][1]
            self.finder.fixup(self.displayable[:])

    @property
    def errors(self):
        e = super().errors
        e[2:8] = hamming8_errors(self._array[2:8])
        e[8:] = self.displayable.errors
        return e


class Triplet(Element):

    def __init__(self, array):
        super().__init__((3,), array)

    def __str__(self):
        return f'triplet'

    @property
    def errors(self):
        e = super().errors
        e[:] = hamming8_errors(self._array[:])
        return e


class PageLink(Page):

    def __init__(self, array, mrag):
        super().__init__((6,), array)
        self._mrag = mrag

    @property
    def subpage(self):
        values = hamming16_decode(self._array[2:6])
        return (values[0] & 0x7f) | ((values[1] & 0x3f) <<8)

    @property
    def magazine(self):
        values = hamming16_decode(self._array[2:6])
        magazine = ((values[0] >> 7) | ((values[1] >> 5) & 0x6)) ^ (self._mrag.magazine & 0x7)
        return magazine or 8

    @subpage.setter
    def subpage(self, subpage):
        if subpage < 0 or subpage > 0x3f7f:
            raise ValueError('Subpage numbers must be between 0 and 0x3f7f.')
        magazine = self.magazine
        self._array[2:6] = hamming16_encode(np.array([
            (subpage & 0x7f) | ((magazine & 1) << 7),
            (subpage >> 8) | ((magazine & 6) << 6),
        ]))

    @magazine.setter
    def magazine(self, magazine):
        if magazine < 0 or magazine > 8:
            raise ValueError('Magazine numbers must be between 0 and 8.')
        magazine = magazine ^ self._mrag.magazine
        subpage = self.subpage
        self._array[3:6:2] = hamming8_encode([
            ((subpage >> 4) & 0x7) | ((magazine & 1) << 3),
            ((subpage >> 12) & 0x3) | ((magazine & 6) << 1),
        ])

    def __str__(self):
        return f'{self.magazine}{self.page:02x}:{self.subpage:04x}'

    @property
    def errors(self):
        e = super().errors
        e[:] = hamming8_errors(self._array[:])
        return e


class DesignationCode(Element):

    @property
    def dc(self):
        return hamming8_decode(self._array[0])

    @dc.setter
    def dc(self, dc):
        self._array[0] = hamming8_encode(dc)

    @property
    def errors(self):
        e = np.zeros_like(self._array)
        e[0] = hamming8_errors(self._array[0])
        return e


class Triplets(DesignationCode):

    def __init__(self, array, mrag):
        super().__init__((40,), array)
        self._mrag = mrag

    @property
    def triplets(self):
        return tuple(Triplet(self._array[n:n+3]) for n in range(1, 40, 3))

    def to_ansi(self, colour=True):
        return f'DC={self.dc:x} ' + ' '.join((str(triplet) for triplet in self.triplets))

    @property
    def errors(self):
        e = super().errors
        for t,n in zip(self.triplets, range(1, 40, 3)):
            e[n:n+3] = t.errors
        return e


class Fastext(DesignationCode):

    def __init__(self, array, mrag):
        super().__init__((40,), array)
        self._mrag = mrag

    @property
    def links(self):
        return tuple(PageLink(self._array[n:n+6], self._mrag) for n in range(1, 36, 6))

    @property
    def control(self):
        return hamming8_decode(self._array[37])

    @control.setter
    def control(self, value):
        self._array[37] = hamming8_encode(value)

    @property
    def checksum(self):
        return self._array[38]<<8 | self._array[39]

    @checksum.setter
    def checksum(self, value):
        self._array[38] = value>>8
        self._array[39] = value&0xff

    def to_ansi(self, colour=True):
        return f'DC={self.dc:x} ' + ' '.join((str(link) for link in self.links)) + f' CRC={self.checksum:04x}'

    @property
    def errors(self):
        e = super().errors
        for l,n in zip(self.links, range(1, 36, 6)):
            e[n:n+6] = l.errors
        return e


class Format1(Element):

    epoch = datetime.date(1858, 11, 17)

    def __init__(self, array):
        super().__init__((9,), array)

    @property
    def network(self):
        return (byte_reverse(self._array[0]) << 8) | byte_reverse(self._array[1])

    @property
    def offset(self):
        hours = 0.5 * ((self._array[2] >> 1) & 0x1f)
        if ((self._array[2] >> 6) & 0x01):
            hours *= -1
        return hours

    @property
    def mjd(self):
        return (bcd8_decode((int(self._array[3])&0xf)|0x10) * 10000) + (bcd8_decode(int(self._array[4])) * 100) + bcd8_decode(int(self._array[5]))

    @property
    def date(self):
        return self.epoch + datetime.timedelta(days=int(self.mjd))

    @property
    def hour(self):
        return bcd8_decode(self._array[6])

    @hour.setter
    def hour(self, value):
        self._array[6] = bcd8_encode(value)

    @property
    def minute(self):
        return bcd8_decode(self._array[7])

    @minute.setter
    def minute(self, value):
        self._array[7] = bcd8_encode(value)

    @property
    def second(self):
        return bcd8_decode(self._array[8])

    @second.setter
    def second(self, value):
        self._array[8] = bcd8_encode(value)

    def to_ansi(self, colour=True):
        return f'NI={self.network:04x} {self.date} {self.hour:02d}:{self.minute:02d}:{self.second:02d} {self.offset}'

    @property
    def errors(self):
        #TODO: detect invalid dates and times
        return 0


class Format2(Element):

    def __init__(self, array):
        super().__init__((13,), array)

    @property
    def day(self):
        return byte_reverse(((hamming16_decode(self._array[3:5]) >> 2) & 0x1f)) >> 3

    @property
    def month(self):
        return byte_reverse((hamming16_decode(self._array[4:6]) >> 3) & 0x0f) >> 4

    @property
    def hour(self):
        return byte_reverse((hamming16_decode(self._array[5:7]) >> 3) & 0x1f) >> 3

    @property
    def minute(self):
        return byte_reverse(hamming16_decode(self._array[7:9]) & 0x3f) >> 2

    @property
    def country(self):
        return byte_reverse(hamming8_decode(self._array[2]) | ((hamming8_decode(self._array[8]) & 0xC) << 2) | ((hamming8_decode(self._array[9]) & 0x3) << 6))

    @property
    def network(self):
        return byte_reverse((hamming8_decode(self._array[3]) & 0x3) | (hamming8_decode(self._array[9]) & 0xC) | (hamming8_decode(self._array[10]) << 4))

    def to_ansi(self, colour=True):
        return f'NI={self.network:02x} C={self.country:02x} {self.day}/{self.month} {self.hour:02d}:{self.minute:02d}'


class BroadcastData(DesignationCode):

    def __init__(self, array, mrag):
        super().__init__((40,), array)
        self._mrag = mrag

    @property
    def displayable(self):
        return Displayable((20,), self._array[20:])

    @property
    def initial_page(self):
        return PageLink(self._array[1:7], self._mrag)

    @property
    def format1(self):
        return Format1(self._array[7:16])

    @property
    def format2(self):
        return Format2(self._array[7:20])

    def to_ansi(self, colour=True):
        if self.dc in [0, 1]:
            return f'{self.displayable.to_ansi(colour)} DC={self.dc} IP={self.initial_page} {self.format1.to_ansi(colour)}'
        elif self.dc in [2, 3]:
            return f'{self.displayable.to_ansi(colour)} DC={self.dc} IP={self.initial_page} {self.format2.to_ansi(colour)}'
        else:
            return f'DC={self.dc}'

    @property
    def errors(self):
        e = super().errors
        e[1:7] = self.initial_page.errors
        e[20:] = self.displayable.errors
        return e


class Celp(Element):

    dblevels = [0, 4, 8, 12, 18, 24, 30, 0]

    servicetypes = [
        'Single-channel mode using 1 VBI line per frame',
        'Single-channel mode using 2 VBI lines per frame',
        'Single-channel mode using 3 VBI lines per frame',
        'Single-channel mode using 4 VBI lines per frame',
        'Mute Channel 1',
        'Two-channel Mode using 2 VBI lines per frame',
        'Mute Channel 2',
        'Two-channel Mode using 4 VBI lines per frame',
    ]

    fmt = 'DCN: {dcn}, {rel}, S: {svc:>7s}, C: {ctl} {frame0} {frame1}'

    def __init__(self, array, mrag):
        super().__init__((40,), array)
        self._mrag = mrag

    @property
    def dcn(self):
        return self._mrag.magazine + ((self._mrag.row & 1) << 3)

    @property
    def service(self):
        return hamming8_decode(self._array[0])

    @service.setter
    def service(self, service):
        self._array[0] = hamming8_encode(service)

    @property
    def control(self):
        return hamming8_decode(self._array[1])

    @control.setter
    def control(self, service):
        self._array[0] = hamming8_encode(service)

    def to_ansi(self, colour=True):
        frame0 = self._array[2:21].tobytes().hex()
        frame1 = self._array[21:40].tobytes().hex()

        if self.dcn == 4:
            return self.fmt.format(
                dcn = self.dcn,
                rel = 'Programme-related audio',
                svc = 'AUDETEL' if self.service == 0 else hex(self.service),
                ctl = f'{hex(self.control)} {self.dblevels[self.control & 0x7]:2d} dB {"muted" if self.control & 0x8 else ""}',
                frame0 = frame0,
                frame1 = frame1,
            )
        elif self.dcn == 12:
            if self.service & 0x8:
                return self.fmt.format(
                    dcn=self.dcn,
                    rel='Programme-independent audio',
                    svc=f'User-defined {hex(self.service&0x7)}',
                    ctl=hex(self._array[1]),
                    frame0=frame0,
                    frame1=frame1,
                )
            else:
                return self.fmt.format(
                    dcn=self.dcn,
                    rel='Programme-independent audio',
                    svc=self.servicetypes[self.service],
                    ctl=hex(self.control),
                    frame0=frame0,
                    frame1=frame1,
                )
        else:
            raise ValueError("Unexpected data channel for CELP.")

    @property
    def errors(self):
        e = np.zeros_like(self._array)
        e[0:2] = hamming8_errors(self._array[0:2])
        return e


================================================
FILE: teletext/file.py
================================================
import io
import itertools
import os
import stat


def PossiblyInfiniteRange(start=0, stop=None, step=1, limit=None):
    if stop is None:
        if limit is None:
            return itertools.count(start, step)
        else:
            return range(start, start + (limit * step), step)
    else:
        if limit is None:
            return range(start, stop, step)
        else:
            return range(start, min(stop, start + (limit * step)), step)


class LenWrapper(object):
    def __init__(self, i, l):
        self.i = i
        self.l = l

    def __iter__(self):
        return self.i

    def __len__(self):
        return self.l


def _chunks(f, size, flines, frange, seek):
    while True:
        if seek:
            f.seek(size * frange.start, os.SEEK_CUR)
        else:
            f.read(size * frange.start)
        for _ in frange:
            b = f.read(size)
            if len(b) < size:
                return
            yield b
        if seek:
            f.seek(size * (flines - frange.stop), os.SEEK_CUR)
        else:
            f.read(size * (flines - frange.stop))


def chunks(f, size, start, step, flines=16, frange=(0, 16), seek=True):
    while True:
        c = _chunks(f, size, flines, frange, seek)
        try:
            for _ in range(start):
                next(c)
            while True:
                yield next(c)
                for i in range(step-1):
                    next(c)
        except StopIteration:
            if seek:
                f.seek(0, os.SEEK_SET)
            else:
                return

def FileChunker(f, size, start=0, stop=None, step=1, limit=None, flines=16, frange=range(0, 16), loop=False, dup_stdin=False):
    seekable = False
    try:
        if hasattr(f, 'fileno') and stat.S_ISFIFO(os.fstat(f.fileno()).st_mode):
            if dup_stdin and f.fileno() == 0:
                f = os.fdopen(os.dup(f.fileno()), 'rb')
            raise io.UnsupportedOperation

        f.seek(0, os.SEEK_END)
        total_lines = f.tell() // size
        total_fields = total_lines // flines
        remainder = max(min((total_lines % flines) - frange.start, len(frange)), 0)
        useful_lines = (total_fields * len(frange)) + remainder

        if stop is None:
            stop = useful_lines
        else:
            stop = min(stop, useful_lines)

        seekable = True
        f.seek(0, os.SEEK_SET)

    except io.UnsupportedOperation:
        # chunks() always seeks to the start
        pass

    r = PossiblyInfiniteRange(start, None if loop else stop, step, limit)
    i = zip(r, chunks(f, size, start, step, flines, frange, seek=seekable))
    if hasattr(r, '__len__'):
        return LenWrapper(i, len(r))
    else:
        return i


================================================
FILE: teletext/finders.py
================================================
import itertools

from .coding import parity_encode, parity_decode


class Finder(object):

    groups = {
        'c': b'abcdefghijklmnopqrstuvwxyz ',
        'C': b'ABCDEFGHIJKLMNOPQRSTUVWXYZ ',
        'D': b'MTWFS',
        'd': b'mtwfs',
        'A': b'OUEHRA',
        'a': b'ouehra',
        'Y': b'NEDUIT',
        'y': b'neduit',
        'M': b'JFMASOND',
        'm': b'jfmasond',
        'O': b'AEPUCO',
        'o': b'aepuco',
        'N': b'NBRYNLGPTVC',
        'n': b'nbrynlgptvc',
        'Z': b'12345678',
        'T': b'0123456789ABCDEFabcdef',
        'U': b'0123456789ABCDEFabcdef',
        'F': b'0123 ',
        'f': b'0123456789',
        'H': b'012 ',
        'h': b'0123456789',
        'L': b'012345',
        'l': b'0123456789',
        'S': b'012345',
        's': b'0123456789',
        'e': b'', # exact match
        '*': b'', # wildcard
        ' ': b'\x00\x01\x02\x03\x04\x05\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f ', # whitespace/spacing attributes
    }

    def __init__(self, match1, match2, name, years, channels):
        if len(match1) != len(match2):
            raise ValueError('Match fields must be equal length.')
        self.match1 = match1.encode('ascii')
        self.match2 = match2
        self.name = name
        self.years = years
        self.channels = channels

    def match(self, arr):
        a = [2 if (m2 == 'e' and m1 == c) else (1 if c in self.groups[m2] else 0) for m1, m2, c in zip(self.match1, self.match2, parity_decode(arr))]
        #print(f'{self.name[:20]:21s}', ''.join(str(n) for n in a))
        return sum(a)

    def fixup(self, arr):
        for n, m1, m2 in zip(itertools.count(), self.match1, self.match2):
            if m2 == 'e':
                arr[n] = parity_encode(m1)


HeaderFinders = [
Finder("CEEFAX 217 \x09Wed 25 Dec\x03 18:29/53",
       "eeeeeeeZTUee"+"DayeFfeMone"+"eHheLleSs",
       "BBC", (0,1996), ['BBC1', 'BBC2']),

Finder("CEEFAX 1 217 Wed 25 Dec\x0318:29/53",
       "eeeeeeeeeZTUeDayeFfeMoneHhe"+"LleSs",
       "BBC1", (1996,3000), ['BBC1']),

Finder("CEEFAX 2 217 Wed 25 Dec\x0318:29/53",
       "eeeeeeeeeZTUeDayeFfeMone"+"HheLleSs",
       "BBC2", (1996,3000), ['BBC2']),

Finder("Central  217 Wed 25 Dec 18:29:53",
       "eeeeeeeeeZTUeDayeFfeMoneHheLleSs",
       "Central", (0, 3000), ['ITV']),

Finder("\x02   ITV SUBTITLES               ",
       "e"+"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
       "ITV Subs.", (0, 3000), ['ITV']),

Finder("\x01\x1d\x07 DBI STATUS PAGE   \x1c  2059:27",
       "e"+"e"+"e"+"eeeeeeeeeeeeeeeeeeee"+"eeHhLleSs",
       "ITV DBI Stat.", (0, 3000), ['ITV']),

Finder("                         2059:27",
       "eeeeeeeeeeeeeeeeeeeeeeeeeHhLleSs",
       "ITV DBI Blank", (0, 3000), ['ITV']),

Finder("                                ",
       "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
       "Subs Blank", (0, 3000), ['BBC1', 'BBC2', 'ITV', 'C4', 'Five']),

Finder("\x01789 DBI TEST PAGE 789\x07  2059:27",
       "e"+"ZTUeeeeeeeeeeeeeeeZTUe"+"eeHhLleSs",
       "ITV DBI Test.", (0, 3000), ['ITV']),

Finder("\x01   DBI/CH4 - BCAST2  \x09\x07 2059:27",
       "e"+"ZTUeeeeeeeeeeeeeeeeeee"+"e"+"eHhLleSs",
       "C4 DBI Test.", (0, 3000), ['C4']),

Finder(" 500     mon 12  may     2059:27",
       "eZTUeeeeedayeFfeemoneeeeeHhLleSs",
       "Five", (1997,1997), ['Five']),

Finder("\x06   5 text   \x07255 02 May\x031835:21",
       "e"+"eeeeeeeeeeeee"+"ZTUeFfeMone"+"HhLleSs",
       "Five", (1997, 2006), ['Five']),

Finder("Five 500  27 Nov        20:59.27",
       "eeeeeZTUeeFfeMonemoneeeeHheLleSs",
       "Five", (1999,3000), ['Five']),

Finder("SOFTEL D1 SUBTITLE INSERTER     ",
       "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
       "Five Subs.", (0,3000), ['Five']),

Finder("\x04\x1d\x03Teletext\x07 \x1c100 May05\x0318:29:53",
       "e"+"e"+"e"+"eeeeeeeee"+"ee"+"ZTUeMonFfe"+"HheLleSs",
       "Teletext Ltd.", (1993, 3000), ['ITV', 'C4']),

Finder("\x04\x1d\x03Teletext \x03\x1c100 May05\x0318:29:53",
       "e"+"e"+"e"+"eeeeeeeeee"+"e"+"ZTUeMonFfe"+"HheLleSs",
       "Teletext Ltd. (Five)", (1999, 3000), ['Five']),

Finder("\x04\x1d\x03Teletext\x07 \x1c100\x03May05\x0318:29:53",
       "e"+"e"+"e"+"eeeeeeeee"+"ee"+"ZTUe"+"MonFfe"+"HheLleSs",
       "Teletext Ltd. (Five)", (1999, 3000), ['Five']),

Finder("4-Tel 307 Sun 26 May\x03C4\x0718:29:53",
       "eeeeeeZTUeDayeFfeMone"+"eee"+"HheLleSs",
       "4-Tel", (0, 3000), ['C4']),

Finder("PLEASE REFER TO PAGE 100 2001:01",
       "eeeeeeeeeeeeeeeeeeeeeeeeeHhLleSs",
       "Oracle Filler", (0,1992), ['ITV', 'C4']),

Finder("ORACLE 200 Sun27 Dec\x03ITV\x032001:01",
       "eeeeeeeZTUeDayFfeMone"+"eeee"+"HhLleSs",
       "Oracle (ITV)", (0,1992), ['ITV']),

Finder("Teletext on 4 100 Jan25\x0320:01:01",
       "eeeeeeeeeeeeeeZTUeMonFfe"+"HheLleSs",
       "Teletext Ltd. (C4 - Early)", (1993,1993), ['C4']),

Finder("100\x02ARD/ZDF\x07Mo 26.12.88\x0222:00:00",
       "ZTUe"+"eeeeeeee"+"Daeeeeeeeeee"+"HheLleSs",
       "ARD", (0,3000), ['ARD']),

Finder("102 BELTEK              22:07:06",
       "ZTU CCCCCCCCCCCCCCCCCCC HheLleSs",
       "TVR", (0,3000), ['ARD']),

]



================================================
FILE: teletext/gui/__init__.py
================================================


================================================
FILE: teletext/gui/classify.py
================================================
import pathlib
import sys

import numpy as np
from PyQt5 import QtWidgets, QtCore

import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure


class Window(QtWidgets.QMainWindow):
    def __init__(self, dir, sampledir, auto, config, app, parent=None):
        super(Window, self).__init__(parent)

        self.app = app
        self.auto = auto
        self.config = config
        self.dir = pathlib.Path(dir)
        self.sampledir = pathlib.Path(sampledir)
        self.files = []
        for f in self.dir.iterdir():
            if f.is_file() and f.suffix == '.vbi':
                s = f.stat().st_size // self.config.line_bytes
                self.files.append((f, s))
            elif f.is_dir():
                for g in f.iterdir():
                    if g.is_file() and g.suffix == '.vbi':
                        s = g.stat().st_size // self.config.line_bytes
                        self.files.append((g, s))

        print(len(self.files))
        self.files.sort(key=lambda x: x[1], reverse=True)
        #self.files = self.files[1000:]
        self.current_file = 0

        self.setWindowTitle("VBI Classify")
        w = QtWidgets.QWidget(self)
        self.setCentralWidget(w)
        layout = QtWidgets.QVBoxLayout(w)
        w.setLayout(layout)

        f = QtWidgets.QFrame()
        f.setFrameShape(QtWidgets.QFrame.Shape.Panel)
        f.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
        f.setLineWidth(2)
        fl = QtWidgets.QVBoxLayout()
        fl.setContentsMargins(0, 0, 0, 0)
        f.setLayout(fl)

        self.canvas = FigureCanvas()
        fl.addWidget(self.canvas)
        layout.addWidget(f, 1)

        self.buttons = {}
        btntmp = ['teletext', 'negatext', 'quiet', 'empty', 'noise', 'mixed']
        if not self.auto:
            self.g = QtWidgets.QWidget(w)
            self.bbox = QtWidgets.QGridLayout(self.g)
            self.g.setLayout(self.bbox)
            layout.addWidget(self.g)

            self.buttonMapper = QtCore.QSignalMapper(self)
            self.buttonMapper.mappedString.connect(self.button_pressed)

            for f in sorted(self.sampledir.iterdir()):
                if f.is_dir():
                    if f.name not in btntmp:
                        btntmp.append(f.name)

            btntmp.append('skip')

            for y in range(10):
                for x in range(5):
                    try:
                        self.add_button(btntmp[(y*5)+x], (y, x))
                    except IndexError:
                        break

        self.progress = QtWidgets.QProgressBar()
        self.progress.setVisible(False)
        self.progress.setFixedWidth(200)
        self.statusBar().addPermanentWidget(self.progress)

        self.enable_buttons(False)
        w.setLayout(layout)
        self.resize(1000, 600)
        self.show()
        self.load()

    def add_button(self, label, pos):
        b = QtWidgets.QPushButton(self.g)
        b.setText(label)
        #b.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
        b.setMinimumHeight(b.height()+10)
        b.clicked.connect(self.buttonMapper.map)
        self.buttonMapper.setMapping(b, label)
        self.bbox.addWidget(b, *pos)
        self.buttons[label] = b

    def enable_buttons(self, en):
        for b in self.buttons.values():
            b.setEnabled(en)

    def button_pressed(self, label):
        if label != 'skip':
            src = self.files[self.current_file][0]
            (self.sampledir / label).mkdir(parents=True, exist_ok=True)
            src.rename(self.sampledir / label / src.name)
            print(self.files[self.current_file], '->', label)
        self.current_file += 1
        if self.current_file >= len(self.files):
            print("All done")
            self.app.quit()
        else:
            self.enable_buttons(False)
            self.load()

    def load(self):
        f = self.files[self.current_file]
        self.statusBar().showMessage(f'{self.current_file + 1}/{len(self.files)} - {f[0]} - {f[1]} lines')

        try:
            result = np.fromfile(f[0].with_suffix('.hist'), dtype=np.uint32).reshape(256, self.config.line_length)
            self.plot(result)
            if self.auto:
                self.button_pressed('skip')
        except FileNotFoundError:
            self.load_thread = VBILoader(f[0], self.config)
            self.load_thread.total.connect(self.progress.setMaximum)
            self.load_thread.update.connect(self.progress.setValue)
            self.load_thread.finished.connect(self.loaded)

            self.progress.setValue(0)
            self.load_thread.start()
            self.progress.setVisible(True)

    def loaded(self):
        result = self.load_thread.result
        del self.load_thread
        self.progress.setVisible(False)
        if self.auto:
            f = self.files[self.current_file][0]
            f = f.with_suffix('.hist')
            result.tofile(f)
            self.button_pressed('skip')
        self.plot(result)

    def plot(self, result):
        fig = Figure()
        ax = fig.subplots(1, 1)
        aa = result == 0
        mn = np.argmin(aa, axis=0)
        mx = 255 - np.argmin(aa[::-1, :], axis=0)

        ax.imshow(result, origin="lower", cmap="hot")
        ax.plot(mn, linewidth=1)
        ax.plot(mx, linewidth=1)
        fig.tight_layout(pad=0)

        self.canvas.figure = fig
        x, y = self.width(), self.height()
        self.resize(x+1, y+1)
        self.resize(x, y)

        # refresh canvas
        self.canvas.draw()
        plt.close(fig)
        self.enable_buttons(True)

class VBILoader(QtCore.QThread):
    total = QtCore.pyqtSignal(int)
    update = QtCore.pyqtSignal(int)

    def __init__(self, file, config):
        self.file = file
        self.config = config
        super().__init__()

    def run(self):
        arr = np.memmap(self.file, dtype=self.config.dtype).reshape(-1, self.config.line_length)
        h = np.zeros((256, self.config.line_length), dtype=np.uint32)
        sel = np.arange(self.config.line_length)
        shift = (np.dtype(self.config.dtype).itemsize - 1) * 8
        self.total.emit(arr.shape[0])
        for n in range(arr.shape[0]):
            l = arr[n] >> shift
            h[l, sel] += 1
            self.update.emit(n)

        for j in range(self.config.line_length):
            h[:,j] = 255*h[:,j]/np.max(h[:,j])
        self.result = h

def classify_gui(dir, sampledir, auto, config):
    app = QtWidgets.QApplication.instance()
    if app is None:
        app = QtWidgets.QApplication(sys.argv)

    main = Window(dir, sampledir, auto, config, app)
    main.show()
    app.exec_()


================================================
FILE: teletext/gui/decoder.py
================================================
import os
import random
import sys
import webbrowser

import numpy as np
from PyQt5.QtCore import QSize, QObject, QUrl
from PyQt5.QtGui import QFont, QColor

from teletext.parser import Parser


class Palette(object):

    def __init__(self, context):
        self._context = context
        self._palette = [
            QColor(0, 0, 0),
            QColor(255, 0, 0),
            QColor(0, 255, 0),
            QColor(255, 255, 0),
            QColor(0, 0, 255),
            QColor(255, 0, 255),
            QColor(0, 255, 255),
            QColor(255, 255, 255),
        ]
        self._context.setContextProperty('ttpalette', self._palette)

    def __getitem__(self, item):
        return (self._palette[item].red(), self._palette[item].green(), self._palette[item].blue())

    def __setitem__(self, item, value):
        self._palette[item].setRed(value[0])
        self._palette[item].setGreen(value[1])
        self._palette[item].setBlue(value[2])
        self._context.setContextProperty('ttpalette', self._palette)


class ParserQML(Parser):

    def __init__(self, tt, row, cells, nextrow):
        self._row = row
        self._cells = cells
        self._nextrow = nextrow
        super().__init__(tt)

    def emitcharacter(self, c):
        self._cells[self._cell].setProperty('c', c)
        for state, value in self._state.items():
            self._cells[self._cell].setProperty(state, value)
        self._dh |= self._state['dh']
        self._cell += 1

    def parse(self):
        self._cell = 0
        self._dh = False
        super().parse()
        self._row.setProperty('rowheight', 2 if self._dh else 1)
        if self._nextrow:
            self._nextrow.setProperty('rowrendered', not (self._row.property('rowrendered') and self._dh))


class Decoder(object):

    def __init__(self, widget):

        self.widget = widget

        self._fonts = [
            [
                [self.make_font(100), self.make_font(50)],
                [self.make_font(200), self.make_font(100)]
            ],
            [
                [self.make_font(120), self.make_font(60)],
                [self.make_font(240), self.make_font(120)]
            ]
        ]

        self.widget.rootContext().setContextProperty('ttfonts', self._fonts)
        self._palette = Palette(self.widget.rootContext())

        qml_file = os.path.join(os.path.dirname(__file__), 'decoder.qml')
        self.widget.setSource(QUrl.fromLocalFile(qml_file))

        self._rows = [self.widget.rootObject().findChild(QObject, 'rows').itemAt(x) for x in range(25)]
        self._cells = [[r.findChild(QObject, 'cols').itemAt(x) for x in range(40)] for r in self._rows]
        self._data = np.zeros((25, 40), dtype=np.uint8)
        self._parsers = [ParserQML(self._data[x], self._rows[x], self._cells[x], self._rows[x+1] if x < 24 else None) for x in range(25)]

        self.zoom = 2

    def __setitem__(self, item, value):
        self._data[item] = value
        if isinstance(item, tuple):
            item = item[0]
        if isinstance(item, int):
            self._parsers[item].parse()
        else:
            for p in self._parsers[item]:
                p.parse()

    def __getitem__(self, item):
        return self._data[item]

    def randomize(self):
        self[1:] = np.random.randint(0, 256, size=(24, 40), dtype=np.uint8)

    def make_font(self, stretch):
        font = QFont('teletext2')
        font.setStyleStrategy(QFont.NoSubpixelAntialias)
        font.setHintingPreference(QFont.PreferNoHinting)
        font.setStretch(stretch)
        return font

    @property
    def palette(self):
        return self._palette

    @property
    def zoom(self):
        return self.widget.rootObject().property('zoom')

    @zoom.setter
    def zoom(self, zoom):
        if 0 < zoom < 5:
            self._fonts[0][0][0].setPixelSize(zoom * 10)
            self._fonts[0][0][1].setPixelSize(zoom * 20)
            self._fonts[0][1][0].setPixelSize(zoom * 10)
            self._fonts[0][1][1].setPixelSize(zoom * 20)
            self._fonts[1][0][0].setPixelSize(zoom * 10)
            self._fonts[1][0][1].setPixelSize(zoom * 20)
            self._fonts[1][1][0].setPixelSize(zoom * 10)
            self._fonts[1][1][1].setPixelSize(zoom * 20)
            self.widget.rootContext().setContextProperty('ttfonts', self._fonts)
            self.widget.rootObject().setProperty('zoom', zoom)
            self.widget.setFixedSize(self.size())

    @property
    def reveal(self):
        return self.widget.rootObject().property('reveal')

    @reveal.setter
    def reveal(self, reveal):
        self.widget.rootObject().setProperty('reveal', reveal)

    @property
    def crteffect(self):
        return self.widget.rootObject().property('crteffect')

    @crteffect.setter
    def crteffect(self, crteffect):
        self.widget.rootObject().setProperty('crteffect', crteffect)

    def size(self):
        sf = self.widget.rootObject().size()
        return QSize(int(sf.width()), int(sf.height()))

    def setEffect(self, e):
        self._effect = bool(e)
        self.widget.rootContext().setContextProperty('tteffect', self._effect)


================================================
FILE: teletext/gui/decoder.qml
================================================
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtGraphicalEffects 1.12


Rectangle {
    property int zoom: 2
    property int borderSize: 10 * zoom
    property bool crteffect: true
    property bool flashsrc: true
    property bool reveal: false
    width: teletext.width + borderSize * 4
    height: teletext.height + borderSize * 2
    border.width: borderSize
    border.color: "black"
    color: "black"
    Column {
        id: teletext
        objectName: "teletext"
        width: 40 * 8 * zoom
        height: 250 * zoom
        x: borderSize * 2
        y: borderSize
        clip: true
        Repeater {
            objectName: "rows"
            model: 25
            Row {
                property int rowheight: 1
                property bool rowrendered: true
                Repeater {
                    objectName: "cols"
                    model: 40
                    Item {
                        property string c: "X"
                        property int bg: 1
                        property int fg: 7
                        property bool dw: false
                        property bool dh: false
                        property bool flash: false
                        property bool mosaic: false
                        property bool solid: true
                        property bool boxed: false
                        property bool conceal: false
                        property bool rendered: true
                        height: 10 * zoom
                        width: 8 * zoom
                        Rectangle {
                            height: rowheight * 10 * zoom
                            width: (dw?2:1) * 8 * zoom
                            clip: true
                            visible: rowrendered && rendered
                            color: ttpalette[bg]
                            Text {
                                renderType: Text.NativeRendering
                                anchors.top: parent.top
                                anchors.horizontalCenter: parent.horizontalCenter
                                color: ttpalette[fg]
                                text: c
                                font: ttfonts[(mosaic && solid && text[0] > "\ue000")?1:0][dw?1:0][dh?1:0]
                                visible: ((!flash) || flashsrc) && (conceal ? reveal : true)
                            }
                        }
                    }
                }
            }
        }
        layer.enabled: crteffect && (zoom > 1)
        layer.effect: ShaderEffect {
            fragmentShader: "
                    uniform lowp sampler2D source;
                    uniform lowp float qt_Opacity;
                    varying highp vec2 qt_TexCoord0;
                    varying lowp vec3 qt_FragCoord0;
                    void main() {
                        lowp vec4 tex = texture2D(source, qt_TexCoord0);
                        int zoom = " + zoom + ";
                        int row = int(gl_FragCoord.y) % zoom;
                        gl_FragColor = (0 < row && (row < 2 || row < (zoom-1))) ? tex : tex*0.6;
                    }
                "
        }
    }
    layer.enabled: crteffect && (zoom > 1)
    layer.effect: GaussianBlur {
        radius: 0.75 * zoom
    }
    SequentialAnimation on flashsrc {
        loops: -1
        running: true
        PropertyAction { value: false }
        PauseAnimation { duration: 333 }
        PropertyAction { value: true }
        PauseAnimation { duration: 1000 }
   
Download .txt
gitextract_t4lypv78/

├── .coveragerc
├── .gitignore
├── HOW_IT_WORKS.md
├── LICENSE
├── README.md
├── TRAINING.md
├── examples/
│   ├── filter
│   ├── maze
│   ├── service
│   ├── template.t42
│   ├── terminal
│   ├── tti
│   └── video
├── misc/
│   ├── teletext-noscanlines.css
│   └── teletext.css
├── setup.py
├── teletext/
│   ├── __init__.py
│   ├── __main__.py
│   ├── celp.py
│   ├── charset.py
│   ├── cli/
│   │   ├── __init__.py
│   │   ├── celp.py
│   │   ├── clihelpers.py
│   │   ├── teletext.py
│   │   ├── training.py
│   │   └── vbi.py
│   ├── coding.py
│   ├── elements.py
│   ├── file.py
│   ├── finders.py
│   ├── gui/
│   │   ├── __init__.py
│   │   ├── classify.py
│   │   ├── decoder.py
│   │   ├── decoder.qml
│   │   ├── editor.py
│   │   ├── editor.ui
│   │   ├── qthelpers.py
│   │   ├── service.py
│   │   └── vbiplot.py
│   ├── image.py
│   ├── interactive.py
│   ├── mp.py
│   ├── packet.py
│   ├── parser.py
│   ├── pipeline.py
│   ├── printer.py
│   ├── service.py
│   ├── servicedir.py
│   ├── sigint.py
│   ├── spellcheck.py
│   ├── stats.py
│   ├── subpage.py
│   ├── ts.py
│   └── vbi/
│       ├── __init__.py
│       ├── clustering.py
│       ├── config.py
│       ├── line.py
│       ├── pattern.py
│       ├── patterncuda.py
│       ├── patternopencl.py
│       ├── training.py
│       └── viewer.py
└── tests/
    ├── __init__.py
    ├── test_cli.py
    ├── test_coding.py
    ├── test_elements.py
    ├── test_file.py
    ├── test_mp.py
    ├── test_packet.py
    ├── test_sigint.py
    ├── test_spellcheck.py
    ├── test_stats.py
    ├── test_subpage.py
    └── vbi/
        ├── __init__.py
        ├── test_line.py
        ├── test_patterncuda.py
        ├── test_patternopencl.py
        └── test_training.py
Download .txt
SYMBOL INDEX (667 symbols across 52 files)

FILE: teletext/celp.py
  class LtpCodebook (line 28) | class LtpCodebook:
    method __init__ (line 29) | def __init__(self, subframe_length):
    method insert (line 34) | def insert(self, subframe):
    method get (line 37) | def get(self, lag):
  class CELPStats (line 44) | class CELPStats:
    method __init__ (line 45) | def __init__(self, decoder):
    method __str__ (line 48) | def __str__(self):
  class CELPDecoder (line 57) | class CELPDecoder:
    method __init__ (line 129) | def __init__(self, lsf_lut='suddle', vec_gain_lut='audetel', sample_ra...
    method lsf_error (line 141) | def lsf_error(self):
    method vector_gain_error (line 145) | def vector_gain_error(self):
    method stats (line 148) | def stats(self):
    method vector_parity (line 151) | def vector_parity(self, data, parity):
    method decode_params (line 156) | def decode_params(self, raw_frame):
    method apply_lpc_filter (line 177) | def apply_lpc_filter(self, lsf, signal):
    method generate_audio (line 183) | def generate_audio(self, raw_frame):
    method decode_packet_stream (line 206) | def decode_packet_stream(self, packets, frame=None):
    method stream_pcm (line 217) | def stream_pcm(self, packets, frame, device):
    method play (line 232) | def play(self, packets, frame=None):
    method convert (line 247) | def convert(self, output, packets, frame=None):
    method plot (line 258) | def plot(cls, packets):

FILE: teletext/cli/celp.py
  function celp (line 7) | def celp():
  function play (line 18) | def play(progress, frame, output, lsf_lut, gain_lut, packets):
  function plot (line 30) | def plot(packets):

FILE: teletext/cli/clihelpers.py
  class BasedIntType (line 21) | class BasedIntType(click.ParamType):
    method convert (line 24) | def convert(self, value, param, ctx):
  function dcnparams (line 37) | def dcnparams(f):
  function filterparams (line 41) | def filterparams(enabled=True):
  function progressparams (line 52) | def progressparams(progress=True, mag_hist=False, row_hist=False, err_hi...
  function carduser (line 66) | def carduser(extended=False):
  function chunkreader (line 89) | def chunkreader(loop=False, dup_stdin=False):
  function packetreader (line 112) | def packetreader(filtered=True, progress=True, mag_hist=False, row_hist=...
  function paginated (line 185) | def paginated(always=False, filtered=True):
  function packetwriter (line 228) | def packetwriter(f):

FILE: teletext/cli/teletext.py
  function teletext (line 40) | def teletext(ctx, unicode, help_all):
  function filter (line 78) | def filter(packets, pages, subpages, paginate, n, keep_empty):
  function grep (line 106) | def grep(packets, pages, subpages, paginate, regex, v, i, n, keep_empty):
  function _list (line 139) | def _list(packets, count, subpages):
  function split (line 174) | def split(packets, pattern, pages, subpages):
  function diff (line 198) | def diff(a, b, mags, rows):
  function finders (line 211) | def finders(packets):
  function scan (line 225) | def scan(packets, lines, frames):
  function squash (line 282) | def squash(packets, min_duplicates, threshold, pages, subpages, ignore_e...
  function spellcheck (line 301) | def spellcheck(packets, language, both, threads):
  function service (line 332) | def service(packets, replace_headers, title):
  function servicedir (line 345) | def servicedir(directory, replace_headers, title):
  function interactive (line 356) | def interactive(packets, initial_page):
  function serial (line 367) | def serial(packets, port):
  function urls (line 397) | def urls(packets, editor, pages, subpages):
  function images (line 412) | def images(packets, outdir, font, pages, subpages):
  function html (line 459) | def html(packets, outdir, template, localcodepage):
  function record (line 476) | def record(output, device, config):
  function vbiview (line 512) | def vbiview(chunker, config, pause, tape_format, n_lines):
  function deconvolve (line 553) | def deconvolve(chunker, mags, rows, pages, subpages, paginate, config, m...

FILE: teletext/cli/training.py
  function training (line 15) | def training():
  function generate (line 22) | def generate(output):
  function split (line 35) | def split(chunker, outdir, config, threads, progress, rejects):
  function training_squash (line 61) | def training_squash(output, indir):
  function showbin (line 69) | def showbin(chunker):
  function build (line 90) | def build(input, output, mode, bits):
  function similarities (line 110) | def similarities(tape_format):
  function crifc (line 124) | def crifc(chunker, config, threads, progress, rejects):

FILE: teletext/cli/vbi.py
  function vbi (line 9) | def vbi():
  function histogram (line 21) | def histogram(output, diff, show, chunker, config, n_lines):
  function plot (line 62) | def plot(chunker, config):
  function classifygui (line 72) | def classifygui(input, sampledir, auto, config):
  function copy (line 82) | def copy(chunker, config, progress, output):
  function linesplit (line 96) | def linesplit(chunker, config, progress, output):
  function cluster (line 114) | def cluster(chunker, config, progress, output, prefix):
  function rendermap (line 129) | def rendermap(config, map, output):

FILE: teletext/coding.py
  function thue_morse (line 31) | def thue_morse(n, even=True):
  function hamming8_encode (line 83) | def hamming8_encode(a):
  function hamming8_decode (line 87) | def hamming8_decode(a):
  function hamming16_encode (line 91) | def hamming16_encode(a):
  function hamming16_decode (line 98) | def hamming16_decode(a):
  function hamming8_correctable_errors (line 105) | def hamming8_correctable_errors(a):
  function hamming8_uncorrectable_errors (line 109) | def hamming8_uncorrectable_errors(a):
  function hamming8_errors (line 113) | def hamming8_errors(a):
  function parity_encode (line 117) | def parity_encode(a):
  function parity_decode (line 121) | def parity_decode(a):
  function parity_errors (line 125) | def parity_errors(a):
  function bcd8_decode (line 129) | def bcd8_decode(a):
  function bcd8_encode (line 135) | def bcd8_encode(a):
  function byte_reverse (line 141) | def byte_reverse(a):
  function crc (line 145) | def crc(n, c):

FILE: teletext/elements.py
  class Element (line 9) | class Element(object):
    method __init__ (line 11) | def __init__(self, shape, array=None):
    method __getitem__ (line 22) | def __getitem__(self, item):
    method __setitem__ (line 25) | def __setitem__(self, item, value):
    method __repr__ (line 28) | def __repr__(self):
    method bytes (line 32) | def bytes(self):
    method sevenbit (line 36) | def sevenbit(self):
    method sevenbit (line 40) | def sevenbit(self, b):
    method errors (line 47) | def errors(self):
  class ElementParity (line 51) | class ElementParity(Element):
    method errors (line 54) | def errors(self):
  class ElementHamming (line 58) | class ElementHamming(Element):
    method errors (line 61) | def errors(self):
  class Mrag (line 65) | class Mrag(ElementHamming):
    method __init__ (line 67) | def __init__(self, array=None):
    method magazine (line 71) | def magazine(self):
    method row (line 76) | def row(self):
    method magazine (line 80) | def magazine(self, magazine):
    method row (line 86) | def row(self, row):
    method __str__ (line 92) | def __str__(self):
  class Displayable (line 96) | class Displayable(ElementParity):
    method place_string (line 98) | def place_string(self, string, x=0, y=None):
    method place_bitmap (line 107) | def place_bitmap(self, bitmap, x=1, y=0, colour=0x17, conceal=False):
    method to_ansi (line 126) | def to_ansi(self, colour=True):
    method _tti_escape (line 134) | def _tti_escape(self, array):
    method to_tti (line 137) | def to_tti(self):
    method bytes_no_parity (line 144) | def bytes_no_parity(self):
  class Page (line 148) | class Page(Element):
    method page (line 151) | def page(self):
    method page (line 155) | def page(self, page):
    method errors (line 161) | def errors(self):
  class Header (line 167) | class Header(Page):
    method __init__ (line 169) | def __init__(self, array):
    method subpage (line 173) | def subpage(self):
    method control (line 178) | def control(self):
    method displayable (line 183) | def displayable(self):
    method codepage (line 187) | def codepage(self):
    method newsflash (line 191) | def newsflash(self):
    method subtitle (line 195) | def subtitle(self):
    method supress_header (line 199) | def supress_header(self):
    method subpage (line 203) | def subpage(self, subpage):
    method control (line 213) | def control(self, control):
    method to_ansi (line 221) | def to_ansi(self, colour=True):
    method apply_finders (line 224) | def apply_finders(self):
    method errors (line 232) | def errors(self):
  class Triplet (line 239) | class Triplet(Element):
    method __init__ (line 241) | def __init__(self, array):
    method __str__ (line 244) | def __str__(self):
    method errors (line 248) | def errors(self):
  class PageLink (line 254) | class PageLink(Page):
    method __init__ (line 256) | def __init__(self, array, mrag):
    method subpage (line 261) | def subpage(self):
    method magazine (line 266) | def magazine(self):
    method subpage (line 272) | def subpage(self, subpage):
    method magazine (line 282) | def magazine(self, magazine):
    method __str__ (line 292) | def __str__(self):
    method errors (line 296) | def errors(self):
  class DesignationCode (line 302) | class DesignationCode(Element):
    method dc (line 305) | def dc(self):
    method dc (line 309) | def dc(self, dc):
    method errors (line 313) | def errors(self):
  class Triplets (line 319) | class Triplets(DesignationCode):
    method __init__ (line 321) | def __init__(self, array, mrag):
    method triplets (line 326) | def triplets(self):
    method to_ansi (line 329) | def to_ansi(self, colour=True):
    method errors (line 333) | def errors(self):
  class Fastext (line 340) | class Fastext(DesignationCode):
    method __init__ (line 342) | def __init__(self, array, mrag):
    method links (line 347) | def links(self):
    method control (line 351) | def control(self):
    method control (line 355) | def control(self, value):
    method checksum (line 359) | def checksum(self):
    method checksum (line 363) | def checksum(self, value):
    method to_ansi (line 367) | def to_ansi(self, colour=True):
    method errors (line 371) | def errors(self):
  class Format1 (line 378) | class Format1(Element):
    method __init__ (line 382) | def __init__(self, array):
    method network (line 386) | def network(self):
    method offset (line 390) | def offset(self):
    method mjd (line 397) | def mjd(self):
    method date (line 401) | def date(self):
    method hour (line 405) | def hour(self):
    method hour (line 409) | def hour(self, value):
    method minute (line 413) | def minute(self):
    method minute (line 417) | def minute(self, value):
    method second (line 421) | def second(self):
    method second (line 425) | def second(self, value):
    method to_ansi (line 428) | def to_ansi(self, colour=True):
    method errors (line 432) | def errors(self):
  class Format2 (line 437) | class Format2(Element):
    method __init__ (line 439) | def __init__(self, array):
    method day (line 443) | def day(self):
    method month (line 447) | def month(self):
    method hour (line 451) | def hour(self):
    method minute (line 455) | def minute(self):
    method country (line 459) | def country(self):
    method network (line 463) | def network(self):
    method to_ansi (line 466) | def to_ansi(self, colour=True):
  class BroadcastData (line 470) | class BroadcastData(DesignationCode):
    method __init__ (line 472) | def __init__(self, array, mrag):
    method displayable (line 477) | def displayable(self):
    method initial_page (line 481) | def initial_page(self):
    method format1 (line 485) | def format1(self):
    method format2 (line 489) | def format2(self):
    method to_ansi (line 492) | def to_ansi(self, colour=True):
    method errors (line 501) | def errors(self):
  class Celp (line 508) | class Celp(Element):
    method __init__ (line 525) | def __init__(self, array, mrag):
    method dcn (line 530) | def dcn(self):
    method service (line 534) | def service(self):
    method service (line 538) | def service(self, service):
    method control (line 542) | def control(self):
    method control (line 546) | def control(self, service):
    method to_ansi (line 549) | def to_ansi(self, colour=True):
    method errors (line 585) | def errors(self):

FILE: teletext/file.py
  function PossiblyInfiniteRange (line 7) | def PossiblyInfiniteRange(start=0, stop=None, step=1, limit=None):
  class LenWrapper (line 20) | class LenWrapper(object):
    method __init__ (line 21) | def __init__(self, i, l):
    method __iter__ (line 25) | def __iter__(self):
    method __len__ (line 28) | def __len__(self):
  function _chunks (line 32) | def _chunks(f, size, flines, frange, seek):
  function chunks (line 49) | def chunks(f, size, start, step, flines=16, frange=(0, 16), seek=True):
  function FileChunker (line 65) | def FileChunker(f, size, start=0, stop=None, step=1, limit=None, flines=...

FILE: teletext/finders.py
  class Finder (line 6) | class Finder(object):
    method __init__ (line 39) | def __init__(self, match1, match2, name, years, channels):
    method match (line 48) | def match(self, arr):
    method fixup (line 53) | def fixup(self, arr):

FILE: teletext/gui/classify.py
  class Window (line 12) | class Window(QtWidgets.QMainWindow):
    method __init__ (line 13) | def __init__(self, dir, sampledir, auto, config, app, parent=None):
    method add_button (line 91) | def add_button(self, label, pos):
    method enable_buttons (line 101) | def enable_buttons(self, en):
    method button_pressed (line 105) | def button_pressed(self, label):
    method load (line 119) | def load(self):
    method loaded (line 138) | def loaded(self):
    method plot (line 149) | def plot(self, result):
  class VBILoader (line 171) | class VBILoader(QtCore.QThread):
    method __init__ (line 175) | def __init__(self, file, config):
    method run (line 180) | def run(self):
  function classify_gui (line 195) | def classify_gui(dir, sampledir, auto, config):

FILE: teletext/gui/decoder.py
  class Palette (line 13) | class Palette(object):
    method __init__ (line 15) | def __init__(self, context):
    method __getitem__ (line 29) | def __getitem__(self, item):
    method __setitem__ (line 32) | def __setitem__(self, item, value):
  class ParserQML (line 39) | class ParserQML(Parser):
    method __init__ (line 41) | def __init__(self, tt, row, cells, nextrow):
    method emitcharacter (line 47) | def emitcharacter(self, c):
    method parse (line 54) | def parse(self):
  class Decoder (line 63) | class Decoder(object):
    method __init__ (line 65) | def __init__(self, widget):
    method __setitem__ (line 93) | def __setitem__(self, item, value):
    method __getitem__ (line 103) | def __getitem__(self, item):
    method randomize (line 106) | def randomize(self):
    method make_font (line 109) | def make_font(self, stretch):
    method palette (line 117) | def palette(self):
    method zoom (line 121) | def zoom(self):
    method zoom (line 125) | def zoom(self, zoom):
    method reveal (line 140) | def reveal(self):
    method reveal (line 144) | def reveal(self, reveal):
    method crteffect (line 148) | def crteffect(self):
    method crteffect (line 152) | def crteffect(self, crteffect):
    method size (line 155) | def size(self):
    method setEffect (line 159) | def setEffect(self, e):

FILE: teletext/gui/editor.py
  class EditorWindow (line 16) | class EditorWindow(QtWidgets.QMainWindow):
    method __init__ (line 17) | def __init__(self):
    method showsubpage (line 56) | def showsubpage(self, index):
    method importt42 (line 63) | def importt42(self, filename=None):
    method importt42done (line 80) | def importt42done(self):
  function main (line 92) | def main():

FILE: teletext/gui/qthelpers.py
  function build_menu (line 4) | def build_menu(window, parent_menu, menu_defs):

FILE: teletext/gui/service.py
  class StdSubpage (line 9) | class StdSubpage(QtGui.QStandardItem):
    method __init__ (line 10) | def __init__(self, subpage, number):
  class StdPage (line 19) | class StdPage(QtGui.QStandardItem):
    method __init__ (line 20) | def __init__(self, page, number):
  class StdMagazine (line 28) | class StdMagazine(QtGui.QStandardItem):
    method __init__ (line 29) | def __init__(self, magazine, number):
  class ServiceModel (line 38) | class ServiceModel(QtGui.QStandardItemModel):
    method __init__ (line 39) | def __init__(self, service = None):
  class ServiceModelLoader (line 46) | class ServiceModelLoader(QtCore.QThread):
    method __init__ (line 50) | def __init__(self, filename):
    method progress (line 54) | def progress(self, chunks):
    method run (line 60) | def run(self):

FILE: teletext/gui/vbiplot.py
  class Window (line 20) | class Window(QMainWindow):
    method __init__ (line 21) | def __init__(self, chunker, config, parent=None):
    method plot (line 47) | def plot(self):
  function vbiplot (line 106) | def vbiplot(chunker, config):

FILE: teletext/image.py
  class PcfFontFileUnicode (line 10) | class PcfFontFileUnicode(PcfFontFile):
    method unicode_glyphs (line 11) | def unicode_glyphs(self):
  function load_glyphs (line 43) | def load_glyphs(fp):
  class PrinterImage (line 48) | class PrinterImage(Parser):
    method __init__ (line 50) | def __init__(self, tt, glyphs, box=False, flash_off=False, colour=True...
    method emitcharacter (line 61) | def emitcharacter(self, c):
    method parse (line 79) | def parse(self):
  function subpage_to_image (line 84) | def subpage_to_image(s, glyphs, background=None, flash_off=False):

FILE: teletext/interactive.py
  function setstyle (line 12) | def setstyle(self, fg=None, bg=None):
  class TerminalTooSmall (line 19) | class TerminalTooSmall(Exception):
  class ParserNcurses (line 23) | class ParserNcurses(Parser):
    method __init__ (line 24) | def __init__(self, tt, scr, row):
    method emitcharacter (line 29) | def emitcharacter(self, c):
    method parse (line 36) | def parse(self):
  class Interactive (line 41) | class Interactive(object):
    method __init__ (line 45) | def __init__(self, packet_iter, scr, initial_page=0x100):
    method set_concealed_pairs (line 79) | def set_concealed_pairs(self, show=False):
    method set_input_field (line 89) | def set_input_field(self, str, clr=0):
    method go_page (line 92) | def go_page(self, magazine, page):
    method addstr (line 98) | def addstr(self, packet):
    method do_alnum (line 105) | def do_alnum(self, i):
    method do_hold (line 127) | def do_hold(self):
    method do_reveal (line 139) | def do_reveal(self):
    method do_input (line 143) | def do_input(self, c):
    method handle_one_packet (line 167) | def handle_one_packet(self):
    method main (line 187) | def main(self):
  function main (line 199) | def main(packets, initial_page):

FILE: teletext/mp.py
  function denumerate (line 11) | def denumerate(work, control, tmp_queue):
  function renumerate (line 28) | def renumerate(iterator, result, tmp_queue):
  function worker (line 43) | def worker(work_port, result_port, control_port, status_port, function, ...
  class _PureGeneratorPoolMP (line 71) | class _PureGeneratorPoolMP(object):
    method __init__ (line 73) | def __init__(self, function, processes=1, *args, **kwargs):
    method __enter__ (line 89) | def __enter__(self):
    method apply (line 131) | def apply(self, iterable):
    method __exit__ (line 181) | def __exit__(self, *args):
  class _PureGeneratorPoolSingle (line 188) | class _PureGeneratorPoolSingle(object):
    method __init__ (line 194) | def __init__(self, function, *args, **kwargs):
    method _work (line 202) | def _work(self):
    method __enter__ (line 209) | def __enter__(self):
    method apply (line 212) | def apply(self, iterable):
    method __exit__ (line 217) | def __exit__(self, *args):
  function PureGeneratorPool (line 224) | def PureGeneratorPool(function, processes, *args, **kwargs):
  function itermap (line 262) | def itermap(function, iterable, processes=1, *args, **kwargs):
  function f (line 272) | def f(iterator, *args, **kwargs):
  function main (line 290) | def main(jobs, threads, verbose):

FILE: teletext/packet.py
  class Packet (line 4) | class Packet(Element):
    method __init__ (line 6) | def __init__(self, array=None, number=None, original=None):
    method __getitem__ (line 11) | def __getitem__(self, item):
    method __setitem__ (line 14) | def __setitem__(self, item, value):
    method is_padding (line 17) | def is_padding(self):
    method number (line 21) | def number(self):
    method type (line 25) | def type(self):
    method mrag (line 47) | def mrag(self):
    method dc (line 51) | def dc(self):
    method header (line 55) | def header(self):
    method displayable (line 59) | def displayable(self):
    method fastext (line 63) | def fastext(self):
    method triplets (line 67) | def triplets(self):
    method broadcast (line 71) | def broadcast(self):
    method celp (line 75) | def celp(self):
    method to_ansi (line 78) | def to_ansi(self, colour=True):
    method to_bytes_no_parity (line 98) | def to_bytes_no_parity(self):
    method to_binary (line 110) | def to_binary(self):
    method to_bytes (line 115) | def to_bytes(self):
    method ansi (line 119) | def ansi(self):
    method text (line 123) | def text(self):
    method bar (line 127) | def bar(self):
    method hex (line 131) | def hex(self):
    method debug (line 135) | def debug(self):
    method vbi (line 142) | def vbi(self):
    method errors (line 148) | def errors(self):

FILE: teletext/parser.py
  class Parser (line 6) | class Parser(object):
    method __init__ (line 10) | def __init__(self, tt, localcodepage=None, codepage=0):
    method reset (line 17) | def reset(self):
    method setstate (line 35) | def setstate(self, **kwargs):
    method ttchar (line 47) | def ttchar(self, c):
    method _emitcharacter (line 62) | def _emitcharacter(self, c):
    method emitcode (line 69) | def emitcode(self):
    method setat (line 78) | def setat(self, **kwargs):
    method setafter (line 82) | def setafter(self, **kwargs):
    method parsebyte (line 86) | def parsebyte(self, b, prev):
    method parse (line 142) | def parse(self):

FILE: teletext/pipeline.py
  function check_buffer (line 13) | def check_buffer(mb, pages, subpages, min_rows=0):
  function packet_squash (line 21) | def packet_squash(packets):
  function bsdp_squash_format1 (line 25) | def bsdp_squash_format1(packets):
  function bsdp_squash_format2 (line 33) | def bsdp_squash_format2(packets):
  function paginate (line 40) | def paginate(packets, pages=range(0x900), subpages=range(0x3f80), drop_e...
  function subpage_group (line 55) | def subpage_group(packet_lists, threshold, ignore_empty):
  function subpage_squash (line 81) | def subpage_squash(packet_lists, threshold=-1, min_duplicates=3, ignore_...
  function to_file (line 115) | def to_file(packets, f, format):

FILE: teletext/printer.py
  class PrinterANSI (line 6) | class PrinterANSI(Parser):
    method __init__ (line 8) | def __init__(self, tt, colour=True, codepage=0):
    method fgChanged (line 12) | def fgChanged(self):
    method bgChanged (line 16) | def bgChanged(self):
    method emitcharacter (line 20) | def emitcharacter(self, c):
    method parse (line 23) | def parse(self):
    method __str__ (line 31) | def __str__(self):
  class PrinterHTML (line 35) | class PrinterHTML(Parser):
    method __init__ (line 37) | def __init__(self, tt, fastext=None, pages_set=range(0x100), localcode...
    method ttchar (line 47) | def ttchar(self, c):
    method stateChanged (line 58) | def stateChanged(self):
    method emitcharacter (line 80) | def emitcharacter(self, c):
    method linkify (line 83) | def linkify(self, html):
    method parse (line 93) | def parse(self):
    method __str__ (line 103) | def __str__(self):

FILE: teletext/service.py
  class Page (line 15) | class Page(object):
    method __init__ (line 16) | def __init__(self):
    method _gen (line 20) | def _gen(self):
    method __iter__ (line 26) | def __iter__(self):
    method __next__ (line 29) | def __next__(self):
  class Magazine (line 33) | class Magazine(object):
    method __init__ (line 34) | def __init__(self, title=None):
    method _gen (line 39) | def _gen(self):
    method __iter__ (line 54) | def __iter__(self):
    method __next__ (line 57) | def __next__(self):
  class Service (line 61) | class Service(object):
    method __init__ (line 62) | def __init__(self, replace_headers=False, title=None):
    method header (line 68) | def header(self, title, mag, page):
    method insert_page (line 72) | def insert_page(self, page):
    method _gen (line 75) | def _gen(self):
    method __iter__ (line 88) | def __iter__(self):
    method __next__ (line 91) | def __next__(self):
    method packets (line 94) | def packets(self, n):
    method all_subpages (line 99) | def all_subpages(self):
    method pages_set (line 106) | def pages_set(self):
    method from_packets (line 110) | def from_packets(cls, packets, replace_headers=False, title=None):
    method from_file (line 124) | def from_file(cls, f):
    method to_html (line 129) | def to_html(self, outdir, template=None, localcodepage=None):

FILE: teletext/servicedir.py
  class ServiceDir (line 10) | class ServiceDir(Service):
    method __init__ (line 20) | def __init__(self, directory, replace_headers, title):
    method file_changed (line 24) | def file_changed(self, f, deleted=False):
    method __enter__ (line 37) | def __enter__(self):
    method __exit__ (line 50) | def __exit__(self, *args, **kwargs):
    method dispatch (line 54) | def dispatch(self, evt):

FILE: teletext/sigint.py
  class SigIntDefer (line 3) | class SigIntDefer(object):
    method __init__ (line 28) | def __init__(self, times=1):
    method __enter__ (line 32) | def __enter__(self):
    method handler (line 37) | def handler(self, f, n):
    method fired (line 44) | def fired(self):
    method __exit__ (line 47) | def __exit__(self, *args, **kwargs):

FILE: teletext/spellcheck.py
  class SpellChecker (line 8) | class SpellChecker(object):
    method __init__ (line 17) | def __init__(self, language='en_GB'):
    method check_pair (line 20) | def check_pair(self, x, y):
    method weighted_hamming (line 25) | def weighted_hamming(self, a, b):
    method case_match (line 28) | def case_match(self, word, src):
    method suggest (line 31) | def suggest(self, word):
    method spellcheck (line 40) | def spellcheck(self, displayable):
  function spellcheck_packets (line 51) | def spellcheck_packets(packets, language='en_GB'):

FILE: teletext/stats.py
  class Histogram (line 4) | class Histogram(object):
    method __init__ (line 10) | def __init__(self, shape=(1000, ), fill=255, dtype=np.uint8):
    method insert (line 14) | def insert(self, value):
    method histogram (line 20) | def histogram(self):
    method render (line 25) | def render(self):
    method __str__ (line 34) | def __str__(self):
  class MagHistogram (line 38) | class MagHistogram(Histogram):
    method __init__ (line 43) | def __init__(self, packets, size=1000):
    method __iter__ (line 47) | def __iter__(self):
  class RowHistogram (line 53) | class RowHistogram(MagHistogram):
    method __iter__ (line 58) | def __iter__(self):
  class Rejects (line 64) | class Rejects(Histogram):
    method __init__ (line 69) | def __init__(self, lines, size=1000):
    method __iter__ (line 73) | def __iter__(self):
    method __str__ (line 78) | def __str__(self):
  class ErrorHistogram (line 84) | class ErrorHistogram(Histogram):
    method __init__ (line 88) | def __init__(self, packets, size=100):
    method __iter__ (line 92) | def __iter__(self):
    method __str__ (line 97) | def __str__(self):
  class StatsList (line 104) | class StatsList(list):
    method __str__ (line 105) | def __str__(self):

FILE: teletext/subpage.py
  class Subpage (line 12) | class Subpage(Element):
    method __init__ (line 14) | def __init__(self, array=None, numbers=None, prefill=False, magazine=1):
    method diff (line 31) | def diff(self, other):
    method numbers (line 38) | def numbers(self):
    method _slot (line 41) | def _slot(self, row, dc):
    method has_packet (line 47) | def has_packet(self, row, dc=0):
    method init_packet (line 50) | def init_packet(self, row, dc=0, magazine=1):
    method packet (line 55) | def packet(self, row, dc=0):
    method mrag (line 63) | def mrag(self):
    method header (line 67) | def header(self):
    method codepage (line 71) | def codepage(self):
    method fastext (line 75) | def fastext(self):
    method displayable (line 79) | def displayable(self):
    method checksum (line 83) | def checksum(self):
    method addr (line 104) | def addr(self):
    method from_packets (line 108) | def from_packets(cls, packets, ignore_empty=False):
    method from_url (line 127) | def from_url(cls, url):
    method from_file (line 152) | def from_file(cls, f):
    method packets (line 158) | def packets(self):
    method mrg_PN (line 164) | def mrg_PN(self):
    method mrg_PS (line 167) | def mrg_PS(self, transmit=False):
    method mrg_SC (line 175) | def mrg_SC(self):
    method url (line 181) | def url(self):
    method to_tti (line 205) | def to_tti(self, cycle_time=None, transmit=True):
    method to_html (line 219) | def to_html(self, pages_set, localcodepage=None):

FILE: teletext/ts.py
  function parse_data (line 9) | def parse_data(data):
  function parse_pes (line 17) | def parse_pes(pes):
  function pidextract (line 25) | def pidextract(packets, pid):

FILE: teletext/vbi/clustering.py
  function cluster (line 8) | def cluster(a, l, clusters=None, steps=None):
  function batched (line 22) | def batched(iterable, n):
  function batch_cluster (line 33) | def batch_cluster(chunks, output, prefix="", lpf=32):
  function rendermap (line 50) | def rendermap(config, map, output):

FILE: teletext/vbi/config.py
  class Config (line 6) | class Config(object):
    method __init__ (line 103) | def __init__(self, card='bt8x8', **kwargs):
    method __repr__ (line 143) | def __repr__(self):
    method line_bytes (line 147) | def line_bytes(self):

FILE: teletext/vbi/line.py
  function normalise (line 25) | def normalise(a, start=None, end=None):
  class Line (line 36) | class Line(object):
    method configure_patterns (line 44) | def configure_patterns(cls, method, tape_format):
    method configure (line 59) | def configure(cls, config, force_cpu=False, prefer_opencl=False, tape_...
    method __init__ (line 72) | def __init__(self, data, number=None):
    method reset (line 87) | def reset(self):
    method resampled (line 100) | def resampled(self):
    method original (line 105) | def original(self):
    method rolled (line 110) | def rolled(self):
    method gradient (line 117) | def gradient(self):
    method fchop (line 120) | def fchop(self, start, stop):
    method chop (line 128) | def chop(self, start, stop):
    method chopped (line 133) | def chopped(self):
    method noisefloor (line 138) | def noisefloor(self):
    method fft (line 147) | def fft(self):
    method find_start (line 155) | def find_start(self):
    method is_teletext (line 210) | def is_teletext(self):
    method start (line 217) | def start(self):
    method deconvolve (line 224) | def deconvolve(self, mags=range(9), rows=range(32), eight_bit=False):
    method slice (line 271) | def slice(self, mags=range(9), rows=range(32), eight_bit=False):
  function process_lines (line 293) | def process_lines(chunks, mode, config, force_cpu=False, prefer_opencl=F...

FILE: teletext/vbi/pattern.py
  class Pattern (line 20) | class Pattern(object):
    method __init__ (line 22) | def __init__(self, filename):
    method match (line 32) | def match(self, inp):
    method similarities (line 43) | def similarities(self):
  class PatternBuilder (line 94) | class PatternBuilder(object):
    method __init__ (line 96) | def __init__(self, inwidth):
    method write_patterns (line 100) | def write_patterns(self, f, start, end):
    method add_pattern (line 116) | def add_pattern(self, key, pattern):
  function build_pattern (line 120) | def build_pattern(chunks, output, start, end, pattern_set=range(256)):

FILE: teletext/vbi/patterncuda.py
  class PatternCUDA (line 27) | class PatternCUDA(Pattern):
    method __init__ (line 114) | def __init__(self, filename):
    method match (line 129) | def match(self, inp):

FILE: teletext/vbi/patternopencl.py
  class PatternOpenCL (line 20) | class PatternOpenCL(Pattern):
    method __init__ (line 145) | def __init__(self, filename):
    method match (line 189) | def match(self, inp):

FILE: teletext/vbi/training.py
  class PatternGenerator (line 27) | class PatternGenerator(object):
    method __init__ (line 33) | def __init__(self):
    method load_pattern (line 38) | def load_pattern(cls):
    method checksum (line 43) | def checksum(self, array):
    method generate_line (line 46) | def generate_line(self, offset):
    method to_file (line 71) | def to_file(self, file):
  function de_bruijn (line 84) | def de_bruijn(k, n):
  function save_pattern (line 105) | def save_pattern(filename):
  class TrainingLine (line 111) | class TrainingLine(Line):
    method tchop (line 115) | def tchop(self, start, stop):
    method lock (line 120) | def lock(self, offset):
    method checksum (line 137) | def checksum(self):
    method offset (line 141) | def offset(self):
  function process_training (line 155) | def process_training(chunks, config):
  function process_crifc (line 168) | def process_crifc(chunks, config):
  function split (line 187) | def split(data, files):
  function squash (line 206) | def squash(output, indir):

FILE: teletext/vbi/viewer.py
  class VBIViewer (line 10) | class VBIViewer(object):
    method __init__ (line 12) | def __init__(self, lines, config, name = "VBI Viewer", width=800, heig...
    method reshape (line 60) | def reshape(self, width, height):
    method keyboard (line 65) | def keyboard(self, key, x, y):
    method mouse (line 94) | def mouse(self, button, state, x, y):
    method dumpline (line 113) | def dumpline(self, x, y, teletext):
    method dumpall (line 124) | def dumpall(self, teletext):
    method set_title (line 135) | def set_title(self):
    method draw_slice (line 138) | def draw_slice(self, slice, r, g, b, a=1.0):
    method draw_h_grid (line 147) | def draw_h_grid(self, r, g, b, a=1.0):
    method draw_bits (line 155) | def draw_bits(self, r, g, b, a=1.0):
    method draw_freq_bins (line 163) | def draw_freq_bins(self, n, r, g, b, a=1.0):
    method draw_lines (line 171) | def draw_lines(self):
    method display (line 203) | def display(self):

FILE: tests/test_cli.py
  class TestCommandTeletext (line 9) | class TestCommandTeletext(unittest.TestCase):
    method setUp (line 12) | def setUp(self):
    method test_help (line 15) | def test_help(self):
  class TestCmdFilter (line 20) | class TestCmdFilter(TestCommandTeletext):
  class TestCmdDiff (line 24) | class TestCmdDiff(TestCommandTeletext):
  class TestCmdFinders (line 28) | class TestCmdFinders(TestCommandTeletext):
  class TestCmdSquash (line 32) | class TestCmdSquash(TestCommandTeletext):
  class TestCmdSpellcheck (line 36) | class TestCmdSpellcheck(TestCommandTeletext):
  class TestCmdService (line 40) | class TestCmdService(TestCommandTeletext):
  class TestCmdInteractive (line 44) | class TestCmdInteractive(TestCommandTeletext):
  class TestCmdUrls (line 48) | class TestCmdUrls(TestCommandTeletext):
  class TestCmdHtml (line 52) | class TestCmdHtml(TestCommandTeletext):
  class TestCmdRecord (line 56) | class TestCmdRecord(TestCommandTeletext):
  class TestCmdVBIView (line 60) | class TestCmdVBIView(TestCommandTeletext):
  class TestCmdDeconvolve (line 64) | class TestCmdDeconvolve(TestCommandTeletext):
  class TestCmdTraining (line 68) | class TestCmdTraining(TestCommandTeletext):
  class TestCmdGenerate (line 72) | class TestCmdGenerate(TestCommandTeletext):
  class TestCmdTrainingSquash (line 76) | class TestCmdTrainingSquash(TestCommandTeletext):
  class TestCmdShowBin (line 80) | class TestCmdShowBin(TestCommandTeletext):
  class TestCmdBuild (line 84) | class TestCmdBuild(TestCommandTeletext):

FILE: tests/test_coding.py
  class ParityEncodeTestCase (line 9) | class ParityEncodeTestCase(unittest.TestCase):
    method _test_array (line 11) | def _test_array(self, array: np.ndarray):
    method test_full_array (line 32) | def test_full_array(self):
    method test_unit_arrays (line 35) | def test_unit_arrays(self):
    method test_array_type (line 39) | def test_array_type(self):
    method test_list_type (line 44) | def test_list_type(self):
    method test_unit_array_type (line 49) | def test_unit_array_type(self):
    method test_unit_list_type (line 54) | def test_unit_list_type(self):
    method test_int_type (line 59) | def test_int_type(self):
    method test_full_list (line 63) | def test_full_list(self):
    method test_unit_list (line 68) | def test_unit_list(self):
    method test_ints (line 73) | def test_ints(self):
  class Hamming8TestCase (line 89) | class Hamming8TestCase(unittest.TestCase):
    method test_all (line 91) | def test_all(self):
  class Reverse8TestCase (line 128) | class Reverse8TestCase(unittest.TestCase):
    method test_all (line 130) | def test_all(self):
  class CRCTestCase (line 139) | class CRCTestCase(unittest.TestCase):
    method test_all (line 141) | def test_all(self):

FILE: tests/test_elements.py
  class TestElement (line 8) | class TestElement(unittest.TestCase):
    method make_element (line 15) | def make_element(self, array):
    method setUp (line 27) | def setUp(self):
    method test_type (line 31) | def test_type(self):
    method test_wrong_shape (line 34) | def test_wrong_shape(self):
    method test_getitem (line 39) | def test_getitem(self):
    method test_setitem (line 45) | def test_setitem(self):
    method test_repr (line 51) | def test_repr(self):
    method test_errors (line 54) | def test_errors(self):
    method test_bytes (line 58) | def test_bytes(self):
  class TestElementParity (line 62) | class TestElementParity(TestElement):
    method test_errors (line 67) | def test_errors(self):
  class TestElementHamming (line 73) | class TestElementHamming(TestElementParity):
    method test_errors (line 78) | def test_errors(self):
  class TestMrag (line 84) | class TestMrag(TestElementHamming):
    method test_magazine (line 90) | def test_magazine(self):
    method test_row (line 97) | def test_row(self):
  class TestDisplayable (line 105) | class TestDisplayable(TestElementParity):
    method test_place_string (line 110) | def test_place_string(self):
  class TestPage (line 115) | class TestPage(TestElementHamming):
  class TestHeader (line 121) | class TestHeader(TestPage):
  class TestPageLink (line 128) | class TestPageLink(TestPage):
  class TestDesignationCode (line 136) | class TestDesignationCode(TestElementHamming):
    method test_set_dc (line 141) | def test_set_dc(self):
  class TestFastext (line 147) | class TestFastext(TestDesignationCode):
    method test_errors (line 155) | def test_errors(self):

FILE: tests/test_file.py
  class TestChunker (line 8) | class TestChunker(unittest.TestCase):
    method setUp (line 9) | def setUp(self):
    method test_basic (line 13) | def test_basic(self):
    method test_step (line 19) | def test_step(self):

FILE: tests/test_mp.py
  function multiply (line 14) | def multiply(it, a):
  function null (line 19) | def null(it, a):
  function callcount (line 25) | def callcount(it):
  function crashy (line 32) | def crashy(it):
  function early_crash (line 40) | def early_crash(it):
  function not_generator (line 44) | def not_generator(it):
  class TestMPSingle (line 48) | class TestMPSingle(unittest.TestCase):
    method setUp (line 52) | def setUp(self):
    method test_single (line 56) | def test_single(self):
    method test_called_once_single (line 62) | def test_called_once_single(self):
    method test_reuse (line 66) | def test_reuse(self):
    method test_called_once_reuse (line 76) | def test_called_once_reuse(self):
    method _crashing_iter (line 82) | def _crashing_iter(self, n):
    method test_crashing_iter (line 86) | def test_crashing_iter(self):
    method test_early_crash (line 91) | def test_early_crash(self):
    method test_not_generator (line 95) | def test_not_generator(self):
    method test_too_many_args (line 99) | def test_too_many_args(self):
  class TestMPMulti (line 104) | class TestMPMulti(TestMPSingle):
    method test_unpickleable_function (line 108) | def test_unpickleable_function(self):
    method test_unpickleable_item_in_args (line 112) | def test_unpickleable_item_in_args(self):
    method test_unpickleable_item_in_iter (line 116) | def test_unpickleable_item_in_iter(self):
    method test_empty_iter (line 120) | def test_empty_iter(self):
  class TestMPMultiSigInt (line 125) | class TestMPMultiSigInt(unittest.TestCase):
    method items (line 129) | def items(self):
    method test_sigint_to_self (line 134) | def test_sigint_to_self(self):
    method test_sigint_to_child (line 141) | def test_sigint_to_child(self):

FILE: tests/test_packet.py
  class TestPacket (line 7) | class TestPacket(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method test_type (line 14) | def test_type(self):

FILE: tests/test_sigint.py
  function ctrl_c (line 10) | def ctrl_c(pid):
  class TestSigInt (line 19) | class TestSigInt(unittest.TestCase):
    method test_ctrl_c (line 21) | def test_ctrl_c(self):
    method test_interrupt (line 25) | def test_interrupt(self):

FILE: tests/test_spellcheck.py
  class TestSpellCheck (line 7) | class TestSpellCheck(unittest.TestCase):
    method setUp (line 9) | def setUp(self):
    method test_case_match (line 12) | def test_case_match(self):
    method test_suggest (line 17) | def test_suggest(self):
    method test_spellcheck (line 23) | def test_spellcheck(self):

FILE: tests/test_stats.py
  class TestStatsList (line 6) | class TestStatsList(unittest.TestCase):
    method test_str (line 8) | def test_str(self):

FILE: tests/test_subpage.py
  class SubpageTestCase (line 8) | class SubpageTestCase(unittest.TestCase):
    method test_checksum (line 10) | def test_checksum(self):

FILE: tests/vbi/test_line.py
  class LineTestCase (line 11) | class LineTestCase(unittest.TestCase):
    method noisegen (line 13) | def noisegen(self, max_loc, max_scale):
    method setUp (line 22) | def setUp(self):
    method test_empty_rejection (line 25) | def test_empty_rejection(self):
    method test_known_teletext (line 32) | def test_known_teletext(self):
    method test_known_reject (line 42) | def test_known_reject(self):

FILE: tests/vbi/test_patterncuda.py
  class PatternCUDATestCase (line 11) | class PatternCUDATestCase(unittest.TestCase):
    method setUp (line 13) | def setUp(self):
    method test_equal_to_cpu (line 18) | def test_equal_to_cpu(self):

FILE: tests/vbi/test_patternopencl.py
  class PatternOpenCLTestCase (line 11) | class PatternOpenCLTestCase(unittest.TestCase):
    method setUp (line 13) | def setUp(self):
    method test_equal_to_cpu (line 18) | def test_equal_to_cpu(self):

FILE: tests/vbi/test_training.py
  class TrainingTestCase (line 10) | class TrainingTestCase(unittest.TestCase):
    method setUp (line 12) | def setUp(self):
    method test_split (line 16) | def test_split(self):
Condensed preview — 78 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (365K chars).
[
  {
    "path": ".coveragerc",
    "chars": 24,
    "preview": "[run]\nsource = teletext\n"
  },
  {
    "path": ".gitignore",
    "chars": 101,
    "preview": "/.idea\n/.coverage\n/venv\n/*.vbi\n*.pyc\n/build\n/dist\n/MANIFEST\n/*.egg-info\n/vbi\n/teletext/vbi/data/*-*\n\n"
  },
  {
    "path": "HOW_IT_WORKS.md",
    "chars": 3762,
    "preview": "HOW IT WORKS\n------------\n\nTeletext is encoded as a non-return-to-zero signal with two levels representing\none and zero."
  },
  {
    "path": "LICENSE",
    "chars": 35149,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
  },
  {
    "path": "README.md",
    "chars": 2415,
    "preview": "This is a suite of tools for processing teletext signals recorded on VHS, as\nwell as tools for processing teletext packe"
  },
  {
    "path": "TRAINING.md",
    "chars": 3039,
    "preview": "Training\n--------\n\n1. Record the training signal to a tape:\n\n```\nteletext training | raspi-teletext -\n```\n\n2. Record it "
  },
  {
    "path": "examples/filter",
    "chars": 1221,
    "preview": "#!/usr/bin/env python3\n\n# An example of making a customized filter in Python.\n\n# This is almost a direct copy-paste of t"
  },
  {
    "path": "examples/maze",
    "chars": 7493,
    "preview": "#!/usr/bin/env python\n\nimport random\n\nimport click\nimport numpy as np\nfrom PIL import Image, ImageDraw\n\nfrom teletext.cl"
  },
  {
    "path": "examples/service",
    "chars": 836,
    "preview": "#!/usr/bin/env python3\n\n# An example of generating a service from scratch.\n\nimport sys\n\nfrom teletext.service import Ser"
  },
  {
    "path": "examples/terminal",
    "chars": 2312,
    "preview": "#!/usr/bin/env python3\n\nimport datetime\nimport os\nimport pty\nimport select\nimport shlex\nimport time\n\nimport click\nimport"
  },
  {
    "path": "examples/tti",
    "chars": 1862,
    "preview": "#!/usr/bin/env python3\n\nimport json\nimport re\nimport requests\nfrom PIL import Image\nimport numpy as np\n\nfrom teletext.su"
  },
  {
    "path": "examples/video",
    "chars": 3112,
    "preview": "#!/usr/bin/env python3\n\nimport datetime\nimport socket\n\nimport click\nimport cv2\nimport srt\n\nfrom teletext.cli.clihelpers "
  },
  {
    "path": "misc/teletext-noscanlines.css",
    "chars": 1226,
    "preview": "body {background: black;}\n\na {color: inherit; text-decoration: none;}\na:hover {color: orange; } a:active {color: red;}\n:"
  },
  {
    "path": "misc/teletext.css",
    "chars": 1773,
    "preview": "@import url(\"teletext-noscanlines.css\");\n\n/* scanlines */\n\n.subpage {\n    /* This gradient looks worse but can be render"
  },
  {
    "path": "setup.py",
    "chars": 1322,
    "preview": "from setuptools import setup\n\nsetup(\n    name='teletext',\n    version='3.1.99',\n    author='Alistair Buxton',\n    author"
  },
  {
    "path": "teletext/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "teletext/__main__.py",
    "chars": 87,
    "preview": "from teletext.cli.teletext import teletext\n\n\nif __name__ == '__main__':\n    teletext()\n"
  },
  {
    "path": "teletext/celp.py",
    "chars": 13159,
    "preview": "import numpy as np\nimport matplotlib.pyplot as plt\nfrom spectrum import lsf2poly\nimport numpy as np\nfrom scipy.signal im"
  },
  {
    "path": "teletext/charset.py",
    "chars": 29619,
    "preview": "#\tName:   Map from Teletext G0 character set to Unicode\n#\tDate:   2018 April 20\n#\tAuthor: Rebecca Bettencourt <support@k"
  },
  {
    "path": "teletext/cli/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "teletext/cli/celp.py",
    "chars": 1286,
    "preview": "import click\n\nfrom teletext.cli.clihelpers import packetreader\nfrom teletext.celp import CELPDecoder\n\n@click.group()\ndef"
  },
  {
    "path": "teletext/cli/clihelpers.py",
    "chars": 10092,
    "preview": "import cProfile\nimport os\nimport stat\nfrom functools import wraps\n\nimport click\nfrom tqdm import tqdm\n\nfrom teletext imp"
  },
  {
    "path": "teletext/cli/teletext.py",
    "chars": 21551,
    "preview": "import itertools\nimport multiprocessing\nimport os\nimport pathlib\nimport platform\n\nimport sys\nfrom collections import def"
  },
  {
    "path": "teletext/cli/training.py",
    "chars": 4623,
    "preview": "import multiprocessing\nimport os\n\nimport click\nfrom tqdm import tqdm\n\nfrom teletext.cli.clihelpers import carduser, chun"
  },
  {
    "path": "teletext/cli/vbi.py",
    "chars": 4745,
    "preview": "import click\nimport pathlib\nimport numpy as np\nfrom tqdm import tqdm\n\nfrom teletext.cli.clihelpers import carduser, chun"
  },
  {
    "path": "teletext/coding.py",
    "chars": 4821,
    "preview": "# * Copyright 2016 Alistair Buxton <a.j.buxton@gmail.com>\n# *\n# * License: This program is free software; you can redist"
  },
  {
    "path": "teletext/elements.py",
    "chars": 16926,
    "preview": "import datetime\n\nfrom .printer import PrinterANSI\nfrom .coding import *\n\nfrom . import finders\n\n\nclass Element(object):\n"
  },
  {
    "path": "teletext/file.py",
    "chars": 2726,
    "preview": "import io\nimport itertools\nimport os\nimport stat\n\n\ndef PossiblyInfiniteRange(start=0, stop=None, step=1, limit=None):\n  "
  },
  {
    "path": "teletext/finders.py",
    "chars": 5146,
    "preview": "import itertools\n\nfrom .coding import parity_encode, parity_decode\n\n\nclass Finder(object):\n\n    groups = {\n        'c': "
  },
  {
    "path": "teletext/gui/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "teletext/gui/classify.py",
    "chars": 6777,
    "preview": "import pathlib\nimport sys\n\nimport numpy as np\nfrom PyQt5 import QtWidgets, QtCore\n\nimport matplotlib.pyplot as plt\nfrom "
  },
  {
    "path": "teletext/gui/decoder.py",
    "chars": 5156,
    "preview": "import os\nimport random\nimport sys\nimport webbrowser\n\nimport numpy as np\nfrom PyQt5.QtCore import QSize, QObject, QUrl\nf"
  },
  {
    "path": "teletext/gui/decoder.qml",
    "chars": 3533,
    "preview": "import QtQuick 2.12\nimport QtQuick.Controls 2.12\nimport QtGraphicalEffects 1.12\n\n\nRectangle {\n    property int zoom: 2\n "
  },
  {
    "path": "teletext/gui/editor.py",
    "chars": 3613,
    "preview": "import os\n\nfrom teletext.file import FileChunker\nfrom teletext.packet import Packet\n\n\ntry:\n    from PyQt5 import QtCore,"
  },
  {
    "path": "teletext/gui/editor.ui",
    "chars": 7268,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>MainWindow</class>\n <widget class=\"QMainWindow\" name=\""
  },
  {
    "path": "teletext/gui/qthelpers.py",
    "chars": 650,
    "preview": "from PyQt5 import QtWidgets\n\n\ndef build_menu(window, parent_menu, menu_defs):\n    for name, action, shortcut in menu_def"
  },
  {
    "path": "teletext/gui/service.py",
    "chars": 2130,
    "preview": "from PyQt5 import QtCore, QtGui, QtWidgets, QtQuickWidgets\nfrom PyQt5.QtCore import QVariant\n\nfrom teletext.file import "
  },
  {
    "path": "teletext/gui/vbiplot.py",
    "chars": 3697,
    "preview": "import sys\n\nimport numpy as np\nfrom PyQt5 import QtWidgets\n\nfrom PyQt5.QtWidgets import QApplication, QPushButton, QMain"
  },
  {
    "path": "teletext/image.py",
    "chars": 4099,
    "preview": "import math\n\nimport numpy as np\nfrom teletext.parser import Parser\n\nfrom PIL import Image\nfrom PIL.PcfFontFile import *\n"
  },
  {
    "path": "teletext/interactive.py",
    "chars": 7182,
    "preview": "import curses\nimport os\nimport time\nimport locale\n\nfrom .file import FileChunker\nfrom .packet import Packet\nfrom .parser"
  },
  {
    "path": "teletext/mp.py",
    "chars": 9271,
    "preview": "import atexit\nimport itertools\nimport pickle\nimport queue\n\nimport multiprocessing as mp\n\nimport zmq\n\n\ndef denumerate(wor"
  },
  {
    "path": "teletext/packet.py",
    "chars": 4567,
    "preview": "from .elements import *\n\n\nclass Packet(Element):\n\n    def __init__(self, array=None, number=None, original=None):\n      "
  },
  {
    "path": "teletext/parser.py",
    "chars": 4839,
    "preview": "from . import charset\n\n_unicode13 = False\n\n\nclass Parser(object):\n\n    \"Abstract base class for parsers\"\n\n    def __init"
  },
  {
    "path": "teletext/pipeline.py",
    "chars": 5018,
    "preview": "from collections import defaultdict\nfrom statistics import mode as pymode\n\nimport numpy as np\n\nfrom scipy.stats.mstats i"
  },
  {
    "path": "teletext/printer.py",
    "chars": 3199,
    "preview": "import re\n\nfrom .parser import Parser\n\n\nclass PrinterANSI(Parser):\n\n    def __init__(self, tt, colour=True, codepage=0):"
  },
  {
    "path": "teletext/service.py",
    "chars": 5336,
    "preview": "import datetime\nimport os\nimport textwrap\n\nfrom collections import defaultdict\n\nfrom tqdm import tqdm\n\nfrom .subpage imp"
  },
  {
    "path": "teletext/servicedir.py",
    "chars": 1797,
    "preview": "import pathlib\n\nfrom watchdog.events import FileModifiedEvent, FileDeletedEvent\nfrom watchdog.observers import Observer\n"
  },
  {
    "path": "teletext/sigint.py",
    "chars": 1449,
    "preview": "import signal\n\nclass SigIntDefer(object):\n\n    \"\"\"\n    SigIntDefer is a context manager which catches SIGINT (aka Keyboa"
  },
  {
    "path": "teletext/spellcheck.py",
    "chars": 1899,
    "preview": "import itertools\n\nimport enchant\n\nfrom .coding import parity_encode\n\n\nclass SpellChecker(object):\n\n    common_errors = s"
  },
  {
    "path": "teletext/stats.py",
    "chars": 2531,
    "preview": "import numpy as np\n\n\nclass Histogram(object):\n\n    bars = ' ▁▂▃▄▅▆▇█'\n    label = 'H'\n    bins = range(2)\n\n    def __ini"
  },
  {
    "path": "teletext/subpage.py",
    "chars": 7678,
    "preview": "import base64\nimport itertools\n\nimport numpy as np\n\nfrom .coding import crc\nfrom .packet import Packet\nfrom .elements im"
  },
  {
    "path": "teletext/ts.py",
    "chars": 1138,
    "preview": "# Based on https://github.com/fsphil/tstxtdump with assistance from the author :)\n\nimport itertools\nimport struct\n\nfrom "
  },
  {
    "path": "teletext/vbi/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "teletext/vbi/clustering.py",
    "chars": 2203,
    "preview": "import pathlib\nimport numpy as np\nfrom collections import defaultdict\nfrom itertools import islice\nfrom binascii import "
  },
  {
    "path": "teletext/vbi/config.py",
    "chars": 4945,
    "preview": "import math\nimport pathlib\n\nimport numpy as np\n\nclass Config(object):\n\n    teletext_bitrate = 6937500.0\n    gauss = 4.0\n"
  },
  {
    "path": "teletext/vbi/line.py",
    "chars": 11904,
    "preview": "# * Copyright 2016 Alistair Buxton <a.j.buxton@gmail.com>\n# *\n# * License: This program is free software; you can redist"
  },
  {
    "path": "teletext/vbi/pattern.py",
    "chars": 4612,
    "preview": "# * Copyright 2016 Alistair Buxton <a.j.buxton@gmail.com>\n# *\n# * License: This program is free software; you can redist"
  },
  {
    "path": "teletext/vbi/patterncuda.py",
    "chars": 6158,
    "preview": "# * Copyright 2016 Alistair Buxton <a.j.buxton@gmail.com>\n# *\n# * License: This program is free software; you can redist"
  },
  {
    "path": "teletext/vbi/patternopencl.py",
    "chars": 8876,
    "preview": "# * Copyright 2023 Dr. David Alan Gilbert <dave@treblig.org>\n# *   based on Alistair's patterncuda.py\n# *\n# * License: T"
  },
  {
    "path": "teletext/vbi/training.py",
    "chars": 6914,
    "preview": "# * Copyright 2016 Alistair Buxton <a.j.buxton@gmail.com>\n# *\n# * License: This program is free software; you can redist"
  },
  {
    "path": "teletext/vbi/viewer.py",
    "chars": 7088,
    "preview": "import time\n\nimport numpy as np\nfrom itertools import islice\n\nfrom OpenGL.GLUT import *\nfrom OpenGL.GL import *\n\n\nclass "
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/test_cli.py",
    "chars": 1843,
    "preview": "import unittest\n\nfrom click.testing import CliRunner\n\nimport teletext.cli.teletext\nimport teletext.cli.training\n\n\nclass "
  },
  {
    "path": "tests/test_coding.py",
    "chars": 4998,
    "preview": "import unittest\n\nimport numpy as np\n\nfrom teletext.coding import parity_encode, parity_decode, parity_errors, hamming8_e"
  },
  {
    "path": "tests/test_elements.py",
    "chars": 3685,
    "preview": "import itertools\nimport unittest\n\nfrom teletext.elements import *\n\n\n\nclass TestElement(unittest.TestCase):\n\n    cls = El"
  },
  {
    "path": "tests/test_file.py",
    "chars": 699,
    "preview": "import io\nimport unittest\n\nimport numpy as np\n\nfrom teletext.file import FileChunker\n\nclass TestChunker(unittest.TestCas"
  },
  {
    "path": "tests/test_mp.py",
    "chars": 4516,
    "preview": "from multiprocessing import current_process\nimport unittest\nfrom functools import wraps\nfrom itertools import count, isl"
  },
  {
    "path": "tests/test_packet.py",
    "chars": 934,
    "preview": "import itertools\nimport unittest\n\nfrom teletext.packet import *\n\n\nclass TestPacket(unittest.TestCase):\n\n    packet = Pac"
  },
  {
    "path": "tests/test_sigint.py",
    "chars": 831,
    "preview": "import os\nimport signal\nimport sys\nimport time\nimport unittest\n\nfrom teletext.sigint import SigIntDefer\n\n\ndef ctrl_c(pid"
  },
  {
    "path": "tests/test_spellcheck.py",
    "chars": 828,
    "preview": "import unittest\n\nfrom teletext.spellcheck import *\nfrom teletext.elements import Displayable\n\n\nclass TestSpellCheck(unit"
  },
  {
    "path": "tests/test_stats.py",
    "chars": 220,
    "preview": "import unittest\n\nfrom teletext.stats import *\n\n\nclass TestStatsList(unittest.TestCase):\n\n    def test_str(self):\n       "
  },
  {
    "path": "tests/test_subpage.py",
    "chars": 295,
    "preview": "import unittest\n\nimport numpy as np\n\nfrom teletext.subpage import Subpage\n\n\nclass SubpageTestCase(unittest.TestCase):\n\n "
  },
  {
    "path": "tests/vbi/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/vbi/test_line.py",
    "chars": 1996,
    "preview": "import os\nimport unittest\n\nimport numpy as np\n\nfrom teletext.file import FileChunker\nfrom teletext.vbi.line import Line\n"
  },
  {
    "path": "tests/vbi/test_patterncuda.py",
    "chars": 774,
    "preview": "import pathlib\nimport unittest\n\nimport numpy as np\n\nfrom teletext.vbi.pattern import Pattern\ntry:\n    from teletext.vbi."
  },
  {
    "path": "tests/vbi/test_patternopencl.py",
    "chars": 907,
    "preview": "import pathlib\nimport unittest\n\nimport numpy as np\n\nfrom teletext.vbi.pattern import Pattern\ntry:\n    from teletext.vbi."
  },
  {
    "path": "tests/vbi/test_training.py",
    "chars": 1224,
    "preview": "import io\nimport unittest\n\nimport numpy as np\n\nfrom teletext.vbi.training import *\nfrom teletext.vbi.config import Confi"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the ali1234/vhs-teletext GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 78 files (341.5 KB), approximately 98.6k tokens, and a symbol index with 667 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!