Setup Wizard¶
The interactive console wizard behind the taters command, and the layers it
is built from. See the wizard guide for what it looks
like to use.
The modules are deliberately separate. introspect, recipes and compose
have no terminal dependency at all, so they can back a different front end — a
GUI, a web page — without the logic moving. prompts defines the seam, and
live is one renderer on the far side of it.
The front door¶
The opening menu, and the registry of things a user can choose to do. Each task is a self-contained flow, so adding one is a new module and a registry entry rather than a change to anything that already works.
taters.ui.hub ¶
The front door: "What would you like to do?"
The wizard used to open with "Where is your data?", which quietly assumed the answer to a question nobody had asked -- that you were here to extract features from files. Running a saved pipeline and managing pipelines involve no data at all, and extracting features and running analyses starts from a spreadsheet's columns rather than from a folder.
So the first question is about intent, and the answer chooses a
:class:~taters.ui.tasks.Task. Data comes up inside the task that needs it.
Backing out
Cancelling inside a task returns here rather than ending the session: a mistyped path should not cost someone the whole run. Cancelling at this menu exits. That is the difference between ctrl-c meaning "not that" and ctrl-c meaning "I am done".
version ¶
version()
The installed version, or "" if it cannot be determined.
Running from a source tree that was never installed has no distribution metadata, and a wizard that refuses to start because it cannot name itself would be a poor trade.
Source code in src\taters\ui\hub.py
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | |
title ¶
title()
Short name plus version, for the progress rail.
Source code in src\taters\ui\hub.py
82 83 84 85 | |
border_style ¶
border_style(at=None)
The frame's color right now, as a hex string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
at
|
float
|
A point on the cycle in seconds. Defaults to the monotonic clock, which is what makes it move; tests pass a value to look at a fixed moment. |
None
|
Source code in src\taters\ui\hub.py
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | |
banner ¶
banner(width=MEASURE - 2)
The header: a potato, and what this program is.
Laid out as art beside text rather than as a stack inside a box. The box is still there, but as a light rounded frame -- the heavy double rule this used to draw made a tool for mashing audio look like a compliance report.
Built rather than hard-coded because the version sits inside it, and a version string of a different length would otherwise push the right-hand border out of line.
Source code in src\taters\ui\hub.py
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | |
version_line ¶
version_line()
The version on its own line, or a note that there is no metadata.
Source code in src\taters\ui\hub.py
223 224 225 226 | |
run_hub ¶
run_hub(prompter, *, cwd=None)
Show the menu, run what is chosen, and come back for the next thing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompter
|
Prompter
|
Where the questions go. |
required |
cwd
|
Path
|
The working folder: pipelines are saved as subfolders of it. Defaults to the current directory. |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
False if anything that ran finished with problems. |
Source code in src\taters\ui\hub.py
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 | |
taters.ui.tasks ¶
The things a user can ask Taters to do, as a registry rather than a flow.
The wizard began as one linear script: where is your data, what do you want, here it is. That shape did not survive: "Run a saved pipeline" and "Manage pipelines" do not involve data at all, and "Extract features and run analyses" -- what words relate to an outcome, whether groups differ on a measure -- starts from a spreadsheet's columns rather than from files. A single hard-coded sequence would have to grow branches at the top for each of those.
So the front door is a registry. Each :class:Task is a self-contained
flow with a label, a one-line explanation, and a run function. Adding a
task later is adding a module and one entry here; it is not a refactor of
anything that already works.
What deliberately does not live here
The pipeline machinery, because it already generalizes. An analysis step is a
GLOBAL step that reads a features CSV, and
:func:taters.ui.compose.resolve_selection already chains backwards from a
goal through the capability graph -- ask for something that needs n-grams and
it will pull in the transcript, the transcription, and the WAV conversion on
its own. Future analysis tasks contribute recipes, not a second execution
model.
TaskContext
dataclass
¶
TaskContext(prompter, cwd)
Everything a task needs from the outside world.
pipelines_dir
property
¶
pipelines_dir
Where the user's own pipelines live. Not created until something is saved.
own_pipelines ¶
own_pipelines()
The user's saved pipelines. Built-ins are never included.
Discovery is delegated to the runner's own rules
(:func:run_pipeline.available_presets) rather than re-implemented:
a private copy lived here and had already drifted -- the runner
searches pipelines/ recursively, the copy did not, so a nested
preset appeared under Run but not under Manage. One set of rules, or
the two screens disagree about what exists.
Source code in src\taters\ui\tasks\__init__.py
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | |
owns_folder
staticmethod
¶
owns_folder(path)
Whether this preset has a folder of its own, named for it.
Source code in src\taters\ui\tasks\__init__.py
66 67 68 69 70 | |
Task
dataclass
¶
Task(id, label, help, run, unavailable_because=None)
One thing the user can choose from the opening menu.
Attributes:
| Name | Type | Description |
|---|---|---|
id, label, help |
str
|
Identity, the menu line, and the explanation under it. |
run |
callable
|
|
unavailable_because |
(callable, optional)
|
|
all_tasks ¶
all_tasks()
The menu, in order.
Imported lazily so that a task module can import the wizard without the wizard's own import of this package becoming a cycle.
Source code in src\taters\ui\tasks\__init__.py
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | |
Building a pipeline¶
taters.ui.wizard ¶
The Taters setup wizard: a console front door for people who do not write code.
Run taters with no arguments and this walks you through four questions --
where your data is, what kind it is, what you want out of it, and which options
to change -- then writes a pipeline folder (<name>/<name>.yaml, in the
working folder) and offers to run it.
Why it writes a file instead of just running
The thing this produces is an ordinary preset, indistinguishable from the ones that ship with Taters. That buys a lot for one design decision:
- the run gets the pipeline runner's concurrency, its resumability, its manifest and its per-file error isolation, none of which the wizard has to reimplement;
- the runner recognizes such folders, so the result turns up under
--list-presetsand can be re-run from the command line forever after; - and the user ends up holding a small readable file they can edit, version, or send to a colleague -- which is how somebody graduates from the wizard to the rest of the tool.
Layering
This module contains no terminal code. It asks questions through the
:class:~taters.ui.prompts.Prompter protocol, which is why the test suite can
drive the whole flow with a scripted list of answers. Swap the prompter and the
same logic backs a GUI.
WizardResult
dataclass
¶
WizardResult(
preset,
preset_path=None,
folder=None,
root_dir=None,
file_type="any",
source="media",
inputs=list(),
ran=False,
manifest=None,
ok=None,
)
What the wizard did, for the caller and for the tests.
SourceSpec
dataclass
¶
SourceSpec(
source,
path,
file_type="any",
inputs=list(),
text_cols=(lambda: ["text"])(),
id_cols=list(),
text_mode="concat",
group_by=list(),
level="",
columns=list(),
delimiter=",",
kinds=dict(),
feature_cols=list(),
)
Everything the rest of the wizard needs to know about the input.
root_dir
property
¶
root_dir
What to hand the runner as root_dir.
None for text sources: those presets are GLOBAL-only, so the runner
skips discovery entirely and the input path travels as a variable
instead.
AnalysisSpec
dataclass
¶
AnalysisSpec(
analyses=list(),
group_col="",
outcome_cols=list(),
class_cols=list(),
control_cols=list(),
categorical_controls=list(),
tables=list(),
per_table=False,
p_adjust="fdr_bh",
filters=list(),
value_filters=dict(),
extra_features=list(),
why_not="",
offered=False,
)
What the optional analysis stage decided.
Empty analyses means the stage was skipped or answered "none", and
nothing about the pipeline changes -- which is the common case: plenty of
datasets have nothing to test, and plenty of users just want the feature
tables.
ask_source ¶
ask_source(
prompter,
analyses=None,
text_only=False,
sources=None,
columns_are_measures=False,
)
Ask what kind of data the user has and where it is.
The kind comes first: it decides whether the next question wants a folder or a file, and -- more importantly -- whether there is anything to transcribe at all.
A source that turns up empty returns to the top of this loop rather than just re-asking the path. Picking "Video files" for a folder of mp3s is an easy mistake, and re-asking only the folder would leave the wrong filter in place however many times they retyped it.
analyses=True means the user came here to run statistics, and only a
spreadsheet carries the columns to run them against. Saying so here costs
one screen; saying it after they have browsed to a folder, picked
features and answered the level question costs all of that.
text_only is the training flow: a model is trained on text that
already exists, so recordings -- which would first have to be
transcribed by a pipeline of their own -- are not offered. sources
narrows further to the named source ids (csv, txt_dir,
media): a step that predicts outcome columns can only read a
spreadsheet, and offering a folder of documents would fail after every
question had been answered.
Source code in src\taters\ui\wizard.py
571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 | |
ask_features ¶
ask_features(prompter, source='media', analyses=False)
Show the checklist of things a user can ask for, and take their picks.
Filtered by source: there is no vocal pitch to measure in a folder of essays, and offering it would only let someone pick an option guaranteed to fail -- after a multi-gigabyte install to find out.
Grayed out for the same reason where the row needs something the user does not have: scoring with a saved model needs a saved model, and a row that is offered plainly and then refused at the preflight screen was offered from a list that gave no hint it was unavailable.
analyses=True is the "+ run analyses" flow. Statistics join per-text
feature tables, and two rows here do not make one -- a document-term
matrix and an n-gram frequency list describe the corpus, not each text
-- so those are marked, and a pick made only of them is turned back
here, at the screen that can change it. It used to be caught two
screens later, after the level question, with a two-line note the
layout then cut down to "Pick something else to extract, whose measures
the statistics" (a real report).
Source code in src\taters\ui\wizard.py
657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 | |
resolve_providers ¶
resolve_providers(prompter, selected, source='media')
Settle any capability that more than one recipe could satisfy.
In practice this is one question: a transcript can come from plain transcription or from diarization. It gets asked when a chosen feature needs a transcript and the user did not tick either producer, and it gets asked again if they ticked both -- two transcription steps writing to the same place is never what anyone meant.
Source code in src\taters\ui\wizard.py
714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 | |
missing_extras ¶
missing_extras(recipe)
Extras this step needs that are not importable right now.
Source code in src\taters\ui\wizard.py
774 775 776 777 778 779 780 781 | |
unavailable_reason ¶
unavailable_reason(recipe)
Why this step cannot run on this Python at all, or "".
A missing extra is usually one pip install away, and the preflight offers to keep the step anyway for exactly that reason. But when our own metadata says the package has no release for this Python (gensim and NeMo on 3.14), no install will ever fix it here -- so the row is grayed out with that reason, rather than offered plainly and refused later. Short, because it has to fit beside the row's label.
Source code in src\taters\ui\wizard.py
784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 | |
preflight ¶
preflight(prompter, steps)
Check the machine can actually run what was chosen.
Returns:
| Type | Description |
|---|---|
list[str]
|
Recipe ids to drop. Empty when everything is satisfied, or when the user chose to keep a step anyway -- they may be about to install the missing piece, and a preset that is slightly ahead of the machine is a perfectly reasonable thing to want. |
Source code in src\taters\ui\wizard.py
805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 | |
ask_level ¶
ask_level(prompter, src, steps=())
Ask what one row of the results should describe.
The single most consequential answer in the whole wizard, and until now not a question at all: the level was fixed per recipe, differently for different recipes. Joining a speaker's utterances before measuring versus measuring each and averaging differ by about a third on vocabulary measures, so a level chosen on the user's behalf is a silent methodological decision in someone's results.
Asked once, for the whole pipeline, rather than per feature. Mixed levels would give feature tables with different row counts that cannot be joined on anything -- which is precisely the defect this replaced on the CSV path, where readability emitted a row per spreadsheet row while sentence embeddings emitted one per participant.
Returns:
| Type | Description |
|---|---|
(level_id, group_by)
|
|
Source code in src\taters\ui\wizard.py
1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 | |
ask_analysis ¶
ask_analysis(
prompter, src, steps, required=False, picked=None
)
The optional statistics stage: what to test, on what, over which features.
Everything here is skippable -- the checklist takes an empty answer, unlike the feature one -- because the feature tables are the deliverable for plenty of runs and statistics are a bonus. When the source cannot support statistics at all, the stage says why instead of vanishing.
required=True is the "+ run analyses" flow, where the user has
already said statistics are the point: the checklist then insists on an
answer, because an empty one there is not a decision but a dead end.
Source code in src\taters\ui\wizard.py
1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 | |
apply_analysis ¶
apply_analysis(spec, src, selected, var_values, overrides)
Write an :class:AnalysisSpec into the things compose reads.
The metadata columns are the subtle part. The statistics read a metadata
table gathered from the same spreadsheet with the same identity, and when
rows are being combined a numeric column cannot ride along untouched --
a group of twelve rows has twelve openness scores. Those become the
group's average, named <column>_mean by the gatherer, so the outcome
names handed to the analyses are rewritten to match. Getting that wrong
is a step that runs happily and correlates nothing, so it is decided here,
once, next to the reason.
Source code in src\taters\ui\wizard.py
2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 | |
is_wired ¶
is_wired(recipe, name)
Is this parameter carrying data from an earlier step?
A recipe template like transcript_csv: "{{pick:diar.raw_files.csv}}" is
the wiring that makes the pipeline a pipeline. Offering it as an editable
option would let someone quietly disconnect their own run, so those
parameters are withheld from the options screen entirely.
{{var:...}} templates are deliberately not wiring: they point at a
named variable that exists precisely so it can be changed.
Source code in src\taters\ui\wizard.py
2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 | |
shared_variables ¶
shared_variables(steps)
The variables more than one step reads, and where to find a spec for each.
overwrite_existing is referenced by thirteen of fifteen recipes, and
device, whisper_model and transcripts_dir by three apiece. They
are one setting each, living in the preset's vars: block -- so offering
them on every step's menu asks the same question over and over and implies
an answer that is local when it is not. Changing device under
"Transcript" changes it for the embeddings step too.
Which ones are shared is counted, not listed, so this stays right as recipes come and go.
Returns:
| Type | Description |
|---|---|
dict
|
Variable name -> the (recipe, parameter) to borrow a description, widget and current value from. Any of the referencing steps would do; the first is taken so the order is stable. |
Source code in src\taters\ui\wizard.py
2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 | |
step_rows ¶
step_rows(
recipe,
spec,
var_specs,
overrides,
var_values,
shared=(),
)
One step's settings as menu rows: ordered, gated, indented.
Source code in src\taters\ui\wizard.py
2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 | |
shared_rows ¶
shared_rows(
shared, var_specs, overrides, var_values, prompter=None
)
The shared section's rows -- (var, owner, param, choice) -- under the
same gates as the steps' own menus.
A shared setting is one row for several steps, so it is gated by the step that lends it its spec (the owner). A dependent shared row sits indented under its gate's shared row when that row exists.
Source code in src\taters\ui\wizard.py
2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 | |
tune_shared ¶
tune_shared(
prompter,
shared,
var_specs,
overrides,
var_values,
tables=None,
)
Change the settings that belong to the pipeline rather than to one step.
Source code in src\taters\ui\wizard.py
2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 | |
tune_one_step ¶
tune_one_step(
prompter,
recipe,
var_specs,
overrides,
var_values,
shared=(),
tables=None,
)
Work through one step's settings, one at a time, until the user says stop.
A menu rather than a march through every parameter in turn. Being asked eleven questions to change one is the reason the old flow gated itself behind "do you want the common options or all of them?" -- a question nobody can answer before they have seen either list. Showing the list, with the current values on it, answers itself.
Source code in src\taters\ui\wizard.py
2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 | |
ask_tuning ¶
ask_tuning(
prompter,
steps,
var_specs,
overrides=None,
var_values=None,
ask_gate=True,
source="media",
level=None,
group_by=(),
selected=None,
)
Offer to change the pipeline's settings, and collect what was changed.
Two nested menus, each with a way back up:
Which part of the pipeline? -> Which setting? -> answer it
^ | |
+---- "Done with X" -----------+ |
^ |
+-------------------------------------------------+
Returning to the list rather than leaving is the point. Changing one step's settings is not evidence that you are finished with the whole screen -- the previous shape asked once, up front, which steps you wanted (a checkbox), walked those in order, and then went straight to saving. Someone who picked the shared settings, changed one, and pressed "Done with shared settings" found their pipeline being written, with no way back to the step they had not thought of yet.
Settings shared by several steps are gathered into one entry of their own
rather than repeated under each. See :func:shared_variables.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
overrides
|
dict
|
What has been changed already. Passed in and mutated in place so that leaving this screen and coming back keeps the work: they used to be built fresh on every call, so backing out of the question after it silently threw away every setting the user had just made. |
None
|
var_values
|
dict
|
What has been changed already. Passed in and mutated in place so that leaving this screen and coming back keeps the work: they used to be built fresh on every call, so backing out of the question after it silently threw away every setting the user had just made. |
None
|
Returns:
| Type | Description |
|---|---|
(overrides, var_values)
|
The same two dicts, for callers that would rather read than mutate.
|
Raises:
| Type | Description |
|---|---|
GoBack
|
If the user backs out of the top of this screen. Raised rather than returned: returning meant Esc here carried on to naming the pipeline, so Esc at naming and Esc here bounced between the two screens with no way out in either direction. |
Source code in src\taters\ui\wizard.py
2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 | |
review ¶
review(prompter, preset, selected, inputs)
Show the composed pipeline as a table before anything is written.
Source code in src\taters\ui\wizard.py
3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 | |
pipeline_folder ¶
pipeline_folder(preset, cwd)
The folder this pipeline owns: <cwd>/<id>/.
Everything a run produces goes in here -- the preset itself, features/,
transcripts/, the manifest. One folder per pipeline beats one shared
features/ that three different runs quietly overwrite in turn, and it
means a whole analysis can be zipped up and sent to someone.
run_pipeline._get_preset_dirs() recognizes a directory holding a YAML
of the same name, so the pipeline is still visible to --list-presets.
Source code in src\taters\ui\wizard.py
3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 | |
write_preset ¶
write_preset(preset, directory)
Write the preset to <directory>/<id>.yaml.
The filename matching the folder name is what makes the folder recognisable as a pipeline folder rather than any old directory with YAML in it.
Source code in src\taters\ui\wizard.py
3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 | |
default_workers ¶
default_workers()
A sensible number of files to work on at once.
Three-quarters of the machine's logical cores -- the same "leave a
quarter for the human" policy the runner's automatic resolve uses
(helpers.parallel_map.auto_workers), so the wizard's recommendation
and what workers: 0 actually does are the same number.
Source code in src\taters\ui\wizard.py
3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 | |
ask_workers ¶
ask_workers(prompter, *, fans_out)
How many files to process at once.
Only asked when the pipeline fans out over files (fans_out: it has
item-scoped steps). Text pipelines parallelize too -- the analyzers spend
the same shared workers variable on reader/scorer processes -- but
their right default is automatic, so instead of a question they get 0
("let the dial decide"): the runner resolves it to one process per core,
and the setting stays editable under Shared settings. Taken as a bool
rather than a recipe list so a saved preset, which is plain dicts, can
ask the very same question.
Source code in src\taters\ui\wizard.py
3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 | |
repro_command ¶
repro_command(
preset_path,
*,
root_dir,
file_type,
workers,
overrides=None
)
The exact terminal command that reproduces a TUI run.
Everything the menus decided is spelled out -- the preset file by path, the inputs, the worker count, and any variable changed at run time -- so re-running from a shell is a paste, not an afternoon of reverse-engineering the clicks. Written into the run manifest, which is where someone looks when they ask "what exactly ran here?".
Source code in src\taters\ui\wizard.py
3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 | |
execute_preset ¶
execute_preset(
prompter,
preset,
*,
root_dir,
file_type,
workers,
work_dir,
preset_name,
vars_ctx=None,
command=None
)
Run a composed or saved preset with the live display, and finish properly.
The one path a run takes, whoever starts it. This block used to exist twice -- here and in "Run a saved pipeline" -- and the copies had already drifted: the wizard asked how many files to work on at once while the saved-pipeline path silently hardcoded four, so the same pipeline ran with different parallelism depending on which menu launched it.
Returns:
| Type | Description |
|---|---|
(ok, manifest)
|
|
Source code in src\taters\ui\wizard.py
3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417 3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 3453 | |
finish_screen ¶
finish_screen(
prompter,
*,
ok,
manifest,
folder,
manifest_path,
failures=()
)
Report the outcome and ask what to do next.
A run that simply stops, leaving the last progress bar on screen, gives no sense of having finished and no idea where the results went. This says what happened, names the folder, and offers the two things anyone wants at that point: do something else, or stop.
Raises:
| Type | Description |
|---|---|
QuitRequested
|
If the user chooses to finish. |
Source code in src\taters\ui\wizard.py
3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 | |
run_wizard ¶
run_wizard(
prompter,
*,
cwd=None,
banner=True,
analyses=None,
preselected=None,
text_only=False,
before_options=None,
var_defaults=None
)
Ask the questions, compose a pipeline, write it, and optionally run it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prompter
|
Prompter
|
Where the questions go. Production passes
:class: |
required |
analyses
|
bool
|
What to do about the statistics stage.
The front menu offers the first two as separate entries. They are different intentions -- "turn my recordings into numbers" and "find out whether these groups differ" -- and one flow that tried to be both asked everyone the analysis question, most of whom had nothing to answer it with. |
None
|
cwd
|
Path
|
The working folder: pipelines are saved as subfolders of it. Defaults to the current directory. |
None
|
banner
|
bool
|
Whether to print the Taters banner first. False when the hub has already printed it, so it does not appear twice. |
True
|
preselected
|
sequence of str
|
Recipe ids chosen before the wizard opens, in place of the feature checklist. The "Train a model" task uses this: what to train was the task's own first question, so the checklist would be one already-ticked row. Everything else -- the source, the level, the options screen, naming, the run -- is the same flow, and the pipeline it saves is re-runnable like any other. |
None
|
text_only
|
bool
|
Offer only text sources (documents, a spreadsheet); see
:func: |
False
|
before_options
|
callable
|
|
None
|
var_defaults
|
dict
|
Pipeline variables the run should start with already answered. A default rather than a decision: every one of them still appears on the options screen, so the user can change it. The analyze-a-spreadsheet task turns the word clouds off this way. |
None
|
Returns:
| Type | Description |
|---|---|
WizardResult
|
|
Raises:
| Type | Description |
|---|---|
Cancelled
|
If the user backs out. :func: |
Source code in src\taters\ui\wizard.py
3913 3914 3915 3916 3917 3918 3919 3920 3921 3922 3923 3924 3925 3926 3927 3928 3929 3930 3931 3932 3933 3934 3935 3936 3937 3938 3939 3940 3941 3942 3943 3944 3945 3946 3947 3948 3949 3950 3951 3952 3953 3954 3955 3956 3957 3958 3959 3960 3961 3962 3963 3964 3965 3966 3967 3968 3969 3970 3971 3972 3973 3974 3975 3976 3977 3978 3979 3980 3981 3982 3983 3984 3985 3986 3987 3988 3989 3990 3991 3992 3993 3994 3995 3996 3997 3998 3999 4000 4001 4002 4003 4004 4005 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 4016 4017 4018 4019 4020 4021 4022 4023 4024 4025 4026 4027 4028 4029 4030 4031 4032 4033 4034 4035 4036 4037 4038 4039 4040 4041 4042 4043 4044 4045 4046 4047 4048 4049 4050 4051 4052 4053 4054 4055 4056 4057 4058 4059 4060 4061 4062 4063 4064 4065 4066 4067 4068 4069 4070 4071 4072 4073 4074 4075 4076 4077 4078 4079 4080 4081 4082 4083 4084 4085 4086 4087 4088 4089 4090 4091 4092 4093 4094 4095 4096 4097 4098 4099 4100 4101 4102 4103 4104 4105 4106 4107 4108 4109 4110 4111 4112 4113 4114 4115 4116 4117 4118 4119 4120 4121 4122 4123 4124 4125 4126 4127 4128 4129 4130 4131 4132 4133 4134 4135 4136 4137 4138 4139 4140 4141 4142 4143 4144 4145 4146 4147 4148 4149 4150 4151 4152 4153 4154 4155 4156 4157 4158 4159 4160 4161 4162 4163 4164 4165 4166 4167 4168 4169 4170 4171 4172 4173 4174 4175 4176 4177 4178 4179 4180 4181 4182 4183 4184 4185 4186 4187 4188 4189 4190 4191 4192 4193 4194 4195 4196 4197 4198 4199 4200 4201 4202 4203 4204 4205 4206 4207 4208 4209 4210 4211 4212 4213 4214 4215 4216 4217 4218 4219 4220 4221 4222 4223 4224 4225 4226 4227 4228 4229 4230 4231 4232 4233 4234 4235 4236 4237 4238 4239 4240 4241 4242 4243 4244 4245 4246 4247 4248 4249 4250 4251 4252 4253 4254 4255 4256 4257 4258 4259 4260 4261 4262 4263 4264 4265 4266 4267 4268 4269 4270 4271 4272 4273 4274 4275 4276 4277 4278 4279 4280 4281 4282 4283 4284 4285 4286 4287 4288 4289 4290 4291 4292 4293 4294 4295 4296 4297 4298 4299 4300 4301 4302 4303 4304 4305 4306 4307 4308 4309 4310 4311 4312 4313 4314 4315 4316 4317 4318 4319 4320 4321 4322 4323 4324 4325 4326 4327 4328 4329 4330 4331 4332 4333 4334 4335 4336 4337 4338 4339 4340 4341 4342 | |
main ¶
main(argv=None)
Console entry point for the taters command.
Source code in src\taters\ui\wizard.py
4345 4346 4347 4348 4349 4350 4351 4352 4353 4354 4355 4356 4357 4358 4359 4360 4361 4362 4363 4364 4365 4366 4367 4368 4369 4370 4371 4372 4373 4374 4375 4376 4377 4378 4379 4380 4381 4382 | |
Describing a function to a UI¶
Reads a function's signature and numpydoc docstring into renderable field descriptions. This is what lets the wizard offer a module's options without anyone hand-writing a list of them.
taters.ui.introspect ¶
Turn a Taters function into a description a user interface can render.
Every analysis function in this package is a plain Python function with type
annotations and a numpydoc docstring. That is already most of a form: the
signature says what the fields are called and what type they hold, and the
Parameters block says what each one means in prose. This module joins the
two into :class:ParamSpec objects so a UI never has to hard-code a list of
options for each module.
Deliberately UI-agnostic: nothing here imports questionary, rich, or
anything else terminal-shaped. The console wizard renders these specs, and a
future GUI or web front end can render the same ones -- they serialize to JSON
cleanly.
What this module can and cannot tell you
It gives you the knobs: names, types, defaults, help text, enumerated
choices. It cannot give you the wiring -- nothing in
analyze_vocal_acoustics's signature says its transcript_csv should be
fed the output of an earlier transcription step. That part is declared by hand
in :mod:taters.ui.recipes.
ParamDoc
dataclass
¶
ParamDoc(name, type_str='', desc='')
The docstring half of a parameter: its prose and its declared type.
ParamSpec
dataclass
¶
ParamSpec(
name,
annotation=None,
annotation_str="",
default=EMPTY,
required=False,
kw_only=False,
desc="",
widget="text",
choices=None,
open_ended=False,
)
One renderable field: everything a UI needs to draw a single input.
as_dict ¶
as_dict()
A JSON-friendly view, for a web UI or an MCP schema.
Source code in src\taters\ui\introspect.py
404 405 406 407 408 409 410 411 412 413 414 415 | |
FunctionSpec
dataclass
¶
FunctionSpec(
name, qualname, summary="", doc="", params=list()
)
A whole function: its summary line and its renderable parameters.
clean_doc ¶
clean_doc(text)
Turn a numpydoc parameter description into a sentence a person can read.
Two things are in the way. Docstrings are hard-wrapped at some column, so a
description arrives as several short lines and anything showing only the
first gets half a sentence. And they carry reStructuredText markup --
None, :func:some.thing -- which renders as literal backticks in a
terminal.
Done here rather than in the wizard so every front end gets readable text from the same place, and so the raw markup never has to be handled twice.
Paragraph breaks are kept; a run of lines within a paragraph is joined.
Source code in src\taters\ui\introspect.py
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |
parse_numpydoc_params ¶
parse_numpydoc_params(doc)
Pull the Parameters block out of a numpydoc docstring.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc
|
str or None
|
A raw docstring. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ParamDoc]
|
Keyed by parameter name. An |
Source code in src\taters\ui\introspect.py
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | |
widget_for ¶
widget_for(annotation, name='', default=EMPTY)
Choose a rendering hint for one parameter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
annotation
|
Any
|
The evaluated annotation (see :func: |
required |
name
|
str
|
The parameter name. Used only to spot path-ish parameters that are
annotated as bare |
''
|
default
|
Any
|
The default value, used as a last resort when there is no annotation. |
EMPTY
|
Returns:
| Type | Description |
|---|---|
str
|
One of |
Notes
choice is not returned here -- it comes from the docstring's literal
set, not the annotation, so :func:describe applies it afterwards.
Source code in src\taters\ui\introspect.py
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 | |
describe ¶
describe(func)
Build a :class:FunctionSpec from a live callable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
func
|
Callable
|
Any Taters analysis function. |
required |
Returns:
| Type | Description |
|---|---|
FunctionSpec
|
Parameters in declaration order. |
Notes
Signatures are read with eval_str=True. Every module in this package
starts with from __future__ import annotations, which makes annotations
plain strings at runtime; without eval_str you would get the string
"Optional[Union[str, Path]]" instead of a type to dispatch on. If
evaluation fails -- a name that only exists under TYPE_CHECKING, say --
we fall back to the unevaluated signature and the string annotations still
give a usable, if coarser, result.
Source code in src\taters\ui\introspect.py
467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 | |
load_target ¶
load_target(target)
Import and return the function named by a "module:function" string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
str
|
E.g. |
required |
Returns:
| Type | Description |
|---|---|
Callable
|
|
Raises:
| Type | Description |
|---|---|
ImportError
|
Propagated unchanged from the import. Callers are expected to catch this and translate it into an install hint -- most Taters modules pull heavy optional dependencies, and this function is the point where that cost is paid, which is exactly why recipes name their target as a string instead of importing it at module load. |
Source code in src\taters\ui\introspect.py
543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 | |
The recipe catalog¶
The declared wiring: which steps exist, what each one needs, and what it
produces. Signatures cannot tell you that the transcription step's output feeds
the acoustics step's transcript_csv, so that part is written down here.
taters.ui.recipes ¶
The declared data-flow catalog: what a user can ask for, and what it needs.
:mod:taters.ui.introspect can read every knob off a function's signature, but
it cannot read the wiring. Nothing in analyze_vocal_acoustics's signature
says its transcript_csv should be fed {{pick:diar.raw_files.csv}} from
an earlier step. That knowledge lives here, declared by hand, one
:class:Recipe per pipeline step.
Every with_ block below is transcribed from the two shipped presets
(conversation_video.yaml and single_speaker_media.yaml). That is
deliberate: those presets are the known-good wiring, and
tests/test_compose.py asserts that selecting the right recipes reproduces
them. If you change a template here, that test tells you.
How dependencies work
Steps are linked by capability strings -- "wav", "transcript_csv",
and so on -- rather than by naming each other directly. A recipe declares what
it requires and what it produces, and :mod:taters.ui.compose walks the
graph. The indirection buys one important thing: transcript_csv has two
providers (plain transcription and diarization), so the user gets to choose how
a requirement is met without any recipe knowing that a choice exists.
auto_with covers the one relationship capabilities cannot express. A gather
step does not provide anything the feature step needs -- it tidies up
afterwards -- so it cannot be pulled in by requires. Naming it in
auto_with says "whenever you include me, include this too."
Level
dataclass
¶
Level(id, label, help, group_by)
One answer to "what should a row of results describe?".
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Stable identifier, stored in the preset's |
label, help |
str
|
What the option says, and the sentence under it. The label alone is never enough: "one row per speaker" does not say what happened to the utterances, and joining a speaker's words before measuring versus averaging their per-utterance scores differ by ~35% on vocabulary measures. The help says which happened. |
group_by |
tuple of str, or None
|
The columns to aggregate on. Empty means no aggregation -- the raw row
is the unit. |
Recipe
dataclass
¶
Recipe(
id,
label,
help,
call,
target,
scope,
save_as,
with_,
requires=frozenset(),
produces=frozenset(),
auto_with=(),
extras=(),
needs_ffmpeg=False,
gpu_use=None,
hidden=(),
vars=dict(),
user_facing=True,
sources=("media",),
text_input=False,
source_with=dict(),
text_help="",
library=dict(),
library_defaults=dict(),
param_when=dict(),
tags=(),
stage="extract",
feature_table=False,
outcome_kind=None,
consumes_feature_tables=False,
encoder_param=None,
feature_tables_optional=False,
takes_level=False,
keys_like_metadata=False,
)
One pipeline step, plus everything a UI needs to offer it.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Stable identifier. This is what the wizard passes to
:func: |
label, help |
str
|
What the checkbox says, and the one-line explanation under it. |
call |
str
|
The preset |
target |
str
|
|
scope |
{'item', 'global'}
|
|
save_as |
str
|
Name the step's result is bound to for later |
requires, produces |
frozenset[str]
|
Capability strings. See the module docstring. |
auto_with |
tuple[str, ...]
|
Recipe ids to include alongside this one -- used for the gather steps that follow a feature step. |
extras |
tuple[str, ...]
|
pip extras this step needs, e.g. |
needs_ffmpeg |
bool
|
Whether the step shells out to ffmpeg. |
gpu_use |
str
|
One of :data: Defaults to |
with_ |
dict
|
The preset |
hidden |
tuple[str, ...]
|
Parameters never offered, even under "show advanced". These are the
alternate-input arguments -- |
vars |
dict[str, dict]
|
Contributions to the preset's |
user_facing |
bool
|
Whether this appears in the feature checklist. Prerequisites and
gathers are |
sources |
tuple[str, ...]
|
Which of :data: |
text_input |
bool
|
Whether this step's input binding is rewritten by :func: |
source_with |
dict[str, dict]
|
Per-source patches merged into |
text_help |
str
|
Replaces |
resolved_gpu_use
property
¶
resolved_gpu_use
This step's declared GPU behavior, or the cautious guess.
A step that reads the device variable and has not declared itself is
treated as "gpu_model_each" -- one file at a time. Wrong-but-slow is
a recoverable mistake; wrong-and-out-of-memory is not, and it fails
halfway through a batch rather than at the start.
to_step ¶
to_step()
Render this recipe as a preset step dict.
Source code in src\taters\ui\recipes.py
437 438 439 440 441 442 443 444 445 446 447 | |
level_aware ¶
level_aware(recipe)
Does the analysis level decide this step's grain?
Derived rather than declared, so a new module needs no wizard work:
- a text analyzer (
text_input) measures text, and the level says what a piece of text is; - a merge (
aggregate: True) that consumes a capability some text step produces is the "measure each, then average" half of a text feature, so it has to collapse on the same key the analyzers grouped by.
One step declares it instead (takes_level): the one that reads the
user's own spreadsheet columns as the measures. It analyzes no text, so
nothing derives it, but "one row per participant rather than one per
spreadsheet row" is exactly the question the level asks, and the answer
decides whether its columns are carried or averaged.
Everything else keeps the grain it declares. That deliberately excludes the
audio features: acoustics groups by speaker within one file because
it is item-scoped, and gather_whisper_embeddings aggregates audio
segments. "One row per utterance" has no meaning for either -- there is no
per-utterance WAV to measure -- and overwriting their keys would silently
change what they average.
Source code in src\taters\ui\recipes.py
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | |
levels_for ¶
levels_for(source)
Every level that makes sense for source.
Source code in src\taters\ui\recipes.py
202 203 204 205 206 | |
level_by_id ¶
level_by_id(source, level=None)
One level, by id, falling back to the source's default.
Source code in src\taters\ui\recipes.py
209 210 211 212 213 214 215 216 | |
gate_of ¶
gate_of(recipe, param)
A gated setting's rule as (gate, op, value), or None when ungated.
Two spellings are accepted -- (gate, value) means == -- and
anything else raises, so a typo in a declaration is a test failure
rather than a row that never shows.
Source code in src\taters\ui\recipes.py
622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 | |
gate_holds ¶
gate_holds(gate, current)
Whether a gated setting should be shown, given its gate's live value.
Fails open: a gate whose value is unknown (None, or the wizard's
EMPTY sentinel) shows the row, because a hidden setting that should be
visible is the one mistake this screen must never make. The comparison
is on the value's text, as the recipes spell it.
Source code in src\taters\ui\recipes.py
641 642 643 644 645 646 647 648 649 650 651 652 653 | |
text_binding ¶
text_binding(
source,
*,
text_cols=("text",),
id_cols=(),
pass_through=False,
text_mode="concat",
group_by=()
)
Build the input arguments a text analyzer needs for a given source.
The analyzers take exactly one of three input modes and the arguments for
the other two are silently ignored, so this returns the complete group
rather than a patch. Callers strip :data:TEXT_INPUT_KEYS first and merge
this in, which makes it impossible for a leftover csv_path to sit
alongside a fresh txt_dir.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
('media', 'txt_dir', 'csv')
|
|
"media"
|
text_cols
|
sequence of str
|
Only meaningful for |
('text',)
|
id_cols
|
sequence of str
|
Only meaningful for |
('text',)
|
text_mode
|
('concat', 'separate')
|
What to do when |
"concat"
|
pass_through
|
bool
|
Whether this step wants |
False
|
Returns:
| Type | Description |
|---|---|
dict
|
Arguments to merge into a step's |
Source code in src\taters\ui\recipes.py
753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 | |
by_id ¶
by_id(recipe_id)
Look a recipe up by id.
Raises:
| Type | Description |
|---|---|
KeyError
|
With the list of valid ids, because this is almost always a typo in a caller and the bare id is not enough to fix it. |
Source code in src\taters\ui\recipes.py
2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 | |
user_facing ¶
user_facing(source='media', stage='extract')
The recipes to show in one wizard checklist, in catalog order.
Filtered by source, so someone who said "I have a folder of essays" is never offered vocal acoustics -- an option that could only ever fail for them, and that costs a multi-gigabyte install to find out. And by stage: the feature checklist and the statistics stage are different questions, and mixing "extract cohesion" with "run an ANOVA" on one screen buries both.
Source code in src\taters\ui\recipes.py
2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 | |
providers_of ¶
providers_of(capability)
Every recipe that can satisfy a capability, in catalog order.
More than one means the user has a choice to make -- transcript_csv is
the case that matters, with plain transcription and diarization both able
to produce it.
Source code in src\taters\ui\recipes.py
3006 3007 3008 3009 3010 3011 3012 3013 3014 | |
Composing a preset¶
Turns a set of chosen features into a runnable preset: resolves prerequisites, orders the steps, and writes the metadata. Pure — no I/O, no analysis imports.
taters.ui.compose ¶
Turn a set of chosen features into a runnable preset.
This module is pure: it takes recipe ids and option overrides, and returns a
dict shaped exactly like the YAML in taters/pipelines/presets/. It touches
no files, imports no analysis code, and runs in microseconds -- which is what
makes it worth testing hard. tests/test_compose.py puts its output through
the same validator that guards the shipped presets, so the composer
structurally cannot emit a step naming a parameter that does not exist.
The job has three parts:
- Closure. The user checks "Readability"; that needs the merged transcript table, which needs a transcript, which needs a WAV. Four steps from one tick.
- Ordering. Dependencies first, and -- matching how both shipped presets are laid out -- every per-file step before every run-once step.
- Metadata. A full
meta:block, so a composed preset is a first-class citizen:--list-presetsand--describe-presetwork on it exactly as they do on the built-ins.
ComposeError ¶
Bases: RuntimeError
A selection that cannot be turned into a runnable pipeline.
slugify ¶
slugify(text)
Reduce a title to a safe preset id / filename stem.
Source code in src\taters\ui\compose.py
52 53 54 55 | |
resolve_selection ¶
resolve_selection(
selected, *, providers=None, source="media"
)
Expand a user's picks into the full, ordered list of steps to run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
selected
|
sequence of str
|
Recipe ids the user checked off. |
required |
providers
|
dict[str, str]
|
How to satisfy a capability that more than one recipe can provide, e.g.
|
None
|
source
|
('media', 'txt_dir', 'csv')
|
Where the text comes from. Anything but |
"media"
|
Returns:
| Type | Description |
|---|---|
list[Recipe]
|
Every step to run, dependencies included, in execution order: |
Raises:
| Type | Description |
|---|---|
ComposeError
|
If a requirement has no provider at all, or if the graph is cyclic. |
Notes
Capability satisfaction is checked against the chosen set, not against
the catalog. That distinction matters: transcript_csv has two providers,
and pulling in both would give the run two transcription steps writing to
the same save_as.
Source code in src\taters\ui\compose.py
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 | |
pending_choices ¶
pending_choices(selected, *, source='media')
Capabilities this selection needs that more than one recipe could satisfy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
selected
|
sequence of str
|
The recipe ids the user ticked. |
required |
source
|
('media', 'txt_dir', 'csv')
|
Where the text comes from. On a text source there is nothing to transcribe, so this returns empty and the wizard skips the question. |
"media"
|
Returns:
| Type | Description |
|---|---|
dict[str, list[Recipe]]
|
Capability -> the recipes that could provide it. Empty when nothing is ambiguous. |
Notes
The need is resolved transitively, which is the whole reason this is not
a one-line check over requires. Someone who ticks only "Readability
scores" has not asked for a transcript and does not mention one anywhere in
their selection -- but readability reads the merged transcript table, and
that merge step is what needs a transcript. Ask them how to make one
anyway, because the two answers differ by a multi-gigabyte install.
A capability whose producer the user ticked directly is not returned: they have already answered.
Source code in src\taters\ui\compose.py
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 | |
feature_tables ¶
feature_tables(
steps,
source="media",
level=None,
group_by=(),
picked=None,
)
The steps whose output joins into an analysis table, and what to call it.
Returns (recipe, name) pairs, in step order.
picked is what the user actually ticked. A feature table pulled in
only as another step's input does not join: the topic model needs a
document-term matrix, and someone who asked for topic scores has not
asked for every one of the matrix's five thousand term columns to be
correlated with their outcome as well. Without the list, every feature
table in steps joins, which is what a hand-written pipeline means.
Two steps can describe one table. "Sentence embeddings" writes a row per utterance; "Merge sentence embeddings" averages those to a row per unit of analysis -- and only one of them is the per-text table the statistics can join. Whichever survives, the name comes from the measure rather than from the plumbing: a screen offering "Merge sentence embeddings" alongside "Sentence embeddings" reads as two feature sets, which is a thing they have never been (a real report).
One rule, one answer: the composer wires the assemble step from this and the wizard builds its picker from it, so the tables offered are exactly the tables used. They were computed separately, and disagreed.
Source code in src\taters\ui\compose.py
498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 | |
table_names ¶
table_names(
steps,
source="media",
level=None,
group_by=(),
picked=None,
*,
overrides=None,
var_values=None,
var_specs=None
)
What each feature table will be called in the analyses, with the step it comes from.
Returns (name, label) pairs in step order -- ("dictionary",
"Dictionary categories") -- for the tables :func:feature_tables says
will join. The name is the stem of the file the step writes, which is how
the assemble step names a feature set and how pca and
unverified_ok refer to one. It is worked out the way the composer
will write it: the step bound to this source and level (a non-default
level suffixes the filename), the user's own override of the output path
on top, and every {{var:...}} in it rendered from the live variable
values -- so a matrix under weighting: tfidf is offered as
doc_term_matrix_tfidf, which is the name the run will use.
This exists so the wizard can offer the names instead of asking for them to be typed: "off, all, or the name of a feature set" was a text box, and the names it wanted were file stems nobody had seen yet.
Source code in src\taters\ui\compose.py
562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 | |
compose ¶
compose(
selected,
*,
providers=None,
overrides=None,
var_values=None,
name="My pipeline",
file_type="any",
root_dir=None,
notes="",
source="media",
input_path=None,
text_cols=("text",),
id_cols=(),
text_mode="concat",
group_by=(),
delimiter=",",
level=None,
model_plans=()
)
Build a complete preset dict from a set of chosen features.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
selected
|
sequence of str
|
Recipe ids the user checked off. Prerequisites are added for you. |
required |
providers
|
dict[str, str]
|
Capability -> recipe id, for capabilities with more than one provider.
In practice this is |
None
|
overrides
|
dict[str, dict]
|
Per-step parameter overrides, keyed by recipe id:
|
None
|
var_values
|
dict[str, Any]
|
Overrides for the preset's |
None
|
name
|
str
|
Human title. The preset id is its slug. |
"My pipeline"
|
file_type
|
('audio', 'video', 'any')
|
Recorded in |
"audio"
|
root_dir
|
str
|
The input folder, baked into |
None
|
notes
|
str
|
Free text appended to |
''
|
source
|
('media', 'txt_dir', 'csv')
|
Where the text comes from. The two non-media sources rewire the text analyzers to read the user's own files, and drop transcription and everything under it. |
"media"
|
input_path
|
str
|
The folder of |
None
|
text_cols
|
sequence of str
|
For |
('text',)
|
id_cols
|
sequence of str
|
For |
('text',)
|
text_mode
|
('concat', 'separate')
|
For |
"concat"
|
Returns:
| Type | Description |
|---|---|
dict
|
A preset with |
Raises:
| Type | Description |
|---|---|
ComposeError
|
Propagated from :func: |
Source code in src\taters\ui\compose.py
635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 | |
Prompting¶
The interface the wizard asks its questions through, and the implementations:
one backed by questionary, one that reads canned answers for the test suite.
taters.ui.prompts ¶
The thin layer between the wizard's questions and whatever is asking them.
:class:Prompter is the whole contract: five ways to ask something, one way to
wait, and two ways to say something. :mod:taters.ui.wizard is written against it and never
imports questionary or rich itself.
That indirection earns its keep twice. It lets the test suite drive the entire
wizard with a scripted answer list and no terminal at all -- see
:class:ScriptedPrompter -- and it means a GUI would replace this one file
rather than the wizard's logic.
QuitRequested ¶
QuitRequested(ok=True)
Bases: Exception
The user chose to finish from inside a task.
Distinct from :class:Cancelled, which means "not this, take me back". A
finished run offers "Quit" as a deliberate ending, and reporting that as
having been backed out of would be a lie about work that succeeded.
Carries the run's verdict, because the exception skips the task's normal
return False path: quitting from a failed run's finish screen used to
exit 0, and anything scripted around the TUI read that as success.
Source code in src\taters\ui\prompts.py
41 42 43 | |
GoBack ¶
Bases: Exception
The user pressed Esc: undo the last question rather than the whole task.
Distinct from :class:Cancelled because the two mean opposite things.
Cancelled is "stop, I did not want this"; GoBack is "keep going, I just
answered something wrong". Collapsing them would make a typo cost the whole
session, which is exactly the sharp edge Esc exists to file off.
Cancelled ¶
Bases: Exception
The user backed out -- Ctrl-C, or Esc on a questionary prompt.
Stage
dataclass
¶
Stage(key, label, status='todo', detail='')
One entry in the wizard's progress rail.
Choice
dataclass
¶
Choice(
value,
label,
help="",
checked=False,
disabled="",
annotation="",
tone="",
)
One option in a select or checkbox list.
Prompter ¶
Bases: Protocol
What the wizard needs from a user interface.
QuestionaryPrompter ¶
QuestionaryPrompter()
Source code in src\taters\ui\prompts.py
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 | |
ticks_in_place
class-attribute
instance-attribute
¶
ticks_in_place = False
A :class:Prompter backed by questionary for input and rich for output.
Both are imported in __init__ rather than at module scope, so that
importing :mod:taters.ui.wizard -- which the tests do -- never requires a
terminal library to be present.
repaint ¶
repaint()
Redraw the screen furniture. A no-op here: this renderer has none.
Source code in src\taters\ui\prompts.py
317 318 319 320 | |
working ¶
working(text)
Announce slow work before it starts, on screen immediately.
A note plus a repaint, which is the pair that guarantees the line is visible before the blocking call rather than after it: the live renderer's screen is wiped per question, and a note printed onto a just-finished screen without the repaint could be cleared before it was ever seen. Dim, because it is narration, not an answer.
Source code in src\taters\ui\prompts.py
322 323 324 325 326 327 328 329 330 331 332 333 | |
clear ¶
clear()
Start on a clean screen.
The scrollback is untouched -- this scrolls the screen rather than erasing history, so whatever the user had before is still there to page back to.
Source code in src\taters\ui\prompts.py
335 336 337 338 339 340 341 342 343 344 345 346 347 | |
note ¶
note(text, *, style='', wrap=True)
Print a note, keeping its left margin on every line.
Callers indent by writing spaces into the string, which reads naturally at the call site but only ever indented the first line: rich wrapped the rest back to column 0. Paragraphs therefore had a ragged left edge that alternated between the margin and the screen edge, which is what made blocks of notes run into each other instead of stacking.
So the leading spaces are read off as a margin and re-applied as a hanging indent, per line, and the text is wrapped inside what is left. Every line of a block now starts in the same column, which is what lets the eye see where one block stops and the next begins.
Source code in src\taters\ui\prompts.py
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 | |
note_width ¶
note_width()
How wide a note's text may be, in cells.
Source code in src\taters\ui\prompts.py
389 390 391 | |
stage ¶
stage(key, label, *, status='active', detail='')
No-op: this renderer prints line by line and has no rail to update.
Source code in src\taters\ui\prompts.py
413 414 415 | |
reset_stages ¶
reset_stages()
No-op, for the same reason: there is no rail to forget.
Source code in src\taters\ui\prompts.py
417 418 | |
reason ¶
reason(text)
Say why the next question is being asked.
Distinct from :meth:note in where it lands and how loudly. A note is
commentary printed at the top of the screen; a reason is the sentence
that makes the question underneath it make sense, and it was getting
lost -- dim, and separated from its question by the whole rail.
Source code in src\taters\ui\prompts.py
420 421 422 423 424 425 426 427 428 429 430 | |
pause ¶
pause(message=PAUSE_MESSAGE)
Wait for the reader, and take no answer from them.
Deliberately not routed through :meth:_ask, which turns Esc into
Cancelled. There is nothing here to cancel -- the screen has already
been shown -- and a reader who presses Esc means the same thing as one
who presses Enter.
Source code in src\taters\ui\prompts.py
454 455 456 457 458 459 460 461 462 463 | |
ScriptedPrompter
dataclass
¶
ScriptedPrompter(
answers=list(),
asked=list(),
presented=list(),
offered=list(),
cycled=list(),
tables=list(),
stages=list(),
reasons=list(),
select_defaults=dict(),
breadcrumbs=list(),
output=list(),
_cursor=0,
)
ticks_in_place
class-attribute
instance-attribute
¶
ticks_in_place = True
A :class:Prompter that reads its answers from a list instead of a person.
This is what makes the wizard testable end to end. Give it the answers a user would have given, in order, and every question is answered without a terminal:
p = ScriptedPrompter(["./media", "audio", ["transcribe"], False, "run", True, False])
Answers are consumed in the order the wizard asks. Everything printed is
kept in :attr:output, every question in :attr:asked, every
pre-filled default in :attr:presented, and every list of options in
:attr:offered -- so a test can assert on what the
user would have seen, not just on what came back. presented matters more
than it looks: a prompt's default is how the wizard shows you the current
value of a setting, and showing the wrong one is a real bug even though the
returned answer is unaffected.
pause ¶
pause(message=PAUSE_MESSAGE)
Recorded, but consumes no scripted answer.
That is the point of it being separate from confirm: a pause is not a
decision, so a test driving a screen that ends in one does not have to
supply an answer for it -- and cannot accidentally feed the pause an
answer meant for the next real question.
Source code in src\taters\ui\prompts.py
722 723 724 725 726 727 728 729 730 731 732 | |
offered_choices ¶
offered_choices(question_startswith)
The options shown for the first matching question.
What a user was offered is as much a part of the interface as what they answered: a checklist that quietly includes vocal acoustics for a folder of essays is a bug no assertion on the return value would catch.
Source code in src\taters\ui\prompts.py
779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 | |
with_annotation ¶
with_annotation(title, annotation)
A rendered row title with its annotation replaced (or added).
Rows are built by _to_q as [(label_class, label),
("class:annotation", " " + annotation)]; a plain string is a row that
never had one. The left/right binding rewrites the pointed row's
annotation in place so the list repaints without being rebuilt.
Source code in src\taters\ui\prompts.py
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | |
flip_tick_mark ¶
flip_tick_mark(title)
"[ ]" <-> "[x]" in a row's rendered title, whichever shape it has.
questionary titles are either a plain string or a list of (style, text) tuples (ours carry the annotation as a second tuple); the mark always lives in the first text segment. Used by the in-place space toggle, where the row must change on screen without the prompt being torn down.
Source code in src\taters\ui\prompts.py
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | |
ask_at_least_one ¶
ask_at_least_one(
prompter, question, choices, *, thing="one", cycle=None
)
A checkbox that will not take "nothing" for an answer.
Three copies of this loop had grown -- the text columns, the grouping columns, the feature checklist -- each with its own wording for the same complaint. An empty answer is never meaningful at any of them: it would either crash later or quietly analyze nothing.
The "space to tick, enter when done" gloss the questions used to carry is
gone: the key hint bar under every checkbox already says [space] tick ·
[enter] confirm, so it was the same instruction twice, and it pushed the
questions past the width prose wraps at.
Source code in src\taters\ui\prompts.py
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | |
terminal_width ¶
terminal_width(default=80)
Usable width, for wrapping help text.
Source code in src\taters\ui\prompts.py
796 797 798 | |
set_chrome_rows ¶
set_chrome_rows(rows)
Tell :func:visible_rows how much of the screen is already spoken for.
Source code in src\taters\ui\prompts.py
821 822 823 824 | |
set_description_rows ¶
set_description_rows(rows)
Hold the description block under a list at a fixed height.
questionary draws the pointed row's description under the list and nothing at all when that row has none, so a list's height changed with every arrow press -- and each change scrolled the terminal, walking the explanation printed above the question up the screen a line or two at a time (a real report: "the yellow text moves up"). Reserving the tallest description's height and padding shorter ones keeps the whole screen still.
Source code in src\taters\ui\prompts.py
832 833 834 835 836 837 838 839 840 841 842 843 844 845 | |
description_rows ¶
description_rows(choices, *, width=None, cap=6)
The lines the tallest description among choices will take, capped
so one essay of a help text cannot eat the list's room.
Source code in src\taters\ui\prompts.py
848 849 850 851 852 853 854 855 856 857 | |
visible_rows ¶
visible_rows(lines=None)
How many choices a list can show before it has to scroll.
Measured from the terminal prompt_toolkit is actually drawing into, not from
shutil. The two agree in production and can differ anywhere the output is
redirected -- a captured session, a test harness -- and a window sized to a
different terminal than the one being drawn puts rows off the bottom while
claiming they are visible.
Source code in src\taters\ui\prompts.py
868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 | |
note_lines ¶
note_lines(text, width)
Exactly the lines :meth:Prompter.note will print for this text.
Wrapped here rather than by rich so the indent is real text instead of padding. Padding fills each line out to the full width, which leaves trailing whitespace on every line of every note -- invisible on screen, and there in anything the user copies out of the terminal.
Shared with the screen painter, which has to know how tall a note is before printing it so a long one cannot crowd the question underneath. Measuring by re-implementing the wrapping is how the two drift apart, so there is one function and the printer calls it too. (Runs of blank lines collapse when printed, which depends on what came before; this returns them uncollapsed, so a height taken from it is an upper bound.)
Source code in src\taters\ui\prompts.py
891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 | |
scroll_long_lists ¶
scroll_long_lists()
Show a window onto a long list, with markers for what is off each end.
prompt_toolkit already scrolls to keep the pointer visible, so a list longer than the terminal has always been navigable -- but nothing said so. Rows simply were not there, with no hint that arrowing further would reveal them, which reads as a list that is missing options rather than one that continues.
Applied to every list at once -- module options, the file browser, the step menus -- because the fix belongs to the renderer rather than to any one question.
Source code in src\taters\ui\prompts.py
958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 | |
style_descriptions_separately ¶
style_descriptions_separately()
Give a choice's description its own style class, so it can be colored.
questionary tags the description with class:text -- the same class it
uses for every unselected option title. Restyling that class would recolour
the whole list, so the description cannot be told apart from the options it
is explaining without this.
The interception is deliberately narrow: it rewrites the class of exactly one token, identified by the prefix questionary itself writes, and leaves every other token as it found it. Applied once, and reported rather than assumed -- if a future questionary builds its tokens differently the styling is simply not applied, which is the state this started in.
Returns:
| Type | Description |
|---|---|
bool
|
Whether the interception is in place. |
Source code in src\taters\ui\prompts.py
1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 | |
wrap_description ¶
wrap_description(text, *, width=None)
Fold a choice's description so all of it is visible.
questionary renders a description as a single run of text and does not wrap it, so anything longer than the terminal is simply cut -- and the sentence that explains an option is exactly the sentence someone is reading when they cannot decide. One real menu ended mid-word: "...which is a large install (NeMo) plu".
Continuation lines are indented to sit under the first rather than returning to column zero, so the block reads as one paragraph attached to the option instead of as unrelated text under the list.
Source code in src\taters\ui\prompts.py
1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 | |
fit ¶
fit(text, *, reserve=0, width=None)
Shorten one line so the terminal never has to break it.
questionary draws a choice on a single line and does no wrapping of its own, so anything too long is hard-wrapped by the terminal -- mid-word, with the remainder dangling on the next line under no pointer. In a menu that turns a tidy list into a wall. Better to lose the tail of a description than the shape of the list, so this elides instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reserve
|
int
|
Columns already spoken for by whatever draws the line -- questionary's pointer and checkbox glyphs, an indent. |
0
|
Source code in src\taters\ui\prompts.py
1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 | |
Choosing a file¶
A filesystem browser built from the ordinary selection prompt, so it needs nothing from the renderer that the scripted prompter cannot also do — which is what makes it testable without a terminal.
taters.ui.browse ¶
Pick a folder or a file by looking at them, rather than by typing a path.
Typing a path is the single most error-prone thing the wizard asks for. It is
also the one place where a mistake is invisible until it is too late: a typo in
a folder name produces "no files found", which reads as "there is nothing here"
rather than "you are looking in the wrong place". People who do not live in a
terminal do not necessarily know where they are, what the working directory is,
or that ~ means anything.
So this is a browser built out of the ordinary select prompt: entries are
the folders and files you can see, plus the moves you can make from here. It
needs nothing from the renderer that the scripted prompter cannot also do,
which is why it is testable without a terminal.
Typing is still available, because for someone who does know the path, browsing to it is the slow way round.
short_path ¶
short_path(path, keep=2)
A path short enough to sit in a question, with the end kept.
The end is the part that identifies it -- …/scratchpad/hub says where
you are; the first sixty characters of a temp directory do not. Home is
abbreviated to ~ for the same reason.
Source code in src\taters\ui\browse.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | |
human_size ¶
human_size(n)
A file size short enough to sit in a column.
Source code in src\taters\ui\browse.py
100 101 102 103 104 105 106 | |
browse_for_folder ¶
browse_for_folder(
prompter,
*,
question="Which folder?",
start=None,
want_files=None
)
Walk the filesystem and return a folder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
want_files
|
sequence of str
|
Suffixes worth counting, e.g. |
None
|
Source code in src\taters\ui\browse.py
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 | |
browse_and_tick ¶
browse_and_tick(
prompter,
*,
question="Which files?",
start=None,
suffixes=(".csv",)
)
A file browser that is also the selector: walk folders, tick files where they are, confirm once.
Space ticks a file in place -- no redraw, no flash -- and the selection survives walking between folders, so a set spread over several folders is still one trip. Enter means proceed: on a folder it opens it, on a file it finishes -- with the ticked set if anything is ticked, or with just that file (the quick single-file path). There is no "Import N files" menu row to hunt for; two earlier shapes hid either the files or the way out.
Returns the chosen files. Esc raises GoBack, like every other screen.
Source code in src\taters\ui\browse.py
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 | |
browse_for_file ¶
browse_for_file(
prompter,
*,
question="Which file?",
start=None,
suffixes=(".csv",)
)
Walk the filesystem and return a file with one of suffixes.
Source code in src\taters\ui\browse.py
418 419 420 421 422 423 424 425 426 427 428 429 430 | |
Showing progress¶
Turns the pipeline runner's event stream into stacked progress bars: one for the run, one for the current step, and one for each file being worked on.
taters.ui.run_display ¶
Live progress for a running pipeline.
The wizard used to print a line per step and then go quiet. For a single global step -- a whole spreadsheet scored in one call -- that meant a cursor sitting on an unchanging line for minutes, which is indistinguishable from a hang. The first thing a user does then is press Ctrl-C, which is the one thing guaranteed to waste the work.
So there are two bars, stacked:
- Overall -- steps finished out of steps planned. Always meaningful.
- Current step -- files finished out of files found, when the step fans out over inputs. A GLOBAL step is one call and cannot report its own internal progress, so it gets an elapsed-time spinner instead: no false precision, but visible proof of life.
run_preset already emits everything needed through its on_event callback,
so nothing in the runner changes to support this.
RunDisplay ¶
RunDisplay(console=None)
A rich progress display driven by run_preset events.
Used as a context manager. Failures are collected rather than printed as they happen: writing into the area a live display owns corrupts it, and a per-file error is better read at the end anyway, next to the count.
Source code in src\taters\ui\run_display.py
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | |
reporter_for ¶
reporter_for(display)
Adapt a display to the on_event signature run_preset expects.
Source code in src\taters\ui\run_display.py
617 618 619 620 621 622 623 | |
The live-region renderer¶
The default look of the taters command: a progress rail pinned above whatever
question is open, drawn inline so the terminal's scrollback survives the run.
taters.ui.live ¶
The application-style renderer: a live region above each question.
:class:~taters.ui.prompts.QuestionaryPrompter asks one question after another
and lets them scroll past, which reads like a shell script rather than a
program. This renderer keeps a progress rail pinned above whatever question
is currently open -- what you have answered, what you are answering, what is
still to come -- so the wizard feels like one application rather than a
sequence of prompts.
Why it is not a full-screen app
Taking over the terminal (the alt screen, like vim or htop) would make
the framing easier, and it would also throw the entire session away the moment
the wizard exits: no scrollback, nothing to copy, nothing to paste into a bug
report. So this renders inline, in the normal buffer. The rail is drawn as
part of the prompt's own layout, which means prompt_toolkit erases it when the
question is answered, leaving only questionary's one-line record of the answer
behind. Scroll up after a run and you see the questions and your answers, in
order, exactly as if they had been printed.
How it works
Every questionary question is a prompt_toolkit Application whose layout
is reachable as question.application.layout. :meth:LivePrompter._ask
wraps that layout in an HSplit with the rail on top and a key hint below,
then hands it back. questionary's own widgets -- and all of their editing,
filtering and validation behavior -- are untouched, which is the whole reason
this is a hundred lines instead of a thousand.
LivePrompter ¶
LivePrompter(title='Taters')
Bases: QuestionaryPrompter
Source code in src\taters\ui\live.py
201 202 203 204 205 206 207 | |
ticks_in_place
class-attribute
instance-attribute
¶
ticks_in_place = True
A :class:~taters.ui.prompts.Prompter that keeps a live progress rail.
Everything :class:~taters.ui.prompts.QuestionaryPrompter does for output
and input is inherited unchanged; this only adds the framing.
set_header ¶
set_header(header)
The banner to redraw at the top of every screen.
Accepts a string, or a zero-argument callable rendered at each paint. The callable is what makes the banner's slow border-color drift real: rendered once into a string, the drift was recomputed exactly once per session and the "animation" never visibly moved.
Source code in src\taters\ui\live.py
210 211 212 213 214 215 216 217 218 219 | |
note ¶
note(text, *, style='', wrap=True)
Print, and remember it for the next screen.
Each question wipes the screen, so a note printed between two questions would vanish before it had been read. Holding onto it until the next question has been answered is what lets "Found 412 .txt files" or "ffmpeg was not found" stay visible for exactly as long as it is about the thing on screen.
Source code in src\taters\ui\live.py
221 222 223 224 225 226 227 228 229 230 231 232 | |
table ¶
table(title, rows, headers)
Print a table, and remember it for the next screen.
Same reason as :meth:note, and the omission was worse here: a whole
setup report would be drawn, then wiped by the very next question,
leaving only the one-line advice underneath a header that suggested
nothing had been printed at all.
Source code in src\taters\ui\live.py
234 235 236 237 238 239 240 241 242 243 244 245 246 247 | |
repaint ¶
repaint()
Redraw the screen furniture without asking anything.
Source code in src\taters\ui\live.py
249 250 251 | |
stage ¶
stage(key, label, *, status='active', detail='')
Add or update one entry in the rail.
Updating in place (rather than appending) is what lets the wizard mark a stage done without knowing whether it had announced it before.
Source code in src\taters\ui\live.py
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 | |
reason ¶
reason(text)
Say why the next question is being asked -- next to the question.
Held rather than printed. A note goes to the console above the rail, so the sentence explaining a question ended up separated from it by the whole rail and every other note on the screen, in a color that read as more commentary. This one is drawn inside the question's own layout, immediately above it, and in a color that is meant to be caught.
Source code in src\taters\ui\live.py
430 431 432 433 434 435 436 437 438 439 440 | |
reset_stages ¶
reset_stages()
Forget the rail.
A rail belongs to the task that raised it. Nothing cleared it, so after the wizard finished -- or was backed out of -- its stages stayed on screen above the main menu, describing a pipeline that was no longer being built.
Source code in src\taters\ui\live.py
455 456 457 458 459 460 461 462 463 464 | |
select ¶
select(
question,
choices,
*,
default=None,
transient=False,
numbered=True,
toggle_values=(),
ticked=None,
navigate=None,
breadcrumb=None,
hint_override=None,
enter_gate=None,
cycle=None
)
Pick one option.
Numbered so a menu can be answered with a single digit as well as with the arrow keys -- and the digit both selects and confirms, where questionary's own shortcuts only move the cursor and still want enter.
A digit can only ever address nine things, so a list longer than that is not numbered at all. Numbering the first nine and leaving the rest bare -- what this used to do -- reads as a list that lost its numbers half way down, and a label reading "10." would advertise a key that does nothing. Either every row carries a number or none does.
Source code in src\taters\ui\live.py
621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 | |
checkbox ¶
checkbox(question, choices, *, cycle=None)
Multi-selection on ONE screen, in the one dialect every tick screen
uses: rows wear [x] boxes, [space] toggles a box in place, and
[enter] does exactly one thing -- confirm, and only while pointing at
the "✓ Done" row with at least one box ticked. Everywhere else,
enter is a no-op: the screen does not move, does not flash, and can
never carry a half-made selection forward.
This is the third iteration of this screen, each driven by a real user report. Stock questionary's enter proceeded with the empty set (dropping the row someone was pointing at); a reloop-per-toggle design fixed that but tore the prompt down on every enter, and the redraw read as "the screen changed" when it had not. The fix is a key-binding gate inside a SINGLE prompt: nothing is rebuilt, space flips marks in place, and enter is inert except on Done.
Source code in src\taters\ui\live.py
878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 | |
pause ¶
pause(message=PAUSE_MESSAGE)
Repaint the screen, then wait to be dismissed.
The paint is the whole reason for the override. Every other prompt gets
it from _ask, which pause deliberately skips -- so without this the
reader is left waiting at a screen the last clear() wiped.
Clearing the held notes afterwards matters just as much, and for the
same reason _ask does it: they have been read now. Without it a whole
setup report was redrawn on the next screen too, and the menu that
followed appeared underneath it, near the bottom of the terminal.
Source code in src\taters\ui\live.py
972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 | |
confirm ¶
confirm(question, *, default=True)
Yes or no, as a list you move through rather than a word you type.
questionary's own confirm renders as (Y/n) and waits on a text
buffer. A single y does answer it, but nothing on screen says so, so
it reads as "type a word and press enter" -- which is the one
interaction in the whole wizard that works differently from the rest.
This is an ordinary two-item selection, so arrow keys and enter behave
exactly as they do everywhere else, with y/n and 1/0 bound as
shortcuts for anyone who would rather not move at all.
Source code in src\taters\ui\live.py
989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 | |
working ¶
working(text)
Announce slow work before it starts, on screen immediately.
A note plus a repaint, which is the pair that guarantees the line is visible before the blocking call rather than after it: the live renderer's screen is wiped per question, and a note printed onto a just-finished screen without the repaint could be cleared before it was ever seen. Dim, because it is narration, not an answer.
Source code in src\taters\ui\prompts.py
322 323 324 325 326 327 328 329 330 331 332 333 | |
clear ¶
clear()
Start on a clean screen.
The scrollback is untouched -- this scrolls the screen rather than erasing history, so whatever the user had before is still there to page back to.
Source code in src\taters\ui\prompts.py
335 336 337 338 339 340 341 342 343 344 345 346 347 | |
note_width ¶
note_width()
How wide a note's text may be, in cells.
Source code in src\taters\ui\prompts.py
389 390 391 | |
glide_positions ¶
glide_positions(
start, target, *, seconds=GLIDE_SECONDS, fps=GLIDE_FPS
)
The pointer positions of a glide from start to target, one per
frame, ending exactly on the target.
Fixed duration, not fixed speed: a list of five hundred rows glides in the same two seconds as a list of forty, so the wait never grows with the list. Never more frames than rows -- a three-row glide is three frames -- and never fewer than one.
Source code in src\taters\ui\live.py
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | |