Skip to main content

max / audiofiles

Describe the tag classifier The largest file in the tree and the last workable one. Nine of its fifteen shapes took the body region and handed it back, so the five sections are `-> Vec<Node>` the screen splices with `extend`, and `unless` is `disabled when` in each control's own body for the third wave running. `busy` is a parameter beside each section's read rather than a field repeated on five structs, which works because `extend` takes one hole and a hole may be a call. The rule editor is what earned quasi-declare's `removes` and `repeats`: its four repeat controls were `Act::new(..)` in argument position, and the only other place to put them was the read, where the description stops saying their labels and their addresses. Two productions for forty-two declarations across this wave. `export` and `importing` earned none.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-09-04 23:59 UTC
Signed with PGP, not checked
Commit: f01b1a8c7dfca51d8cddfc0da35f16c37880a00a
Parent: 2e1e25c
2 files changed, +295 insertions, -177 deletions
M Cargo.lock +5 -5
@@ -4257,7 +4257,7 @@
4257 4257
4258 4258 [[package]]
4259 4259 name = "quasi-declare"
4260 - version = "0.1.5"
4260 + version = "0.1.6"
4261 4261 dependencies = [
4262 4262 "proc-macro2",
4263 4263 "quote",
@@ -7594,10 +7594,6 @@
7594 7594 name = "quasi-webview"
7595 7595 version = "0.101.1"
7596 7596
7597 - [[patch.unused]]
7598 - name = "quasi-type"
7599 - version = "0.1.3"
7600 -
7601 7597 [[patch.unused]]
7602 7598 name = "kberg"
7603 7599 version = "0.1.0"
@@ -7609,3 +7605,7 @@
7609 7605 [[patch.unused]]
7610 7606 name = "painhours"
7611 7607 version = "0.1.0"
7608 +
7609 + [[patch.unused]]
7610 + name = "quasi-type"
7611 + version = "0.1.3"
@@ -124,10 +124,10 @@
124 124 //! [`Slot::removes`]: quasi_router::Slot::removes
125 125 //! [`Slot::repeating`]: quasi_router::Slot::repeating
126 126
127 - use quasi_router::layout::{FieldKind, Readiness as Ready, Tone};
127 + use quasi_declare::declare;
128 + use quasi_router::layout::Tone;
128 129 use quasi_router::{
129 - Accepted, Act, Action, Choice, Field, Locating, Node, RegionKind, Repeating, Request, Response,
130 - RouteError, Router, Row, Screen, Slot, Sought, Tag,
130 + Accepted, Action, Choice, Locating, Request, Response, RouteError, Router, Sought, Tag,
131 131 };
132 132
133 133 use super::{Panels, Part, Shareable};
@@ -212,7 +212,7 @@
212 212
213 213 /// `GET /classifier`
214 214 fn index(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> {
215 - Ok(screen(state).into())
215 + Ok(showing(state))
216 216 }
217 217
218 218 /// `POST /classifier/rules/new`
@@ -639,7 +639,7 @@
639 639
640 640 /// The screen again, which is what every act here answers with.
641 641 fn settled(state: &Panels<'_>) -> Result<Response, RouteError> {
642 - Ok(screen(state).into())
642 + Ok(showing(state))
643 643 }
644 644
645 645 /// A captured name, refused when it is not there.
@@ -680,410 +680,394 @@
680 680 request.payload.get(name)?.parse().ok()
681 681 }
682 682
683 + // --- The screen --------------------------------------------------------------
683 684 // --- The screen --------------------------------------------------------------
684 685
685 - /// The whole window.
686 - fn screen(state: &Panels<'_>) -> Screen {
687 - let mut body = Slot::new(BODY, RegionKind::Pane).with(Node::page("Tag classifier"));
686 + /// The screen, read and then described.
687 + fn showing(state: &Panels<'_>) -> Response {
688 + Response::from(screen(&read(state)))
689 + }
688 690
689 - // What the shipped section put above everything for the same reason: the
690 - // footer status line was hidden behind the Settings modal, so a running job
691 - // and a failed one had to say so here.
692 - if let Some(doing) = state.classifier.busy() {
693 - body = body.with(Node::StandIn {
694 - state: Ready::Pending,
695 - message: doing,
696 - act: None,
697 - });
698 - } else if let Some(why) = state.classifier.failed() {
699 - body = body.with(Node::banner(Tone::Warning, why));
700 - }
701 -
702 - body = rules(body, state);
703 - body = autotag(body, state);
704 - body = clusters(body, state);
705 - body = folders(body, state);
706 - body = sharing(body, state);
707 -
708 - Screen::sidebar_content("Tag classifier").with(body)
691 + /// The whole window: five sections, and the rules editor that takes one over.
692 + struct Classifying {
693 + /// What is running, while something is.
694 + ///
695 + /// What the shipped section put above everything for the same reason: the
696 + /// footer status line was hidden behind the Settings modal, so a running job
697 + /// and a failed one had to say so here.
698 + doing: Option<String>,
699 + /// What went wrong, while nothing is running.
700 + failed: Option<String>,
701 + /// Whether anything is running, which deadens most of this screen.
702 + busy: bool,
703 + rules: Rules,
704 + autotag: Autotag,
705 + clusters: Clusters,
706 + folders: Folders,
707 + sharing: Shared,
709 708 }
710 709
711 710 /// Tag Rules: the list, or the editor when one is open.
712 - fn rules(body: Slot, state: &Panels<'_>) -> Slot {
713 - let body = body.with(Node::section("Tag Rules")).with(Node::text(
714 - "Deterministic rules that auto-apply tags by sample metadata and audio features. Rules never remove tags you added by hand.",
715 - ));
711 + struct Rules {
712 + /// The rules as they stand, while none is being authored.
713 + listing: Option<RuleList>,
714 + /// The rule being authored, in place of the list.
715 + editor: Option<Editor>,
716 + }
716 717
717 - match state.classifier.authoring() {
718 - Some(draft) => editor(body, state, &draft),
719 - None => rule_list(body, state),
718 + /// The rules as they stand.
719 + struct RuleList {
720 + /// What the last apply changed, where there has been one.
721 + last: Option<String>,
722 + /// Whether there are no rules at all.
723 + bare: bool,
724 + each: Vec<Listed>,
725 + }
726 +
727 + /// One rule in the list.
728 + struct Listed {
729 + id: String,
730 + /// What it is called, or that it is not.
731 + name: String,
732 + /// How much it tests and how much it does.
733 + meta: String,
734 + enabled: bool,
735 + /// Whether it is already the first, and whether it is already the last.
736 + first: bool,
737 + last: bool,
738 + /// What deleting it takes with it, and what it does not.
739 + confirm: String,
740 + }
741 +
742 + /// The rule being authored.
743 + struct Editor {
744 + name: String,
745 + /// Why the name was refused, while it was.
746 + ///
747 + /// Gated on a refusal rather than on emptiness: a freshly opened editor
748 + /// complaining about a name nobody has typed is scolding an empty form.
749 + problem: Option<&'static str>,
750 + enabled: &'static str,
751 + /// Whether every condition must hold, or any one of them.
752 + matching: &'static str,
753 + conditions: Vec<Condition>,
754 + actions: Vec<Doing>,
755 + /// What the last test counted, where there has been one.
756 + matched: Option<String>,
757 + }
758 +
759 + /// One condition, as three questions grouped.
760 + struct Condition {
761 + /// Its position, which is its region's name and its address.
762 + at: usize,
763 + /// Every field a rule may test.
764 + fields: Vec<Choice>,
765 + field: String,
766 + /// Only the comparisons this field allows, which is the whole reason the
767 + /// app hands over the vocabulary rather than the screen holding it.
768 + ops: Vec<Choice>,
769 + op: String,
770 + /// The operand, only where the comparison wants one.
771 + ///
772 + /// `op_needs_value` said in the description instead of enforced while
773 + /// drawing.
774 + value: Option<String>,
775 + }
776 +
777 + /// One action, as two questions grouped.
778 + struct Doing {
779 + /// Its position, which is its region's name and its address.
780 + at: usize,
781 + kind: String,
782 + /// The tag, except where the action is Stop.
783 + tag: Option<String>,
784 + }
785 +
786 + /// Auto-Tagging: the pass, the review door, the head, and the thresholds.
787 + struct Autotag {
788 + /// What the last run applied, where there has been one.
789 + last: Option<String>,
790 + /// What the review control reads while a queue is already waiting.
791 + ///
792 + /// Re-entry without a rescan: the queue outlives the screen, and a pass over
793 + /// a large library costs seconds.
794 + reopen: Option<String>,
795 + /// What the last review said, where there has been one.
796 + reviewed: Option<String>,
797 + head: Head,
798 + thresholds: Thresholds,
799 + }
800 +
801 + /// The optional trained head.
802 + struct Head {
803 + /// What it holds, or what training one would be worth.
804 + ///
805 + /// The size hint is only worth showing while there is no model, which is
806 + /// why one field answers both.
807 + said: Option<String>,
808 + /// What the training control reads, which is where it says whether there is
809 + /// already a model.
810 + train: &'static str,
811 + /// Whether there is a model to clear.
812 + trained: bool,
813 + }
814 +
815 + /// The per-tag thresholds.
816 + struct Thresholds {
817 + each: Vec<Threshold>,
818 + /// The tag being named for a threshold it does not have yet.
819 + typing: String,
820 + }
821 +
822 + /// One tag's two thresholds.
823 + struct Threshold {
824 + tag: String,
825 + review: String,
826 + auto: String,
827 + }
828 +
829 + /// Clustering: the cold-start grouping.
830 + struct Clusters {
831 + /// How many groups to ask for.
832 + k: String,
833 + piles: Vec<Pile>,
834 + }
835 +
836 + /// One group of similar samples.
837 + struct Pile {
838 + /// Its position, which is its region's name and its address.
839 + at: usize,
840 + /// How many samples are in it.
841 + said: String,
842 + name: String,
843 + /// Whether its representative is still there.
844 + ///
845 + /// The representative can be gone: deleting a sample must not take the pile
846 + /// or its name with it.
847 + playable: bool,
848 + /// Whether it has been named, which is what wakes Tag.
849 + named: bool,
850 + }
851 +
852 + /// Folder Tags: turn directories that hold samples into tags.
853 + struct Folders {
854 + /// Whether a scan happened and found nothing.
855 + ///
856 + /// Never scanned is not the same as a scan that found nothing, and the
857 + /// shipped section drew neither in that state, so this is false in both the
858 + /// unscanned case and the case with folders to show.
859 + bare: bool,
860 + each: Vec<Harvested>,
861 + }
862 +
863 + /// One folder that directly holds samples.
864 + struct Harvested {
865 + /// Its position, which is its region's name and its address.
866 + at: usize,
867 + /// What it is called, and how much is in it.
868 + said: String,
869 + tag: String,
870 + /// Whether a tag has been typed, which is what wakes Apply.
871 + named: bool,
872 + }
873 +
874 + /// Shared Classifiers: export, import, and the layers an import leaves.
875 + struct Shared {
876 + /// What the export file will be called.
877 + name: String,
878 + /// The three parts, each ticked or not.
879 + parts: Vec<Included>,
880 + /// Whether there is anything worth writing.
881 + worth: bool,
882 + /// What the last export said, where there has been one.
883 + exported: Option<String>,
884 + /// What the last import said, where there has been one.
885 + imported: Option<String>,
886 + layers: Vec<Layer>,
887 + }
888 +
889 + /// One kind of thing an export may carry.
890 + struct Included {
891 + name: &'static str,
892 + label: &'static str,
893 + /// On or off, as the control carries it.
894 + value: &'static str,
895 + /// Said rather than silently contributing nothing: a part that is ticked
896 + /// and does not exist is why `worth_writing` asks about both.
897 + hint: Option<&'static str>,
898 + }
899 +
900 + /// One imported layer.
901 + struct Layer {
902 + id: String,
903 + name: String,
904 + /// What it brought with it.
905 + said: String,
906 + enabled: &'static str,
907 + /// How much it counts next to your own tagging.
908 + weight: String,
909 + }
910 +
911 + /// What the screen draws, read off the app.
912 + fn read(state: &Panels<'_>) -> Classifying {
913 + let doing = state.classifier.busy();
914 + let busy = doing.is_some();
915 + Classifying {
916 + // A failure is only worth reporting while nothing is running, which is
917 + // what the shipped section's `else if` said.
918 + failed: (!busy).then(|| state.classifier.failed()).flatten(),
919 + doing,
920 + busy,
921 + rules: rules_read(state),
922 + autotag: autotag_read(state),
923 + clusters: clusters_read(state),
924 + folders: folders_read(state),
925 + sharing: sharing_read(state),
926 + }
927 + }
928 +
929 + /// The rules, or the one being authored in place of them.
930 + fn rules_read(state: &Panels<'_>) -> Rules {
931 + let authoring = state.classifier.authoring();
932 + Rules {
933 + listing: authoring.is_none().then(|| rule_list_read(state)),
934 + editor: authoring.map(|draft| editor_read(state, &draft)),
720 935 }
721 936 }
722 937
723 938 /// The rules as they stand.
724 - fn rule_list(body: Slot, state: &Panels<'_>) -> Slot {
725 - let busy = state.classifier.busy().is_some();
726 - let mut body = body
727 - .with(Node::Act(unless(
728 - Act::new("New rule", Action::post("/classifier/rules/new")),
729 - busy,
730 - )))
731 - .with(Node::text(
732 - "Re-evaluate every rule across the whole library.",
733 - ))
734 - .with(Node::Act(unless(
735 - Act::new("Apply rules now", Action::post("/classifier/rules/apply")),
736 - busy,
737 - )))
738 - .with(Node::text(
739 - "Starter rules cover the common instrument and format words (Kick.wav becomes instrument.drum.kick). They arrive disabled: review them, then enable the ones you want.",
740 - ))
741 - .with(Node::Act(unless(
742 - Act::new("Add starter rules", Action::post("/classifier/rules/starter")),
743 - busy,
744 - )));
745 -
746 - if let Some(changed) = state.classifier.last_apply() {
747 - body = body.with(Node::text(format!(
748 - "Last apply: {changed} sample{} updated",
749 - plural(changed),
750 - )));
751 - }
752 -
939 + fn rule_list_read(state: &Panels<'_>) -> RuleList {
753 940 let all = state.classifier.rules();
754 - if all.is_empty() {
755 - return body.with(Node::empty(
756 - "No rules yet. New rules start empty; you decide what gets tagged.",
757 - ));
941 + RuleList {
942 + last: state
943 + .classifier
944 + .last_apply()
945 + .map(|changed| format!("Last apply: {changed} sample{} updated", plural(changed))),
946 + bare: all.is_empty(),
947 + each: all
948 + .iter()
949 + .map(|rule| {
950 + let named = if rule.name.trim().is_empty() {
951 + "(unnamed)"
952 + } else {
953 + rule.name.trim()
954 + };
955 + Listed {
956 + id: rule.id.clone(),
957 + name: named.to_owned(),
958 + meta: format!("{} cond \u{2192} {} act", rule.conditions, rule.actions),
959 + enabled: rule.enabled,
960 + first: rule.first,
961 + last: rule.last,
962 + confirm: format!(
963 + "Delete the rule \"{named}\"? Tags it already applied stay where they are."
964 + ),
965 + }
966 + })
967 + .collect(),
758 968 }
759 -
760 - let rows = all
761 - .iter()
762 - .map(|rule| {
763 - let named = !rule.name.trim().is_empty();
764 - let mut row = Row::new(if named { &rule.name } else { "(unnamed)" })
765 - .meta(format!(
766 - "{} cond \u{2192} {} act",
767 - rule.conditions, rule.actions
768 - ))
769 - .toggling(
770 - rule.enabled,
771 - Action::post(format!("/classifier/rules/{}/enabled", rule.id)),
772 - );
773 - if !rule.enabled {
774 - row = row.token(Tag::badge("off"));
775 - }
776 - row.act(Act::new(
777 - "Edit",
778 - Action::post(format!("/classifier/rules/{}/edit", rule.id)),
779 - ))
780 - .act(unless(
781 - Act::new(
782 - "Earlier",
783 - Action::post(format!("/classifier/rules/{}/up", rule.id)),
784 - ),
785 - rule.first,
786 - ))
787 - .act(unless(
788 - Act::new(
789 - "Later",
790 - Action::post(format!("/classifier/rules/{}/down", rule.id)),
791 - ),
792 - rule.last,
793 - ))
794 - .act(
795 - Act::new(
796 - "Delete",
797 - Action::post(format!("/classifier/rules/{}/delete", rule.id)),
798 - )
799 - .tone(Tone::Danger)
800 - .confirm(format!(
801 - "Delete the rule \"{}\"? Tags it already applied stay where they are.",
802 - if named { &rule.name } else { "(unnamed)" },
803 - )),
804 - )
805 - })
806 - .collect();
807 -
808 - body.with(Node::List { rows, more: None })
809 969 }
810 970
811 - /// The rule being authored, in place of the list.
812 - fn editor(body: Slot, state: &Panels<'_>, draft: &super::Authoring) -> Slot {
813 - let mut name = Field::new(FieldKind::Text, "name", "Name")
814 - .value(draft.name.clone())
815 - .writes(Action::post("/classifier/draft/name"));
816 - name.required = true;
817 - name.placeholder = Some("Kick drums".to_owned());
818 - // Gated on a refusal rather than on emptiness: a freshly opened editor
819 - // complaining about a name nobody has typed is scolding an empty form.
820 - if draft.refused && draft.name.trim().is_empty() {
821 - name = name.error("Name the rule before saving.");
822 - }
823 -
824 - let mut body = body
825 - .with(Node::Field(Box::new(name)))
826 - .with(Node::Field(Box::new(
827 - Field::new(FieldKind::Checkbox, "enabled", "Enabled")
828 - .value(if draft.enabled { "on" } else { "" })
829 - .writes(Action::post("/classifier/draft/enabled")),
830 - )))
831 - .with(Node::Field(Box::new(
832 - Field::radio(
833 - "match",
834 - "Match",
835 - vec![
836 - Choice::new("all", "All of these conditions"),
837 - Choice::new("any", "Any of these conditions"),
838 - ],
839 - )
840 - .value(if draft.all { "all" } else { "any" })
841 - .writes(Action::post("/classifier/draft/match")),
842 - )))
843 - .with(Node::section("When"));
844 -
845 - // The conditions and the actions, each as slots of one repeating question.
846 - // quasicoherent `f7abbc08`: the regions were always here and nothing said
847 - // they were slots, so no renderer could number them and "at least one
848 - // condition" was this file disabling its own last Remove.
849 - //
850 - // `least(1)` is that rule, said once. `least(0)` on the actions is the
851 - // honest answer for them: a rule with no actions is describable and the
852 - // editor has never stopped anyone writing one.
971 + /// The rule being authored, read off the draft.
972 + fn editor_read(state: &Panels<'_>, draft: &super::Authoring) -> Editor {
853 973 let testable = state.classifier.testable();
854 - let mut conditions = Slot::new("conditions", RegionKind::Group).repeating(
855 - Repeating::new(
856 - "Condition",
857 - Act::new(
858 - "Add condition",
859 - Action::post("/classifier/draft/conditions/add"),
860 - ),
861 - )
862 - .least(1),
863 - );
864 - for (at, condition) in draft.conditions.iter().enumerate() {
865 - conditions = conditions.with(condition_region(at, condition, &testable));
974 + Editor {
975 + name: draft.name.clone(),
976 + problem: (draft.refused && draft.name.trim().is_empty())
977 + .then_some("Name the rule before saving."),
978 + enabled: switched(draft.enabled),
979 + matching: if draft.all { "all" } else { "any" },
980 + conditions: draft
981 + .conditions
982 + .iter()
983 + .enumerate()
Lines truncated