File size: 279,104 Bytes
4eb2e64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b527d78
4eb2e64
34e63ff
c62a604
4eb2e64
 
 
 
 
 
 
 
 
 
 
 
ffc2106
40f7998
b527d78
6a2e4ce
c168285
addf569
de0692a
6a2e4ce
baba9d1
34e63ff
b527d78
d45d7f9
4eb2e64
f9fec79
42c1f5f
 
1b5cbdc
 
4eb2e64
9fef789
b345806
4eb2e64
 
de0692a
4eb2e64
 
 
 
 
 
 
 
 
 
 
 
 
 
fea940a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9fef789
 
 
 
 
 
 
 
 
 
fea940a
 
 
 
 
 
 
 
 
 
 
 
b527d78
 
 
 
 
 
 
 
 
 
 
 
 
0ddeb3b
 
7a27d01
 
 
 
0ddeb3b
 
4eb2e64
 
 
34e63ff
4eb2e64
 
 
 
1b8d4ec
 
 
 
 
 
 
 
 
4eb2e64
34e63ff
 
 
 
 
 
 
 
 
 
 
 
 
 
4eb2e64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a4efe63
b527d78
a4efe63
1a50976
 
 
 
 
 
 
c62a604
1a50976
 
 
 
10c6485
1a50976
 
10c6485
 
 
 
1a50976
 
 
 
 
 
 
 
 
 
a4efe63
a0b8f0c
b527d78
 
 
a0b8f0c
 
 
 
 
 
a4efe63
10c6485
 
 
 
 
a4efe63
 
 
1a50976
 
 
 
 
 
 
 
a4efe63
b527d78
 
 
 
 
 
 
 
 
 
 
14be7b9
 
 
 
 
 
 
 
f9fec79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14be7b9
 
f9fec79
14be7b9
 
b527d78
 
bd54f8a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f9fec79
 
 
 
bd54f8a
 
 
f9fec79
bd54f8a
 
 
daaa1b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c62a604
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a4efe63
e9e62d7
a4efe63
 
 
 
 
 
e9e62d7
a4efe63
e9e62d7
 
 
 
 
 
 
 
 
 
 
 
b0688e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a4efe63
 
 
 
 
c62a604
 
 
 
 
 
a4efe63
c62a604
a4efe63
 
b0688e1
a4efe63
 
 
 
 
c62a604
 
 
 
a4efe63
 
4eb2e64
 
 
 
 
 
 
 
 
 
 
 
b345806
 
 
 
 
 
 
4eb2e64
 
 
34e63ff
4eb2e64
 
b527d78
 
 
 
 
 
 
4eb2e64
 
 
 
 
 
b527d78
b345806
 
b527d78
 
 
4eb2e64
b345806
4eb2e64
 
 
 
1b5cbdc
 
 
 
 
 
 
 
 
 
 
 
34e63ff
1b5cbdc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9dd435f
1b5cbdc
 
 
 
 
d45d7f9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b8d4ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d45d7f9
1b8d4ec
d45d7f9
 
 
1b8d4ec
 
 
 
 
 
 
d45d7f9
 
 
 
1b8d4ec
bd54f8a
 
 
 
 
 
 
 
 
 
1b8d4ec
 
 
 
bd54f8a
 
 
 
 
 
 
 
1b8d4ec
 
 
bd54f8a
 
1b8d4ec
 
 
d45d7f9
1b8d4ec
 
 
 
 
 
 
 
 
 
e346b01
 
 
 
 
 
 
1b8d4ec
e346b01
1b8d4ec
 
 
 
 
 
 
 
 
 
e346b01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bd54f8a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e346b01
 
 
1b8d4ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d45d7f9
1b8d4ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d45d7f9
 
 
1b8d4ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d45d7f9
 
bd54f8a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b8d4ec
 
 
 
 
 
 
 
bd54f8a
 
 
1b8d4ec
 
d45d7f9
bd54f8a
1b8d4ec
bd54f8a
1b8d4ec
 
 
 
 
bd54f8a
1b8d4ec
 
 
 
 
 
 
bd54f8a
1b8d4ec
 
 
d45d7f9
1b8d4ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d45d7f9
4eb2e64
 
9dd435f
 
 
addf569
 
 
 
 
 
9dd435f
addf569
 
b345806
 
 
 
 
 
 
 
 
4eb2e64
 
 
 
b345806
4eb2e64
 
 
 
14be7b9
 
 
 
 
 
 
411deaa
 
 
 
b527d78
411deaa
 
4eb2e64
 
 
a4efe63
b527d78
a4efe63
 
b527d78
 
a4efe63
e9e62d7
 
 
 
b527d78
 
 
addf569
 
 
 
 
 
b527d78
 
 
411deaa
 
 
 
 
 
 
 
a4efe63
 
 
 
411deaa
addf569
411deaa
 
 
 
a4efe63
 
b527d78
 
e9e62d7
 
 
 
 
 
f9fec79
e9e62d7
addf569
f9fec79
addf569
e9e62d7
 
a4efe63
e9e62d7
 
 
 
 
 
 
b527d78
 
e9e62d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b527d78
e9e62d7
 
fea940a
 
 
 
a4efe63
 
021839a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4eb2e64
 
b345806
 
 
021839a
 
 
 
fea940a
021839a
fea940a
021839a
 
 
 
 
 
4eb2e64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34e63ff
4eb2e64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bd54f8a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
daaa1b6
 
 
 
 
 
 
 
 
 
 
bd54f8a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f9fec79
bd54f8a
 
 
 
f9fec79
 
 
 
 
bd54f8a
 
 
 
 
 
 
 
 
f9fec79
 
 
 
 
 
 
 
 
 
 
 
bd54f8a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f9fec79
 
 
 
 
 
 
 
 
 
 
 
 
 
9fef789
 
 
 
 
 
 
 
49b00dd
 
 
 
 
 
9fef789
 
bd54f8a
9fef789
 
 
 
 
476275f
 
 
 
 
 
 
 
9fef789
 
 
 
 
 
 
 
 
 
 
bd54f8a
 
 
 
 
 
 
 
 
 
7a27d01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0fdef1c
 
 
 
 
 
 
7a27d01
 
 
 
 
 
 
 
 
 
 
0fdef1c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f5beb63
 
0fdef1c
8e787d7
 
 
0fdef1c
 
 
8e787d7
 
 
 
 
7a27d01
bd54f8a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f5beb63
 
 
 
 
 
 
 
0fdef1c
f5beb63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0fdef1c
8e787d7
 
 
 
 
 
 
 
 
f5beb63
 
 
 
 
0fdef1c
f5beb63
 
8e787d7
f5beb63
 
 
 
8e787d7
 
f5beb63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8e787d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f5beb63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8e787d7
 
 
 
 
 
 
 
 
f5beb63
0fdef1c
f5beb63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8e787d7
 
 
 
 
f5beb63
8e787d7
 
 
 
 
f5beb63
0fdef1c
 
 
 
8e787d7
 
 
 
 
0fdef1c
f5beb63
8e787d7
 
f5beb63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8e787d7
 
 
 
f5beb63
8e787d7
 
0fdef1c
 
 
8e787d7
f5beb63
 
8e787d7
f5beb63
 
0fdef1c
 
 
 
 
f5beb63
0fdef1c
f5beb63
 
 
 
8e787d7
 
 
0fdef1c
f5beb63
 
 
 
 
8e787d7
 
 
 
0fdef1c
 
 
f5beb63
 
 
 
 
 
 
 
 
0fdef1c
 
 
 
 
 
 
f5beb63
 
0fdef1c
f5beb63
8e787d7
 
0fdef1c
 
8e787d7
0fdef1c
 
 
 
8e787d7
f5beb63
8e787d7
 
0fdef1c
8e787d7
 
f5beb63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4eb2e64
 
 
 
 
 
 
 
e346b01
 
4eb2e64
 
 
fa6e5e5
 
 
 
 
 
c62a604
 
 
 
 
b527d78
c62a604
b0688e1
 
b527d78
 
b0688e1
 
b527d78
 
 
 
 
 
b0688e1
b527d78
4eb2e64
bd54f8a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4eb2e64
a4efe63
b527d78
a4efe63
 
b527d78
 
 
 
 
 
a4efe63
b527d78
 
 
 
 
 
 
 
 
a4efe63
b527d78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a4efe63
b527d78
 
 
bd54f8a
fea940a
 
bd54f8a
42c1f5f
bd54f8a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6cd34e4
a4efe63
bd54f8a
 
 
a4efe63
 
 
 
 
 
 
bd54f8a
 
10c6485
 
 
 
 
 
 
b527d78
 
bd54f8a
 
 
 
 
10c6485
 
 
b527d78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fea940a
 
 
 
 
 
 
 
 
bd54f8a
f9fec79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bd54f8a
f9fec79
 
fea940a
 
1a50976
 
 
 
 
 
a4efe63
 
c62a604
10c6485
 
 
 
 
 
 
 
1a50976
 
 
 
 
 
 
 
 
 
 
c62a604
1a50976
 
 
 
 
 
 
 
10c6485
1a50976
c62a604
1a50976
b527d78
1a50976
 
 
 
 
 
 
 
 
 
f9fec79
 
 
 
 
 
1a50976
 
 
b527d78
1a50976
b527d78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bd54f8a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b527d78
 
 
c62a604
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b527d78
 
 
 
 
 
 
 
 
 
 
a4efe63
 
bd54f8a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b527d78
 
021839a
b527d78
021839a
 
 
 
 
bd54f8a
b527d78
021839a
f9fec79
 
 
 
 
14be7b9
 
 
 
 
 
 
 
 
 
 
 
 
daaa1b6
 
 
 
 
 
 
 
14be7b9
 
 
 
 
 
 
daaa1b6
 
f9fec79
 
daaa1b6
b527d78
f9fec79
 
b527d78
021839a
 
 
 
 
 
 
 
f9fec79
021839a
 
 
 
 
 
 
71a213d
 
 
 
 
 
 
 
b527d78
71a213d
021839a
 
 
 
 
 
71a213d
021839a
 
 
 
 
 
 
 
 
 
 
 
 
fea940a
021839a
71a213d
fea940a
 
 
 
 
71a213d
fea940a
 
 
021839a
fea940a
 
021839a
 
 
 
 
 
 
bd54f8a
021839a
 
 
 
 
f9fec79
021839a
b527d78
c55bcb6
476275f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b527d78
 
 
476275f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b527d78
 
 
 
 
 
 
 
476275f
 
 
 
 
 
 
 
 
 
71a213d
 
 
 
 
 
 
 
 
 
 
 
476275f
 
 
 
 
 
 
 
fea940a
71a213d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fea940a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71a213d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
021839a
c55bcb6
 
 
 
 
021839a
c55bcb6
 
 
 
 
 
 
 
 
 
 
021839a
 
 
bd54f8a
021839a
c55bcb6
 
 
 
 
 
 
021839a
 
 
4eb2e64
0ddeb3b
 
 
 
 
 
 
 
c49c94d
 
 
 
 
 
 
0fdef1c
 
 
 
 
 
 
 
 
 
 
0ddeb3b
0fdef1c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b2b28b5
0fdef1c
 
 
f5beb63
 
 
0ddeb3b
f5beb63
b2b28b5
 
 
 
f5beb63
 
 
 
 
 
 
 
 
 
 
b2b28b5
 
 
 
 
 
 
 
 
0ddeb3b
b527d78
 
 
 
 
bd54f8a
b527d78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4eb2e64
 
e89deea
 
 
 
 
 
 
 
 
4eb2e64
 
 
 
 
021839a
4eb2e64
 
 
e737b41
 
 
 
 
 
 
 
021839a
e737b41
 
 
 
a4efe63
 
 
 
 
b527d78
a4efe63
 
 
b527d78
a4efe63
 
 
 
b527d78
a4efe63
d45d7f9
a4efe63
d45d7f9
 
bd54f8a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a4efe63
bd54f8a
 
a4efe63
 
e9e62d7
d45d7f9
1a50976
 
a4efe63
 
 
 
 
 
 
 
 
e9e62d7
 
 
a4efe63
 
 
 
 
 
 
 
 
 
 
 
9c0a0df
4eb2e64
13fc71f
9c0a0df
17de1bb
4eb2e64
9c0a0df
 
fff8133
4eb2e64
1b5cbdc
 
4eb2e64
 
42c1f5f
 
 
 
 
 
 
 
 
1b5cbdc
 
 
 
 
42c1f5f
 
17de1bb
42c1f5f
 
 
 
fff8133
42c1f5f
 
 
 
 
 
 
 
 
13fc71f
9c0a0df
17de1bb
4eb2e64
42c1f5f
 
 
 
 
 
 
 
 
 
 
 
 
021839a
 
 
 
 
 
 
 
 
 
42c1f5f
 
 
 
 
021839a
42c1f5f
 
 
021839a
42c1f5f
021839a
 
4eb2e64
13fc71f
 
 
 
 
 
1b5cbdc
 
 
 
 
 
13fc71f
 
4eb2e64
9fef789
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b345806
9fef789
 
 
 
 
fea940a
9fef789
fea940a
 
 
b345806
 
9fef789
 
 
 
b345806
 
9fef789
 
 
 
b345806
 
 
9fef789
 
 
 
b345806
 
fff8133
 
 
 
 
 
 
 
 
b527d78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6a2e4ce
f4fbfbb
1b8d4ec
40f7998
c168285
6a2e4ce
 
 
 
9dd435f
f5e4044
0de015d
3e4fa4d
 
 
b527d78
3e4fa4d
40f7998
 
 
c168285
fa6e5e5
 
 
 
 
 
5b36642
 
 
 
 
4a9a298
 
 
 
 
 
 
 
 
 
 
5b36642
 
1b8d4ec
 
 
 
baba9d1
 
 
 
 
9dd456d
 
 
 
f4fbfbb
ffc2106
6a2e4ce
 
 
9dd456d
ffc2106
3e4fa4d
40f7998
c168285
6a2e4ce
 
 
ffc2106
c168285
 
9dd435f
 
 
 
 
2b16378
 
fff8133
5b36642
 
 
 
9dd435f
 
 
 
fa6e5e5
9dd435f
baba9d1
 
ffc2106
1b8d4ec
3e4fa4d
40f7998
c168285
6a2e4ce
 
 
ffc2106
c168285
 
d45d7f9
 
 
 
 
 
 
 
6a2e4ce
 
d45d7f9
 
 
 
6a2e4ce
 
 
 
d45d7f9
 
6a2e4ce
fff8133
6a2e4ce
 
 
 
 
 
 
 
c179977
9dd456d
ffc2106
1b8d4ec
3e4fa4d
40f7998
c168285
c179977
 
c62a604
 
 
 
 
 
34e63ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e9e62d7
 
 
 
a4efe63
c62a604
 
 
 
 
 
 
 
 
 
 
 
 
 
b0688e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a4efe63
 
 
 
 
 
 
 
 
 
 
 
34e63ff
 
 
c179977
34e63ff
 
 
 
 
c179977
 
 
 
 
 
 
 
 
 
ffc2106
40f7998
c168285
34e63ff
 
 
de0692a
34e63ff
baba9d1
 
 
 
 
de0692a
 
 
34e63ff
 
addf569
 
 
 
 
 
 
b0688e1
 
 
 
 
 
 
 
 
 
 
 
 
34e63ff
 
b0688e1
 
 
 
 
 
 
 
 
 
 
34e63ff
c62a604
 
 
 
 
 
 
34e63ff
 
 
 
 
c62a604
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e9e62d7
 
 
c62a604
e9e62d7
 
 
c62a604
 
34e63ff
 
 
 
c62a604
 
34e63ff
a4efe63
b0688e1
a4efe63
34e63ff
02fee20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34e63ff
02fee20
 
 
 
34e63ff
addf569
 
34e63ff
 
 
 
 
 
 
 
 
 
c62a604
34e63ff
 
 
 
 
 
f9fec79
34e63ff
c62a604
 
 
a4efe63
 
 
 
 
 
 
 
 
 
b0688e1
a4efe63
34e63ff
 
 
 
 
c62a604
 
 
 
 
34e63ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d1a4c6c
 
 
 
 
c62a604
 
 
34e63ff
 
 
 
f9fec79
c62a604
 
34e63ff
b0688e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c179977
 
 
 
a4efe63
 
b0688e1
34e63ff
 
 
 
b0688e1
 
 
 
 
 
 
 
 
 
 
 
 
c179977
34e63ff
a4efe63
 
34e63ff
 
c62a604
34e63ff
 
addf569
 
 
 
 
 
 
 
 
 
 
 
 
34e63ff
addf569
 
 
 
 
 
 
f9fec79
 
addf569
 
 
 
 
 
b527d78
addf569
 
c179977
f9fec79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e9e62d7
f9fec79
e9e62d7
 
f9fec79
e9e62d7
b0688e1
 
 
 
 
 
 
f9fec79
 
 
 
b0688e1
 
f9fec79
b0688e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c62a604
 
 
 
 
 
 
 
 
 
 
b527d78
1a50976
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a4efe63
c168285
1a50976
b527d78
 
a4efe63
 
1a50976
 
 
b527d78
 
 
a4efe63
 
ffc2106
a4efe63
 
ffc2106
a4efe63
 
c168285
1a50976
b527d78
a4efe63
 
1a50976
 
b527d78
 
c168285
a4efe63
ffc2106
1a50976
 
b527d78
 
ffc2106
b527d78
10c6485
b527d78
d45d7f9
1a50976
 
 
10c6485
 
b527d78
 
 
ffc2106
 
 
1a50976
 
b527d78
 
1a50976
c168285
 
ffc2106
49b00dd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4eb2e64
 
 
 
49b00dd
 
 
 
 
4eb2e64
49b00dd
 
4eb2e64
e737b41
 
49b00dd
4eb2e64
fea940a
 
 
 
 
 
9fef789
 
 
 
 
 
 
 
fea940a
 
 
9fef789
 
fea940a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b527d78
14be7b9
 
 
 
 
 
 
 
b527d78
 
 
14be7b9
b527d78
14be7b9
b527d78
14be7b9
 
 
b527d78
 
 
 
 
021839a
b527d78
14be7b9
 
 
b527d78
4eb2e64
 
 
 
e737b41
4eb2e64
 
 
 
 
e737b41
4eb2e64
f9fec79
fff8133
 
f9fec79
 
 
 
 
 
 
 
 
 
 
 
 
 
b527d78
 
 
 
 
 
 
476275f
 
 
 
 
 
 
 
 
 
 
4eb2e64
476275f
 
b527d78
 
 
 
476275f
b527d78
 
 
476275f
9c0a0df
fff8133
f9fec79
fff8133
b527d78
f9fec79
 
b527d78
 
f9fec79
 
 
 
b527d78
f9fec79
 
 
476275f
 
4eb2e64
c55bcb6
fea940a
 
 
 
4eb2e64
 
fea940a
 
 
 
 
 
c55bcb6
e737b41
fea940a
c55bcb6
4eb2e64
c55bcb6
 
4eb2e64
c55bcb6
 
4eb2e64
1b5cbdc
 
 
 
 
 
 
 
 
 
 
 
49b00dd
 
 
 
 
 
 
 
 
 
 
 
 
1b5cbdc
 
 
 
49b00dd
 
 
 
 
1b5cbdc
fa6e5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4eb2e64
 
1b5cbdc
 
49b00dd
1b5cbdc
49b00dd
 
 
4eb2e64
1b5cbdc
 
 
 
 
 
 
 
 
 
 
476275f
1b5cbdc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
021839a
1b5cbdc
 
 
49b00dd
1b5cbdc
 
 
 
4eb2e64
021839a
 
 
 
4eb2e64
 
021839a
 
4eb2e64
021839a
4eb2e64
a4985fd
 
 
 
 
 
021839a
 
a4985fd
 
 
021839a
a4985fd
a4efe63
 
 
b527d78
 
 
 
 
 
 
 
 
 
 
fea940a
 
 
 
 
1a50976
fea940a
 
a4efe63
1a50976
fea940a
 
 
a4efe63
 
0ddeb3b
 
 
 
 
0fdef1c
 
b2b28b5
0fdef1c
 
 
 
 
b2b28b5
 
0fdef1c
 
 
 
b2b28b5
 
 
0fdef1c
b2b28b5
 
 
0fdef1c
 
 
b2b28b5
8e787d7
 
 
 
 
 
f5beb63
0fdef1c
0ddeb3b
 
 
 
 
 
 
 
 
 
0fdef1c
 
 
 
 
 
 
0ddeb3b
 
 
0fdef1c
0ddeb3b
 
 
0fdef1c
 
 
 
 
0ddeb3b
 
 
 
 
 
 
 
 
 
 
 
0fdef1c
 
0ddeb3b
8e787d7
 
 
 
0ddeb3b
 
 
 
 
 
 
 
 
 
 
 
c49c94d
 
0ddeb3b
 
 
 
 
 
f5beb63
c49c94d
f5beb63
 
 
 
 
8e787d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f5beb63
b2b28b5
 
c49c94d
8e787d7
 
 
 
 
f5beb63
 
b2b28b5
c49c94d
b2b28b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f5beb63
 
b2b28b5
 
 
 
f5beb63
b2b28b5
 
 
 
c49c94d
b2b28b5
4eb2e64
021839a
4eb2e64
a4efe63
 
 
bd54f8a
a4efe63
4eb2e64
49b00dd
 
 
 
4eb2e64
 
 
 
fea940a
 
 
 
 
 
 
49b00dd
 
4eb2e64
14be7b9
 
 
 
 
 
b527d78
fff8133
b527d78
 
14be7b9
 
 
 
 
 
 
 
 
 
 
f9fec79
 
83d4cbf
 
 
f9fec79
 
 
 
 
 
 
83d4cbf
f9fec79
 
 
 
 
fff8133
14be7b9
 
b527d78
f9fec79
 
b527d78
83d4cbf
 
14be7b9
f9fec79
 
 
bd54f8a
 
b527d78
 
 
 
 
 
 
 
14be7b9
 
 
 
 
 
 
 
021839a
 
 
 
 
 
 
 
 
9fef789
 
 
 
 
 
b527d78
9fef789
 
 
 
 
b527d78
14be7b9
021839a
b527d78
4eb2e64
 
 
 
 
 
 
 
 
 
9dd435f
4eb2e64
49b00dd
 
 
 
4eb2e64
1b5cbdc
 
b527d78
1a50976
 
 
1b5cbdc
b7a4261
 
 
 
 
 
 
 
 
 
 
 
1b5cbdc
b527d78
1b5cbdc
b527d78
 
 
 
 
 
0ddeb3b
 
 
0fdef1c
 
 
 
 
 
 
 
b2b28b5
f5beb63
 
 
 
 
 
 
 
 
b2b28b5
 
 
 
 
 
c49c94d
0fdef1c
 
 
 
 
 
 
 
c49c94d
0fdef1c
 
 
b2b28b5
 
 
 
 
 
0fdef1c
b2b28b5
 
 
c49c94d
b2b28b5
 
0fdef1c
b2b28b5
 
0ddeb3b
b527d78
9c0a0df
b527d78
1b5cbdc
b527d78
 
 
 
 
 
 
1a50976
 
 
 
 
 
 
 
 
 
f9fec79
1a50976
 
 
 
f9fec79
1a50976
 
f9fec79
4a9a298
f4fbfbb
 
 
 
 
 
 
ffc2106
 
 
f4fbfbb
 
ffc2106
 
f4fbfbb
 
 
ffc2106
f4fbfbb
b527d78
 
 
 
 
 
1a50976
b527d78
 
 
 
 
 
 
 
 
 
34e63ff
 
c62a604
 
 
 
 
b527d78
 
f9fec79
e9e62d7
b527d78
b0688e1
 
 
 
 
b527d78
 
 
 
 
 
 
 
b0688e1
f9fec79
b527d78
 
 
 
e9e62d7
b527d78
e9e62d7
f9fec79
 
b527d78
c62a604
 
 
 
b527d78
 
 
 
 
14be7b9
 
b527d78
14be7b9
 
b527d78
14be7b9
 
 
 
bd54f8a
 
 
 
 
 
14be7b9
f9fec79
14be7b9
 
 
 
 
 
 
bd54f8a
14be7b9
f9fec79
 
 
 
14be7b9
f9fec79
14be7b9
 
f9fec79
14be7b9
 
 
 
 
 
 
 
 
 
 
 
b527d78
fff8133
14be7b9
 
 
 
 
476275f
 
 
 
14be7b9
 
 
f9fec79
 
476275f
b527d78
fea940a
 
 
 
 
b527d78
fea940a
49b00dd
fea940a
b527d78
 
fea940a
 
b527d78
 
 
c55bcb6
b527d78
c55bcb6
b527d78
 
fa6e5e5
b527d78
fa6e5e5
b527d78
4eb2e64
1b5cbdc
a4efe63
b527d78
 
1b5cbdc
b527d78
 
c55bcb6
a4985fd
c55bcb6
4eb2e64
b527d78
 
 
 
 
 
 
9fef789
 
c62a604
b527d78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a4efe63
bd54f8a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7a27d01
bd54f8a
 
 
4eb2e64
 
 
 
 
2b37458
 
4eb2e64
b7a4261
4eb2e64
b7a4261
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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
237
238
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
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
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
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
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
541
542
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
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
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
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
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
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
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
1131
1132
1133
1134
1135
1136
1137
1138
1139
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
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
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
2006
2007
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
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
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
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
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
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
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
2906
2907
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
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
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
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
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
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
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
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
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
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
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
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
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
4343
4344
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
4383
4384
4385
4386
4387
4388
4389
4390
4391
4392
4393
4394
4395
4396
4397
4398
4399
4400
4401
4402
4403
4404
4405
4406
4407
4408
4409
4410
4411
4412
4413
4414
4415
4416
4417
4418
4419
4420
4421
4422
4423
4424
4425
4426
4427
4428
4429
4430
4431
4432
4433
4434
4435
4436
4437
4438
4439
4440
4441
4442
4443
4444
4445
4446
4447
4448
4449
4450
4451
4452
4453
4454
4455
4456
4457
4458
4459
4460
4461
4462
4463
4464
4465
4466
4467
4468
4469
4470
4471
4472
4473
4474
4475
4476
4477
4478
4479
4480
4481
4482
4483
4484
4485
4486
4487
4488
4489
4490
4491
4492
4493
4494
4495
4496
4497
4498
4499
4500
4501
4502
4503
4504
4505
4506
4507
4508
4509
4510
4511
4512
4513
4514
4515
4516
4517
4518
4519
4520
4521
4522
4523
4524
4525
4526
4527
4528
4529
4530
4531
4532
4533
4534
4535
4536
4537
4538
4539
4540
4541
4542
4543
4544
4545
4546
4547
4548
4549
4550
4551
4552
4553
4554
4555
4556
4557
4558
4559
4560
4561
4562
4563
4564
4565
4566
4567
4568
4569
4570
4571
4572
4573
4574
4575
4576
4577
4578
4579
4580
4581
4582
4583
4584
4585
4586
4587
4588
4589
4590
4591
4592
4593
4594
4595
4596
4597
4598
4599
4600
4601
4602
4603
4604
4605
4606
4607
4608
4609
4610
4611
4612
4613
4614
4615
4616
4617
4618
4619
4620
4621
4622
4623
4624
4625
4626
4627
4628
4629
4630
4631
4632
4633
4634
4635
4636
4637
4638
4639
4640
4641
4642
4643
4644
4645
4646
4647
4648
4649
4650
4651
4652
4653
4654
4655
4656
4657
4658
4659
4660
4661
4662
4663
4664
4665
4666
4667
4668
4669
4670
4671
4672
4673
4674
4675
4676
4677
4678
4679
4680
4681
4682
4683
4684
4685
4686
4687
4688
4689
4690
4691
4692
4693
4694
4695
4696
4697
4698
4699
4700
4701
4702
4703
4704
4705
4706
4707
4708
4709
4710
4711
4712
4713
4714
4715
4716
4717
4718
4719
4720
4721
4722
4723
4724
4725
4726
4727
4728
4729
4730
4731
4732
4733
4734
4735
4736
4737
4738
4739
4740
4741
4742
4743
4744
4745
4746
4747
4748
4749
4750
4751
4752
4753
4754
4755
4756
4757
4758
4759
4760
4761
4762
4763
4764
4765
4766
4767
4768
4769
4770
4771
4772
4773
4774
4775
4776
4777
4778
4779
4780
4781
4782
4783
4784
4785
4786
4787
4788
4789
4790
4791
4792
4793
4794
4795
4796
4797
4798
4799
4800
4801
4802
4803
4804
4805
4806
4807
4808
4809
4810
4811
4812
4813
4814
4815
4816
4817
4818
4819
4820
4821
4822
4823
4824
4825
4826
4827
4828
4829
4830
4831
4832
4833
4834
4835
4836
4837
4838
4839
4840
4841
4842
4843
4844
4845
4846
4847
4848
4849
4850
4851
4852
4853
4854
4855
4856
4857
4858
4859
4860
4861
4862
4863
4864
4865
4866
4867
4868
4869
4870
4871
4872
4873
4874
4875
4876
4877
4878
4879
4880
4881
4882
4883
4884
4885
4886
4887
4888
4889
4890
4891
4892
4893
4894
4895
4896
4897
4898
4899
4900
4901
4902
4903
4904
4905
4906
4907
4908
4909
4910
4911
4912
4913
4914
4915
4916
"""
app.py
======

Interactive music-generation & editing demo (Gradio MVP).

Thesis: the differentiator is *control and localized editing* that text-to-music
tools can't do. Text and preset "real songs" are on-ramps that seed a
conditioning trajectory; the user then edits any raw conditioning signal over a
region using a library of parametric shape functions, regenerates individual
sections (inpaint), makes variations, and A/B-compares.

Editing model
-------------
Instead of composite "macros", the user picks ONE raw conditioning signal
(rms / density / zcr / flatness / bpm), a measure range, and a *shape function*
(constant / ramp / envelope / periodic / step / random). The shape is evaluated
over the range and written to the signal verbatim — no clamping, no projection.
`replace` overwrites the range; `add` layers the shape on top of what's there.

Runs today against MockBackend (no GPU). To use the real model, construct a
RealBackend and pass it to build_app().

    python app.py            # launches with the mock backend
"""

from __future__ import annotations

import os
import tempfile
import time
import traceback

import numpy as np
import gradio as gr

from backend import (
    MockBackend,
    ModelBackend,
    Track,
    ALL_SIGNALS,
    EDITABLE_SIGNALS,
    SIGNAL_LABELS,
    SHAPES,
    BPM_STEP_HOLD,
    CONTINUE_CTX_NOISE, CONTINUE_HISTORY, CONTINUE_MEASURES,
    CROSSFADE_MS,
    FF_MAX_MEASURES,
    HG_MODE, HISTORY_GUIDANCE, HISTORY_GUIDANCE_FREQ,
    LEAD_IN_WARM_S,
    arm_idle_stream,
    plan_fast_forward,
    style_is_off,
    STREAM_HEADROOM_S, STREAM_LEG_MEASURES, STREAM_MAX_MEASURES,
    STYLE_AXIS_RANDOM, STYLE_PACE, STYLE_SPAN,
    angle_deg, unit, style_destination,
    N_CHUNKS,
    EDIT_GUARD, EDIT_MAX_MEASURES, REGEN_STRENGTH,
    BPM_MIN,
    BPM_MAX,
    TARGET_SIG,
    bpm_curve,
    eval_shape,
    key_from_chroma,
    signals_for_stage,
    to_wav_bytes,
)
from gapless_audio import GaplessAudio

# Human labels for EVERY conditioning channel (not just the 1-D editable ones),
# used by the per-measure on/off "coverage" controls.
ALL_SIGNAL_LABELS = {
    "style": "style",
    "chroma": "harmony (chroma)",
    "bpm": "tempo (bpm)",
    "rms": "energy (rms)",
    "density": "density",
    "zcr": "brightness (zcr)",
    "flatness": "flatness",
}
ALL_SIGNAL_CHOICES = [(ALL_SIGNAL_LABELS[s], s) for s in ALL_SIGNALS]

# A value for the coverage picker that means "every signal", so that turning the
# lot on or off is the same two buttons as turning one on or off. Sentinel-
# prefixed because it shares a dropdown with real signal names and is passed to
# the same handler; nothing in ALL_SIGNALS can ever start with "~".
COV_ALL = "~all"
COV_ALL_LABEL = "All signals"

# Edit's one picker: the shapeable curves, then the three edits that are not
# curves. All four write conditioning for the same window and none of them makes
# a sound — what differs is the controls each needs, so they share a selector
# that reveals the right ones (see the editor block in build_app). Sentinel-
# prefixed for the same reason COV_ALL is: they travel through handlers that
# also take real signal names.
EDIT_KEY, EDIT_MORPH, EDIT_TEXT = "~key", "~morph", "~text"
EDIT_SPECIALS = (EDIT_KEY, EDIT_MORPH, EDIT_TEXT)
# Every label here starts with the signal's name from ALL_SIGNAL_LABELS -- the
# name Select's coverage panel calls it by -- because these ARE those signals,
# written a different way. "key" and "harmony (chroma)" being the same channel
# under two names was the one thing in this UI you could not work out from the
# UI. What follows the dash is how the edit is entered, which is why chroma has
# one entry and style has two.
EDIT_SPECIAL_CHOICES = [
    (f"{ALL_SIGNAL_LABELS['chroma']} — set a key", EDIT_KEY),
    (f"{ALL_SIGNAL_LABELS['style']} — morph toward a preset", EDIT_MORPH),
    (f"{ALL_SIGNAL_LABELS['style']} — from text", EDIT_TEXT)]
EDIT_TARGETS = ([(SIGNAL_LABELS[s], s) for s in EDITABLE_SIGNALS]
                + EDIT_SPECIAL_CHOICES)
_EDIT_ACTIONS = {EDIT_KEY: "Set key",
                 EDIT_MORPH: "Morph style",
                 EDIT_TEXT: "Set style from text"}


def _edit_action(target: str) -> str:
    """The Apply button's label for a target. Every curve shares one label
    because for a curve the shape dropdown is the verb, not the signal."""
    return _EDIT_ACTIONS.get(target, "Apply shape")

# The third tab is a lab bench rather than a feature: which model stage, the two
# history knobs whose right value is still an open question, and the A/B between
# the two guidance compositions. Off on a public Space, on with SHOW_TESTING=1.
#
# It is BUILT either way, just not shown. A hidden gr.Tab still holds live
# components and still submits their values, so every handler that reads one
# gets the documented default instead of needing a second code path -- turning
# the flag on can reveal behaviour, it cannot change it. Building it
# conditionally would mean every wiring site below had to cope with the names
# not existing, which is a much larger surface than one `visible=`.
SHOW_TESTING = os.environ.get("SHOW_TESTING", "").strip().lower() in (
    "1", "true", "yes", "on")

# The Weather tab, same flag pattern and same build-it-anyway rule. Separate
# from SHOW_TESTING because it is not a lab bench -- it is a whole alternative
# way of driving the conditioning, and it is the one tab that reaches the
# network. ON by default, unlike Testing: it is meant to be listened to, and a
# tab nobody can find gets no ears on it. Set SHOW_WEATHER=0 to hide it.
SHOW_WEATHER = os.environ.get("SHOW_WEATHER", "1").strip().lower() in (
    "1", "true", "yes", "on")

try:
    import matplotlib
    matplotlib.use("Agg")
    from matplotlib.figure import Figure
    _HAS_MPL = True
except Exception:  # pragma: no cover
    _HAS_MPL = False

# Plotly is used for exactly one figure, the style map (see _style_map for why
# that one needs pan/zoom/hover and the others do not). Guarded separately from
# matplotlib so a Space missing it loses the map and nothing else.
try:
    import plotly.subplots as _psub
    _HAS_PLOTLY = True
except Exception:  # pragma: no cover
    _HAS_PLOTLY = False


def _subplots(*args, figsize=None, **kwargs):
    """plt.subplots, minus pyplot's global figure registry.

    Every figure `plt.subplots` makes is retained by pyplot until something
    calls plt.close, and nothing here ever does -- the figures are handed to
    gradio, which renders and forgets them. One per press was a slow leak
    nobody noticed. Stream now redraws the curves at every leg boundary and
    runs until it is stopped, which turns that into an unbounded one (and
    matplotlib starts warning about it at 20). Building the Figure directly
    never registers it, so it is collected as soon as gradio is done."""
    fig = Figure(figsize=figsize)
    return fig, fig.subplots(*args, **kwargs)


# -----------------------------------------------------------------------------
# Shape parameter specification
# -----------------------------------------------------------------------------
# Each shape maps to an ordered list of (param_name, default) shown in generic
# numeric slots. Up to MAX_PARAMS are used; the "periodic" shape also uses a
# wave dropdown. These are written verbatim into the signal (no clamping), so
# defaults are just reasonable starting points — the user is free to go anywhere.
SHAPE_PARAMS = {
    "constant": [("value", 0.5)],
    "ramp":     [("start", 0.2), ("end", 0.8), ("curve", 1.0)],
    "envelope": [("low", 0.1), ("peak", 0.9), ("attack", 0.3), ("release", 0.7)],
    "periodic": [("center", 0.5), ("amp", 0.3), ("cycles", 2.0), ("phase", 0.0)],
    "step":     [("low", 0.2), ("high", 0.8), ("n_steps", 4)],
    "random":   [("center", 0.5), ("amount", 0.2), ("smoothness", 3), ("seed", 0)],
}
MAX_PARAMS = 4
SHAPE_HELP = {
    "constant": "Hold a single value across the whole range.",
    "ramp":     "Interpolate start→end. curve>1 eases in, curve<1 eases out.",
    "envelope": "Rise from low→peak (attack), hold, fall→low (release). "
                "attack/release are fractions in [0,1].",
    "periodic": "Oscillate around center with amplitude amp over `cycles` cycles. "
                "Pick sine / triangle / saw.",
    "step":     "Staircase from low→high in n_steps discrete levels.",
    "random":   "Smoothed random walk of given amount around center "
                "(smoothness = window; seed for repeatability).",
}


# -----------------------------------------------------------------------------
# Generate tab: how the style is kept moving over a long rollout
# -----------------------------------------------------------------------------
# Every answer to the long-rollout loop problem is one entry in this radio: a
# typed destination, the two walks, holding still, and no style channel at all.
# They are MUTUALLY EXCLUSIVE in the backend -- `_steer_or_walk` returns on the
# typed target before the walk branch is reachable, and style-off gates both --
# so a radio is not a simplification of the model, it is its actual shape. What
# used to be a separate textbox alongside the radio could be read as "a steer
# AND a walk"; it never was, and now it cannot look like one.
#
# Each entry is (walk_mode, style_off, label), i.e. exactly the two Track fields
# the Continue/Stream handlers need, so nothing here has to be interpreted
# twice. "toward" carries ("off", False) because a typed steer IS the walk being
# off -- the destination comes from the textbox the radio reveals, and an empty
# box therefore behaves as "Hold", which is the only sensible reading of
# "head toward nothing in particular".
#
# These are MECHANISMS, NOT DISTANCES. The labels are one word each and the
# info string underneath says what each one does, because a label long enough
# to explain itself is a label people read as a scale -- an earlier one ("how
# far it may get") implied exactly that, an ordered scale nothing
# measures and the data does not support: the reference pool the wander mode
# draws from is 88 distinct style vectors after cos>0.995 dedup, and their
# pairwise angles are bimodal -- p10 16.1 deg, median 77.2 deg, with 14.1% of
# pairs under 20 deg and a hole in between. "A bit further" is not a place you
# can ask this pool to go. What actually sets distance is the RATE, and there is
# now one slider for it: `style_span` for a typed steer and `style_pace` for a
# walk leg are the same quantity wearing two names -- measures for one journey
# -- and since the two branches are exclusive, one control can feed both. Their
# DEFAULTS differ (16 vs 96) and that difference is real, so picking a mode
# writes that mode's default into the slider; see S_MOVE_JOURNEY.
#
# One mechanism, one name, everywhere. "drift" is what the backend calls the
# held-heading walk, so it is what the radio says too; an earlier label called
# it "Keep going" and spent the word "drift" on "close" instead, which left the
# word pointing at the mode that does not use it.
#
# "close" is not "no movement" ON THE RADIO: it leaves the walk off, and
# backend.arm_idle_stream then turns an unsteered Stream into a slow drift,
# because take after take of identical conditioning is precisely how this model
# settles into a loop. Continue and Fast-forward do NOT call it, so there the
# style really is one constant vector for the whole press.
S_MOVE = {
    "close":  ("off", False, "Hold"),
    "toward": ("off", False, "Text"),
    "wander": ("wander", False, "Wander"),
    "far":    ("drift", False, "Drift"),
    "loose":  ("off", True, "Unconditional"),
}
S_MOVE_DEFAULT = "close"

# What one journey is worth, per mode. A typed steer is a single trip and
# arrives fast by default; a walk is a sequence of legs and each one is long.
# Written into the slider when the mode changes rather than baked into the
# slider, because the number is still the user's to set afterwards -- and a
# programmatic write does not fire `.release`, so this cannot ping-pong.
S_MOVE_JOURNEY = {"toward": STYLE_SPAN}
S_MOVE_JOURNEY_DEFAULT = STYLE_PACE


# -----------------------------------------------------------------------------
# The Edit tab's window picker
# -----------------------------------------------------------------------------
# The model sees exactly N_CHUNKS measures at a time and that is not negotiable,
# so editing a long track means choosing WHICH N_CHUNKS. Everything here is
# display: `backend.seat_window` does the work, and it is a lossless slice of the
# measure bank (see backend.seat_window), so the picker is free to move.
#
# Song measures, not window offsets. The picker is the one place in the app that
# talks about the whole track, and "measure 104" has to mean the same thing here
# as it does in the status line after a Continue.
def _sel_window(total: int, a, b) -> tuple:
    """`(first, a, b)` — a legal selection, and the window that will hold it.

    The selection IS the edit: you say which measures to rewrite and nothing
    else. Everything the model needs beyond that is derived here, in one place,
    so there is no second control to disagree with the first.

    Two rules, both forced rather than reported. The selection is at most
    EDIT_MAX_MEASURES, which is the window MINUS a guard at each end, not the
    window itself -- see EDIT_GUARD in backend.py for why an edit is never
    allowed to fill the sequence it is being written into. And `b` defaults to
    `a + EDIT_MAX_MEASURES`: the common case is "rewrite a span", so asking only
    for where it starts is enough.

    The selection is CENTRED in the window rather than left-aligned, and that is
    the point of doing this here. Rewriting measures 104-108 with the window
    seated at 104 would leave the model no written music in front of the change
    -- the transition INTO it would be the first thing it has to invent.
    Centring gives it frozen real latents on both sides, which is what makes a
    repair sound like a repair and not a splice. The cap and the centring
    together are what guarantee the guard: at the widest legal selection `pad`
    is exactly EDIT_GUARD, and it only grows from there.

    At the ends of the track there is nothing to centre into, so the window
    clamps and the padding all lands on the side that has room. A selection at
    measure 0 gets no guard in front of it, and that is correct rather than
    degraded -- there is nothing in front of it to be coherent with."""
    total = max(1, int(total))
    a = max(0, min(int(a), total - 1))
    b = max(a + 1, min(int(b), min(a + EDIT_MAX_MEASURES, total)))
    pad = (N_CHUNKS - (b - a)) // 2
    return max(0, min(a - pad, total - N_CHUNKS)), a, b


def _window_updates(total: int, a: int, b: int) -> tuple:
    """The two selection sliders, as updates that CARRY THEIR BOUNDS.

    Every handler that writes those sliders must go through here, and must
    never return a bare number for them. Gradio keeps a slider's minimum and
    maximum per session and only re-reads them from an update dict, so a
    handler that returns a plain int moves the knob to a value the server still
    believes is out of range -- and the next event to read that slider dies in
    `preprocess` with `Value 32 is less than minimum value 33` before any
    handler of ours runs. That is not one failed press: `refresh_window` reads
    both sliders too and is the only thing that could widen the bounds again,
    so the session can never recover and every chain in the app (they all end
    in the same tail) keeps throwing. The nav buttons used to return bare ints
    and did exactly this.

    The bounds are also where the width cap is enforced for real -- the last
    slider cannot be dragged past `a + EDIT_MAX_MEASURES` -- which is why `a`
    and `b` must already have been through `_sel_window`: the value written here
    has to be inside the range written with it."""
    total = max(1, int(total))
    a, b = int(a), int(b)
    return (gr.update(maximum=total - 1, value=a),
            gr.update(minimum=a + 1, maximum=min(total, a + EDIT_MAX_MEASURES),
                      value=b))


class SnapSlider(gr.Slider):
    """A Slider that SNAPS a value outside its range instead of refusing it.

    `Slider.preprocess` calls `Number.raise_if_out_of_bounds`, which raises
    `gradio.exceptions.Error` -- and it does so BEFORE any handler of ours runs,
    so there is nothing we can do about it from a callback. That turns one bad
    number into a dead event chain, and on the selection sliders it is worse
    than that: every chain in the app ends in the same tail, which reads both of
    them, so the whole UI stops responding until the browser happens to correct
    itself.

    Bounds validation is not what keeps the selection legal -- `_sel_window`
    does that, on every press, and writes the legal pair back. The bounds are
    there to make the max window size visible while you DRAG, which is a
    frontend property of the range input and is untouched by this. So the
    server-side check has no job here except to fail.

    And it does fail, on ordinary use. A gradio slider is a range input plus a
    number box, and the number box accepts anything you type: the built
    frontend binds it straight to the value with no clamp, and its blur handler
    dispatches `release` with the raw value and only clamps AFTERWARDS --

        function L(){ t.dispatch("release", t.props.value),
                      t.props.value = Math.min(Math.max(t.props.value,
                                      t.props.minimum), t.props.maximum) }

    -- so typing the same number into both boxes, or one past the end of the
    song, sends a value the server then rejects. Nothing in the UI hints that
    the box is narrower than the numbers in it.

    `is_template` is gradio's own mechanism for a Python-side subclass with no
    frontend of its own: `Block.get_block_name` returns the PARENT's class name
    when it is set, so this renders as an ordinary slider. Same idiom as
    `gradio.templates.TextArea`."""

    is_template = True

    def preprocess(self, payload: float) -> float:
        if payload is not None:
            payload = min(max(payload, self.minimum), self.maximum)
        return super().preprocess(payload)


# -----------------------------------------------------------------------------
# Telling a listener's pause from the player running dry
# -----------------------------------------------------------------------------
# Seconds of audio that must still be unplayed for a `pause` event to be read as
# a person pressing pause. Below this the player is assumed to have paused
# itself because it reached the end of what had been sent, which is a thing that
# really happens: the radio opens with one window of lead-in (~48 s at 120 BPM,
# ~53 s at 109) and a cold ZeroGPU worker can take longer than that to return
# its first take. WaveSurfer forwards the raw media `pause` either way, so the
# event alone cannot tell them apart -- and wiring it to `cancels=` meant an
# underrun killed the run at the exact moment it was about to have something to
# play. The threshold is deliberately generous in the direction of ignoring:
# a missed pause costs one press of ⏹ Stop, a false one costs the feature.
PAUSE_MIN_BUFFER_S = 3.0


# -----------------------------------------------------------------------------
# ZeroGPU authorisation: react, never predict
# -----------------------------------------------------------------------------
# A GPU allocation on ZeroGPU is authorised by the `X-IP-Token` JWT belonging to
# the GRADIO REQUEST that dispatched the event: spaces.zero.client.schedule()
# reads it back out of `request.headers` on EVERY @spaces.GPU call, and the
# request context does not change while an event is running. One Stream press is
# one request and makes one allocation per leg for as long as the listener keeps
# listening, so leg 20 is scheduled with the token minted for leg 1.
#
# THERE WAS A `_gpu_token_deadline` HERE. It parsed that JWT's `exp`, subtracted
# a 45 s margin and ended the press at the last commit point before it, on the
# theory that /schedule would start answering 401. DO NOT PUT IT BACK. It capped
# every radio press at ONE leg: these tokens are minted with a short lifetime and
# refreshed by the page, so `exp` is usually a minute or two out at press time,
# while the allocations themselves keep being granted long past it -- the endless
# stream this is supposed to be worked fine before the check existed and stopped
# dead the moment it did. Predicting a failure that empirically does not happen
# on schedule cost the whole feature; if the token really does expire the leg
# raises, `_gpu_failure` below turns that into a sentence, and the press ends at
# the same commit point it would have anyway, one leg later and only when it is
# actually true.
#
# WHAT DOES FIX IT is on the other side of the same fact. If the token belongs
# to the request, then a new request is a new token, and nothing in `spaces`
# needs to cooperate for that: client.schedule() calls _get_headers(request) on
# every allocation and `request` is gradio's per-event LocalContext.request, so
# the ONLY lever is how many events a listen is made of. So the radio stops
# being one long press and becomes a chain of them -- see RELAY in on_stream.
# That is not the prediction this note warns about, and the asymmetry is the
# whole reason it is allowed: the deadline GUESSED at a failure and gave up
# early, while the relay WAITS for the failure and then carries on. Guessing
# wrong there ended the feature; guessing wrong here is impossible, because
# there is no guess.
def _gpu_token_expired(exc) -> bool:
    """Did this leg die of an expired X-IP-Token rather than anything real?

    Verified against spaces 0.51.2: an UNAUTHORIZED from the /schedule endpoint
    is raised as `error("ZeroGPU client error", "Expired ZeroGPU proxy token")`
    (spaces/zero/client.py), so both words are present. Matched loosely because
    the wording is not ours and has changed before.

    Worth separating from every other failure because it is the only one that
    is a pure PROTOCOL failure: /schedule refuses before any GPU is allocated,
    so the leg costs nothing, nothing is half-written, and the identical call
    from a fresh request succeeds. That is what makes it safe to retry
    automatically rather than making the listener press the button -- see
    RELAY in on_stream.
    """
    low = (str(exc) or "").lower()
    return "token" in low and ("expired" in low or "unauthorized" in low)


def _gpu_failure(exc) -> str:
    """Turn a leg that raised into a sentence, keeping the ZeroGPU cases apart.

    The distinction matters to the reader: an expired token means "press Stream
    again and it will just work", a quota error means "it will not".

    The status line gets one sentence; the traceback goes to stderr, i.e. the
    Space log. Catching the exception here is what stops a dead leg from
    throwing a red toast, and without this print that is also what would stop
    it leaving any trace at all -- a quiet status line is a fine thing for the
    listener and a terrible one for whoever has to work out why.
    """
    traceback.print_exc()
    msg = str(exc).strip() or type(exc).__name__
    low = msg.lower()
    if _gpu_token_expired(exc):
        return ("the GPU authorisation for this press ran out — ZeroGPU issues "
                "one token per browser request and a press is one request, so "
                "a long enough press outlives it")
    if "quota" in low or "gpu" in low:
        return f"ZeroGPU stopped it: {msg}"
    # Type included because plenty of exceptions stringify to something
    # useless ("" , "0", a bare path) and "the last leg failed — 0" tells the
    # reader nothing they can act on or report.
    return f"the last leg failed — {type(exc).__name__}: {msg}"


# -----------------------------------------------------------------------------
# View helpers
# -----------------------------------------------------------------------------
def _audio_to_file(audio: np.ndarray, sr: int) -> str:
    """Write a temp wav and return its path (Gradio Audio accepts filepaths)."""
    path = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
    with open(path, "wb") as f:
        f.write(to_wav_bytes(audio, sr))
    return path


def _curves_figure(track: Track):
    """Read-only visualization of the per-measure conditioning.

    Only the signals the track's stage actually reads are drawn: stage 1 routes
    everything but style and tempo through a local_embedder that is frozen at
    zero, so plotting its energy/brightness/density/flatness curves would show
    four lines that provably do nothing to the render."""
    curves = track.cond.as_curves(signals_for_stage(track.stage))
    if not _HAS_MPL:
        return None
    n = len(curves)
    fig, axes = _subplots(n, 1, figsize=(9, 1.25 * n), sharex=True)
    if n == 1:
        axes = [axes]
    # SONG measures, not window offsets. The window can now be seated anywhere
    # in the track, so "measure 6" on its own answers nothing -- and the picker
    # this plot sits under is numbered in song measures, so matching it is the
    # only way the two can be read together. The editor sliders further down are
    # still window-relative and still say so on their own labels.
    first = int(track.prefix_measures)
    x = np.arange(first, first + track.cond.T)
    for ax, (name, y) in zip(axes, curves.items()):
        ax.plot(x, y, marker="o", ms=3, lw=1.4, color="#3b6fb0")
        ax.fill_between(x, y, np.min(y), alpha=0.12, color="#3b6fb0")
        ax.set_ylabel(name, fontsize=8, rotation=0, ha="right", va="center")
        ax.tick_params(labelsize=7)
        ax.grid(alpha=0.2)
    axes[-1].set_xlabel("song measure", fontsize=8)
    extra = "  ·  stage 1: tempo is the only curve it reads" \
        if int(track.stage) == 1 else ""
    if track.total_measures > track.cond.T:
        extra += (f"  ·  window {first}–{first + track.cond.T}"
                  f" of {track.total_measures}")
    axes[0].set_title(f"Conditioning  ·  {track.label}  ·  seed {track.seed}"
                      f"  ·  ~{track.bpm_global:.0f} BPM{extra}", fontsize=9)
    fig.tight_layout()
    return fig


def _bpm_figure(mean, variance, seed, track=None):
    """Live preview of the tempo curve the sliders describe.

    Deliberately independent of `track` so it renders before anything has been
    generated — this is the only feedback the tempo control has at that point.
    It calls the SAME bpm_curve() the entry points and Apply-tempo call with the
    same seed, so what is drawn is exactly what will be used, not an impression
    of it. When a track is loaded its current tempo is underlaid for contrast."""
    if not _HAS_MPL:
        return None
    y = bpm_curve(float(mean), float(variance), N_CHUNKS, int(seed))
    x = np.arange(N_CHUNKS)
    fig, ax = _subplots(figsize=(9, 2.2))
    if track is not None:
        ax.step(x, track.cond.bpm, where="mid", color="#b0b0b0", lw=1.3,
                label=f"current track (~{track.bpm_global:.0f})")
    ax.axhline(float(mean), color="#c04040", ls=":", lw=0.9)
    ax.step(x, y, where="mid", color="#c04040", lw=1.8,
            label=f"pending ({float(mean):.0f} ± {float(variance):g})")
    ax.fill_between(x, y, float(mean), step="mid", alpha=0.12, color="#c04040")
    ax.set_xlabel("measure", fontsize=8)
    ax.set_ylabel("BPM", fontsize=8)
    ax.tick_params(labelsize=7)
    ax.grid(alpha=0.2)
    ax.legend(fontsize=7, loc="best")
    # BPM sets each measure's length at decode time, so the curve also fixes the
    # render's duration -- worth showing, it's the least obvious consequence.
    ax.set_title(f"{N_CHUNKS} measures · "
                 f"≈{float((60.0 * TARGET_SIG / y).sum()):.0f}s per window",
                 fontsize=9)
    fig.tight_layout()
    return fig


# -----------------------------------------------------------------------------
# The style map
# -----------------------------------------------------------------------------
# A fixed 2-D projection of the 512-d style space, with the walk's path drawn
# on it. The one decision that makes this useful rather than decorative:
#
#   THE BASIS IS FIT ON THE REFERENCE POINTS, NEVER ON THE TRAJECTORY.
#
# Fitting PCA on the path (or worse, re-fitting it each take) would rescale and
# rotate the map every time the music moved, so a path that wandered and a path
# that went straight would look identical and the anchors would slide around
# underneath them. Fit once on `backend.style_points()` -- which does not change
# while the Space is up -- and the map is the same map all session: two presses
# apart are comparable, and a point that moves moved.
#
# How much this projection hides, measured on the five shipped presets (88
# deduped points): PC1 46.5%, PC2 26.1%, so 2-D keeps 72.7% of the variance.
# That is high enough to be worth drawing, and every pair of presets separates
# by 15-55x the within-preset spread, so the five songs land as five islands
# rather than a blur. It is still 27% hidden, which is why the map ships next to
# a panel that does not project at all (see below).
_STYLE_BASIS: dict = {}


def _style_basis(backend):
    """(mean, 2x512 components, anchor xy, anchor groups, variance explained).

    Cached per backend: style_points() is O(n^2) in the dedup and the SVD is
    pointless to repeat, but neither is the real reason -- the cache is what
    guarantees every redraw uses the SAME basis, which is the whole point.
    Keyed on the backend object, not id(backend), so a collected backend cannot
    hand its basis to whatever gets allocated at the same address next. On the
    Space there is exactly one backend for the app's lifetime either way."""
    key = backend
    if key in _STYLE_BASIS:
        return _STYLE_BASIS[key]
    try:
        pts = backend.style_points() or []
    except Exception:
        pts = []
    if len(pts) < 3:
        _STYLE_BASIS[key] = None
        return None
    names = [str(n) for n, _ in pts]
    X = np.stack([unit(v) for _, v in pts]).astype(np.float64)
    mu = X.mean(0)
    # Full SVD on an (n x 512) with n < 200 is microseconds; no need for a
    # randomized solver, and the deterministic one keeps the map reproducible.
    U, S, Vt = np.linalg.svd(X - mu, full_matrices=False)
    comp = Vt[:2]
    ev = (S ** 2 / max(1e-12, float((S ** 2).sum())))[:2]
    xy = (X - mu) @ comp.T
    # style_points labels every point `preset#measure` (real backend) or just
    # the preset name (mock), so the part before '#' is the song it came from.
    groups = [n.split("#")[0] for n in names]
    out = (mu, comp, xy, groups, names, float(ev.sum()))
    _STYLE_BASIS[key] = out
    return out


def _style_path(track):
    """Every measure's style, oldest first: the banked trail plus the live window.

    The window is deliberately not in `style_trail` (see Track.style_trail), so
    it is joined on here. Returns (path, tags) with one row per measure of the
    whole track."""
    tl = getattr(track, "style_trail", None)
    tags = list(getattr(track, "style_trail_tag", None) or [])
    live = np.asarray(track.cond.style, dtype=np.float32)
    path = live if tl is None or len(tl) == 0 else \
        np.concatenate([np.asarray(tl, np.float32), live])
    # The live window is tagged with the destination as it stands now, the same
    # call record_style_trail will make when these measures are banked -- so the
    # tail of the drawn path matches what the trail will say about it later.
    now, _ = style_destination(track)
    tags = tags + [now] * len(live)
    return path, tags[:len(path)]


def _style_map(track, backend):
    """Two views of the same walk: a map that projects, and a panel that does not.

    The right-hand panel is not decoration and not a duplicate. A 2-D map can
    only ever be suggestive -- roughly a quarter of the variance is off the page,
    and a path that appears to overshoot a destination may simply be moving in a
    direction the projection flattens. Angle-to-destination is computed in the
    full 512-d space, so it answers the actual question ("is it getting there?")
    with a number that cannot be a projection artefact. A working wander is a
    sawtooth: down to ~0 as a leg completes, then a jump as the next destination
    is picked. A walk that is circling shows a flat line however pretty the map
    looks.

    Plotly rather than matplotlib, and that is the one thing in this file that
    breaks the house style on purpose. Every other figure here is a fixed
    readout of 24 measures; this one is a scatter of up to a thousand path
    points over 88 anchors, and at that density the questions people actually
    have -- which measure is this, which song is that dot, did it really reach
    the target or just get close -- are pan/zoom/hover questions. A PNG cannot
    answer them. gr.Plot renders a plotly Figure interactively with no extra
    wiring, so the cost is one dependency and a graceful None if it is missing.
    """
    if not _HAS_PLOTLY or track is None:
        return None
    basis = _style_basis(backend)
    if basis is None:
        fig = _psub.make_subplots(rows=1, cols=1)
        fig.add_annotation(text="No reference points to map against.",
                           showarrow=False, xref="paper", yref="paper",
                           x=0.5, y=0.5, font=dict(size=13, color="#666666"))
        fig.update_xaxes(visible=False)
        fig.update_yaxes(visible=False)
        fig.update_layout(**_PLOT_LAYOUT)
        return fig
    mu, comp, axy, groups, names, evr = basis
    path, tags = _style_path(track)
    P = (np.stack([unit(v) for v in path]).astype(np.float64) - mu) @ comp.T
    legs = _style_legs(track, len(path))
    # The right panel only exists when it has a line to draw. It used to be
    # drawn always, with "Nothing steering yet" written across an empty pair of
    # axes -- which is a caption apologising for a chart, taking 45% of the
    # width off the chart next to it, and saying nothing the left panel does
    # not already show by having a path that goes nowhere in particular.
    # Unsteered is the DEFAULT here (a plain Continue steers nothing), so that
    # was the state most people saw most of the time.
    prog = _style_progress_legs(legs)
    off = [j for j, t in enumerate(tags) if t == "off"]
    two = bool(prog or off)
    # Both titles up front. make_subplots only creates annotations for the
    # titles it is given, so passing "" for the left one and writing it back
    # into annotations[0] later silently overwrote the RIGHT panel's title --
    # the map ended up captioned as the graph and the graph as nothing.
    titles = [f"style space · this 2-D view keeps {evr * 100:.0f}% of the "
              f"variance in {len(names)} reference points"]
    grid = {}
    if two:
        titles.append("distance still to travel (full 512-d, not projected)")
        grid = dict(column_widths=[0.55, 0.45], horizontal_spacing=0.11)
    fig = _psub.make_subplots(rows=1, cols=2 if two else 1,
                              subplot_titles=titles, **grid)
    for ann in fig.layout.annotations:
        ann.update(font=dict(size=11.5))
    _style_map_panel(fig, P, tags, legs, track, mu, comp, axy, groups, names)
    if two:
        _style_progress_panel(fig, path, prog, off)
    fig.update_layout(**_PLOT_LAYOUT)
    return fig


# One place for the chrome, so the two panels cannot drift apart and so the
# height is set once. Fixed height because gr.Plot in an accordion has no
# natural one and a figure that resizes with the browser makes the "equal
# axes" guarantee below a lie on some windows.
_PLOT_LAYOUT = dict(
    height=520,
    # The bottom margin is the legend's. It is a horizontal legend that wraps
    # to two rows once the reference songs are in it, and at the default margin
    # the second row landed on top of the "PC1" and "measure" axis titles.
    margin=dict(l=56, r=16, t=58, b=104),
    # Both white, and they have to match. A transparent paper with a white plot
    # area leaves the axes as a white island inside the margin band -- which is
    # not a thin border here but 58px of title above and 104px of legend below,
    # showing whatever fill gradio puts behind the block. That reads as a grey
    # frame squeezing the figure, and it puts the legend and the colorbar (the
    # two things that say what the colours MEAN) on a different background from
    # the marks they describe.
    plot_bgcolor="#ffffff",
    paper_bgcolor="#ffffff",
    font=dict(size=11),
    hovermode="closest",
    legend=dict(orientation="h", yanchor="top", y=-0.22, xanchor="left",
                x=0, font=dict(size=9.5), itemsizing="constant",
                tracegroupgap=4),
    dragmode="pan",
)
_PLOT_COLORS = ["#4c72b0", "#dd8452", "#55a868", "#c44e52", "#8172b3",
                "#937860", "#da8bc3", "#8c8c8c"]

# Every figure in this app is drawn on white -- the matplotlib ones because
# that is the default and the plotly one because _PLOT_LAYOUT says so above.
# The block gradio puts them in is NOT white in dark mode (--block-background-
# fill is neutral-800 there), so without this each figure is a white card in a
# grey surround, which is the eyesore, and the plotly figure is worse than the
# others: its paper used to be transparent, so the grey came INSIDE the frame
# and put the legend and colorbar labels -- dark text -- on a dark background.
#
# Painting the block to match the figures is the fix that keeps one rule for
# all six plots. The alternative, redrawing the figures dark to match the
# block, means theming every mark in them (the viridis ramp, the black "now"
# star, the white marker outlines) against a background gradio can change out
# from under us, and matplotlib figures cannot follow the browser's theme at
# all -- they are rendered server-side, before anyone knows which it is.
#
# Injected as a <style> tag rather than passed as `css=`, because gradio 6
# moved that argument off Blocks and onto launch() -- and launch() is called in
# two places this file does not own (its own __main__ and app_hf.py), so the
# styling would have to be remembered at both or the Space would silently not
# get it. A tag inside build_app travels with the components it is about.
_PLOT_CSS = """
<style>
.plot-card, .plot-card > .block, .plot-card .plot-container,
.plot-card .svg-container, .plot-card .image-container {
    background: #ffffff !important;
}
/* Radius only. No `overflow: hidden` to go with it: a background respects the
   radius on its own, and clipping children would also clip plotly's hover
   labels where they run past the edge of the block. */
.plot-card { border-radius: var(--block-radius); }
/* Plotly paints its own paper now (see _PLOT_LAYOUT); let it, so the two
   cannot disagree at the edges. */
.plot-card .main-svg { background: transparent !important; }
/* The injector itself is not a widget and must not take up a row. The rule
   applies from inside a hidden element -- a <style> tag is not rendered, so
   display:none on its parent does not stop it. */
.style-inject { display: none !important; }
/* The help panel. Boxed, because it drops in above the tab strip and pushes
   the whole app down: without an edge it reads as page content that has always
   been there rather than as something you opened and can close again. */
.help-panel {
    background: var(--block-background-fill);
    border: 1px solid var(--border-color-primary);
    border-radius: var(--block-radius);
    padding: 12px 18px;
    max-height: 60vh;
    overflow-y: auto;
}
/* The title shares a row with the Help button, so it must not carry a
   markdown block's default vertical margin or the two sit at different
   heights. */
.app-title h1 { margin: 0; }
</style>
"""


def _style_map_panel(fig, P, tags, legs, track, mu, comp, axy, groups, names):
    """Left panel: the reference songs, and the path drawn over them.

    Several traces are deliberately kept OUT of the legend (showlegend=False).
    A horizontal legend wraps, and every entry it gains pushes the plots up; an
    entry earns its place only if the mark it describes is ambiguous without
    it. The path line, the per-measure dots and the dotted still-to-go segment
    are all self-evident beside the markers they connect, and the colour bar
    already says what the dots' colour means."""
    import plotly.graph_objects as go

    # Anchors first so the path draws over them, but with a white outline so
    # they stay visible where it does -- a walk steers TO reference points, so
    # its legs end exactly on them, which is the one place you most want to see
    # whether it arrived.
    for i, g in enumerate(sorted(set(groups))):
        m = [j for j, gg in enumerate(groups) if gg == g]
        fig.add_trace(go.Scatter(
            x=axy[m, 0], y=axy[m, 1], mode="markers", name=f"{g} ({len(m)})",
            legendgroup=g,
            marker=dict(size=8, color=_PLOT_COLORS[i % len(_PLOT_COLORS)],
                        opacity=0.8, line=dict(width=0.8, color="#ffffff")),
            text=[names[j] for j in m],
            hovertemplate="%{text}<extra></extra>"), row=1, col=1)

    # The path. Markers are colour-graded by measure, which is how direction is
    # read here -- plotly cannot fade a single line the way the matplotlib
    # version did, and a colourbar is more legible than the fade was anyway.
    lab = [t if t else "(not steering)" for t in tags]
    fig.add_trace(go.Scatter(
        x=P[:, 0], y=P[:, 1], mode="lines", name="path", showlegend=False,
        line=dict(color="#444444", width=1.4), opacity=0.55,
        hoverinfo="skip"), row=1, col=1)
    fig.add_trace(go.Scatter(
        x=P[:, 0], y=P[:, 1], mode="markers", name="each measure",
        showlegend=False,
        customdata=np.stack([np.arange(len(P)), lab], -1),
        marker=dict(size=5, color=np.arange(len(P)), colorscale="Viridis",
                    showscale=True,
                    # Horizontal, under the left panel. Vertical put it in the
                    # gutter between the two subplots, where it read as
                    # belonging to neither and stole width from both.
                    # Title to the RIGHT of the bar, not above it: above put it
                    # on the same line as the centred "PC1" axis title.
                    colorbar=dict(title=dict(text="measure", side="right"),
                                  orientation="h", thickness=9, len=0.30,
                                  x=0.0, xanchor="left", y=-0.14,
                                  yanchor="top", tickfont=dict(size=8.5),
                                  title_font=dict(size=9))),
        hovertemplate="measure %{customdata[0]}<br>→ %{customdata[1]}"
                      "<extra></extra>"), row=1, col=1)

    # Where each leg began. Hollow, and a diamond rather than another circle,
    # because "start" is already a circle.
    if len(legs) > 1:
        b = [a for a, _, _, _ in legs[1:] if a < len(P)]
        fig.add_trace(go.Scatter(
            x=P[b, 0], y=P[b, 1], mode="markers",
            name=f"re-targeted ({len(b)})",
            marker=dict(size=11, symbol="diamond-open",
                        color="#222222", line=dict(width=1.6)),
            text=[tags[j] for j in b],
            hovertemplate="re-targeted at measure %{x:.3f}"
                          "<extra>%{text}</extra>"), row=1, col=1)
    # Hollow: a track seeded from a preset starts ON that preset's anchor, and
    # a filled marker there hides the reference point most worth seeing.
    fig.add_trace(go.Scatter(
        x=[P[0, 0]], y=[P[0, 1]], mode="markers", name="start",
        marker=dict(size=13, symbol="circle-open", color="#222222",
                    line=dict(width=2.2)),
        hovertemplate="start<extra></extra>"), row=1, col=1)

    dname, dest = style_destination(track)
    if dest is not None:
        d = (dest.astype(np.float64) - mu) @ comp.T
        fig.add_trace(go.Scatter(
            x=[P[-1, 0], d[0]], y=[P[-1, 1], d[1]], mode="lines",
            line=dict(color="#c44e52", width=1.6, dash="dot"),
            name="still to go", showlegend=False, hoverinfo="skip"),
            row=1, col=1)
        fig.add_trace(go.Scatter(
            x=[d[0]], y=[d[1]], mode="markers",
            name=f"heading for {(dname or 'target').split('#')[0]}",
            marker=dict(size=14, symbol="x", color="#c44e52",
                        line=dict(width=1, color="#ffffff")),
            hovertemplate=f"destination: {dname}<extra></extra>"),
            row=1, col=1)
    # Drawn last so it wins the pixel: on an arrived leg "now" and the
    # destination land on the same point, and where it IS matters more than
    # where it was going.
    fig.add_trace(go.Scatter(
        x=[P[-1, 0]], y=[P[-1, 1]], mode="markers",
        name=f"now (measure {len(P) - 1})",
        marker=dict(size=17, symbol="star", color="#111111",
                    line=dict(width=1, color="#ffffff")),
        hovertemplate="now<extra></extra>"), row=1, col=1)

    fig.update_xaxes(title_text="PC1", row=1, col=1, **_AXIS)
    # Equal aspect is not cosmetic: both axes are principal components of the
    # same space, so a unit along PC1 and a unit along PC2 are the same
    # distance. Letting one stretch would make "how far did it move" depend on
    # which direction it moved in. scaleanchor keeps that true through zoom.
    fig.update_yaxes(title_text="PC2", row=1, col=1, scaleanchor="x",
                     scaleratio=1, **_AXIS)


def _style_legs(track, n_path):
    """[(start, stop, name, dest_vector)] for the whole track, oldest first.

    Straight off `style_leg_log`, which `stamp_new_measures` wrote at the
    moment each leg's measures were written. Deliberately NOT re-derived from
    the per-measure tags plus the current destination: the tags give the right
    boundaries but only one destination vector is still in memory, and an
    earlier version of this panel filled the gap by scoring each finished leg
    against its own last measure -- which makes every leg arrive at 0 by
    construction, i.e. draws a success whether or not one happened."""
    log = [e for e in (getattr(track, "style_leg_log", None) or [])
           if isinstance(e, dict)]
    out = []
    for i, e in enumerate(log):
        a = max(0, min(int(e.get("start", 0)), n_path))
        b = n_path if i + 1 >= len(log) else \
            max(a, min(int(log[i + 1].get("start", n_path)), n_path))
        if b > a:
            out.append((a, b, str(e.get("name", "")), e.get("dest")))
    return out


def _style_progress_legs(legs):
    """The legs the progress panel can actually draw: `(a, b, name, dest, live,
    divider)`, oldest first.

    An unsteered stretch ("") and a style-off stretch have no destination, so
    there is no distance to draw and they are dropped -- the gap in the line is
    itself the honest statement that nothing was steering.

    Separate from the drawing so the caller can ask whether there is anything
    to draw BEFORE it decides to make room for the panel (see `_style_map`).
    `live` and `divider` are computed here because both are about a leg's place
    in the unfiltered list, which is gone by the time the panel sees it."""
    return [(a, b, name, tgt, k == len(legs) - 1, bool(k))
            for k, (a, b, name, tgt) in enumerate(legs)
            if tgt is not None and name not in ("", "off") and b - a >= 2]


def _style_progress_panel(fig, path, prog, off):
    """Right panel: angle to each measure's OWN destination, in full 512-d.

    "Its own" is the load-bearing word. Every measure is scored against the
    destination that was in force when it was written, recovered exactly from
    the leg log rather than guessed. Scoring the whole track against wherever
    the walk happens to be going now would draw every completed journey as a
    failure; guessing a finished leg's target from where it ended would draw
    every one as a success.

    Only called when `prog` or `off` is non-empty, so there is no empty state
    to caption -- see `_style_map`.
    """
    import plotly.graph_objects as go

    for i, (a, b, name, tgt, live, divider) in enumerate(prog):
        tgt = unit(tgt)
        y = [angle_deg(path[j], tgt) for j in range(a, b)]
        fig.add_trace(go.Scatter(
            x=list(range(a, b)), y=y, mode="lines",
            name=("this leg → " + name.split("#")[0]) if live
            else "completed legs",
            legendgroup="live" if live else "done",
            showlegend=live or not i,
            line=dict(color="#c44e52" if live else "#8c8c8c",
                      width=2.4 if live else 1.3,
                      dash=None if live else "dash"),
            hovertemplate="measure %{x}<br>%{y:.1f}° to go"
                          f"<extra>{name}</extra>"), row=1, col=2)
        # One divider per re-target, so the sawtooth's teeth line up with the
        # diamonds on the map and the two panels read as one picture.
        if divider:
            fig.add_vline(x=a, line=dict(color="#dddddd", width=1),
                          row=1, col=2)

    if off:
        fig.add_trace(go.Scatter(
            x=off, y=[0] * len(off), mode="markers", name="style off",
            marker=dict(size=6, symbol="line-ns-open", color="#b0b0b0"),
            hovertemplate="measure %{x}: style off<extra></extra>"),
            row=1, col=2)
    # Always the whole track, even though the first window is never steered:
    # an x-axis that silently started at measure 24 made the seed look like it
    # had been skipped rather than simply not aimed anywhere.
    fig.update_xaxes(title_text="measure", range=[0, max(1, len(path) - 1)],
                     row=1, col=2, **_AXIS)
    fig.update_yaxes(title_text="degrees to destination", rangemode="tozero",
                     row=1, col=2, **_AXIS)


_AXIS = dict(showgrid=True, gridcolor="#eeeeee", zeroline=False,
             linecolor="#cccccc", ticks="outside", tickfont=dict(size=9),
             title_font=dict(size=10))

def _summary(track: Track) -> str:
    c = track.cond
    span = (f"{c.T} measures" if not track.prefix_measures else
            f"{track.total_measures} measures "
            f"(editing {track.prefix_measures}–{track.total_measures - 1})")
    # "last take", not "seed": `Track.seed` is not the track's identity, it is
    # whatever seed the most recent sampling call used. continue_track, vary and
    # render all overwrite it, and the radio advances it by one per 8-measure
    # take (four per leg), so a track started at 42 reads 45 after one press.
    # Calling that "seed" says the number reproduces what you are hearing; it
    # only reproduces the last take, given the same context in front of it.
    head = (f"**{track.label or 'Untitled'}**  ·  {span}  ·  "
            f"~{track.bpm_global:.0f} BPM  ·  last take seed "
            f"`{track.seed}`\n\n")
    if int(track.stage) == 1:
        # The DSP means would be a readout of numbers stage 1 never looked at.
        head += ("stage 1 — style + tempo only; the DSP signals are not wired "
                 "into this model. Switch to stage 2 to shape them.")
    else:
        head += (f"mean energy {c.rms.mean():.2f} · "
                 f"brightness {c.zcr.mean():.3f} · "
                 f"density {c.density.mean():.2f} · "
                 f"flatness {c.flatness.mean():.4f}")
    if c.is_unconditional():
        head += "\n\n⚪ **Unconditional** — every signal is off; generation is " \
                "driven by the seed alone."
    else:
        off = [ALL_SIGNAL_LABELS[s] for s in signals_for_stage(track.stage)
               if not bool(c.active[s].all())]
        if off:
            head += "\n\n🎚️ Partial conditioning: " + ", ".join(off) + \
                    " are off for some measures."
    # Deliberately NOT `style_note`, the steer/walk progress readout ("🎯
    # steering to X 45% · 32 measures to go"). This caption sits on Edit, which
    # is about a window of measures that already exist; how far a walk started
    # on the other tab has got is a report on a press you did not make here and
    # cannot act on from here. The style map on Generate is where that question
    # is asked and where it is now answered.
    #
    # Loop escape, when it is engaged. Shown rather than handled silently
    # because the intervention is AUDIBLE -- at level 3 the music jumps to a
    # different style mid-press -- and an unexplained lurch reads as a bug.
    # Written by backend.note_loop_check; empty at level 0, which is almost
    # always. See _simple_summary for why Generate shows it too.
    if getattr(track, "loop_note", ""):
        head += "\n\n" + track.loop_note
    return head


def _simple_summary(track: Track) -> str:
    """The player caption on Generate: what you are listening to.

    Not `_summary` with the numbers filed off -- it answers a different
    question. Edit's caption describes the CONDITIONING (mean energy, which
    signals are off for which measures) because that is what the controls
    surrounding it change. Over here the only controls are Continue and the
    radio, so all that belongs is how much music there is and how long it runs.

    Deliberately NOT `style_note`, which is where the style-walk compass
    ("wander ▓░░░ 7° from where the walk started · nearest reference:
    8_cond#10") goes. That readout lives in the accordion below the buttons,
    where someone choosing a pace has gone looking for what it bought. In the
    caption there is nothing to act on and it reads as
    instrumentation bolted to a radio.

    Deliberately NOT `track.seed` either, for a sharper reason: it would
    contradict the seed box sitting a few inches above it. That field holds the
    seed of the LAST sampling call, not the one you typed -- the radio steps it
    by one per 8-measure take -- so a track started at 42 captions itself 45
    after one press, 49 after two. A build report can label that honestly as
    the last take's seed; a caption under a player cannot, and the only reading
    available is "the box is lying to me".

    `loop_note` IS shown here, and the distinction from the two above is the
    one this whole docstring is drawing: those are instrumentation for a
    decision, and there is no decision here. The loop escape is not
    instrumentation -- it CHANGES WHAT YOU HEAR, at level 3 by moving the music
    somewhere else mid-press. A listener who gets an unexplained lurch and no
    caption concludes the radio broke. Condensed rather than passed through,
    because the escape level is exactly the part of it that is instrumentation."""
    if track is None:
        return ""
    secs = 0.0 if track.audio is None else len(track.audio) / track.sr
    m, s = divmod(int(round(secs)), 60)
    line = (f"**{track.label or 'Untitled'}**  ·  {track.total_measures} "
            f"measures  ·  {m}:{s:02d}  ·  ~{track.bpm_global:.0f} BPM")
    if getattr(track, "loop_note", ""):
        line += ("\n\n🔁 It had started repeating itself — nudging it "
                 "somewhere new.")
    return line


# Openings of the status lines the handlers write, mapped to what Generate says
# instead. The full line is a build report -- legs, measure windows, take
# counts, the diag fence, the ZeroGPU token -- written for someone who is about
# to go and edit the thing, and none of it is a fact a listener can act on.
# Prefix-matching rather than making every handler produce two strings keeps ONE
# source of truth for what happened, and the fallback below is short enough that
# a prefix drifting out of date degrades to "less detail", not "wrong".
_SIMPLE_STATUS = {
    "🔴 Streaming": "🔴 Playing, and still writing. ⏹ Stop Radio when you have "
                    "had enough.",
    "⏹ Stopped": "⏹ Stopped — what you heard is back in the player above, "
                 "ready to play again. Press 🔴 Start Radio to carry on from "
                 "there.",
}


def _simple_status(msg: str) -> str:
    """Reduce a handler's status line to something worth showing a listener."""
    body = msg.split("```", 1)[0].strip()      # never the diag fence
    paras = [p.strip() for p in body.split("\n\n") if p.strip()]
    head = paras[0] if paras else ""
    # A trailing paragraph starting with ▶ is the one kind that survives: by
    # convention that marks something the LISTENER has to do (press play again
    # after an underrun). The rest of the tail is commentary -- why it sounds
    # the way it does, and the style-walk compass.
    act = [p for p in paras[1:] if p.lstrip("*").startswith("▶")]

    def out(head_txt: str) -> str:
        return "\n\n".join([t for t in [head_txt, *act] if t])

    for pre, short in _SIMPLE_STATUS.items():
        if head.startswith(pre):
            return out(short)
    if head.startswith("Streamed:"):
        # Keep only the clause after the em dash -- WHY it ended, which is the
        # one part of that sentence a listener might need (they paused it, or
        # something failed) -- and drop the measure and leg accounting.
        why = head.split("—", 1)[1].split(". ", 1)[0].strip() if "—" in head \
            else ""
        return out(f"⏹ Stopped — {why}." if why
                   else _SIMPLE_STATUS["⏹ Stopped"])
    # Anything else is a one-line handler message ("Start a track first.") or an
    # edit report from controls Generate does not have; the latter is bookkeeping
    # about a track the listener can already hear, so it says nothing.
    return out("" if head.startswith(
        ("Continued", "Fast-forwarded", "Regenerated", "Rewrote", "Rendered",
         "Applied", "Replaced", "Added", "Set ", "Style set", "Morphed",
         "Turned", "Coverage", "Undo", "Seeded", "Extracted",
         "Unconditional")) else head)


def _coverage_table(track: Track) -> str:
    """On/off per signal, as a table.

    This used to be a measures-wide heatmap, and a heatmap was the right
    picture while coverage was per-measure. It is not any more: a signal is on
    for the whole window or off for it (see the coverage panel), so every row
    of that grid was 24 identical cells and the whole plot carried seven bits.
    A table carries the same seven bits in a tenth of the height and does not
    invite the reader to look for structure along an axis that cannot have any.

    `partial` is still a state the table can show. Nothing in the UI can
    produce it now, but a preset loaded from an .npz carries whatever mask was
    written into it, and a view that silently rounded that to on or off would
    be hiding exactly the configuration this checkpoint handles worst."""
    if track is None:
        return ""
    c = track.cond
    T = c.T
    # Same stage filter as the curves: a stage-1 row for chroma or rms would be
    # a switch that is wired to nothing.
    sigs = signals_for_stage(track.stage)
    rows = []
    for s in sigs:
        n_on = int(np.count_nonzero(np.asarray(c.active[s], dtype=bool)))
        if n_on == T:
            state = "🟩 **on**"
        elif n_on == 0:
            state = "⬜ off"
        else:
            state = f"🟨 partial — on for {n_on} of {T}"
        rows.append(f"| {ALL_SIGNAL_LABELS[s]} | {state} |")
    note = ("\n\n<sub>Stage 1: style + tempo only.</sub>"
            if int(track.stage) == 1 else "")
    return ("| signal | |\n|---|---|\n" + "\n".join(rows) + note)


def _param_updates(shape: str):
    """Return Gradio updates for the MAX_PARAMS numeric slots + wave dropdown
    given the selected shape."""
    spec = SHAPE_PARAMS.get(shape, [])
    updates = []
    for i in range(MAX_PARAMS):
        if i < len(spec):
            name, default = spec[i]
            updates.append(gr.update(visible=True, label=name, value=default))
        else:
            updates.append(gr.update(visible=False))
    wave_update = gr.update(visible=(shape == "periodic"))
    help_update = gr.update(value=f"*{SHAPE_HELP.get(shape, '')}*")
    return (*updates, wave_update, help_update)


def _collect_params(shape, wave, pvals):
    """Build the params dict expected by eval_shape from the generic slot values."""
    spec = SHAPE_PARAMS.get(shape, [])
    params = {}
    for (name, _default), v in zip(spec, pvals):
        # n_steps / smoothness / seed are conceptually ints; eval_shape casts too
        params[name] = v
    if shape == "periodic":
        params["wave"] = wave
    return params


def _preview_figure(track, signal, start, end, shape, mode, wave, *pvals):
    """Live, no-render preview of an edit.

    Two stacked panels:
      (top)    the raw *generated shape* over the edited window, on its own —
               so you can always see the curve the function produces (in
               `replace` mode it would otherwise sit hidden under the result).
      (bottom) the signal in context: the *current* signal (what's there now)
               vs the *result* after the shape is written into [start, end)
               with the chosen mode.

    Nothing is committed; this only visualizes what `apply_shape` would produce,
    verbatim (no clamping)."""
    if not _HAS_MPL:
        return None
    start, end = int(start), int(end)
    if end <= start:
        end = start + 1
    lbl = SIGNAL_LABELS.get(signal, signal)

    fig, (ax_shape, ax_sig) = _subplots(
        2, 1, figsize=(9, 4.4),
        gridspec_kw={"height_ratios": [1, 1.4]})

    # ---- top: generated shape alone ----
    if track is not None:
        T = track.cond.T
        start = max(0, min(start, T - 1))
        end = max(start + 1, min(end, T))
    n = end - start
    shp = eval_shape(shape, n, _collect_params(shape, wave, pvals))
    xs = np.arange(start, end)
    ax_shape.plot(xs, shp, marker="o", ms=3, lw=1.8, color="#d1495b")
    ax_shape.fill_between(xs, shp, np.min(shp) if len(shp) else 0,
                          alpha=0.12, color="#d1495b")
    ax_shape.set_title(f"Generated shape · {shape} · measures [{start},{end})",
                       fontsize=9)
    ax_shape.set_ylabel("value", fontsize=8)
    ax_shape.tick_params(labelsize=7)
    ax_shape.grid(alpha=0.2)

    # ---- bottom: signal in context ----
    if track is not None:
        x = np.arange(T)
        current = np.asarray(getattr(track.cond, signal), dtype=float).copy()
        result = current.copy()
        result[start:end] = (current[start:end] + shp) if mode == "add" else shp

        ax_sig.axvspan(start - 0.5, end - 0.5, color="#3b6fb0", alpha=0.08,
                       zorder=0)
        ax_sig.plot(x, current, marker="o", ms=3, lw=1.2, color="#b0b0b0",
                    label="current", zorder=1)
        ax_sig.plot(x, result, marker="o", ms=3, lw=1.8, color="#3b6fb0",
                    label="after edit", zorder=2)
        ax_sig.set_title(f"{lbl} · {mode}", fontsize=9)
        ax_sig.legend(fontsize=7, loc="best")
    else:
        ax_sig.text(0.5, 0.5, f"Start a track to see this applied to {lbl}.",
                    ha="center", va="center", fontsize=9, color="#888",
                    transform=ax_sig.transAxes)
        ax_sig.set_title(lbl, fontsize=9)
    ax_sig.set_xlabel("measure", fontsize=8)
    ax_sig.set_ylabel(lbl, fontsize=8)
    ax_sig.tick_params(labelsize=7)
    ax_sig.grid(alpha=0.2)

    fig.tight_layout()
    return fig


# -----------------------------------------------------------------------------
# Help
# -----------------------------------------------------------------------------
# One panel, opened from the button in the header, showing the section for the
# tab you are on. It exists so that the tabs themselves can stay bare: every
# sentence that explains a concept rather than a control belongs here, and a
# caption under a widget is only justified when it says something the widget
# cannot say itself.
#
# Per tab rather than one long page because the question a user has is always
# "what is THIS", and per-tab is also what lets the panel be short enough to
# read in place without scrolling the controls off screen.
#
# Keep this in step with README.md, which carries the same text for people
# reading the repo rather than using the Space. If you change what a tab does,
# change its section here in the same commit -- a help panel that has drifted is
# worse than none, because it is believed.
HELP_INTRO = (
    "**What this is.** A model that writes music 24 measures at a time from "
    "seven per-measure control signals — tempo, energy, brightness, density, "
    "flatness, harmony and style. Everything here is a way of setting those "
    "signals and asking for the audio back. Unlike a text-to-music box, you "
    "can go back to a span you have already written, change one signal over "
    "it, and rewrite only that.\n\n"
    "**There is one track, shared by every tab.** Switching tabs costs "
    "nothing and loses nothing. The tabs are in the order a session runs in:\n\n"
    "- **Generate** — start a track, from a real song, your own audio or a "
    "description.\n"
    "- **Continue** — make it longer, and steer where the style goes while it "
    "grows.\n"
    "- **Select** — pick up to 24 measures anywhere in the song, and see what "
    "the model is being told about them.\n"
    "- **Edit** — change what it is being told over those measures, and "
    "rewrite just them.\n"
    "- **Export** — hear the whole thing, and save the audio or the "
    "conditioning.\n\n---\n\n")

HELP = {
    "Generate": (
        "### Generate — start a track\n\n"
        "Three ways in, and they differ only in where the starting "
        "conditioning comes from:\n\n"
        "- **A real song** — the seven signals measured off one of the "
        "reference recordings. The closest thing to “sound like this”.\n"
        "- **Your own audio** — the same measurement, run on a file you "
        "upload. Nothing of the recording itself is used, only its curves.\n"
        "- **A description** — style only, from the text. The other six "
        "signals are left for the model to invent, except tempo if you set it "
        "below.\n\n"
        "Each writes the first 24 measures. To get more, go to **Continue**.\n\n"
        "**Seed** — the same seed with the same settings gives the same music. "
        "Change it for a different take of the same idea.\n\n"
        "**Tempo** — a global BPM plus how much it wobbles measure to measure. "
        "It feeds a start from a description, and *Apply tempo* rewrites the "
        "tempo curve of a track you already have and re-renders it.\n\n"
        "The player is the current track, and it is the same file **Continue** "
        "shows."),
    "Continue": (
        "### Continue — make it longer\n\n"
        "Every press writes new measures onto the end of what you have. The "
        "model reads the measures before them as context, so takes join up.\n\n"
        "**How it moves** is where the style goes while it writes:\n\n"
        "- **Hold** — stay where you are.\n"
        "- **Text** — head for something you describe, arrive, and stay "
        "there.\n"
        "- **Wander** — aim at a different reference song every so often.\n"
        "- **Drift** — hold one direction for a while, then strike out in a "
        "new one.\n"
        "- **Unconditional** — no style instruction at all; only the bars so "
        "far.\n\n"
        "**Measures per journey** is the rate: how long one trip or one leg "
        "takes. It is also how far one *Fast-Forward* writes.\n\n"
        "**History guidance** is how hard the new measures are pulled toward "
        "the old ones (1.0 is off, 1.5 is the usual answer, high loops). "
        "**History noise** is how loosely the past is held — the other end of "
        "the same trade.\n\n"
        "**The buttons**, across the top: *Generate 8 Measures* is one short "
        "take. *Fast-Forward* writes a whole journey in one go. *Start Radio* "
        "generates continuously and streams it as it goes — the same button "
        "turns into *Stop Radio* while it runs, and ends it. *Undo* takes back "
        "the last press.\n\n"
        "**Where the style has travelled** maps the reference songs as dots "
        "with this track's path drawn over them. A second panel appears "
        "beside it only when something is actually steering, and shows the "
        "distance still to go."),
    "Select": (
        "### Select — choose what to rewrite\n\n"
        "This tab picks the span the **Edit** tab works on. Nothing here "
        "changes the music.\n\n"
        "**first / last measure** put the selection anywhere in the song, not "
        "just at the end. It can be at most 16 measures wide, and the sliders "
        "will not let you ask for more. The buttons jump it back, forward, or "
        "to the end.\n\n"
        "**Why 16 and not 24.** The model reads 24 measures at once, and the "
        "8 that are not yours — 4 at each end — are held at the real music "
        "that is there while your selection is rewritten. Those frozen "
        "measures are the whole reason a rewrite comes back sounding like part "
        "of the same piece rather than a clip dropped into a gap. A selection "
        "filling all 24 would leave none, so the cap is what makes editing "
        "editing. At the very start or end of a song there is nothing to "
        "freeze on one side; the selection still works, and the line under the "
        "player says what it actually got.\n\n"
        "Moving it is free: every measure you have written is kept, so "
        "re-seating the window is a lookup rather than a re-render.\n\n"
        "**The player** is just the selected measures, so you can hear what "
        "you have picked before editing it.\n\n"
        "**Conditioning curves** are the values the model is being given over "
        "that window.\n\n"
        "**Conditioning coverage** is which of them it is allowed to read. A "
        "signal that is *on* is followed; *off* means the model makes that one "
        "up. Everything off is unconditional. Pick a signal (or *All "
        "signals*) and press On or Off.\n\n"
        "On or off applies to the **whole window**: this checkpoint never saw "
        "a signal that was on for some measures and off for others. To free a "
        "signal over part of a song, move the selection there and do it a "
        "window at a time."),
    "Edit": (
        "### Edit — change the selected measures\n\n"
        "Everything here writes conditioning for the window **Select** picked. "
        "None of it makes a sound on its own: you change the conditioning, "
        "then press **Regenerate** to hear it.\n\n"
        "**Regenerate** rewrites the selected measures from whatever the "
        "conditioning now says, holding the measures either side of them "
        "fixed.\n\n"
        "**Edit strength** is how far it moves. At **0** you get back exactly "
        "the take you already have — nothing is re-rolled, so this is the "
        "setting for changing one curve and leaving the performance alone. At "
        "**1** the span is written from scratch against its frozen neighbours. "
        "In between, the rewrite *starts* from the music that is there and "
        "walks away from it, so the phrasing survives at low values and stops "
        "surviving somewhere around 0.8. If you have made a big change to the "
        "conditioning, turn it up: at a low strength the music you already "
        "have pulls back toward the curves it was written from.\n\n"
        "**Seed** picks which re-roll you get, not how big a one. Change it "
        "and press again for a different take of the same instruction.\n\n"
        "**what to edit** picks one target and shows only its controls. It "
        "offers the signals that are switched ON for this track on **Select**, "
        "under the names Select calls them by: a signal you have turned off is "
        "not handed to the model at all, so shaping it here would change "
        "nothing. Turn it back on over there and it reappears. Each target "
        "opens on what the track already says — the curves are drawn on the "
        "plot, and the three below read the key and the style back off the "
        "track the same way:\n\n"
        "- **a curve** (energy, brightness, density, flatness, tempo) — a "
        "**shape** written over the selection: constant, ramp, envelope, "
        "periodic, step or random walk. *replace* overwrites the curve, *add* "
        "adds to it. The preview plot is live and is not applied until you "
        "press Apply. Shapes are written verbatim — no clamping — so a value "
        "the music has never seen is yours to ask for.\n"
        "- **harmony (chroma) — set a key** — a root and major/minor, written "
        "as harmony over the window. Opens on the key the window reads as.\n"
        "- **style — morph toward a preset** — move the window's style some "
        "fraction of the way toward a reference song. Opens on the last "
        "preset this track was given.\n"
        "- **style — from text** — set the window's style from a description. "
        "An empty box is filled with the last description written; anything "
        "you have typed is left alone.\n\n"
        "**The four buttons** sit between the two players because that is what "
        "they act on. *Regenerate* rewrites A. *Undo* steps backwards. "
        "*Snapshot* parks the current version in B; *Swap* exchanges B with "
        "what you are editing, so you can go back to an earlier version and "
        "keep working — nothing is discarded, swap again to return. B holds "
        "the same 24 measures A does, so the two are directly comparable; "
        "press play on either. The whole song is on **Export**, and it is "
        "always whichever version is in A.\n\n"
        "**Regenerate always re-rolls A**, never B — B is somewhere to keep a "
        "version, not something the model reads. Below strength 1 it starts "
        "from the take you are listening to, so pressing it again carries on "
        "from the last result rather than from where you began: that is how "
        "you walk a passage toward what you want, a bit at a time. To try "
        "several *different* things from the same starting point, press "
        "**Undo** between attempts — every Regenerate is undoable, so Undo, "
        "change the seed, press again gives you a second take of the same "
        "base rather than a take of your first attempt. Snapshot and Swap do "
        "the same job when you want to keep an attempt while you try the "
        "next.\n\n"
        "**Guidance** is how literally the conditioning is followed; **Draft** "
        "halves the sampling steps for a faster, rougher render."),
    "Export": (
        "### Export — take it with you\n\n"
        "The player here is the **whole song** end to end, not just the "
        "selected window.\n\n"
        "**Export WAV** writes the audio. **Export conditioning (.npz)** "
        "writes the seven signals for every measure. That file is also a save "
        "format: drop it into the app's `presets/` folder and it comes back as "
        "a preset you can start from on **Generate**."),
    "Weather": (
        "### Weather — one hour of a city, one measure of music\n\n"
        "An experiment, and a self-contained one: everything it knows lives "
        "in `weather.py`, and the only mark it leaves on the rest of the app "
        "is an optional per-measure conditioning author that is unset on every "
        "other track.\n\n"
        "**The clock.** Open-Meteo returns one row per hour and one row "
        "becomes one measure — no averaging, no resampling, no stretch dial. "
        "It starts at the hour you press the button and walks *backwards* "
        "through about a year of record, so each 24-measure window is one "
        "whole day and night, and the diurnal cycle is the shape you hear "
        "first.\n\n"
        "**Why places sound different.** Scaling a city against its own year "
        "puts every city's median in the middle and makes them all sound "
        "alike; scaling against fixed global bounds makes a mild city sit "
        "still. So the dial is both: where the city's annual mean falls on a "
        "fixed global scale, plus this hour's deviation from that city's own "
        "year. The slider sets how hard the second term pushes, and its range "
        "is not arbitrary: at 1 a full standard deviation of local weather "
        "moves a signal by 15% of its range, and an ordinary day is about "
        "±1.5σ, so 1 is a quiet setting. At 0 every signal is a flat constant "
        "— the place and nothing else. Past about 3 the pushes start clipping "
        "against the ends of each signal's band, so the peaks flatten instead "
        "of growing; that is where the slider stops.\n\n"
        "**What drives what.** Sunlight → energy. Temperature → tempo, which "
        "lags sunlight by about three hours, so the tempo peak arrives after "
        "the loudness peak on its own. Rain → density, the one signal that is "
        "literally a count of events per second. Gusts → flatness. Cloud "
        "cover, inverted → brightness. Nothing drives harmony: no weather "
        "variable has any business picking chords.\n\n"
        "**Style is off**, deliberately, so the five weather signals are the "
        "only description the model gets and what you hear is the mapping "
        "rather than a preset. That removes a term from the guidance sum, so "
        "the same number is a weaker push than elsewhere — raise it if a run "
        "sounds limp before it sounds free.\n\n"
        "**Finding a place.** The box searches as you type and **Matches** "
        "shows what it found with the best one already selected, because a "
        "name is not a location: there are nine Washingtons and eight Austins "
        "and the geocoder will happily pick one for you. Writing the state "
        "after the city works — *Washington, DC* — even though Open-Meteo's "
        "own geocoder returns nothing at all for that string; the query is "
        "retried without the suffix and the suffix is then used to sort what "
        "comes back. If the box has moved on since the list was filled the "
        "text wins and is looked up again, so pressing the button early does "
        "what you meant rather than what was selected.\n\n"
        "**One button.** It fetches, renders the opening measures and starts "
        "the radio, because a weather track that is not running is twenty "
        "seconds of one hour. Every control the Continue tab offers is pinned "
        "here to the one answer a weather track wants — nothing steers, style "
        "stays off, and the tempo ramp is off because the trajectory writes "
        "the tempo of every measure itself. This radio and Continue's are "
        "separate: starting one does not stop the other.\n\n"
        "**The view** scrolls the five signals past a playhead in step with "
        "the audio, dark where the hour being played is night and light where "
        "it is day, with the city's own local time running along the top. "
        "Each dot is one hour, which is one measure. Each lane keeps to its "
        "own band and the scale never changes with it — the same height is "
        "the same rms in Reykjavík as in Singapore.\n\n"
        "It is drawn in the browser off the player's own clock rather than "
        "pushed from the server, because the stream deliberately runs 45 "
        "seconds ahead of your ear to keep the buffer full and a server-drawn "
        "frame would be 45 seconds early. The server sends one number, once: "
        "how far into the song the press begins. After that the view advances "
        "by exactly what the player advanced by and nothing corrects it, "
        "which is why a leg boundary and a relay hop both pass without the "
        "picture moving.\n\n"
        "Set `SHOW_WEATHER=0` to hide the tab."),
    "Testing": (
        "### Testing — a lab bench\n\n"
        "None of these is a feature. They are settings whose right value is "
        "still an open question, exposed so the alternatives can be compared "
        "on a real GPU instead of argued about. They are live inputs to the "
        "generation buttons whether or not this tab is on screen.\n\n"
        "Hidden unless `SHOW_TESTING` is set."),
}

# Insertion order, which is tab order: the panel has to open on SOMETHING
# before any tab has been selected, and that is whichever tab the browser shows
# first. Derived rather than written out again so the two cannot disagree.
HELP_ORDER = list(HELP)
HELP_CLOSED, HELP_OPENED = "❔ Help", "✕ Close"


def _help_text(tab: str) -> str:
    """The panel's contents for one tab: what the app is, then what this tab
    is. The intro is repeated on every tab rather than shown once somewhere
    because the panel is the only place it appears, and someone who opens it on
    Edit should not have to go and find it."""
    return HELP_INTRO + HELP.get(tab, HELP[HELP_ORDER[0]])


# -----------------------------------------------------------------------------
# The Weather tab's scrolling view. Both of these belong to that tab and to
# nothing else; deleting the tab means deleting them and weather.py.
# -----------------------------------------------------------------------------
# A canvas rather than a gr.Plot, because a gr.Plot is an image rendered on the
# SERVER: a frame of it costs a round trip, and sixty of those a second is not
# a thing to ask of a Space that is also holding a GPU.
_WX_VIEW = """
<div id="wx-view" style="width:100%;height:430px;border-radius:12px;
     overflow:hidden;background:#0a1020;
     box-shadow:inset 0 0 40px rgba(0,0,0,.45)">
  <canvas style="width:100%;height:100%;display:block"></canvas>
</div>
"""

# WHY THE BROWSER HAS TO DO THE SCROLLING. on_stream deliberately runs ahead of
# the listener -- it throttles to keep STREAM_HEADROOM_S of audio in the
# player's buffer -- so anything pushed from the generator describes music that
# will not be heard for another forty seconds. The only honest playhead is
# `audio.currentTime`, and that is here.
#
# WHERE THE STREAM SITS IN THE SONG. `currentTime` is zero at the start of the
# PRESS, not of the song, and a press starts with a lead-in of already-rendered
# audio whose length this does not know. So it is measured rather than
# predicted: `stream_forward` guarantees the chunks concatenate to the song's
# tail exactly, so `song_seconds_rendered - buffered.end()` is the offset, and
# it holds for the whole press because both sides grow by the same audio. The
# server re-sends that first number at the head of every press and at every leg
# boundary; a relay hop -- which resets `currentTime` to 0 against a completely
# different measure -- therefore re-anchors itself without anyone tracking it.
#
# WHERE A MEASURE ENDS. Every bpm on a weather track has been through
# `snap_bpm`, so it is BPM_GRID_NUM/k and the measure lasts exactly k/50
# seconds. `payload()` sends k, and cumulating it is the clock. No drift.
#
# THE CLOCK IS ANCHORED ONCE AND THEN LEFT ALONE. It is set when the first
# chunk of the first press arrives and after that it advances by whatever the
# PLAYER advanced by -- the delta of `currentTime`, which is zero while paused,
# zero while stalled, and zero across a relay hop's reset rather than a leap.
# Nothing corrects it afterwards, because nothing knows better: within a press
# the offset cannot change, and the only numbers that claimed otherwise were
# derived from `buffered.end()`, which is a decoder's progress and moves
# underneath you for a second after every burst of audio. Re-reading it at each
# leg boundary is what used to make the view jump.
_WX_JS = r"""
() => {
  if (window.__wxLoop) return;
  window.__wxLoop = true;

  var WIN_S = 26.0;      // seconds of weather across the canvas, ~a day of it
  var HEAD  = 0.34;      // where the playhead sits, as a fraction of width
  var PRIME = 0.064;     // the one AAC priming frame at the head of a stream
  var PAD_R = 16, PAD_T = 44, PAD_B = 22;

  var PAL = {rms:"#ffb347", bpm:"#ff6b6b", density:"#5aa9e6",
             zcr:"#c792ea", flatness:"#4fd1c5"};

  var S = {d:null, dseen:null, cum:[0], off:0, sy:null, sseen:null,
           mode:null, anch:false, shown:null, ct:null};

  function vis(e){ return e && e.getClientRects().length > 0; }

  function pick(){
    var l = document.querySelector("#wx-live audio");
    if (vis(l)) return ["live", l];
    var a = document.querySelector("#wx-audio audio");
    if (vis(a)) return ["static", a];
    return [null, null];
  }

  // Wrapped, like the lanes are: past a year the trajectory starts the same
  // year again, and the clock should say so rather than run on into dates
  // nobody fetched.
  function epochAt(i){
    var j = i % S.d.n;
    return S.d.epoch ? S.d.epoch[j] : S.d.t0 - 3600 * j;
  }
  function kAt(i){ return S.d.k[i % S.d.n]; }

  // Cumulative measure start times, extended on demand. One entry per measure
  // of listening -- a couple of thousand an hour, which is nothing.
  function cumTo(t){
    var c = S.cum;
    while (c[c.length - 1] < t) c.push(c[c.length - 1] + kAt(c.length - 1) / 50);
    return c;
  }
  function measAt(t){
    if (!(t > 0)) return 0;
    var c = cumTo(t + WIN_S + 8), lo = 0, hi = c.length - 1, m;
    while (hi - lo > 1){ m = (lo + hi) >> 1; if (c[m] <= t) lo = m; else hi = m; }
    return lo + (t - c[lo]) / (c[lo + 1] - c[lo]);
  }

  // Local solar-ish time, from longitude alone. Good to well under an hour
  // everywhere, which is the resolution of the data.
  function localHour(i){
    var h = ((epochAt(i) + S.d.lon * 240) / 3600) % 24;
    return h < 0 ? h + 24 : h;
  }
  function daylight(i){
    var s = Math.sin(Math.PI * (localHour(i) - 6) / 12);
    return s > 0 ? s : 0;
  }
  var DAYS = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
  var MONS = ["Jan","Feb","Mar","Apr","May","Jun",
              "Jul","Aug","Sep","Oct","Nov","Dec"];
  function stamp(i){
    var d = new Date((epochAt(i) + S.d.lon * 240) * 1000);
    var hh = d.getUTCHours();
    return DAYS[d.getUTCDay()] + " " + d.getUTCDate() + " " +
           MONS[d.getUTCMonth()] + "  " + (hh < 10 ? "0" : "") + hh + ":00";
  }

  // The left gutter is MEASURED, not chosen. A fixed margin is a guess about
  // font metrics -- the browser's, at the reader's zoom, in whatever ui-sans
  // resolves to on their machine -- and the guess was wrong: "cloud,
  // inverted" ran off the edge. Asking the context how wide the longest of
  // the ten strings actually is cannot be wrong, and costs ten measureText
  // calls a frame.
  var NAME_F = "600 12px ui-sans-serif,system-ui,sans-serif";
  var FROM_F = "10px ui-sans-serif,system-ui,sans-serif";
  function gutterFor(g, lanes){
    var w = 0, i;
    g.font = NAME_F;
    for (i = 0; i < lanes.length; i++)
      w = Math.max(w, g.measureText(lanes[i].name).width);
    g.font = FROM_F;
    for (i = 0; i < lanes.length; i++)
      w = Math.max(w, g.measureText("← " + lanes[i]["from"]).width);
    return Math.ceil(w) + 24;      // 12 either side of the text
  }

  function mix(a, b, t){
    return "rgb(" + Math.round(a[0] + (b[0] - a[0]) * t) + "," +
                    Math.round(a[1] + (b[1] - a[1]) * t) + "," +
                    Math.round(a[2] + (b[2] - a[2]) * t) + ")";
  }
  var NIGHT = [10, 16, 32], DAY = [34, 58, 92];

  function frame(){
    requestAnimationFrame(frame);
    var cv = document.querySelector("#wx-view canvas");
    if (!cv || !vis(cv)) return;

    // Data and sync arrive as plain globals set by two js-only listeners, so
    // nothing here depends on which of the three ran first.
    if (window.__wxData !== S.dseen){
      S.dseen = window.__wxData;
      var d = window.__wxData;
      S.d = (d && d.n && d.k && d.lanes) ? d : null;
      S.cum = [0]; S.off = 0; S.sy = null; S.mode = null;
      S.anch = false; S.shown = null; S.ct = null;
    }
    // Remembered rather than acted on here, because the sync and the swap to
    // the streaming player are two updates and either can land first.
    if (window.__wxSync !== S.sseen){
      S.sseen = window.__wxSync;
      if (window.__wxSync && typeof window.__wxSync.off === "number")
        S.sy = window.__wxSync;
    }

    var dpr = window.devicePixelRatio || 1;
    var W = cv.clientWidth, H = cv.clientHeight;
    if (!W || !H) return;
    if (cv.width !== Math.round(W * dpr) || cv.height !== Math.round(H * dpr)){
      cv.width = Math.round(W * dpr); cv.height = Math.round(H * dpr);
    }
    var g = cv.getContext("2d");
    g.setTransform(dpr, 0, 0, dpr, 0, 0);
    g.clearRect(0, 0, W, H);
    g.fillStyle = "#0a1020"; g.fillRect(0, 0, W, H);

    if (!S.d){
      g.fillStyle = "#54687f";
      g.font = "13px ui-sans-serif,system-ui,sans-serif";
      g.textAlign = "center";
      g.fillText("Fetch a place to see its weather here.", W / 2, H / 2);
      return;
    }

    // WHICH PLAYER, AND WHERE IT STARTS. Swapping players is the one event
    // that invalidates the clock, so it is also the only thing that re-arms
    // it. The plain one holds the whole track from bar one, so its offset is
    // zero and needs nothing from the server; the streaming one needs the
    // anchor, and waits for it rather than arming itself at zero.
    var got = pick(), mode = got[0], el = got[1];
    if (mode !== S.mode){ S.mode = mode; S.anch = false; S.ct = null; }
    if (!S.anch && mode === "static"){
      S.off = 0; S.anch = true; S.shown = null;
    } else if (!S.anch && mode === "live" && S.sy){
      S.off = S.sy.off; S.anch = true; S.shown = null;
    }

    var ct = el ? el.currentTime : 0;
    var step = (S.ct === null) ? 0 : ct - S.ct;
    S.ct = ct;
    // Not time: a stall, a pause, or a relay hop putting the media clock back
    // to zero under audio that is still playing out of the old buffer.
    if (!(step > 0) || step > 1.0) step = 0;
    if (S.shown === null) S.shown = Math.max(0, S.off + ct - PRIME);
    else if (S.anch) S.shown += step;   // held still until the anchor lands
    var songT = Math.max(0, S.shown);

    var PAD_L = gutterFor(g, S.d.lanes);

    var t0 = songT - HEAD * WIN_S, t1 = t0 + WIN_S;
    var i0 = Math.max(0, Math.floor(measAt(Math.max(0, t0))) - 1);
    cumTo(t1 + 4);
    var i1 = Math.min(S.cum.length - 2, Math.floor(measAt(t1)) + 2);
    var x = function(t){ return (t - t0) / WIN_S * W; };

    // Day and night, one band per hour. This is the thing that makes a year of
    // weather legible at a glance -- the music's diurnal shape and the sky's
    // are the same shape, and here they are drawn on top of each other.
    var i;
    for (i = i0; i <= i1; i++){
      var xa = x(S.cum[i]), xb = x(S.cum[i + 1]);
      g.fillStyle = mix(NIGHT, DAY, daylight(i));
      g.fillRect(xa, 0, xb - xa + 1, H);
    }
    // Midnight lines, so the days are countable.
    for (i = i0; i <= i1; i++){
      if (localHour(i) < 1){
        g.fillStyle = "rgba(255,255,255,.14)";
        g.fillRect(x(S.cum[i]), 0, 1, H);
      }
    }

    // Five lanes, each inside its own share of the height and none of them
    // reaching into anyone else's: a curve that can cross into the lane above
    // is a curve you have to decide about before you can read it. The height
    // they get to move in comes from the canvas being tall instead.
    var lanes = S.d.lanes, nL = lanes.length;
    var laneH = (H - PAD_T - PAD_B) / nL;
    var span = laneH - 12;
    var li, L, col, base, pts, mid, grad;
    for (li = 0; li < nL; li++){
      L = lanes[li]; col = PAL[L.key] || "#9fb3c8";
      base = PAD_T + (li + 1) * laneH - 6;

      g.strokeStyle = "rgba(255,255,255,.07)";
      g.beginPath(); g.moveTo(PAD_L, base + 0.5); g.lineTo(W - PAD_R, base + 0.5);
      g.stroke();

      // Straight segments between measure midpoints, with the midpoints
      // marked. The data is hourly and one hour is one measure, so the dots
      // are the actual resolution of the thing -- drawing them is the
      // difference between a curve and a curve you can count.
      pts = [];
      for (i = i0; i <= i1; i++){
        mid = (S.cum[i] + S.cum[i + 1]) / 2;
        pts.push([x(mid), base - L.unit[i % S.d.n] * span]);
      }
      if (pts.length > 1){
        g.save();
        g.beginPath();
        g.rect(PAD_L, base - span - 2, W - PAD_L - PAD_R, span + 3);
        g.clip();

        g.beginPath();
        g.moveTo(pts[0][0], base);
        for (i = 0; i < pts.length; i++) g.lineTo(pts[i][0], pts[i][1]);
        g.lineTo(pts[pts.length - 1][0], base);
        g.closePath();
        // Two fills: the sky behind is knocked back first, so the colour sits
        // on a known background instead of on whatever hour it happens to be
        // passing over.
        g.fillStyle = "rgba(10,16,32,.5)"; g.fill();
        grad = g.createLinearGradient(0, base - span, 0, base);
        grad.addColorStop(0, col + "4d");
        grad.addColorStop(1, col + "12");
        g.fillStyle = grad; g.fill();

        g.beginPath();
        g.moveTo(pts[0][0], pts[0][1]);
        for (i = 1; i < pts.length; i++) g.lineTo(pts[i][0], pts[i][1]);
        g.strokeStyle = col; g.lineWidth = 1.8;
        g.shadowColor = col; g.shadowBlur = 8;
        g.stroke();
        g.shadowBlur = 0;

        g.fillStyle = col;
        for (i = 0; i < pts.length; i++){
          g.beginPath();
          g.arc(pts[i][0], pts[i][1], 1.9, 0, 6.2832);
          g.fill();
        }
        g.restore();
      }
    }

    // The names, in the gutter `gutterFor` already reserved for them.
    var lft = g.createLinearGradient(0, 0, PAD_L + 18, 0);
    lft.addColorStop(0, "rgba(10,16,32,.95)");
    lft.addColorStop(1, "rgba(10,16,32,0)");
    g.fillStyle = lft; g.fillRect(0, 0, PAD_L + 18, H);

    g.textAlign = "right";
    for (li = 0; li < nL; li++){
      L = lanes[li]; col = PAL[L.key] || "#9fb3c8";
      base = PAD_T + (li + 1) * laneH - 6;
      g.fillStyle = col;
      g.font = NAME_F;
      g.fillText(L.name, PAD_L - 12, base - span * 0.5);
      g.fillStyle = "rgba(255,255,255,.38)";
      g.font = FROM_F;
      g.fillText("← " + L["from"], PAD_L - 12, base - span * 0.5 + 13);
    }

    // The playhead, and the clock that rides it.
    var px = Math.round(W * HEAD) + 0.5;
    g.strokeStyle = "rgba(255,255,255,.85)"; g.lineWidth = 1;
    g.shadowColor = "#ffffff"; g.shadowBlur = 10;
    g.beginPath(); g.moveTo(px, PAD_T - 14); g.lineTo(px, H - 6); g.stroke();
    g.shadowBlur = 0;

    var cur = Math.min(S.cum.length - 2, Math.max(0, Math.floor(measAt(songT))));
    var st = stamp(cur);
    g.textAlign = "left";
    g.fillStyle = "#e8eef6";
    g.font = "600 15px ui-sans-serif,system-ui,sans-serif";
    g.fillText(st, 14, 26);
    var stw = g.measureText(st).width;   // measured in the font it was drawn in
    g.fillStyle = "rgba(255,255,255,.42)";
    g.font = "11px ui-sans-serif,system-ui,sans-serif";
    g.fillText(S.d.place + " · local time · running backwards", 14 + stw + 14, 26);
  }
  requestAnimationFrame(frame);
}
"""


# -----------------------------------------------------------------------------
# App
# -----------------------------------------------------------------------------
def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
    backend = backend or MockBackend()
    presets = backend.list_presets()

    with gr.Blocks(title="Conditioning Composer") as demo:
        gr.HTML(_PLOT_CSS, elem_classes="style-inject")

        # ---- state ----------------------------------------------------------
        cur = gr.State(None)   # current Track
        prev = gr.State(None)  # previous Track (for A/B)
        # Undo stack, oldest first. Distinct from `prev`: that is a deliberate
        # parking slot you manage, this is automatic and only goes backwards.
        # It exists mostly for Continue -- a rollout is a chain of irreversible
        # commits, so without this a bad continuation is permanent and you
        # cannot re-roll one with a different seed or a different steer.
        hist = gr.State([])
        # The one piece of state that is written WHILE a handler is running,
        # and the reason it can be: gr.State.preprocess passes the session's
        # object straight through, so this dict is the same object in the
        # pause listener and in the on_stream generator that is already in
        # flight. Every other input is a value copied into the request that
        # started the event.
        pause_req = gr.State({"t": None})
        # Written the same way and for the same reason, but read one step
        # later: on_stream stamps it as it ends, and the `.then` chain behind
        # the Play button reads it to decide whether that ending was really an
        # ending. See RELAY in on_stream.
        resume_req = gr.State({"go": False, "hops": 0, "dry": 0,
                               "lead_s": 0.0, "at": 0.0})
        # The counter itself. It exists only to be changed: a gr.State would
        # keep the value server-side, and it is precisely the ROUND TRIP to the
        # browser that this needs, so it has to be a real component whose new
        # value is sent out and echoed back as a fresh request. Hidden, and
        # never touched by anything but _relay_tick. One counter, because there
        # is now one place a stream can be started from.
        s_relay = gr.Number(value=0, visible=False, precision=0,
                            elem_id="relay-generate")

        # Which tab the help panel is describing, and whether it is open. Both
        # have to be server-side state because neither is readable off a
        # component: a handler is told a component's VALUE, never its
        # visibility, and a tab's selection is an event rather than an input.
        help_tab = gr.State(HELP_ORDER[0])
        help_open = gr.State(False)

        # Title left, help right. One line, because the old three-paragraph
        # header was the single largest piece of clutter in the app and every
        # sentence in it is now in HELP where it is asked for rather than
        # imposed.
        with gr.Row():
            gr.Markdown("# Conditioning Composer", elem_classes="app-title")
            # scale=0 so the column is only as wide as the button: in a Row the
            # last element with scale gets the slack, and giving it to the title
            # is what puts this against the right edge.
            with gr.Column(scale=0, min_width=110):
                help_btn = gr.Button(HELP_CLOSED, size="sm")
        help_md = gr.Markdown(visible=False, elem_classes="help-panel")

        # =====================================================================
        # Three tabs, ONE track
        # =====================================================================
        # `cur`, `prev` and `hist` are declared above the tabs on purpose: this
        # is not three apps, it is three things to do with the same one. A track
        # started on Generate is the same object Edit takes apart, so the way
        # you use this is to put something on that you like and then go and
        # change part of it -- switching tabs is free and loses nothing.
        #
        # What lives where:
        #
        #   GENERATE  makes the track longer. Everything about a continuation
        #             is here: where it heads, how fast, how hard it holds on
        #             to what it has written, and the tempo it starts from.
        #   EDIT      changes 24 measures of it. Everything here is aimed at
        #             the window the picker at the top has selected, and the
        #             window can be seated ANYWHERE in the song (see
        #             backend.seat_window) -- not just at the end.
        #   TESTING   is a lab bench: settings whose right value is still an
        #             open question. Hidden unless SHOW_TESTING is set.
        #
        # Three consequences to keep in mind when adding to any of them:
        #
        #   1. EVERY handler has to write BOTH visible tabs' views, or the one
        #      you are not looking at goes stale and lies to you the moment you
        #      switch. `_mirror` below does that mechanically -- see its
        #      docstring.
        #   2. A setting shown on ONE tab is read directly by the other tab's
        #      events. There is one component per quantity and no mirroring to
        #      keep in step: Edit's Continue-shaped operations read Generate's
        #      guidance knobs, Generate's buttons read Edit's `draft`, and both
        #      read Testing's radios whether or not that tab is on screen. A
        #      hidden component still submits its value, which is exactly why
        #      Testing can be hidden rather than skipped.
        #   3. The two exceptions are `draft` and `guidance`, which have a
        #      linked copy on Generate because they change what you HEAR and
        #      hiding them behind a tab switch made both tabs worse. They are
        #      kept equal by the `_link` pairs at the end of the wiring; if you
        #      add another pair, add its link in the same commit.
        with gr.Tabs():
            # =================================================================
            # GENERATE
            # =================================================================
            with gr.Tab("Generate") as tab_generate:
                # Starting a track and extending one were two halves of one tab
                # and are two different sittings: you start once, then spend the
                # session on **Continue**.
                gr.Markdown(
                    "Pick something to start from, then keep it going on "
                    "**Continue**.")

                # Seed and player on one line, above everything they apply to.
                # The seed belongs to all three entry points rather than to any
                # one of them, so it cannot sit inside a column with one of
                # them; and the player has to be here at all because pressing a
                # Start button with no player on the tab gives no sign that
                # anything happened until you change tabs.
                with gr.Row():
                    s_seed = gr.Number(
                        value=0, label="Seed", precision=0, scale=1,
                        info="Where the music starts from: the same seed "
                             "and the same settings open the same way. "
                             "Change it for a different take of the same "
                             "idea.")
                    # A second component holding the same file as Continue's
                    # player, for the reason `_snippets` has two: a component
                    # belongs to one tab. Written by the same mirror.
                    g_audio = gr.Audio(label="Current track", type="filepath",
                                       interactive=False, scale=3)

                with gr.Row():
                    with gr.Column(scale=1):
                        # The two entry points that arrive with real music
                        # behind them, and so with a measured tempo of their
                        # own. Nothing in this column reads the tempo block.
                        s_preset = gr.Dropdown(presets, value=presets[0],
                                               label="A real song")
                        s_preset_btn = gr.Button("Start from this song",
                                                 variant="primary")
                        s_upload = gr.Audio(label="…or your own audio",
                                            type="filepath", sources=["upload"])
                        s_upload_btn = gr.Button("Start from this audio")

                    with gr.Column(scale=1):
                        s_text = gr.Textbox(
                            label="…or a description",
                            placeholder="e.g. dark cinematic flamenco, building",
                            info="Leaves harmony, energy and density **off** — "
                                 "turn them on over on Edit once you have heard "
                                 "it.")
                        s_text_btn = gr.Button("Start from this description")

                        # ---- tempo ------------------------------------------
                        # Under the description, because that is the pairing: a
                        # song and an upload arrive with a measured tempo and
                        # keep it, a description says nothing about tempo at all
                        # and takes whatever this block says. Adjacency is the
                        # whole explanation, which is why there is no longer a
                        # paragraph making it. The preview is live and works
                        # with no track loaded -- dial the curve in BEFORE
                        # generating, which is the whole reason it is up here.
                        gr.Markdown("### Tempo")
                        bpm_preview = gr.Plot(label="Per-measure tempo (live)",
                                              elem_classes="plot-card")
                        with gr.Row():
                            bpm_mean = gr.Slider(BPM_MIN, BPM_MAX, value=120,
                                                 step=1, label="global BPM",
                                                 scale=2)
                            bpm_var = gr.Slider(0.0, 20.0, value=2.0, step=0.5,
                                                label="variance (± BPM std)",
                                                scale=2)
                        with gr.Row():
                            bpm_seed = gr.Number(value=0, label="tempo seed",
                                                 precision=0, scale=1)
                            bpm_btn = gr.Button("Apply tempo to current track",
                                                scale=2)
                        gr.Markdown(
                            "<sub>One global tempo, each measure wandering "
                            "around it — real recordings drift, and a perfectly "
                            "flat BPM line is off-manifold, so *variance* is the "
                            "per-measure standard deviation. Presets and uploads "
                            "keep their own measured tempo until you hit "
                            "**Apply**. BPM also sets each measure's length at "
                            "decode time, so it is never ignored, even with "
                            "every signal off.</sub>")

            # =================================================================
            # CONTINUE
            # =================================================================
            # Extending a track, and hearing it. One section, not two, because
            # the three buttons at the bottom are the same operation at three
            # lengths -- eight measures, a leg, or until you stop it -- and all
            # of them read the controls above them. Splitting "continue" from
            # "radio" would put those controls under one heading and silently
            # apply them to the other.
            with gr.Tab("Continue") as tab_continue:
                # Every button in one bar, across the top, above both columns.
                # They are one operation at four lengths -- eight measures, a
                # leg, until you stop it, or back -- and all four read the same
                # settings below, so putting them anywhere inside a column
                # implies they belong to that column and not the other. At the
                # top they belong to the tab, which is the truth.
                #
                # Start/Stop Radio is ONE button in the eye and two components
                # in the tree, swapped by visibility, for the same reason the
                # two players below are (see s_live): gradio decides at build
                # time which events a press cancels, so a single component
                # cannot both start a stream and cancel the stream it started.
                # `_radio_on`/`_radio_off` already swap the players on exactly
                # the transitions this button has to follow, so the swap costs
                # two more outputs on functions that were doing it anyway.
                with gr.Row():
                    s_cont_btn = gr.Button(
                        f"⏩ Generate {CONTINUE_MEASURES} Measures", scale=3)
                    s_ff_btn = gr.Button("⏭ Fast-Forward", scale=3)
                    s_stream_btn = gr.Button("🔴 Start Radio",
                                             variant="primary", scale=3)
                    s_stop_btn = gr.Button("⏹ Stop Radio", variant="stop",
                                           scale=3, visible=False)
                    s_undo_btn = gr.Button("↩ Undo", scale=1)

                # Two columns, split by question rather than by component type:
                # the left one is WHERE the music goes, the right one is
                # hearing it and the two knobs you reach for between presses.
                with gr.Row():
                    with gr.Column(scale=1):
                        # One control for every way the style can move, because
                        # in the backend they are one choice: the typed steer,
                        # the two walks and style-off are exclusive branches of
                        # `_steer_or_walk`. The mapping lives in S_MOVE, which
                        # also records why this is not the ordered "how far"
                        # dial it looks like it wants to be.
                        s_move = gr.Radio(
                            [(v[2], k) for k, v in S_MOVE.items()],
                            value=S_MOVE_DEFAULT, label="How it moves",
                            info="Hold keeps the style still (the radio nudges "
                                 "it so it cannot loop). Text goes somewhere you "
                                 "describe and stays there. Wander heads for a "
                                 "different reference song every so often. Drift "
                                 "holds one direction, then strikes out in a new "
                                 "one. Unconditional drops the style instruction "
                                 "entirely, leaving just the bars so far. How "
                                 "long each takes is below.")
                        # Revealed by "toward" rather than sitting there greyed
                        # out: the radio already says what the box is for, so an
                        # always-present box would only be asking to be typed in
                        # while four of the five modes ignore it.
                        s_steer = gr.Textbox(
                            label="Where to head", visible=False,
                            placeholder="e.g. sparser, brushed drums, minor key",
                            info="A destination, not a nudge: it moves there "
                                 "over the measures below and then holds it. "
                                 "Leave it empty and this behaves like “stay "
                                 "close”.")
                        # The distance dial, and the honest one: the mode above
                        # picks the kind of movement, this picks the rate. One
                        # slider for both branches -- see S_MOVE_JOURNEY for why
                        # that is sound and why picking a mode rewrites it.
                        #
                        # Live in EVERY mode, including the two that do not
                        # travel: Fast-forward writes this many measures
                        # (on_simple_ff passes it as max_measures), which is the
                        # only length Generate shows. A slider that greyed out
                        # under "Hold" would take the length control away
                        # from the one mode most likely to be fast-forwarded.
                        s_pace = gr.Slider(
                            CONTINUE_MEASURES, 256, value=STYLE_PACE,
                            step=CONTINUE_MEASURES,
                            label="Measures per journey",
                            info=f"How long one trip takes: for a walk, how long "
                                 f"a leg lasts before it picks the next place to "
                                 f"go; for a typed destination, how long it takes "
                                 f"to arrive. 96 measures ≈ 3 min at 120 BPM, and "
                                 f"about as different as an unrelated piece; "
                                 f"lower is faster and less musical. Spread "
                                 f"across as many takes as it needs — "
                                 f"{CONTINUE_MEASURES} = done this take, 64 = "
                                 f"eight takes each moving an eighth of the way. "
                                 f"It is also how far one **Fast-Forward** "
                                 f"writes, in every mode — up to "
                                 f"{FF_MAX_MEASURES} measures in a single "
                                 f"press, above which it writes that much and "
                                 f"the line under the player says what is "
                                 f"left.")
                        # Starts greyed because the default mode is "stay
                        # close", which has no legs to filter; the radio turns it
                        # on for "wander" and off again for everything else.
                        cross_cluster_cb = gr.Checkbox(
                            value=True, interactive=False,
                            label="Each leg must move to a different song",
                            info="On by default, and that is about the size of "
                                 "the reference set rather than about the "
                                 "feature. Wander picks its destination from "
                                 "every measure of every preset — 88 points, but "
                                 "only 5 recordings, so most of the pool is the "
                                 "same five places listed over and over. Left "
                                 "free, most legs are a hop between two measures "
                                 "of the song already playing: a genuine journey "
                                 "in the embedding, an inaudible one in the "
                                 "music. This forces every leg to cross to a "
                                 "different song. Untick to see the unfiltered "
                                 "behaviour; with more reference material it "
                                 "stops being needed. Ignored by drift and by a "
                                 "typed steer.")

                    with gr.Column(scale=1):
                        s_audio = gr.Audio(label="Current track",
                                           type="filepath", interactive=False)
                        # The radio's player lives HERE, in the same slot as the
                        # track player and hidden until the radio is running,
                        # rather than appearing under the button that started
                        # it. One player, one place to look. It cannot be the
                        # same component: gradio's audio frontend computes
                        # `show_waveform = show_recording_waveform && !is_stream`
                        # and a streamed value always carries is_stream, so a
                        # live stream gets a plain <audio> element and no
                        # waveform no matter what the component is configured
                        # as. Two components swapped by visibility is the
                        # closest thing to one player that gradio allows.
                        s_live = GaplessAudio(label="Radio — playing live",
                                              streaming=True, autoplay=True,
                                              interactive=False, visible=False)
                        # Hidden, not deleted: it is written by every handler
                        # that touches out_main (see _mirror), so removing it
                        # would mean changing the arity of all of them. What it
                        # said -- measure counts, the seed, four DSP means -- is
                        # bookkeeping about a track you can hear.
                        s_summary = gr.Markdown(visible=False)

                        # Continuation is what all three buttons below do, and
                        # these two are the knobs that decide how it sounds --
                        # the most-reached-for pair in the whole app.
                        with gr.Row():
                            s_hist_g = gr.Slider(
                                1.0, 4.0, value=HISTORY_GUIDANCE, step=0.1,
                                label="History guidance", scale=1,
                                info="How hard the new measures are steered "
                                     "toward the ones before them. 1.0 = off; "
                                     "1.5 is the sweet spot; high over-drives "
                                     "into repetition.")
                            s_ctx_noise = gr.Slider(
                                0.0, 0.6, value=CONTINUE_CTX_NOISE, step=0.05,
                                label="History noise", scale=1,
                                info="How loosely the bars so far are held. "
                                     "0.2 keeps takes continuous without "
                                     "restating each other; lower holds harder "
                                     "and risks looping, above ~0.3 washes "
                                     "out.")
                        with gr.Row():
                            cont_seed = gr.Number(
                                value=3, label="Continuation seed", precision=0,
                                scale=1,
                                info="Separate from the start seed above: "
                                     "re-rolling a bad take should not mean "
                                     "re-rolling the whole track.")
                            g_draft = gr.Checkbox(
                                value=False, scale=1,
                                label="Draft (16 steps instead of 32, faster)")

                        s_status = gr.Markdown()

                # Full width, under both columns rather than beside them: the
                # map is a scatter of 88 anchors you are meant to pan around in,
                # and it now grows a second panel only when there is a journey
                # to plot (see _style_map) -- neither of which fits in half a
                # tab. Open by default, because it is the only place that
                # answers whether the mode picked above is doing anything.
                # Updated on every press and at every leg boundary of a live
                # stream.
                with gr.Accordion("Where the style has travelled", open=True):
                    s_map = gr.Plot(show_label=False,
                                    elem_classes="plot-card")
                    gr.Markdown(
                        "<sub>A map of the sound, with the reference songs as "
                        "dots and this track's journey drawn over them. When "
                        "something is steering, a second panel appears showing "
                        "how far it still has to go — a journey that is working "
                        "falls to zero, then jumps when it picks somewhere new. "
                        "The map is flattened from 512 dimensions to two, so "
                        "use it for the shape of the trip; the panel beside it "
                        "is the exact one.</sub>")
                    gr.Markdown(
                        "<sub>⚠️ The map is only as good as the points it is "
                        "fit to, and right now that is the five shipped "
                        "reference songs. Distances between those five are "
                        "meaningful; absolute position in a region none of them "
                        "occupy is extrapolation.</sub>")

                with gr.Accordion("Take the tempo along (experimental)",
                                  open=False):
                    gr.Markdown(
                        "<sub>Both of these ramp the tempo toward the "
                        "tempo of whatever recording **wander** is "
                        "heading for, over the same leg. Only wander "
                        "does this: a typed steer aims at text and "
                        "drift aims along an axis, and neither has a "
                        "recording to read a tempo off.</sub>")
                    bpm_follow_cb = gr.Checkbox(
                        value=False,
                        label="…as a smooth ramp",
                        info="⚠️ Off by default because it measurably "
                             "hurts. Real per-measure tempo is white "
                             "jitter around a constant — across the "
                             "demo presets a linear trend explains "
                             "0.0–1.8% of the variation and the "
                             "song-median predicts each measure better "
                             "than a fitted trend does. A ramp is 100% "
                             "trend with no jitter, i.e. the opposite "
                             "shape, and bpm is not a hint the model "
                             "can soften: the decoder turns it "
                             "straight into each measure's length. The "
                             "ramp is geometric (even accelerando) and "
                             "octave-folded, so a destination is always "
                             "within a factor of two of where you are — "
                             "it cannot lurch. Turn off if you have "
                             "pinned a tempo with **Apply tempo** and "
                             "want it held.")
                    bpm_step_cb = gr.Checkbox(
                        value=False,
                        label="…as a staircase",
                        info="The same idea with the shape the "
                             "measurement asked for. The tempo holds one "
                             "value for a stretch of measures, plus "
                             "±1.5% white jitter — which is what the "
                             "demo presets actually look like (rel-std "
                             "1.2–1.6%, jitter index ≈1, i.e. white) — "
                             "then steps, by at most 10% at a time, "
                             "toward the destination tempo. How long it "
                             "holds is the slider below, and that slider "
                             "is the whole trade. Wins over the ramp "
                             "above if both are ticked, since the two "
                             "write the same measures.")
                    bpm_hold_sl = gr.Slider(
                        8, 96, value=BPM_STEP_HOLD, step=8,
                        interactive=False,   # the step box turns it on
                        label="Tempo holds for (measures)",
                        info=f"Only used by the stepped mode, and it "
                             f"is the whole trade: ARRIVING against "
                             f"SOUNDING RIGHT, with no setting that "
                             f"wins both. Short holds step often, so "
                             f"the tempo keeps up with the style, but a "
                             f"step lands inside most "
                             f"{N_CHUNKS}-measure windows and those "
                             f"windows read as a ramp. Long holds look "
                             f"like real tempo data and move too slowly "
                             f"to arrive before wander picks a new "
                             f"destination. Measured over a 96-measure "
                             f"wander — tempo error when the style "
                             f"arrives, and the share of windows not "
                             f"distinguishable from real per-measure "
                             f"tempo: 16 → 1% off, 24% of windows; "
                             f"{N_CHUNKS} → 6% off, 32%; 32 → 12% off, "
                             f"44%; 48 → 28% off, 76%; 96 (a whole leg) "
                             f"→ 48% off, 79%. The ramp above is the "
                             f"far end: 0% off, no window looks real. "
                             f"Below {N_CHUNKS} buys nothing — a value "
                             f"held for less than one window is in no "
                             f"window on its own, so it cannot look "
                             f"real however short it gets.")

            # =================================================================
            # SELECT
            # =================================================================
            # Choosing what to work on, and looking at it. Split off from Edit
            # because the two are different activities on different timescales:
            # you seat a window once and then make a dozen changes to it, and
            # the picker plus two full-width figures were pushing every control
            # that actually changes something below the fold.
            with gr.Tab("Select") as tab_select:
                gr.Markdown(
                    f"Pick the measures you want to change, then work on them "
                    f"in **Edit**. A selection is at most "
                    f"{EDIT_MAX_MEASURES} measures: the model reads "
                    f"{N_CHUNKS} at a time, and the {EDIT_GUARD} either side of "
                    f"your selection are held fixed so the rewrite has real "
                    f"music to join onto at both ends.")

                # ---- the selection -----------------------------------------
                # ONE control: the measures you want rewritten. Everything the
                # model needs on top of that -- which N_CHUNKS it reads, how
                # much frozen music sits either side -- is derived in
                # `_sel_window`, so there is no window picker to get out of step
                # with the range it is supposed to contain.
                #
                # Sliders, not number boxes, for the reason `.release` exists:
                # a Number fires `change` on every keystroke, so typing "104"
                # asks to seat the window at 1, then 10, then 104. A slider
                # commits once, on mouse-up.
                #
                # SnapSlider, not gr.Slider, because these two are the only
                # controls in the app whose bounds MOVE -- the last measure's
                # floor follows the first, and both ceilings follow the length
                # of the song -- and a moving bound is the one thing gradio
                # answers by killing the event. Snapping instead is the
                # behaviour the picker already has everywhere else: `_sel_window`
                # takes any pair and returns a legal one. See SnapSlider.
                #
                # Moving the window is free and lossless -- the measure bank
                # keeps the conditioning and latents of every measure that has
                # scrolled out, so seating is an exact slice rather than a
                # re-encode (backend.seat_window) -- which is what makes it
                # reasonable to re-seat on every drag.
                gr.Markdown("### What gets rewritten")
                with gr.Row():
                    sel_from = SnapSlider(0, 1, value=0, step=1, scale=1,
                                          label="first measure")
                    sel_to = SnapSlider(1, EDIT_MAX_MEASURES,
                                        value=EDIT_MAX_MEASURES, step=1,
                                        scale=1, label="last measure")
                with gr.Row():
                    win_back = gr.Button(f"◀ {EDIT_MAX_MEASURES}", scale=1)
                    win_fwd = gr.Button(f"{EDIT_MAX_MEASURES} ▶", scale=1)
                    win_end = gr.Button("⏭ end", scale=1)

                # ---- snippet player -----------------------------------------
                # What the selection sounds like, and the reason this tab is
                # worth visiting before Edit: the sliders are abstract until you
                # hear the 24 measures they landed on. Short enough to replay on
                # a loop while you drag. (Edit has its own copy at the top of
                # that tab -- same audio, same tail, see `_snippets`.)
                snip_sel = gr.Audio(
                    label=f"The {N_CHUNKS} measures the model is reading",
                    type="filepath", interactive=False)

                # ---- what the model is being told ---------------------------
                # Side by side: they are two views of one object, the curves
                # being the values and the table being which of them the model
                # is allowed to read. Stacked, the second was always off-screen
                # while you were looking at the first.
                #
                # Three columns, 2:1:1, because that is what each one needs. The
                # curves are a figure and take every pixel they are given; the
                # table is seven short rows and was being handed a half-width
                # column to set them in; the switch is a dropdown and two
                # buttons, and it belongs NEXT to the table rather than under it
                # -- it is how you change the thing the table is showing, and at
                # the bottom of a column it read as a third, separate control.
                with gr.Row():
                    with gr.Column(scale=2):
                        gr.Markdown(
                            "### Conditioning curves (read-only)\n"
                            "The selected window only, numbered in song "
                            "measures.")
                        curves_plot = gr.Plot(label="Per-measure signals",
                                              elem_classes="plot-card")
                    with gr.Column(scale=1):
                        # Whole window, no ranges. This checkpoint was only ever
                        # trained with each signal on for ALL 24 measures or off
                        # for all 24 -- its per-measure dropout path was
                        # disabled throughout training (`p_ind_low =
                        # p_ind_high = 0`), so a signal on for some measures and
                        # off for others is a configuration it has never seen,
                        # and the measures next to the on->off step come back
                        # distorted or at wild volume. The controls used to
                        # offer a range with a warning attached; an option that
                        # is wrong every time it is used is not an option.
                        # Turning a signal off over PART of a song is still
                        # reachable, and correctly: seat the window there and
                        # edit it as its own window.
                        gr.Markdown("### Conditioning coverage")
                        coverage_tbl = gr.Markdown()
                    with gr.Column(scale=1):
                        # "All signals" is an entry in the picker rather than a
                        # second pair of buttons. Four buttons where two would
                        # do, and the two extra ones were the same verb again
                        # with a different scope -- scope is what the dropdown
                        # above them is for.
                        gr.Markdown("### Turn one on or off")
                        cov_signal = gr.Dropdown(
                            [(COV_ALL_LABEL, COV_ALL)] + ALL_SIGNAL_CHOICES,
                            value=COV_ALL, label="signal")
                        with gr.Row():
                            cov_on = gr.Button("On", variant="primary")
                            cov_off = gr.Button("Off")

            # =================================================================
            # EDIT
            # =================================================================
            # Everything that CHANGES the selected measures. No picker and no
            # figures: those are Select's, and by the time you are here the
            # question is not which measures but what to do to them.
            with gr.Tab("Edit") as tab_edit:
                # The window, at the top, before anything that could change it.
                # No full-song player: what you are editing is 24 measures, and
                # a six-minute file with a playhead somewhere in it is not a way
                # to hear a change to four of them. The whole song is on Export.
                snip_edit = gr.Audio(
                    label=f"The {N_CHUNKS} measures the model is reading",
                    type="filepath", interactive=False)

                # ---- this take ----------------------------------------------
                # BETWEEN THE TWO PLAYERS, because that is what all four act on:
                # Regenerate rewrites what A is playing, Snapshot and Swap move
                # it to and from B, Undo puts it back. Regenerate leads and is
                # the only primary -- it is the one that makes a sound, and the
                # only one of the four you press without having decided anything
                # first.
                #
                # No "Play B": every gradio audio player has its own transport,
                # and `on_save`/`on_swap` write the file AND the label into the
                # B slot as they run, so the button's whole job was to put the
                # same file back into the same player it was already in.
                with gr.Row():
                    regen_btn = gr.Button("♫ Regenerate", variant="primary",
                                          scale=2)
                    undo_btn = gr.Button("↩ Undo", scale=1)
                    save_btn = gr.Button("💾 Snapshot", scale=1)
                    swap_btn = gr.Button("⇄ Swap", scale=1)
                ab_audio = gr.Audio(label="B — (empty)", type="filepath",
                                    interactive=False)
                # Hidden, not deleted: it is out_main[4] and every handler in
                # the app returns a string for it, so the component has to exist
                # to be written to. What it said -- measure counts, the seed,
                # four DSP means -- is bookkeeping about a track you can hear.
                summary_md = gr.Markdown(visible=False)

                # ---- what the button will do --------------------------------
                # Two columns, and the split is between the two questions a
                # rewrite answers: HOW it gets rendered, and WHAT it is being
                # told. They used to be stacked, with Regenerate wedged between
                # them, which put the seed at the top of the tab and the thing
                # it re-rolls at the bottom.
                #
                # Both columns are inputs to the button ABOVE them. That is the
                # right way round: you press Regenerate, then adjust, then press
                # it again -- the button is the fixed point of the loop and the
                # settings are what you reach down to.
                with gr.Row():
                    with gr.Column(scale=1):
                        gr.Markdown("### Render settings")
                        # Strength first: it is the one that decides HOW FAR the
                        # music moves, and the seed only picks which way. The
                        # seed used to sit alone next to a "Vary" button doing
                        # the same operation from a different box, so the two
                        # could disagree about which take you were hearing.
                        # Regenerate with the seed changed IS Vary.
                        rg_strength = gr.Slider(
                            0.0, 1.0, value=REGEN_STRENGTH, step=0.05,
                            label="Edit strength",
                            info="How far the selected measures are re-rolled. "
                                 "**0** hands back exactly what is already "
                                 "there — the take is kept, so this is the "
                                 "setting for \"I changed one curve, leave "
                                 "everything else alone\". **1** writes the "
                                 "span from scratch against its frozen "
                                 "neighbours. In between, the rewrite starts "
                                 "from the existing performance and moves away "
                                 "from it: the phrasing survives at low values "
                                 "and stops surviving somewhere around 0.8. A "
                                 "big conditioning change wants a high value — "
                                 "at a low one the music you already have "
                                 "pulls back toward the curves it was written "
                                 "from.")
                        rg_seed = gr.Number(value=1, label="Seed",
                                            precision=0)
                        guidance_sl = gr.Slider(
                            1.0, 8.0, value=3.0, step=0.5,
                            label="Guidance (CFG scale)",
                            info="1.0 = plain conditional (matches training's "
                                 "*_cond*). ~3.0 = joint CFG (matches "
                                 "training's *_cfg*). Higher = stronger "
                                 "adherence to the conditioning, but can "
                                 "distort.")
                        draft = gr.Checkbox(
                            value=False,
                            label="Draft mode (16 steps instead of 32, faster)")
                    with gr.Column(scale=1):
                        # ONE picker for everything that can be changed, and one
                        # button that does it. Key and style used to be a second
                        # column with two more buttons in it, which read as a
                        # different kind of operation; they are not -- all four
                        # write conditioning for the same window and none of
                        # them makes a sound. What differs is the controls each
                        # one needs, so the picker reveals those directly under
                        # itself. EDIT_TARGETS is the whole mapping.
                        #
                        # ALL OF IT IN THIS COLUMN, under the one heading that
                        # describes it. The revealed controls used to be full
                        # width below both columns, which left the picker
                        # pointing off the bottom of its own column at something
                        # that looked like a third section, and put a control
                        # ("mode", "amount") directly beneath an unrelated one
                        # on the left ("Draft mode"). A tall right-hand column
                        # is the honest shape: the left column is four settings
                        # and is finished, this one is a small application.
                        gr.Markdown("### Edit the conditioning")
                        signal_dd = gr.Dropdown(
                            EDIT_TARGETS, value=EDITABLE_SIGNALS[0],
                            label="what to edit")
                        gr.Markdown(
                            "*Nothing below makes a sound on its own. Apply "
                            "writes the conditioning; **Regenerate** is what "
                            "plays it.*")

                        # Groups, not individual components, because
                        # `_param_updates` already owns the visibility of
                        # `pslots` and `wave_dd` (per shape) -- two handlers
                        # writing the same `visible` flag from different
                        # triggers is a race with no winner. A container's
                        # visibility is a flag nothing else touches.
                        # No range picker of its own: the shape is written over
                        # the SELECTION, which is what Select is for. There used
                        # to be a second pair of sliders here ("from
                        # measure"/"to measure", in window offsets), and it was
                        # a second answer to a question that already had one --
                        # numbered differently (window offsets against song
                        # measures), so the two did not even look like the same
                        # quantity, and either could be left somewhere you had
                        # forgotten. Worse, they could disagree with the
                        # selection, and then Apply wrote conditioning over
                        # measures that Regenerate was not going to rewrite.
                        # Every other edit on this tab -- key, both style edits,
                        # coverage -- follows the selection already.
                        with gr.Column(visible=True) as curve_grp:
                            # Plot UNDER the controls, not beside them: half a
                            # tab is enough width for a figure and not enough
                            # for two things side by side, and the order is the
                            # order you work in -- pick a shape, type its
                            # numbers, look at what you drew.
                            shape_dd = gr.Dropdown(SHAPES, value=SHAPES[0],
                                                   label="shape")
                            shape_help = gr.Markdown(
                                f"*{SHAPE_HELP[SHAPES[0]]}*")
                            # generic parameter slots (relabelled per shape)
                            pslots = [gr.Number(value=0.0, label=f"p{i}",
                                                visible=False)
                                      for i in range(MAX_PARAMS)]
                            wave_dd = gr.Dropdown(["sine", "triangle", "saw"],
                                                  value="sine", label="wave",
                                                  visible=False)
                            mode_rb = gr.Radio(["replace", "add"],
                                               value="replace", label="mode")
                            preview_plot = gr.Plot(
                                label="Preview (live — not yet applied)",
                                elem_classes="plot-card")

                        with gr.Column(visible=False) as key_grp:
                            with gr.Row():
                                root_dd = gr.Dropdown(
                                    [("C", 0), ("C#", 1), ("D", 2), ("D#", 3),
                                     ("E", 4), ("F", 5), ("F#", 6), ("G", 7),
                                     ("G#", 8), ("A", 9), ("A#", 10),
                                     ("B", 11)],
                                    value=0, label="root")
                                minor_cb = gr.Checkbox(value=False,
                                                       label="minor")

                        with gr.Column(visible=False) as morph_grp:
                            style_dd = gr.Dropdown(presets, value=presets[0],
                                                   label="morph style toward")
                            style_amt = gr.Slider(0.0, 1.0, value=0.5,
                                                  step=0.05, label="amount")

                        with gr.Column(visible=False) as text_grp:
                            # Whole window, like the two above it and like
                            # coverage. It used to take a range, which was the
                            # same untrained configuration the coverage panel
                            # now refuses to make: this writes
                            # `active["style"] = True` over the range it is
                            # given, so a range on a track whose style was off
                            # produced a window with style on for part of it.
                            style_text = gr.Textbox(
                                label="style prompt",
                                placeholder="e.g. smoky late-night piano trio")

                        apply_btn = gr.Button(_edit_action(EDITABLE_SIGNALS[0]),
                                              variant="primary")

                # Kept, and hidden. Every handler in the app still returns its
                # sentence as the last element of out_main, and Generate's own
                # status line is DERIVED from it (see _mirror / _simple_status),
                # so the string has to keep being produced -- but Edit is a
                # panel of controls whose effect you judge by listening, and a
                # running commentary under them was noise.
                status = gr.Markdown(visible=False)

            # =================================================================
            # EXPORT
            # =================================================================
            # The one place the whole song is. Generate listens forward and Edit
            # works on 24 measures at a time; neither of them wants a player
            # holding six minutes of audio, and both of them used to have one.
            # `audio_out` is still written by every handler in the app (it is
            # out_main[1]) -- it just lives here now, so leaving a tab and
            # coming back to this one is how you hear the result end to end.
            #
            # A player and two buttons, and nothing else. There is no state here
            # a caption could explain that the file browser does not.
            with gr.Tab("Export") as tab_export:
                audio_out = gr.Audio(label="The whole song", type="filepath",
                                     interactive=False, autoplay=False)
                with gr.Row():
                    export_btn = gr.Button("⬇ Export WAV", variant="primary")
                    export_cond_btn = gr.Button("⬇ Export conditioning (.npz)")
                with gr.Row():
                    export_file = gr.File(label="Exported WAV")
                    export_cond_file = gr.File(label="Exported conditioning")
                # Written by both export handlers, shown only when one of them
                # fails -- on success the File slot beside it is the message.
                ex_status = gr.Markdown(visible=False)

            # =================================================================
            # WEATHER
            # =================================================================
            # An experiment, hidden the same way and for the same reason as
            # Testing. Everything it knows lives in `weather.py`; the only mark
            # it leaves on the rest of the app is `Track.cond_hook`, which is
            # None on every track made anywhere else.
            with gr.Tab("Weather", visible=SHOW_WEATHER) as tab_weather:
                # No prose in the tab, by request: the controls, the view and
                # the player, and nothing written down beside them. Everything
                # this paragraph used to say -- backwards through the year,
                # style off, what the slider does -- is still in
                # HELP["Weather"], which is a panel the reader opens rather
                # than a wall they have to get past to reach the button.
                #
                # A box to type in and a list of what that found, rather than
                # one combined searchable dropdown. gradio's Dropdown filters
                # its own choices client-side on a plain substring of the
                # label, and it recomputes that filter only when the INPUT
                # changes -- never when the choices do. An autocomplete whose
                # choices arrive from the network a moment after the keystroke
                # therefore shows the previous query's matches, and hides
                # "Washington D.C." from anyone who typed "washington, dc"
                # because the label does not literally contain that string.
                # Neither is reachable from python. `filterable=False` below is
                # what switches that filter off for the list.
                with gr.Row():
                    w_city = gr.Textbox(
                        label="Place", value="Seattle", scale=2,
                        placeholder="Start typing a city…",
                        info="City, state and city, country both work.")
                    w_hits = gr.Dropdown(
                        label="Matches", choices=[], value=None, scale=3,
                        filterable=False,
                        info="What the search found, best first. The top one "
                             "is taken unless you open this and pick another.")
                w_weight = gr.Slider(
                    0.0, 3.0, value=1.5, step=0.1,
                    label="How hard the weather pushes",
                    info="A signal sits where the city's yearly average puts "
                         "it and each hour pushes it off that mark. This is "
                         "the size of the push: at 1, a full standard "
                         "deviation of local weather moves the signal 15% of "
                         "its range, and an ordinary day is about ±1.5σ. At 0 "
                         "nothing moves and you hear the city's climate as a "
                         "single held setting. Past about 3 the peaks flatten "
                         "against the ends of the range rather than growing, "
                         "which is where the slider stops.")
                # One button. It fetches, renders the first measures and puts
                # the radio straight on, because there was never a reason to
                # stop in between: a weather track that is not running is a
                # twenty-second clip of an hour of weather.
                with gr.Row():
                    w_go_btn = gr.Button("🎧 Listen to the Weather",
                                         variant="primary")
                    w_stop_btn = gr.Button("⏹ Stop", visible=False)
                # The scrolling view. It follows whichever of the two players is
                # showing, which is why both carry an elem_id -- see _WX_JS.
                w_view = gr.HTML(_WX_VIEW)
                w_audio = gr.Audio(label="Current track", type="filepath",
                                   interactive=False, elem_id="wx-audio")
                # Swapped for w_audio while the radio is on -- see _swap for
                # why this cannot be the same component.
                w_live = GaplessAudio(label="Radio — playing live",
                                      streaming=True, autoplay=True,
                                      interactive=False, visible=False,
                                      elem_id="wx-live")
                # The two channels the view reads, both write-only and both
                # hidden. `w_data` is the whole trajectory, sent once per fetch;
                # `w_sync` is where the stream currently sits in the song, sent
                # at the head of every press and at every leg boundary. `at` is
                # in there so that two identical totals still count as a change
                # -- a relay hop re-sends the same number and must still
                # re-anchor. See _WX_JS.
                w_data = gr.JSON(visible=False)
                w_sync = gr.JSON(visible=False)
                # The relay counter and the two shared dicts, one set per
                # radio. Declared in the tab rather than beside Continue's so
                # that deleting the tab deletes them. See s_relay, pause_req
                # and resume_req for what each is for.
                w_relay = gr.Number(value=0, visible=False, precision=0,
                                    elem_id="relay-weather")
                w_pause = gr.State({"t": None})
                w_resume = gr.State({"go": False, "hops": 0, "dry": 0,
                                     "lead_s": 0.0, "at": 0.0})

            # =================================================================
            # TESTING
            # =================================================================
            # Built whether or not it is shown -- see SHOW_TESTING at the top of
            # the module for why that is the safe direction.
            with gr.Tab("Testing", visible=SHOW_TESTING) as tab_testing:
                gr.Markdown(
                    "### Settings whose right value is still an open question\n"
                    "None of these is a feature. They are here so the two "
                    "formulations can be A/B'd on a real GPU instead of argued "
                    "about, and they are live inputs to **Generate**'s buttons "
                    "whether or not this tab is on screen.")
                stage_rb = gr.Radio(
                    [("Stage 2 — all signals (full control)", 2),
                     ("Stage 1 — style + tempo only (better unconditional)", 1)],
                    value=2, label="Model",
                    info="Stage 1 routes everything but style and tempo through "
                         "a local_embedder frozen at zero, so the DSP curves "
                         "provably do nothing to its render. Essentially "
                         "everything uses stage 2.")
                s_hist_freq = gr.Slider(
                    0.0, 0.8, value=HISTORY_GUIDANCE_FREQ or 0.0, step=0.05,
                    label="Guidance blur",
                    info="What History guidance amplifies. 0 = off, the past "
                         "exactly as written, which pushes toward copying it. "
                         "Higher amplifies only its coarse shape — groove, "
                         "register, harmony — and leaves the notes free. The "
                         "paper's 0.4 is a PIXEL-space number; this model's "
                         "history is tokenizer latents, whose within-measure "
                         "spectrum measures as f^-0.27 against raw audio's "
                         "f^-2.55, so the red-spectrum property the trick "
                         "exploits has largely been whitened away. Treat the "
                         "useful range as unknown.")
                s_hist_m = gr.Radio(
                    [("16 measures (all of it)",
                      N_CHUNKS - CONTINUE_MEASURES),
                     ("8 measures (half)", CONTINUE_MEASURES)],
                    value=CONTINUE_HISTORY or N_CHUNKS - CONTINUE_MEASURES,
                    label="How much past it reads",
                    info="How many of the written measures are handed to the "
                         "model as history. The window does not change size and "
                         "neither does the cost — the measures that stop being "
                         "history are regenerated and thrown away.")
                s_hg_mode = gr.Radio(
                    [("Nested (ours)", "nested"), ("Flat (paper)", "dfot")],
                    value=HG_MODE, label="Guidance formula",
                    info="How the conditioning and the history are combined. "
                         "Nested steers on each separately — Guidance for the "
                         "signals, History guidance for the past. Flat is the "
                         "paper's: both ride one scale together, so Guidance "
                         "stops doing anything and History guidance is the only "
                         "dial (they use 4.0). Flat is also cheaper: 2 or 3 "
                         "model passes per step against nested's 4.")

        # ---- callbacks ------------------------------------------------------
        def _steps(is_draft):
            # 32 is not a round number pulled out of the air: it is exactly what
            # train_modern_fancy_dit_measures.save_samples uses, and the same
            # value drives BOTH samplers (predict_measures hands its n_steps to
            # the DiT's FMEulerSampler *and* to tokenizer.decode). Those wavs are
            # the reference this demo is judged against, so the full path matches
            # them step for step. Don't raise it thinking more is better -- more
            # Euler steps is a different operating point than the one the
            # checkpoints were listened to at.
            return 16 if is_draft else 32

        def _render_outputs(track: Track):
            return (
                _audio_to_file(track.audio, track.sr),
                _curves_figure(track),
                _coverage_table(track),
                _summary(track),
            )

        # Edit-only outputs: update state + plots + summary but DO NOT re-render
        # audio. Leaves the current audio player untouched (gr.update()) so the
        # user hears the last render until they hit "Regenerate".
        def _edit_outputs(track: Track, msg: str):
            return (
                track,
                gr.update(),                 # audio_out unchanged
                _curves_figure(track),
                _coverage_table(track),
                _summary(track) + "\n\n*Edited — hit **Regenerate** to hear it.*",
                msg,
            )

        # ---- keeping the two tabs in step ------------------------------------
        # There is one track and two tabs showing it, so every handler has to
        # write both or the tab you are not looking at goes stale. Rather than
        # give twenty handlers a second set of return values -- and rather than
        # trust twenty wiring sites to remember three extra outputs -- these two
        # wrappers append the Generate tab's mirrors to whatever a handler already
        # returned, and every handler is passed through one of them in a single
        # block below, where a missing name is visible.
        #
        # The mirrors are NOT the Edit values repeated. The caption is
        # rebuilt from the Track the handler returned -- which is right there in
        # the tuple -- and the status line is stripped of its diagnostic fence.
        # Same state, different amount of it. Anything that is not a Track (an
        # error path yielding gr.update()) mirrors as gr.update(), i.e. leaves
        # the Generate view alone, which is what the Edit view is doing too.
        def _mirror_vals(r, off):
            """The trailing derived outputs for a result tuple whose `out_main`
            block starts at index `off` (0 everywhere except undo and stream,
            which return the history / the live chunk in front of it).

            Four of them are the Generate and Continue tabs' view of what the
            handler already returned. The fifth, the style map, is derived from
            the Track. Doing it here rather than as a `.then` tail on each chain
            is not just brevity: the tails are per-chain and there are twenty of
            them, so the next handler anyone adds would silently not update the
            map. This way nothing can be missed, including Undo (which must
            redraw, or the map keeps showing a trajectory the Track no longer
            has) and the two streaming handlers, which come through _stream_ui
            and so redraw the path at every leg boundary while the radio plays.

            The audio goes out twice, to Continue's player and Generate's, and
            the second copy is a COPY. gradio's `postprocess_update_dict` pops
            "value" out of the dict it is handed, so sending one `gr.update()`
            object to two components leaves the second one with an update that
            has no value in it -- the player would silently stop refreshing,
            but only on the handlers that return an update rather than a
            path."""
            track, msg = r[off], r[off + 5]
            aud = r[off + 1]
            return (aud, dict(aud) if isinstance(aud, dict) else aud,
                    _simple_summary(track) if isinstance(track, Track)
                    else gr.update(),
                    _simple_status(msg) if isinstance(msg, str)
                    else gr.update(),
                    _style_map(track, backend) if isinstance(track, Track)
                    else gr.update())

        def _mirror(fn, off=0):
            def wrapped(*a):
                r = tuple(fn(*a))
                return r + _mirror_vals(r, off)
            # __name__ by hand rather than functools.wraps, and that is not
            # fussiness: wraps also sets __wrapped__, which inspect.signature
            # follows, so gradio would read the WRAPPED function's parameters
            # off something that actually takes *a -- and gradio uses exactly
            # that to decide arity and where a gr.Request would go. The stream
            # pair keeps its real signature and mirrors through _stream_ui
            # instead of coming through here at all.
            # __name__ still has to be right, because it is what gradio names
            # the generated API endpoint.
            wrapped.__name__ = getattr(fn, "__name__", "wrapped")
            wrapped.__doc__ = fn.__doc__
            return wrapped

        def _stream_ui(gen):
            """Mirror a stream handler's yields. Not `_mirror`: see above."""
            for r in gen:
                r = tuple(r)
                yield r + _mirror_vals(r, 1)

        def on_new_preset(name, seed, is_draft, stage, guidance):
            tr = backend.seed_from_preset(name, int(seed))
            tr = backend.set_stage(tr, int(stage))
            tr = backend.set_guidance(tr, float(guidance))
            tr = backend.render(tr, n_steps=_steps(is_draft))
            a, c, cov, s = _render_outputs(tr)
            return (tr, a, c, cov, s,
                    f"Seeded from preset (stage {int(stage)}, "
                    f"guidance {float(guidance):g})." + _diag_note(tr))

        def on_new_text(prompt, seed, is_draft, stage, guidance, bpm, bpmv,
                        bpmseed):
            if not prompt.strip():
                return (gr.update(),) * 5 + ("Enter a text prompt first.",)
            try:
                tr = backend.seed_from_text(prompt, int(seed),
                                            bpm=float(bpm),
                                            bpm_variance=float(bpmv))
            except Exception as e:  # noqa: BLE001
                # CLAP/T5 load or encode failure -- surface it instead of
                # dropping a traceback into the Space logs.
                return (gr.update(),) * 5 + (
                    f"Text encoding failed: {type(e).__name__}: {e}",)
            # Re-apply through the same call the live preview draws, so the curve
            # the user dialled in IS the curve they get. seed_from_text already
            # made one, but off the *track* seed, not the tempo seed.
            tr = backend.set_bpm_global(tr, float(bpm), float(bpmv),
                                        int(bpmseed))
            tr = backend.set_stage(tr, int(stage))
            tr = backend.set_guidance(tr, float(guidance))
            tr = backend.render(tr, n_steps=_steps(is_draft))
            a, c, cov, s = _render_outputs(tr)
            return (tr, a, c, cov, s,
                    f"Seeded from text (CLAP text embedding, "
                    f"{float(bpm):.0f}±{float(bpmv):g} BPM). Style + tempo are "
                    f"on; the DSP signals are off." + _diag_note(tr))

        def on_new_upload(path, seed, is_draft, stage, guidance):
            if not path:
                return (gr.update(),) * 5 + ("Upload an audio file first.",)
            try:
                tr = backend.seed_from_audio(path, int(seed))
            except Exception as e:  # noqa: BLE001
                return (gr.update(),) * 5 + (
                    f"Extraction failed: {type(e).__name__}: {e}",)
            tr = backend.set_stage(tr, int(stage))
            tr = backend.set_guidance(tr, float(guidance))
            tr = backend.render(tr, n_steps=_steps(is_draft))
            a, c, cov, s = _render_outputs(tr)
            return tr, a, c, cov, s, f"Extracted conditioning from {tr.label}."

        def on_set_bpm(track, bpm, variance, seed):
            if track is None:
                return (gr.update(),) * 5 + (
                    "Start a track first (the tempo controls also apply to new "
                    "text/unconditional tracks).",)
            tr = backend.set_bpm_global(track, float(bpm), float(variance),
                                        int(seed))
            return _edit_outputs(
                tr, f"Tempo set to {float(bpm):.0f} BPM ± {float(variance):g} "
                    f"(per-measure Gaussian, seed {int(seed)}).")

        def on_style_from_text(track, prompt):
            """Replace the window's style vector with a text embedding.

            Whole window. It used to take a range, and a range was the one
            control left that could still build the configuration the coverage
            panel now refuses to: `set_style_from_text` sets
            `active["style"] = True` over whatever it is given, so a range on a
            track with style off left style on for part of the window and off
            for the rest -- a per-measure coverage pattern this checkpoint
            never saw (`p_ind_low = p_ind_high = 0`)."""
            if track is None:
                return (gr.update(),) * 5 + ("Start a track first.",)
            if not prompt.strip():
                return (gr.update(),) * 5 + ("Enter a style prompt first.",)
            try:
                tr = backend.set_style_from_text(track, prompt, 0, track.cond.T)
            except Exception as e:  # noqa: BLE001
                return (gr.update(),) * 5 + (
                    f"Text encoding failed: {type(e).__name__}: {e}",)
            first = int(track.prefix_measures)
            return _edit_outputs(
                tr, f"Style set from text over measures {first}–"
                    f"{first + track.cond.T}.")

        def on_set_stage(track, stage):
            """Switch the active model for an existing track (edit only — the
            new stage takes effect on the next Regenerate)."""
            if track is None:
                return (gr.update(),) * 5 + (
                    f"Model set to stage {int(stage)}. Start a track to hear it.",)
            if int(stage) == int(track.stage):
                # No-op: the radio already agreed with the track. Matters because
                # on_swap pushes the restored track's stage back into this radio,
                # which re-fires .change — without this guard the swap's status
                # and summary get clobbered by a spurious "switched" message.
                return (gr.update(),) * 6
            tr = backend.set_stage(track, int(stage))
            return _edit_outputs(tr, f"Switched to stage {int(stage)} model.")

        def _edit_controls(track, stage, cur_sig, cur_cov, cur_text):
            """Restrict the two pickers to the signals that are worth editing,
            and fill the three non-curve editors from the track.

            TWO FILTERS, ANSWERING DIFFERENT QUESTIONS, and they do not apply to
            the same picker. `stage` is what the selected MODEL reads: offering
            "energy (rms)" under stage 1 invites an edit that provably cannot
            change the render, so it gates both pickers. The coverage flags are
            what THIS TRACK sends: a signal switched off on Select is one the
            sampler is never handed, so shaping its curve on Edit is the same
            dead end — but the coverage picker is where it gets switched back
            on, so that one keeps offering it. Filtering Edit is also how the
            two tabs stay honest with each other: the list on Edit is exactly
            the rows that read ON over on Select.

            Driven off stage_rb rather than track.stage because stage_rb is what
            the entry points read for the NEXT generation — and on_swap pushes
            the restored track's stage back into it, so swapping is covered for
            free. The current selection is kept whenever it survives.

            Key and the two style edits are filtered by the channel they write
            (chroma, style), which is also how the old greyed-out key button's
            job gets done now that there is no button to grey.

            THE DEFAULTS BELOW are the same idea as the curve editor drawing on
            top of the curve that is already there: every other target shows you
            what the track currently says before you change it, and these three
            used to open on C major and an empty box no matter what the track
            was. The key is read back out of the chroma rows (see
            key_from_chroma); the two style controls are read off the track,
            which records the words it was last given. The text box is only
            filled when it is EMPTY — it is the one control here holding
            something a user typed rather than picked, and this runs after every
            action on the tab, including ones pressed mid-sentence."""
            sigs = signals_for_stage(int(stage))
            on = (lambda s: True) if track is None else (
                lambda s: bool(track.cond.active[s].any()))
            ed = [(SIGNAL_LABELS[s], s) for s in EDITABLE_SIGNALS
                  if s in sigs and on(s)]
            chan = lambda t: "chroma" if t == EDIT_KEY else "style"
            ed += [(lbl, t) for lbl, t in EDIT_SPECIAL_CHOICES
                   if chan(t) in sigs and on(chan(t))]
            cov = ([(COV_ALL_LABEL, COV_ALL)]
                   + [(ALL_SIGNAL_LABELS[s], s) for s in ALL_SIGNALS
                      if s in sigs])

            def keep(v, opts):
                # `None` when nothing is editable at all, which is what an
                # all-off track (the unconditional entry point) gives you.
                if not opts:
                    return None
                return v if any(v == o[1] for o in opts) else opts[0][1]

            root, minor = (0, False) if track is None else key_from_chroma(
                track.cond.chroma)
            preset = (track.style_preset if track is not None else "")
            text = cur_text or (track.style_text if track is not None else "")
            return (
                gr.update(choices=ed, value=keep(cur_sig, ed)),
                gr.update(choices=cov, value=keep(cur_cov, cov)),
                gr.update(value=int(root)),
                gr.update(value=bool(minor)),
                gr.update(value=preset) if preset in presets else gr.update(),
                gr.update(value=text),
            )

        def _diag_note(track):
            """The worker's diagnostic, fenced, or "" when it had nothing to
            say. tr.diag is the only channel out of the forked @spaces.GPU
            process -- its stdout is not in the Space log (see Track.diag) --
            so anything that goes wrong there and is not fatal arrives here or
            not at all."""
            d = getattr(track, "diag", "")
            return f"\n\n```\n{d}\n```" if d else ""

        def _splice_note(track):
            """Say so, in the status line, when a render has just been joined
            onto music that came after it.

            Not a warning about a bug -- the splice is exact, the measures after
            the window are the same samples they were and the seam gets the same
            crossfade every other join in the app gets. What it cannot promise
            is MUSICAL continuity: the model wrote the new window against what
            precedes it and has never seen what follows, so the bar after the
            seam may simply not be where the phrase was going. Worth one
            sentence every time rather than a note in the docs read once."""
            n = int(getattr(track, "suffix_measures", 0) or 0)
            if n <= 0:
                return ""
            return (f"\n\n⚠️ The {n} measure{'' if n == 1 else 's'} after the "
                    f"window were kept byte-for-byte and spliced back on with a "
                    f"{CROSSFADE_MS} ms crossfade. The join is clean as a "
                    f"*signal*; it is **not** guaranteed to be continuous as "
                    f"*music*.")

        def _steering(track, guidance, hist_g, ctx_noise, steer, span, walk,
                      pace, axis, style_off=False, bpm_follow=False,
                      bpm_step=False, bpm_hold=BPM_STEP_HOLD,
                      cross_cluster=True, hist_freq=HISTORY_GUIDANCE_FREQ,
                      hist_measures=CONTINUE_HISTORY, hg_mode=HG_MODE):
            """Copy the Continue panel's settings onto a Track.

            Shared by Continue and Fast-forward so the two cannot drift apart
            about what the controls mean."""
            tr = backend.set_guidance(track, float(guidance))
            tr.hist_guidance = float(hist_g)
            tr.ctx_noise = float(ctx_noise)
            # 0 means OFF, not "blur by zero", and that is the paper's own
            # convention: their k_H = 0 is vanilla guidance. Written every time
            # rather than only when set, so a Track that picked the value up on
            # Generate does not carry it silently into an Edit press.
            tr.hist_freq = float(hist_freq) if hist_freq else None
            # Same "written every press" reasoning as hist_freq: None means the
            # whole context, and _continue_step clamps it to what the window has.
            tr.hist_measures = int(hist_measures) if hist_measures else None
            tr.hg_mode = str(hg_mode or HG_MODE)
            # Resolve the steer text HERE, in the CPU process: on ZeroGPU the
            # continue call runs in a forked @spaces.GPU worker, and the CLAP
            # text tower has no business being loaded on billed GPU time. What
            # crosses the boundary is 512 floats on the Track.
            steer = (steer or "").strip()
            tr.style_target = backend.embed_style_text(steer) if steer else None
            tr.style_span = int(span)
            # Identifies the journey. Same prompt AND same span -> the steer
            # already in flight advances; change either and it restarts from
            # here. See backend.apply_style_steering.
            tr.style_key = steer
            # A TYPED STEER AND A WALK ARE EXCLUSIVE, and the Track is where
            # that is decided, so every entry point agrees and neither backend
            # has to. They were never composable: `_steer_or_walk` returns on
            # the typed target before the walk branch is reachable, so a walk
            # selected alongside text did nothing except -- until the guard
            # order in `arm_idle_stream` was fixed -- suppress the release that
            # stops an arrived steer holding one vector forever. Forcing "off"
            # here makes the disabled radio honest rather than decorative. It
            # costs nothing on the way back: this runs per press from the
            # control's own value, so clearing the box restores the walk.
            tr.style_walk = "off" if steer else str(walk or "off")
            tr.style_pace = int(pace)
            tr.style_axis = str(axis or "")
            # Unconditional for the same reason as style_off below: the flag
            # lives on the Track and survives Continues, so writing it only
            # when true would make the box impossible to untick.
            tr.walk_cross_cluster = bool(cross_cluster)
            # Set unconditionally rather than only when true, so unticking the
            # box actually turns style back on: the flag is what
            # backend._style_off_gate reads to know it has to restore the
            # active flags that shift_window would otherwise carry forward.
            tr.style_off = bool(style_off)
            # Same unconditional-set reasoning as style_off above: the flag
            # lives on the Track and survives Continues, so only writing it
            # when true would make the box impossible to untick.
            tr.bpm_follow = bool(bpm_follow)
            tr.bpm_step = bool(bpm_step)
            tr.bpm_hold = int(bpm_hold)
            return tr

        def on_continue(track, seed, is_draft, guidance, hist_g, ctx_noise,
                        steer, span, walk, pace, axis, style_off,
                        bpm_follow=False, bpm_step=False,
                        bpm_hold=BPM_STEP_HOLD, cross_cluster=True,
                        hist_freq=HISTORY_GUIDANCE_FREQ,
                        hist_measures=CONTINUE_HISTORY, hg_mode=HG_MODE):
            if track is None:
                return (gr.update(),) * 5 + ("Start a track first.",)
            tr = _steering(track, guidance, hist_g, ctx_noise, steer, span,
                           walk, pace, axis, style_off, bpm_follow, bpm_step,
                           bpm_hold, cross_cluster, hist_freq, hist_measures,
                           hg_mode)
            tr = backend.continue_track(tr, n_new=CONTINUE_MEASURES,
                                        seed=int(seed),
                                        n_steps=_steps(is_draft))
            a, c, cov, s = _render_outputs(tr)
            first = tr.prefix_measures
            # tr.diag is the only way numbers get out of the ZeroGPU worker --
            # its stdout is not in the Space log. See Track.diag.
            diag = _diag_note(tr)
            # style_note is the persistent readout (it also renders under the
            # player via _summary); repeat it here so the thing you just did is
            # described where you are already looking.
            how = f"\n\n{tr.style_note}" if getattr(tr, "style_note", "") else ""
            return (tr, a, c, cov, s,
                    f"Continued: +{CONTINUE_MEASURES} measures "
                    f"({tr.total_measures} total). The editable window is now "
                    f"measures {first}–{tr.total_measures - 1}; everything "
                    f"before it is fixed." + how + diag)

        def on_fast_forward(track, seed, is_draft, guidance, hist_g,
                            ctx_noise, steer, span, walk, pace, axis,
                            style_off, bpm_follow=False, bpm_step=False,
                            bpm_hold=BPM_STEP_HOLD, cross_cluster=True,
                            idle="refuse", max_measures=None,
                            hist_freq=HISTORY_GUIDANCE_FREQ,
                            hist_measures=CONTINUE_HISTORY, hg_mode=HG_MODE):
            if track is None:
                return (gr.update(),) * 5 + ("Start a track first.",)
            tr = _steering(track, guidance, hist_g, ctx_noise, steer, span,
                           walk, pace, axis, style_off, bpm_follow, bpm_step,
                           bpm_hold, cross_cluster, hist_freq, hist_measures,
                           hg_mode)
            # `max_measures` is how far this press is ALLOWED to go, and the
            # clamp to FF_MAX_MEASURES is the GPU ceiling, applied here so both
            # tabs get it from one place. None means "the ceiling", which is
            # what a steer wants: its length is its span, and plan_fast_forward
            # takes the smaller of the two anyway.
            cap = FF_MAX_MEASURES if max_measures is None else min(
                int(max_measures), FF_MAX_MEASURES)
            cap = max(CONTINUE_MEASURES, cap)
            # Plan on this side of the GPU boundary too. The backend re-plans
            # identically, but finding out there is nothing to steer toward is
            # not worth a ZeroGPU reservation and a cold model load. `idle` and
            # `cap` both have to match what the backend is handed, or the two
            # plans disagree about how far there is to go.
            measures, note = plan_fast_forward(tr, cap, idle=idle)
            if measures <= 0:
                return (gr.update(),) * 5 + (note,)
            before = tr.total_measures
            tr = backend.fast_forward(tr, seed=int(seed),
                                      n_steps=_steps(is_draft),
                                      max_measures=cap, idle=idle)
            a, c, cov, s = _render_outputs(tr)
            diag = _diag_note(tr)
            how = f"\n\n{tr.style_note}" if getattr(tr, "style_note", "") else ""
            return (tr, a, c, cov, s,
                    f"Fast-forwarded: +{tr.total_measures - before} measures "
                    f"in {measures // CONTINUE_MEASURES} takes "
                    f"({tr.total_measures} total). The editable window is now "
                    f"measures {tr.prefix_measures}–{tr.total_measures - 1}."
                    + how + diag)

        def on_stream(track, seed, is_draft, guidance, hist_g, ctx_noise,
                      steer, span, walk, pace, axis, style_off,
                      bpm_follow=False, bpm_step=False,
                      bpm_hold=BPM_STEP_HOLD, cross_cluster=True,
                      pause=None, resume=None,
                      hist_freq=HISTORY_GUIDANCE_FREQ,
                      hist_measures=CONTINUE_HISTORY, hg_mode=HG_MODE):
            """Generator handler: yields (live chunk, *out_main) per step.

            Runs until it is asked to stop, not until a measure count runs
            out. There are two ways to ask: ⏹ Stop cancels the event outright
            (`cancels=` in the wiring below), and pausing the player writes a
            timestamp into the shared `pause` dict, which this reads at its
            own yield points -- see PAUSE below for why those are different.
            Three things make the run itself work:

            LEGS. `backend.stream_forward` is one GPU reservation's worth of
            takes and a reservation cannot be held forever, so this loops:
            feed each leg's Track into the next with `lead_in=False`, and the
            audio joins seamlessly across the boundary. The seed advances by a
            whole leg each time so no leg re-runs the previous leg's noise --
            with conditioning this static, repeating the seed would be
            repeating the music.

            THROTTLE. A take is ~16 s of music for ~7 s of compute, so left
            alone this outruns the listener by 2.3x and spends GPU on music
            nobody reaches. Between legs it therefore sleeps until the buffer
            has drained back to STREAM_HEADROOM_S. Playback position is
            estimated as wall-clock since the first chunk went out, which is
            right while the player is playing and OVER-estimates once it is
            not -- erring toward generating early, never toward an underrun.
            The sleep is here rather than inside the backend because inside
            the backend it would be sleeping on a held GPU.

            COMMIT POINTS. The leg boundary is where a completed Track exists,
            so that is where the full outputs are written -- state, player,
            curves, coverage. Stopping keeps everything through the last
            finished leg instead of discarding the whole press; only the leg
            in flight is lost. Mid-leg only the streaming player moves, since
            until the leg ends the curves would be describing a half-finished
            window. It is also where a leg that RAISES is handled, because a
            commit point is the one place where stopping costs nothing but the
            leg in flight. Nothing here tries to predict such a failure --
            see the ZeroGPU note above _gpu_failure for what that cost.

            PAUSE. `pause` is a mutable dict living in a gr.State, so unlike
            every ordinary input it CAN be written while this is running: a
            gr.State's preprocess passes the session's object through by
            reference, and the pause listener is a separate event that just
            stamps `t` on it. It is a request rather than a cancel because a
            `pause` event does not mean the listener paused -- the player also
            emits one when it reaches the end of what has been sent, and the
            lead-in is long enough that a cold worker loses that race. So a
            pause is honoured only if audio was still buffered when it
            happened; otherwise it is reported and the run continues. See
            PAUSE_MIN_BUFFER_S. ⏹ Stop stays a real cancel, because there is
            nothing ambiguous about it and a cancel lands immediately instead
            of at the next yield.

            RELAY. One press cannot last forever, and the limit is not GPU
            time -- it is that ZeroGPU authorises an allocation with the token
            of the REQUEST that dispatched the event, and a press is one
            request (see the note above _gpu_failure). Around four legs in, the
            token stops being accepted and the fifth leg raises before it
            reaches a GPU. So a listen is a CHAIN of presses: on that specific
            failure this stamps `resume`, and the wiring turns that into a tick
            on a hidden counter whose .change re-enters this handler in a new
            request, with a new token, continuing from the Track this one just
            committed. Nothing here loops on its own -- the browser is what
            makes the next request, which is the entire point, since making the
            next request is the only thing that mints a new token.

            The seam is covered by the throttle, not hidden by cleverness:
            STREAM_HEADROOM_S of audio is sitting in the player's buffer when
            the press ends, so the listener keeps hearing music through the
            round trip. What they must not get is a jump, so the relay carries
            `lead_s` -- how far ahead of the listener the old stream had got --
            and the next press opens with exactly that much of the existing
            audio, minus however long the round trip actually took. Overshoot
            repeats a second; undershoot skips one. Compare `lead_in=False`,
            which would skip everything still buffered, up to the whole
            headroom.

            EVERY SETTING IS READ ONCE, HERE. gradio sends the value of each
            input component with the request that starts the event, and there
            is no channel for a later value to reach a generator that is
            already running: moving the history-noise slider or typing in the
            steer box mid-press changes what the NEXT press will do and nothing
            about this one. `_steering` is therefore called once, before the
            loop, and every leg continues from a Track carrying that one
            snapshot. Live mid-press control is possible -- a mutable gr.State
            written by .change() handlers and re-read at each leg boundary,
            which is the natural place to apply it -- but it is not what this
            does today, and a slider that silently does nothing is worse than
            one that says when it takes effect.

            Audio chunks go out as encoded WAV bytes on purpose --
            gr.Audio.postprocess passes bytes through untouched but
            PEAK-NORMALISES a (sr, ndarray) tuple, which would put a different
            gain on every chunk and make each take jump in volume. The waits
            yield gr.update() for that slot instead, which blocks.py skips
            before it reaches the stream (`is_prop_update`), so a wait cannot
            disturb the AAC stream the way a None would -- None is the
            end-of-stream signal GaplessAudio flushes on."""
            hold = (gr.update(),) * 5
            if track is None:
                yield (None,) + hold + ("Start a track first.",)
                return
            if track.audio is None:
                yield (None,) + hold + ("Render the track first — Stream "
                                        "plays what is there while it writes "
                                        "what comes next.",)
                return
            tr = _steering(track, guidance, hist_g, ctx_noise, steer, span,
                           walk, pace, axis, style_off, bpm_follow, bpm_step,
                           bpm_hold, hist_freq=hist_freq,
                           hist_measures=hist_measures, hg_mode=hg_mode)
            # Arm here as well as in the backend so the readout while the run
            # is in flight matches what the backend will actually run. It is
            # idempotent: a Track that is already steering is left alone.
            armed = arm_idle_stream(tr)
            note = plan_fast_forward(tr, STREAM_LEG_MEASURES, idle="run")[1]
            drive = ("style off — no style channel at all, so every take is "
                     "written from the bars before it, the tempo, and the "
                     "model's own idea of what follows"
                     if style_is_off(tr) else
                     note.split("— ", 1)[-1] if not armed else
                     f"“{steer.strip()}” arrived, drifting on from there"
                     if (steer or "").strip() else
                     "drifting slowly — nothing was steering, and identical "
                     "conditioning take after take is what makes it loop")
            per_leg = STREAM_LEG_MEASURES // CONTINUE_MEASURES
            # Second and later presses join with a short tail instead of the
            # whole window, so pressing ▶ again does not open with ~48 s of
            # music that just played -- which is the entire reason it looked
            # like the radio was starting over rather than carrying on. Set
            # before the first leg so the Track that comes back carries it.
            warm = LEAD_IN_WARM_S if getattr(tr, "streamed", False) else 0.0
            tr.streamed = True
            # A relay is not a second press, so it does not get the warm lead-in
            # a second press gets: the listener never stopped, and the only
            # audio worth re-sending is the part they had not reached yet.
            # `at` is a monotonic stamp taken when the previous press ended, in
            # this same process, so subtracting it charges the round trip to the
            # lead-in instead of replaying it. Floored rather than allowed to go
            # negative -- a relay slow enough to exhaust the buffer wants the
            # smallest overlap that still joins, not a gap.
            hop = 0
            if isinstance(resume, dict) and resume.pop("go", False):
                hop = int(resume.get("hops", 0))
                gap = max(0.0, time.monotonic() - float(resume.get("at", 0.0)))
                warm = max(1.0, float(resume.get("lead_s", 0.0)) - gap)

            out, started = tr, tr.total_measures
            # STREAM_MAX_MEASURES bounds a LISTEN, not a hop, so the handovers
            # inherit the count instead of each starting a fresh 1024. Resetting
            # it per hop would quietly turn the ceiling into "1024 measures per
            # renewal", i.e. into nothing at all -- and the ceiling is the only
            # bound left on a radio that now renews its own authorisation. It
            # also makes the "+N measures" in the closing line the length of the
            # whole listen, which is the number the listener was counting.
            if hop:
                started = int(resume.get("started", started))
            elif isinstance(resume, dict):
                resume["started"] = started
            grew, leg, sent_s, t0 = tr.total_measures, 0, 0.0, None
            # (monotonic time, cumulative seconds sent by then), one per chunk.
            # Needed because a pause has to be judged against the buffer AT THE
            # MOMENT IT HAPPENED, and by the time this sees it another take may
            # have landed -- using the current total would make every underrun
            # look like a well-buffered deliberate pause, i.e. the bug.
            marks: list[tuple[float, float]] = []
            notes: list[str] = []   # one-off remarks, appended to the status

            def ahead() -> float:
                """Seconds of audio sent that the listener cannot have reached."""
                return 0.0 if t0 is None else sent_s - (time.monotonic() - t0)

            def buffered_at(tp: float) -> float:
                """`ahead()` as it stood at `tp`, from what had been sent by then."""
                cum = 0.0
                for t, c in marks:
                    if t > tp:
                        break
                    cum = c
                return cum - (tp - t0)

            def pause_asked() -> str | None:
                """Consume a pending pause; return a stop reason if it was real.

                Anything stamped before the first chunk went out is discarded
                unread: the browser fires `pause` when the PREVIOUS run's
                stream is torn down, and that must not stop this one.
                """
                tp = pause.get("t") if isinstance(pause, dict) else None
                if tp is None:
                    return None
                pause["t"] = None
                if t0 is None or tp < t0:
                    return None
                if buffered_at(tp) >= PAUSE_MIN_BUFFER_S:
                    return "you paused the player"
                if not notes:
                    # The leading ▶ is a convention, not decoration: it is
                    # how _simple_status tells a paragraph the LISTENER has to
                    # act on from the ones explaining what the model is doing.
                    notes.append(
                        "▶ the player reached the end of what had been "
                        "written and paused itself — press play to pick it up "
                        "again; the radio did not stop")
                return None

            def stat(head: str) -> str:
                """Status line: what is happening now, then why it sounds the
                way it does. `drive` is a sentence, and inlining it pushed the
                numbers -- the part that changes -- off the end of the box."""
                return (f"🔴 Streaming — {head}\n\n*{drive}*"
                        + "".join(f"\n\n*{n}*" for n in notes))

            stopped = None      # why the loop ended, when it was not the ceiling
            expired = False     # ...and whether that reason was a dead token

            while out.total_measures - started < STREAM_MAX_MEASURES:
                # THE SEED COUNTS MEASURES, NOT LEGS. `stream_forward` uses
                # seed+k for the k-th take in its leg, so the caller has to hand
                # each leg a base that has not been used before -- and `leg`
                # cannot supply one, because a relay re-enters this handler with
                # leg back at 0 while `seed` is the same unchanged control
                # value. Every handover therefore replayed the previous hop's
                # noise from the top, and a handover is not rare: the relay is
                # what renews a dead ZeroGPU token, so a press that loses its
                # worker restarts its noise sequence each time it picks one back
                # up. With a radio's conditioning nearly static across takes --
                # 16 of 24 measures frozen context, DSP signals dropped
                # window-wide, style moving a few degrees a take -- the same
                # seed on nearly the same conditioning is a request for nearly
                # the same music, which is what "it loops after a minute or two"
                # was. `total_measures` is monotone across takes, legs, hops and
                # presses alike, so deriving the base from it cannot repeat.
                #
                # `hop` is in there as well, and it is not redundant. A leg that
                # DIES mid-flight has already streamed the takes it finished but
                # commits none of them, so `out` -- and therefore
                # `total_measures` -- is exactly where it was before the leg
                # started. Measure count alone would hand the retry the same
                # base and regenerate audio the listener just heard, which is
                # the same bug wearing a different hat, and the likeliest one to
                # hit repeatedly since a dying leg is what causes the handover in
                # the first place. A whole listen is bounded at
                # STREAM_MAX_MEASURES, so striding by that many takes gives each
                # hop a block of seeds the others cannot reach into.
                gen = backend.stream_forward(
                    out,
                    seed=int(seed)
                    + hop * (STREAM_MAX_MEASURES // CONTINUE_MEASURES)
                    + out.total_measures // CONTINUE_MEASURES,
                    n_steps=_steps(is_draft),
                    max_measures=STREAM_LEG_MEASURES, lead_in=(leg == 0),
                    lead_in_s=warm)
                try:
                    for kind, payload in gen:
                        if kind != "audio":
                            out = payload
                            continue
                        if not len(payload):
                            continue
                        if t0 is None:
                            t0 = time.monotonic()      # playback starts ~now
                        sent_s += len(payload) / out.sr
                        marks.append((time.monotonic(), sent_s))
                        # `out` is still the PREVIOUS leg's Track here -- this
                        # one does not exist until its last yield -- so the
                        # measure count is what is saved, not what is playing.
                        yield (to_wav_bytes(payload, out.sr),) + hold + (
                            stat(f"writing leg {leg + 1}; "
                                 f"{out.total_measures - started} measures "
                                 f"saved, {ahead():.0f}s buffered. ⏹ Stop Radio "
                                 f"when you have had enough."),)
                        if (asked := pause_asked()):
                            stopped = asked
                            break
                except Exception as exc:  # noqa: BLE001
                    # A leg that dies takes the press down with it, but not the
                    # track: `out` is still the last COMMITTED leg (a leg's
                    # Track is its final yield), so everything through the last
                    # boundary is already in the UI and in state. Ending the
                    # loop here converts a red toast into a status line that
                    # says what happened and what to do about it. Deliberately
                    # `Exception`, not bare except: cancellation arrives as
                    # GeneratorExit, which must keep propagating.
                    stopped = _gpu_failure(exc)
                    expired = _gpu_token_expired(exc)
                    break
                finally:
                    # Cancellation closes THIS generator, which must close the
                    # backend's too: on ZeroGPU that is what ends the @spaces.GPU
                    # call and hands the reservation back.
                    gen.close()
                if stopped:
                    # A pause mid-leg, so the leg in flight is lost -- the same
                    # trade ⏹ Stop makes, and checked before the no-growth test
                    # below, which would otherwise fire first and misreport why.
                    break
                if out.total_measures <= grew:
                    break     # a leg that adds nothing would spin this forever
                grew = out.total_measures
                leg += 1

                a, c, cov, s = _render_outputs(out)
                yield (gr.update(), out, a, c, cov, s,
                       stat(f"leg {leg} done and saved — "
                            f"{out.total_measures - started} new measures "
                            f"({out.total_measures} total), "
                            f"{ahead():.0f}s buffered.")
                       + (f"\n\n{out.style_note}"
                          if getattr(out, "style_note", "") else ""))

                # Let the buffer drain back to the headroom before spending
                # another reservation -- but only if there is going to be one,
                # or the press ends by sitting out its own buffer. Sliced so a
                # cancel lands within a second rather than at the end of a 40 s
                # sleep: gradio can only stop a sync generator between yields.
                if out.total_measures - started >= STREAM_MAX_MEASURES:
                    break
                while (spare := ahead() - STREAM_HEADROOM_S) > 0:
                    # Floored, because `spare` approaches zero from above as the
                    # wait ends: without it the last stretch is a busy-spin of
                    # millisecond sleeps, each one pushing another identical
                    # status line at the frontend for no gain.
                    time.sleep(max(0.25, min(1.0, spare)))
                    if (asked := pause_asked()):
                        stopped = asked
                        break
                    yield (gr.update(),) + hold + (
                        stat(f"{ahead():.0f}s buffered — waiting for that to "
                             f"drop to {STREAM_HEADROOM_S:.0f}s before writing "
                             f"leg {leg + 1}, so it is not generating music "
                             f"you never reach. ⏹ Stop Radio any time."),)
                if stopped:
                    break

            # Hand the press on, if a dead token was the only thing wrong with
            # it. `dry` is the guard against the one way this could spin: a
            # press that expired without writing a single leg has nothing to
            # show for the round trip, so it is allowed one retry and then has
            # to stop. A press that DID write music resets it, because a relay
            # that keeps producing music is exactly what is wanted however long
            # it goes on. Two round trips is also the most a genuinely broken
            # token can cost, which is the number that matters here -- there is
            # no listener-visible difference between "expired" and "wrong", and
            # only one of them ever recovers.
            dry = 0 if leg else int((resume or {}).get("dry", 0)) + 1
            relaying = bool(expired and isinstance(resume, dict) and dry <= 1)
            if relaying:
                resume.update(go=True, hops=hop + 1, dry=dry,
                              lead_s=max(0.0, ahead()), at=time.monotonic())
            elif isinstance(resume, dict):
                resume.update(go=False, hops=0, dry=0)

            diag = (f"\n\n```\n{out.diag}\n```"
                    if getattr(out, "diag", "") else "")
            how = (f"\n\n{out.style_note}"
                   if getattr(out, "style_note", "") else "")
            why = (stopped if stopped else
                   f"that is the {STREAM_MAX_MEASURES}-measure ceiling for one "
                   f"listen — handovers included — so it stopped on its own"
                   if out.total_measures - started >= STREAM_MAX_MEASURES else
                   "the last leg added nothing, so there was no point asking "
                   "for another")
            a, c, cov, s = _render_outputs(out)
            if relaying:
                # Deliberately still 🔴 Streaming: from the listener's side
                # nothing has stopped -- there is buffered audio playing and
                # more coming -- and a status line that says a press ended
                # would be describing the plumbing rather than the music.
                yield (None, out, a, c, cov, s,
                       f"🔴 Streaming — renewing the GPU authorisation "
                       f"(handover {hop + 1}); {out.total_measures} measures "
                       f"written so far and about {ahead():.0f}s of them still "
                       f"in the player, which is what you are hearing while "
                       f"this happens. It picks up on its own.\n\n*{drive}*"
                       + "".join(f"\n\n*{n}*" for n in notes))
                return
            yield (None, out, a, c, cov, s,
                   f"Streamed: +{out.total_measures - started} measures in "
                   f"{leg} leg{'' if leg == 1 else 's'} "
                   f"({out.total_measures} total) — {why}. Press "
                   f"Stream again to keep going. The editable window is now "
                   f"measures {out.prefix_measures}–{out.total_measures - 1}."
                   + "".join(f"\n\n*{n}*" for n in notes) + how + diag)

        def _stream_stopped(track):
            """Re-arm the track player and say where the take stands.

            Returns (audio, status) for BOTH tabs' players, and re-rendering
            the audio here is the point rather than a convenience. Cancelling a
            generator is not a return: on_stream never reaches its final yield,
            so the value it would have written to the player never arrives, and
            the frontend has just torn down the streaming element the listener
            was hearing. Left to itself the slot comes back visible and empty --
            the waveform gone and no way to replay a run that had been playing
            a second earlier. `cur` still holds the last committed leg (a leg's
            Track is its final yield), so everything through that boundary can
            simply be re-rendered from state, which is also the honest thing to
            show: the leg that was in flight is the part that is gone."""
            if track is None:
                return gr.update(), "Stopped.", gr.update(), "Stopped."
            a = (_audio_to_file(track.audio, track.sr)
                 if track.audio is not None else gr.update())
            msg = (f"⏹ Stopped at {track.total_measures} measures. Everything "
                   f"through the last completed leg is kept and is back in the "
                   f"player above, ready to replay. Up to "
                   f"{STREAM_LEG_MEASURES} measures that were still being "
                   f"written are lost. Press 🔴 Start Radio to carry on from "
                   f"there.")
            # Both tabs, one press: the same rule every other handler follows
            # through _mirror, done by hand because this one is wired to a
            # `cancels=` listener rather than going through out_all.
            return a, msg, a, _simple_status(msg)

        def _s_stream_stopped(track):
            """Generate's ⏹, same work in Generate's output order."""
            a, msg, _, s_msg = _stream_stopped(track)
            return a, s_msg, a, msg

        # Two pairs that take turns together: the track player and the radio's
        # player occupy one slot, and Start Radio and Stop Radio occupy
        # another. Both pairs flip on exactly the same three transitions, so
        # they flip in the same three functions -- a fourth place that decided
        # when the radio is "on" is a fourth place to get it wrong, and the one
        # that gets it wrong leaves a Stop button over a dead stream.
        #
        # Separate events rather than extra outputs on on_stream: the streaming
        # slot carries audio chunks, and a visibility update sent down it would
        # be dropped as a prop update anyway (blocks.py skips `is_prop_update`
        # values before they reach stream_output).
        # A fresh dict per output, never a shared one: gradio's
        # postprocess_update_dict MUTATES the update it is handed (it pops
        # "value" out of it), so one object wired to two components is a bug
        # waiting for somebody to add a value to it.
        def _swap(on):
            hide, show = (lambda: gr.update(visible=False),
                          lambda: gr.update(visible=True))
            a, b = (hide, show) if on else (show, hide)
            # plain player, streaming player, Start Radio, Stop Radio
            return a(), b(), a(), b()

        def _radio_on():
            return _swap(True)

        def _radio_off():
            return _swap(False)

        def _radio_off_unless_relaying(resume):
            """`_radio_off`, except between the hops of one continuous listen.

            Swapping the streaming player back for the plain one here would
            tear down the MediaSource that is currently the only thing making
            sound -- the buffered audio covering the handover is IN it. So a
            relaying press leaves both players exactly as they are and the next
            hop streams into the same component. The button pair is left alone
            for the plainer reason that the radio has not stopped: a Start
            button flashing up between hops would offer to begin something
            already playing.
            """
            if isinstance(resume, dict) and resume.get("go"):
                return (gr.update(),) * 4
            return _radio_off()

        def _relay_cancel(resume):
            """⏹ Stop means stop, including a handover already in the post.

            A cancel closes the generator mid-yield, so on_stream's tail never
            runs and never gets to clear the flag it set on the way to being
            cancelled. Left alone, `go` would survive the stop and the NEXT
            press -- a deliberate one, minutes later -- would read itself as a
            handover and open with a one-second lead-in instead of the warm
            one. Clearing `lead_s` and `at` too, so nothing stale is left to be
            interpreted.
            """
            if isinstance(resume, dict):
                resume.update(go=False, hops=0, dry=0, lead_s=0.0, at=0.0)

        def _relay_tick(resume, n):
            """Bump a hidden counter so the BROWSER makes the next request.

            This is the whole relay mechanism and it is deliberately this
            small. It cannot be a loop inside on_stream, or a `.then` chain of
            a fixed length, because what has to be renewed is the request
            itself: gradio dispatches a .change from the frontend, that
            dispatch is a new HTTP call, and the ZeroGPU proxy stamps a new
            X-IP-Token on it. Anything that stays inside the current event --
            however it is written -- keeps the token that is already dead.

            Returning gr.update() rather than the same number matters: writing
            an unchanged value would still be a value, and .change would fire
            on it, so a press that ended for any other reason would relay
            itself forever. `go` is cleared by on_stream on the way IN rather
            than here, so that the swap-back above and this always read the
            same answer for one press however gradio orders them.
            """
            if not (isinstance(resume, dict) and resume.get("go")):
                return gr.update()
            return int(n or 0) + 1

        def _note_pause(pause):
            """Record that a player paused. `on_stream` decides what it meant.

            No outputs on purpose: this cannot say anything true yet. Whether
            the run is about to end depends on how much audio was buffered when
            the pause landed, which only the generator knows, and it writes the
            status either way within a take. See PAUSE_MIN_BUFFER_S.
            """
            if isinstance(pause, dict):
                pause["t"] = time.monotonic()

        # Generate's three buttons. `move` is the one control that stands in for
        # the whole style-movement question -- S_MOVE expands it into a walk
        # mode and the style-off flag, and `_steer` below turns it into the
        # typed destination or the empty string -- and everything else is passed
        # straight through. None of them takes a gr.Request: the `spaces`
        # package reads the X-IP-Token off a contextvar, not off anything we
        # pass. They keep explicit signatures anyway; a *args wrapper would make
        # gradio's arity check useless.
        #
        # `journey` goes in as BOTH span and pace: the branches are exclusive so
        # only one of them is ever read, and the slider is labelled for the mode
        # that will read it. The drift axis is not a control any more -- every
        # drift heads along an axis drawn between two presets, which is what
        # STYLE_AXIS_RANDOM means -- so it is passed as the constant it now is.
        def _steer(move, text):
            """The typed destination, or "" if this mode does not have one."""
            return str(text or "") if move == "toward" else ""

        def on_simple_stream(track, seed, is_draft, guidance, hist_g,
                             ctx_noise, hist_freq, hist_m, hg_mode, steer,
                             move, journey, bpm_follow, bpm_step,
                             bpm_hold, cross_cluster, pause, resume):
            """The radio: Continue held down, with `move` expanded via S_MOVE."""
            walk, style_off, _ = S_MOVE.get(move, S_MOVE[S_MOVE_DEFAULT])
            yield from _stream_ui(on_stream(
                track, seed, is_draft, guidance, hist_g, ctx_noise,
                _steer(move, steer), journey, walk, journey, STYLE_AXIS_RANDOM,
                style_off, bpm_follow=bpm_follow,
                bpm_step=bpm_step, bpm_hold=bpm_hold,
                cross_cluster=cross_cluster, pause=pause, resume=resume,
                hist_freq=hist_freq, hist_measures=hist_m, hg_mode=hg_mode))

        # Bound before the mirroring block at the end of this section rebinds
        # the names, so that these call the six-value handlers and get mirrored
        # once rather than twice.
        _raw_continue = on_continue
        _raw_fast_forward = on_fast_forward

        def on_simple_continue(track, seed, is_draft, guidance, hist_g,
                               ctx_noise, hist_freq, hist_m, hg_mode, steer,
                               move, journey, bpm_follow, bpm_step,
                               bpm_hold, cross_cluster):
            walk, style_off, _ = S_MOVE.get(move, S_MOVE[S_MOVE_DEFAULT])
            return _raw_continue(track, seed, is_draft, guidance, hist_g,
                                 ctx_noise, _steer(move, steer), journey, walk,
                                 journey, STYLE_AXIS_RANDOM,
                                 style_off, bpm_follow, bpm_step, bpm_hold,
                                 cross_cluster, hist_freq=hist_freq,
                                 hist_measures=hist_m, hg_mode=hg_mode)

        def on_simple_ff(track, seed, is_draft, guidance, hist_g, ctx_noise,
                         hist_freq, hist_m, hg_mode, steer, move, journey,
                         bpm_follow, bpm_step, bpm_hold, cross_cluster):
            """Fast-forward: on_fast_forward with `move` expanded, and two
            settings it makes rather than exposes.

            `idle="run"`. Refusing without a steer would leave the button dead
            in the default configuration -- "Hold" IS the idle case -- so
            it runs the leg instead.

            `max_measures=journey`. The length comes from the "Measures per
            journey" slider in the same section, which is the only length this
            tab shows, so the button writes what that slider says. It
            matters most in exactly the case the ceiling used to swallow:
            "Hold" has no leg to run out, so without this the press fell back on
            FF_MAX_MEASURES and the slider above it did nothing, which reads as
            the button ignoring its own section. Over the ceiling it caps and
            the status line says how much is left over.
            """
            walk, style_off, _ = S_MOVE.get(move, S_MOVE[S_MOVE_DEFAULT])
            return _raw_fast_forward(track, seed, is_draft, guidance, hist_g,
                                     ctx_noise, _steer(move, steer), journey,
                                     walk, journey, STYLE_AXIS_RANDOM,
                                     style_off, bpm_follow, bpm_step, bpm_hold,
                                     cross_cluster, idle="run",
                                     max_measures=int(journey),
                                     hist_freq=hist_freq, hist_measures=hist_m,
                                     hg_mode=hg_mode)

        def _win_range(track, start, end):
            """The selection, as offsets into the window the model is holding.

            The picker is numbered in SONG measures (see `_sel_window`) and the
            backend counts within the window, so somebody has to subtract
            `prefix_measures`. `on_regen` has always done it; now the shape
            editor does too, off the same two sliders, which is what keeps
            "change this curve" and "rewrite these measures" talking about the
            same bars.

            Clamped rather than rejected, for the reason on_regen gives: a
            Continue moves the window on under a selection made before it, and
            losing the press because two numbers went stale is worse than doing
            the part still in range."""
            if track is None:
                return 0, N_CHUNKS
            first, T = int(track.prefix_measures), track.cond.T
            return (max(0, min(int(start) - first, T)),
                    max(0, min(int(end) - first, T)))

        def on_apply(track, signal, start, end, shape, mode, wave, is_draft,
                     *pvals):
            if track is None:
                return (gr.update(),) * 5 + ("Start a track first.",)
            a, b = _win_range(track, start, end)
            if b <= a:
                return (gr.update(),) * 5 + (
                    f"Nothing to edit: measures {int(start)}–{int(end)} are "
                    f"outside the window. Move the selection.",)
            params = _collect_params(shape, wave, pvals)
            tr = backend.apply_shape(track, signal, a, b, shape, params,
                                     mode=mode)
            lbl = SIGNAL_LABELS.get(signal, signal)
            return _edit_outputs(
                tr, f"{mode.title()}d {shape} on **{lbl}** over "
                    f"measures {int(start)}–{int(end)}.")

        def _target_controls(target):
            """Show the controls the picked target needs, hide the rest, and
            say on the button what pressing it will do.

            Containers, not components: `_param_updates` owns `pslots` and
            `wave_dd` per shape, and two handlers writing one `visible` flag
            from different triggers is a race.

            `target` is None when the picker has no choices at all, which is an
            all-off track: nothing to show and nothing Apply could write, so
            everything goes and the button greys out rather than throwing on a
            signal named None."""
            return (gr.update(visible=bool(target)
                              and target not in EDIT_SPECIALS),
                    gr.update(visible=target == EDIT_KEY),
                    gr.update(visible=target == EDIT_MORPH),
                    gr.update(visible=target == EDIT_TEXT),
                    gr.update(value=_edit_action(target),
                              interactive=bool(target)))

        def on_edit_apply(track, target, start, end, shape, mode, wave,
                          root, minor, style, amount, prompt, is_draft,
                          *pvals):
            """The one Apply button. Four edits, one press, chosen by the
            picker -- the handlers below are unchanged, this only routes to
            them. They all return `_edit_outputs`, so the wiring downstream
            cannot tell which one ran."""
            if target == EDIT_KEY:
                return on_set_key(track, root, minor, is_draft)
            if target == EDIT_MORPH:
                return on_morph_style(track, style, amount, is_draft)
            if target == EDIT_TEXT:
                return on_style_from_text(track, prompt)
            return on_apply(track, target, start, end, shape, mode, wave,
                            is_draft, *pvals)

        # ---- moving the editable window --------------------------------------
        # Not a control of its own any more: the selection says which measures
        # to rewrite, `_sel_window` says which N_CHUNKS have to be loaded to do
        # it, and this puts them there. It costs nothing and changes nothing
        # audible -- the measure bank keeps the conditioning and latents of
        # every measure that has scrolled out, so seating the window is an exact
        # slice of what is already in memory: no decode, no model, no GPU. See
        # backend.seat_window.
        def on_seat(track, a, b):
            if track is None:
                return (gr.update(),) * 5 + (
                    "Start a track first — **Generate** is the tab for that.",)
            first, a, b = _sel_window(track.total_measures, a, b)
            T = track.cond.T
            if first == int(track.prefix_measures):
                return (gr.update(),) * 5 + (
                    f"Rewriting measures {a}–{b}; the model reads "
                    f"{first}–{first + T}.",)
            tr = backend.seat_window(track, first)
            # gr.update() for the player on purpose: seating does not touch a
            # sample of the audio, and re-emitting the same wav would restart
            # playback under someone who is listening for the spot they want to
            # fix. The plots and the summary DO change -- they describe the
            # window -- so they are rebuilt.
            return (tr, gr.update(), _curves_figure(tr), _coverage_table(tr),
                    _summary(tr),
                    f"Rewriting measures {a}–{b}; the model has been moved onto "
                    f"{first}–{first + T} of {tr.total_measures} to do it. "
                    f"Nothing was re-rendered.")

        def on_set_key(track, root, minor, is_draft):
            if track is None:
                return (gr.update(),) * 5 + ("Start a track first.",)
            tr = backend.set_key(track, int(root), bool(minor))
            return _edit_outputs(tr, "Set key.")

        def on_morph_style(track, style, amount, is_draft):
            if track is None:
                return (gr.update(),) * 5 + ("Start a track first.",)
            tr = backend.morph_style(track, style, float(amount))
            return _edit_outputs(tr, f"Morphed style toward {style}.")

        def on_regen(track, start, end, seed, is_draft, guidance, strength):
            """Rewrite the selected measures.

            ONE model call, at every width. This button used to choose between
            two: a selection that filled the window went to `backend.render`,
            which samples from pure noise and never reads the past, and anything
            shorter went to the in-painter. They were not two settings of one
            operation, and the wide one is why an untouched Regenerate came back
            as unrelated music. `EDIT_MAX_MEASURES` now keeps every selection
            below the window width, so there is always frozen music either side
            and the in-painter is always the right call.

            What replaces the "same seed reproduces the take" promise that
            branch was making is **Edit strength**: 0 hands back the latents
            already in hand, 1 writes the span from noise, and the dial between
            is how far the take moves. See REGEN_STRENGTH. The seed picks WHICH
            re-roll, not which take you get back.

            `start`/`end` arrive in SONG measures, because that is what the
            picker they come from is numbered in, and the backend counts within
            the window -- so the conversion happens here, once, rather than in
            four wiring sites. Clamped rather than rejected: a Continue moves
            the window on under a selection that was made before it, and losing
            the press because two numbers went stale would be worse than doing
            the part that is still in range and saying so.

            THE TWO SNIPPET PLAYERS ARE RETURNED FROM HERE, not left to the
            `_snippets` tail, and that is about the wait rather than the file.
            gradio shows its pending animation on the components an event
            DECLARES as outputs, for as long as that event runs -- so a player
            written by a later `.then` sits there looking idle through the whole
            model call, which on this tab is the only player you are looking at.
            Every other tab's audio is in `out_main` and pulses for free. The
            tail still writes them afterwards; it is the same file, and the
            second write is what keeps every OTHER chain correct."""
            if track is None:
                return ((gr.update(),) * 5 + ("Start a track first.",)
                        + (gr.update(),) * 2)
            first, T = int(track.prefix_measures), track.cond.T
            a0 = max(0, min(int(start) - first, T))
            b0 = max(0, min(int(end) - first, T))
            if b0 <= a0:
                return ((gr.update(),) * 5 + (
                    f"Nothing to rewrite: measures {int(start)}–{int(end)} are "
                    f"outside the window, which is {first}–{first + T}. Move "
                    f"the window or change the range.",)
                    + (gr.update(),) * 2)
            tr = backend.set_guidance(track, float(guidance))
            # The seed the box holds is the one the render is asked for, so the
            # box is the whole story about which re-roll you get.
            tr.seed = int(seed)
            tr = backend.regenerate_region(tr, a0, b0, int(seed),
                                           n_steps=_steps(is_draft),
                                           strength=float(strength))
            clamped = ("" if (a0, b0) == (int(start) - first, int(end) - first)
                       else "  (clamped to the window)")
            # Which guards it actually got, rather than the nominal four: at the
            # start or end of a song the window cannot centre and one side runs
            # out. Reported because it is the thing that decides how well the
            # rewrite joins on, and it is not visible anywhere else.
            return (tr, *_render_outputs(tr),
                    f"Rewrote measures {first + a0}–{first + b0} at strength "
                    f"{float(strength):g}; {a0} measures before and "
                    f"{T - b0} after were held frozen.{clamped}"
                    + _splice_note(tr) + _diag_note(tr),
                    *_snippets(tr))

        def _apply_coverage(track, signal, on):
            """Toggle a signal — or all of them — on/off across the WHOLE
            window (edit only, no re-render). Not a range: see the comment on
            the coverage panel. COV_ALL is handled here rather than by its own
            pair of buttons, because "which signals" is the dropdown's job."""
            if track is None:
                return (gr.update(),) * 5 + ("Start a track first.",)
            sigs = ALL_SIGNALS if signal == COV_ALL else [signal]
            tr = track
            for s in sigs:
                tr = backend.set_active(tr, s, 0, tr.cond.T, bool(on))
            lbl = (COV_ALL_LABEL.lower() if signal == COV_ALL
                   else f"**{ALL_SIGNAL_LABELS.get(signal, signal)}**")
            first = int(track.prefix_measures)
            return _edit_outputs(
                tr, f"Turned {lbl} {'on' if on else 'off'} for measures "
                    f"{first}–{first + track.cond.T}.")

        def on_cov_on(track, signal):
            return _apply_coverage(track, signal, True)

        def on_cov_off(track, signal):
            return _apply_coverage(track, signal, False)

        # ---- A/B slots ------------------------------------------------------
        # A = `cur` (being edited), B = `prev` (parked snapshot). Both hold a
        # whole Track, so a swap carries the conditioning arrays, per-measure
        # active flags, seed, stage, guidance, cached latents and rendered audio
        # together — there is no separate cache to keep in sync.
        def _audio_or_none(track):
            """Temp-wav path for a Track, or None if it has never been rendered
            (an edited-but-not-regenerated snapshot has cond but no audio)."""
            if track is None or track.audio is None:
                return None
            return _audio_to_file(track.audio, track.sr)

        def _slot_snippet(track):
            """B's audio: the parked track's WINDOW, not its whole song.

            A on this tab is `snip_edit`, the 24 measures being edited, and B
            is there to be compared against it. Parking the whole song would
            put minutes of audio in the slot, all but 24 measures of which are
            byte-identical to A's -- you would be hunting for the difference
            somewhere in the middle of a file instead of hearing it. The full
            song is on Export, where both versions still are."""
            if track is None or track.window_audio is None:
                return None
            return _audio_to_file(track.window_audio, track.sr)

        def _slot_label(track, slot="B"):
            if track is None:
                return f"{slot} — (empty)"
            name = track.label or "Untitled"
            stale = "" if track.window_audio is not None else " · not rendered"
            return (f"{slot} — {name} · measures "
                    f"{int(track.prefix_measures)}–"
                    f"{int(track.prefix_measures) + track.cond.T} · "
                    f"stage {track.stage} · g{track.guidance:g}{stale}")

        # ---- undo ------------------------------------------------------------
        # Depth. Each entry is a full Track, and a Track owns its own copy of
        # the whole song (prefix_audio grows by 8 measures per Continue), so
        # this is the one control in the app with a real memory cost: roughly
        # depth x song-length x 4 bytes at 16 kHz mono, i.e. ~19 MB per entry
        # for a five-minute rollout. Six is deep enough to walk back a bad
        # stretch and shallow enough to survive a Space.
        HISTORY_MAX = 6

        def _snapshot(track, history):
            """Push the PRE-action track. Runs as the first link of a button's
            chain, before the handler that replaces `cur` -- .then() would be
            too late, and a second listener on the same event has no ordering
            guarantee against the first."""
            if track is None:
                return history
            return (list(history) + [track.copy()])[-HISTORY_MAX:]

        def on_undo(history):
            if not history:
                return (history, gr.update(), gr.update(), gr.update(),
                        gr.update(), gr.update(), "Nothing to undo.")
            rest, tr = list(history[:-1]), history[-1]
            a, c, cov, s = _render_outputs(tr)
            return (rest, tr, a, c, cov, s,
                    f"Undone — back to {tr.total_measures} measures. "
                    f"{len(rest)} step{'' if len(rest) == 1 else 's'} left.")

        def on_save(track, prev_track):
            if track is None:
                return prev_track, gr.update(), "Nothing to snapshot yet."
            return (track,
                    gr.update(value=_slot_snippet(track),
                              label=_slot_label(track)),
                    f"Measures {int(track.prefix_measures)}–"
                    f"{int(track.prefix_measures) + track.cond.T} of "
                    f"“{track.label or 'Untitled'}” parked in B.")

        def on_swap(track, prev_track, ui_stage, ui_guidance):
            """Exchange the A and B slots and rebuild every A-facing control.

            stage_rb and guidance_sl are inputs to the *next* render rather than
            reflections of state, so they must be pushed back to the restored
            track's values — otherwise swapping to a stage-1 track and hitting
            Regenerate would silently sample it with stage 2. They are only
            emitted when they actually differ, so an unchanged radio doesn't
            re-fire stage_rb.change."""
            if prev_track is None:
                msg = ("Nothing to swap yet." if track is None
                       else "B is empty — hit Snapshot first.")
                return (gr.update(),) * 5 + (msg,) + (gr.update(),) * 4
            new_cur, new_prev = prev_track, track
            stage_up = (gr.update(value=int(new_cur.stage))
                        if int(ui_stage) != int(new_cur.stage) else gr.update())
            guid_up = (gr.update(value=float(new_cur.guidance))
                       if float(ui_guidance) != float(new_cur.guidance)
                       else gr.update())
            note = ("" if new_cur.audio is not None else
                    "\n\n*Restored from B — no render cached; hit "
                    "**Regenerate** to hear it.*")
            msg = (f"Swapped: now editing “{new_cur.label or 'Untitled'}” "
                   f"(stage {new_cur.stage}, guidance {new_cur.guidance:g}). "
                   f"Previous version parked in B.")
            return (
                new_cur,
                _audio_or_none(new_cur),
                _curves_figure(new_cur),
                _coverage_table(new_cur),
                _summary(new_cur) + note,
                msg,
                new_prev,
                gr.update(value=_slot_snippet(new_prev),
                          label=_slot_label(new_prev)),
                stage_up,
                guid_up,
            )

        # Both of these return a visibility update rather than a sentence,
        # because on success the File slot beside the button IS the message and
        # a line of prose under it says the same thing again. It appears only
        # when there is nothing in that slot to explain itself.
        def on_export(track):
            if track is None:
                return None, gr.update(value="Start a track first.",
                                       visible=True)
            path = _audio_to_file(track.audio, track.sr)
            return path, gr.update(value="", visible=False)

        def on_export_cond(track):
            """Dump the current conditioning trajectory to a .npz. Drop the file
            into the presets/ folder to reload it as a preset (round-trips via
            Conditioning.from_npz) — handy for saving state and for sanity-
            checking the pipeline with a known-good array."""
            if track is None:
                return None, gr.update(value="Start a track first.",
                                       visible=True)
            path = tempfile.NamedTemporaryFile(suffix="_cond.npz",
                                               delete=False).name
            track.cond.to_npz(path)
            return path, gr.update(value="", visible=False)

        # ---- mirroring -------------------------------------------------------
        # Every handler that writes `out_main` goes through _mirror exactly
        # once, here, in one list. Adding a handler and forgetting to add it
        # here is the failure this block exists to make obvious: the Generate
        # tab would keep showing the previous track after that one action, and
        # only after that one action, which is a bug you find by accident weeks
        # later. `on_undo` and the stream handler offset by one because they
        # return the history / the live audio chunk ahead of out_main.
        #
        # `on_continue` and `on_fast_forward` are deliberately absent: nothing
        # is wired to them directly any more. They are reached only through
        # `_raw_continue` / `_raw_fast_forward`, which were bound above this
        # block precisely so the Generate wrappers get mirrored once rather
        # than twice.
        #
        # `on_apply`, `on_set_key`, `on_morph_style` and `on_style_from_text`
        # are absent for the same reason: `on_edit_apply` is the only thing
        # wired to them, and mirroring both it and them would append the four
        # mirror outputs twice.
        (on_new_preset, on_new_text, on_new_upload, on_set_bpm,
         on_set_stage, on_simple_continue, on_simple_ff, on_edit_apply,
         on_regen, on_cov_on, on_cov_off, on_swap, on_seat) = [
            _mirror(f) for f in (
                on_new_preset, on_new_text, on_new_upload,
                on_set_bpm, on_set_stage,
                on_simple_continue, on_simple_ff, on_edit_apply,
                on_regen, on_cov_on, on_cov_off, on_swap, on_seat)]
        on_undo = _mirror(on_undo, off=1)

        # ---- weather ---------------------------------------------------------
        # Self-contained block: one handler, one wrapper, one wiring site, and
        # the only thing it needs from the rest of the app is `Track.cond_hook`.
        # Deleting the Weather tab means deleting this, the tab above, and
        # weather.py.
        def on_place_typed(text):
            """Refill the match list from Open-Meteo as the box is typed.

            The top hit is selected as well as listed, so the button works
            without a second click and so the box shows a resolved place --
            with its region and its coordinates -- while you are still typing
            the name. That readback is the whole point: a name is not a
            location, and the alternatives are one click away in the list.

            Errors are swallowed to a no-op on purpose. This fires per
            keystroke against a keyless public endpoint; a dropped lookup
            means the list is one letter stale, and an error banner per
            keystroke would be worse than that by a distance. The button
            reports properly.
            """
            try:
                import weather as _weather
                hits = _weather.search(text, count=6)
            except Exception:  # noqa: BLE001 -- see above
                return gr.update()
            if not hits:
                return gr.update(choices=[], value=None)
            return gr.update(choices=[(p.label, p.token) for p in hits],
                             value=hits[0].token)

        def _wav_seconds(b) -> float:
            """How long a chunk from `to_wav_bytes` plays for."""
            import io
            import wave
            with wave.open(io.BytesIO(b), "rb") as w:
                return w.getnframes() / float(w.getframerate())

        def on_weather(city, pick, weight, seed, is_draft, guidance):
            """Fetch, build the trajectory, and start a track carrying it.

            STAGE 2, not the stage picker's value: style-off is a stage-2
            configuration and `style_is_off` refuses it on stage 1, where
            p_ind_uncond was 0 and no training window ever dropped style. A
            weather track on stage 1 would silently be a styled track.

            The preset seed is only there to give the style array something
            valid to hold; `active["style"]` is False on every measure the
            trajectory writes, so nothing reads it.

            RAISES rather than returning a sentence, which is the one place
            this tab does not follow the rest of the app. The button it hangs
            off also starts the radio, and gradio's only way to stop a `.then`
            chain is for a link in it to fail -- a returned message would put
            a red line in the status box and then cheerfully stream a track
            that was never built. `.success` does the rest. See the wiring.
            """
            try:
                import weather as _weather
                tj = _weather.build(_weather.choose(city, pick), float(weight))
            except Exception as e:  # noqa: BLE001
                # Network, geocoding and Open-Meteo's own errors all land here
                # and all mean the same thing to the person pressing the
                # button: no data, no track.
                raise gr.Error(
                    "Could not fetch weather for "
                    f"{(city or '').strip() or 'that'}: "
                    f"{type(e).__name__}: {e}") from e
            tr = backend.seed_from_preset(backend.list_presets()[0], int(seed))
            tr = backend.set_stage(tr, 2)
            tr = backend.set_guidance(tr, float(guidance))
            tr.style_off = True
            tr.label = f"weather: {tj.place.name}"
            tr.cond_hook = tj
            tr.cond = tj(tr.cond, 0)
            tr = backend.render(tr, n_steps=_steps(is_draft))
            a, c, cov, s = _render_outputs(tr)
            return (tr, a, c, cov, s,
                    f"**{tj.place.label}** — {tj.n} hours fetched, "
                    f"{tj.when(0, tr.cond.T)}.\n\n"
                    f"Stage 2, style off, guidance {float(guidance):g}. "
                    f"Running backwards through the year."
                    + _diag_note(tr),
                    # No anchor with the fetch: the clip below the view is the
                    # whole track from bar one, so the view's offset is zero
                    # and it works that out from which player is on screen.
                    tj.payload(), gr.update())

        def _weather_mirror(fn):
            """`_mirror` plus this tab's own player.

            Not a sixth entry in `_mirror_vals`: that would append a value to
            every one of the twenty handlers passed through it, and this one
            component is the only thing the Weather tab adds. The audio dict is
            copied for the reason `_mirror_vals` copies it -- gradio pops
            "value" out of an update dict, so two components cannot share one.
            """
            def wrapped(*a):
                r = tuple(fn(*a))
                aud = r[1]
                extra = (dict(aud) if isinstance(aud, dict) else aud,)
                return r + extra + _mirror_vals(r, 0)
            wrapped.__name__ = fn.__name__
            return wrapped

        on_weather = _weather_mirror(on_weather)

        def _weather_stream_ui(gen, total0):
            """`_stream_ui` plus this tab's player and view anchor.

            Same extras as `_weather_mirror` and appended for the same reason,
            one index later because a stream handler's first yielded value is
            the audio chunk.

            THE ANCHOR is one number sent once, at the first audio chunk of
            the press, and it is arithmetic rather than a measurement.
            `stream_forward` opens with its whole lead-in in a single yield, so
            that chunk IS the tail of the song: the press therefore begins
            `total0 - (that chunk's duration)` seconds in, and the browser has
            to be told only that.

            This used to send the song's length instead and let the browser
            subtract `buffered.end()` from it. That is the same number in
            principle and it measured right, but `buffered.end()` is a
            decoder's progress: right after a burst of chunks it is tens of
            seconds behind what has been sent, and it catches up over the
            following second. Re-deriving the anchor from it at every leg
            boundary meant re-deriving it from a moving target, and the view
            stepped sideways each time a leg landed. Nothing here is measured
            now, and nothing is re-sent -- within a press the offset cannot
            change, because both sides grow by the same audio."""
            first = True
            for r in gen:
                r = tuple(r)
                aud = r[2]
                if first and not isinstance(r[0], dict):
                    first = False
                    sync = {"off": max(0.0, float(total0)
                                       - _wav_seconds(r[0])),
                            "at": time.monotonic()}
                else:
                    sync = gr.update()
                yield (r + _mirror_vals(r, 1)
                       + (sync, dict(aud) if isinstance(aud, dict) else aud))

        def on_weather_stream(track, seed, is_draft, guidance, pause, resume):
            """The radio with every style control pinned off.

            `on_simple_stream` reads the Continue panel; this reads nothing,
            because for a weather track there is only one right answer to each
            of those questions and it is not the listener's to get wrong.
            Nothing steers (`steer=""`, `walk="off"`), style is off, and
            `bpm_follow` is False because the trajectory writes the tempo of
            every measure itself and a tempo ramp would be a second author
            arguing with it. The rest are the module defaults on_stream
            already carries.

            `style_off=True` matters here and not only for honesty: the
            trajectory switches style off on every window regardless, so
            passing False would generate identical audio and then describe it
            with a style-walk readout that nothing is listening to."""
            total0 = (len(track.audio) / float(track.sr)
                      if track is not None and track.audio is not None else 0.0)
            yield from _weather_stream_ui(on_stream(
                track, seed, is_draft, guidance, HISTORY_GUIDANCE,
                CONTINUE_CTX_NOISE, "", STYLE_SPAN, "off", STYLE_PACE,
                STYLE_AXIS_RANDOM, True, bpm_follow=False,
                pause=pause, resume=resume), total0)

        def _w_stream_stopped(track):
            """Weather's ⏹, this tab's player first. See _stream_stopped."""
            a, msg, _, s_msg = _stream_stopped(track)
            return a, a, msg, a, s_msg

        # ---- wiring ---------------------------------------------------------
        out_main = [cur, audio_out, curves_plot, coverage_tbl, summary_md,
                    status]
        # Appended to every out_main list rather than folded into it, because
        # the mirrors always come LAST in a handler's return tuple and two of
        # the output lists below (swap, undo) have their own extras in between.
        s_mirror = [s_audio, g_audio, s_summary, s_status, s_map]
        out_all = out_main + s_mirror

        # Inputs that fully determine the shape preview. The two selection
        # sliders are in here so the preview redraws when you move the
        # selection on Select -- the range it shades is theirs now.
        preview_inputs = [cur, signal_dd, sel_from, sel_to, shape_dd, mode_rb,
                          wave_dd, *pslots]

        def refresh_preview(track, signal, start, end, shape, mode, wave,
                            *pvals):
            """The live shape preview, or nothing when the picker is not on a
            curve. `_preview_figure` reads `cond.<signal>`, and the three
            special targets are not channels -- the plot is hidden with the
            rest of the curve controls in that case, so there is nothing to
            draw either."""
            if signal in EDIT_SPECIALS:
                return gr.update()
            return _preview_figure(track, signal, *_win_range(track, start, end),
                                   shape, mode, wave, *pvals)

        # --- the selection's own view -----------------------------------------
        # Everything here is derived from the Track and the two sliders, and it
        # has to be redrawn after ANY handler: a Continue makes the song longer
        # (so the sliders' ceilings move) and moves the window to the tail, and
        # an Undo puts all of that back. Driven off the returned Track rather
        # than tracked separately, so there is nothing to keep in sync.
        window_inputs = [cur, sel_from, sel_to]
        window_outputs = [sel_from, sel_to, rg_seed]

        def refresh_window(track, a, b):
            """Redraw the picker, and make the selection follow the track when
            something has moved the window out from under it.

            The selection is normally the master -- you pick measures, the
            window is seated to hold them. But a Continue writes at the tail and
            re-seats there, an Undo restores wherever the old track was, and a
            fresh Generate starts over: after any of those the boxes would be
            pointing at measures the model is no longer reading. Selection and
            window are one thing on this tab, so when they disagree the track
            wins and the sliders snap to what it is actually holding.

            This is also where the width cap is made real. `_sel_window` has
            always enforced it internally, but silently: the sliders kept
            showing the illegal pair the user had dragged, so a selection of
            0-48 looked like it had been accepted and then behaved like 0-24.
            The legal pair is written back, and the last-measure slider's own
            ceiling is moved to first + EDIT_MAX_MEASURES, so the next drag
            cannot reach an illegal value in the first place.

            When the selection has fallen outside the window entirely it is
            reset to the widest legal one, CENTRED -- which for a full window is
            the guard layout, EDIT_GUARD frozen measures at each end. Resetting
            to the whole window would write a pair the cap forbids.

            The seed box comes along for the ride. It is a re-roll number, not a
            record: `track.seed` is whatever the LAST take through here was
            asked for, and a window assembled by three continuations has three
            of them. What makes Regenerate reproducible is Edit strength at 0,
            which returns the latents already in hand and never consults a seed.

            A programmatic write does not fire `.release`, so writing the values
            back here cannot re-enter the seat chain."""
            if track is None:
                return _window_updates(
                    N_CHUNKS, 0, EDIT_MAX_MEASURES) + (gr.update(),)
            first, T = int(track.prefix_measures), track.cond.T
            total = int(track.total_measures)
            _, a, b = _sel_window(total, a, b)
            if not (first <= a < b <= first + T):
                w = min(T, EDIT_MAX_MEASURES)
                off = (T - w) // 2
                a, b = first + off, first + off + w
            return _window_updates(total, a, b) + (
                gr.update(value=int(track.seed)),)

        # --- one tail for every chain -----------------------------------------
        # Three live views hang off state rather than off any one handler: the
        # shape preview, the tempo preview and the window picker. Each of them
        # would be stale after a press that did not think to redraw it, and the
        # way that bug arrives is always the same -- somebody adds a chain and
        # copies four of the five `.then`s. So there is exactly one tail and
        # every chain goes through it.
        def _snippet(track):
            """The window on its own, as a file. gr.update() rather than None
            when there is no track: an empty player where one was playing reads
            as the snippet having been taken away."""
            if track is None or track.window_audio is None:
                return gr.update()
            return _audio_to_file(track.window_audio, track.sr)

        def _snippets(track):
            """The same snippet for both players. Select and Edit each have one
            -- you pick the measures on one tab and work on them on the other,
            and neither is usable without hearing them -- and a component
            belongs to exactly one tab, so there are two of them holding one
            file."""
            s = _snippet(track)
            return s, s

        # What Edit's controls offer and open on depends on the track, so it is
        # rebuilt by the same tail as every other view of the track.
        edit_ctl_in = [cur, stage_rb, signal_dd, cov_signal, style_text]
        edit_ctl_out = [signal_dd, cov_signal, root_dd, minor_cb, style_dd,
                        style_text]

        def tail(ev):
            # `_edit_controls` leads, because it can rewrite signal_dd and
            # signal_dd is in `preview_inputs`: run it after the preview and the
            # preview is drawn for the target the track just stopped offering.
            return (ev.then(_edit_controls, edit_ctl_in, edit_ctl_out)
                      .then(refresh_preview, preview_inputs, preview_plot)
                      .then(_bpm_figure, bpm_inputs, bpm_preview)
                      .then(refresh_window, window_inputs, window_outputs)
                      .then(_snippets, [cur], [snip_sel, snip_edit]))

        # relabel/show parameter slots when the shape changes, then refresh preview
        shape_dd.change(_param_updates, [shape_dd],
                        [*pslots, wave_dd, shape_help]).then(
            refresh_preview, preview_inputs, preview_plot)
        # initialize slots + preview on load
        demo.load(_param_updates, [shape_dd],
                  [*pslots, wave_dd, shape_help]).then(
            refresh_preview, preview_inputs, preview_plot)

        # live preview: any editor control change re-draws it (no audio render)
        for comp in [signal_dd, mode_rb, wave_dd, *pslots]:
            comp.change(refresh_preview, preview_inputs, preview_plot)
        # The selection sliders do not get a listener of their own: they are
        # already in `preview_inputs`, and every chain they start ends in
        # `tail`, which redraws the preview. A second listener here would only
        # race the first.

        # --- tempo preview -------------------------------------------------
        # `cur` is an input so the loaded track's tempo can be underlaid, but the
        # figure works with cur=None: that is the whole point of keeping this
        # control beside the entry points, since a description has no tempo
        # until you set one and there is nothing else to look at before the
        # first generate.
        bpm_inputs = [bpm_mean, bpm_var, bpm_seed, cur]
        # .release, NOT .change, on the two sliders. A Slider dispatches `change`
        # on every mutation of its value -- including each keystroke in its
        # companion number box -- and Gradio validates the payload against the
        # slider's bounds in preprocess, BEFORE the handler runs. So a live
        # `change` listener turns typing "180" into three requests (1, 18, 180)
        # and the first two die with "Value 18 is less than minimum value 40.0".
        # `release` fires on pointer-up and on committing the number box, which
        # is exactly when a redraw is wanted anyway. bpm_seed is a Number with no
        # bounds, so it has nothing to fail and can stay on `change`.
        bpm_mean.release(_bpm_figure, bpm_inputs, bpm_preview)
        bpm_var.release(_bpm_figure, bpm_inputs, bpm_preview)
        bpm_seed.change(_bpm_figure, bpm_inputs, bpm_preview)
        demo.load(_bpm_figure, bpm_inputs, bpm_preview)
        demo.load(refresh_window, window_inputs, window_outputs)

        # =====================================================================
        # GENERATE
        # =====================================================================
        tail(s_preset_btn.click(_snapshot, [cur, hist], hist).then(
            on_new_preset, [s_preset, s_seed, draft, stage_rb, guidance_sl],
            out_all))
        # =====================================================================
        # WEATHER
        # =====================================================================
        # `always_last` is what makes the search feel like a search. Without
        # it every keystroke is its own request, several are in flight at once,
        # and whichever comes back last wins however old its query was -- the
        # list settling on the answer to something typed three letters ago.
        # With it, one request runs and the keystrokes behind it collapse to
        # the most recent, which is then asked for once.
        w_city.input(on_place_typed, w_city, w_hits,
                     show_progress="hidden", trigger_mode="always_last")

        # The view's three JS hooks. The two listeners have no python side at
        # all -- `fn=None` with `js=` is a client-only event, so a keystroke of
        # trajectory does not become a request. They write plain globals rather
        # than calling into the loop, so none of the three depends on which of
        # them ran first. See _WX_JS.
        w_data.change(None, w_data, None, js="(d) => { window.__wxData = d; }")
        w_sync.change(None, w_sync, None, js="(s) => { window.__wxSync = s; }")
        demo.load(None, None, None, js=_WX_JS)

        # This tab's radio. Shape for shape the one behind Continue's ▶ -- the
        # same swap, the same relay, the same cancel -- on its own components
        # and its own counter, so the two can be running or stopped
        # independently and neither can end the other. Every comment on the
        # Continue block applies here and is not repeated.
        w_stream_in = [cur, s_seed, draft, guidance_sl, w_pause, w_resume]
        w_stream_out = [w_live] + out_all + [w_sync, w_audio]
        # The one button, as one chain: snapshot so the weather track is
        # undoable like any other, fetch and render, swap in the streaming
        # player, run. `.success` twice rather than `.then`, because a fetch
        # that raised must not be followed by a radio: a dependency that does
        # not fire does not fire what hangs off it either, so one check at the
        # top covers both of the steps after it.
        w_fetch_evt = w_go_btn.click(_snapshot, [cur, hist], hist).then(
            on_weather, [w_city, w_hits, w_weight, s_seed, draft, guidance_sl],
            out_main + [w_data, w_sync, w_audio] + s_mirror)
        tail(w_fetch_evt)
        w_stream_evt = w_fetch_evt.success(
            _radio_on, None, [w_audio, w_live, w_go_btn, w_stop_btn]).success(
            on_weather_stream, w_stream_in, w_stream_out)
        w_relay_evt = w_relay.change(
            on_weather_stream, w_stream_in, w_stream_out)
        for ev in (w_stream_evt, w_relay_evt):
            tail(ev)
            ev.then(_radio_off_unless_relaying, [w_resume],
                    [w_audio, w_live, w_go_btn, w_stop_btn]).then(
                _relay_tick, [w_resume, w_relay], w_relay)
        w_stop_btn.click(
            _w_stream_stopped, [cur],
            [w_audio, audio_out, status, s_audio, s_status],
            cancels=[w_stream_evt, w_relay_evt])
        w_stop_btn.click(_radio_off, None,
                         [w_audio, w_live, w_go_btn, w_stop_btn])
        w_stop_btn.click(_relay_cancel, [w_resume], None)
        w_live.pause(_note_pause, [w_pause], None)

        tail(s_text_btn.click(_snapshot, [cur, hist], hist).then(
            on_new_text,
            [s_text, s_seed, draft, stage_rb, guidance_sl, bpm_mean, bpm_var,
             bpm_seed],
            out_all))
        tail(s_upload_btn.click(_snapshot, [cur, hist], hist).then(
            on_new_upload, [s_upload, s_seed, draft, stage_rb, guidance_sl],
            out_all))
        tail(bpm_btn.click(_snapshot, [cur, hist], hist).then(
            on_set_bpm, [cur, bpm_mean, bpm_var, bpm_seed], out_all))

        # The radio owns every control that only some modes read: the textbox
        # only exists for "toward", the journey slider is ignored by the two
        # modes that have no journey, and cross-cluster is a wander-only filter.
        # Exclusivity used to be enforced the other way round -- typing in the
        # box greyed the walk out -- which meant the two could be set to
        # contradict each other and something downstream had to pick a winner.
        # One radio cannot contradict itself.
        #
        # The journey slider is WRITTEN here, not just enabled, because a typed
        # steer and a walk leg want different defaults out of the same control
        # (S_MOVE_JOURNEY).
        def _move_excl(move):
            journey = S_MOVE_JOURNEY.get(move, S_MOVE_JOURNEY_DEFAULT)
            return (gr.update(visible=move == "toward"),
                    gr.update(value=journey),
                    gr.update(interactive=move == "wander"))

        s_move.change(_move_excl, s_move,
                      [s_steer, s_pace, cross_cluster_cb])

        # The two tempo modes are exclusive for a plainer reason than the steer
        # and the walk: they write the same measures, so "both" is not a third
        # behaviour, it is whichever ran last. The backend settles that (step
        # wins), and these make the losing box go dead so the tick that does
        # nothing cannot be left sitting there looking like it does. Whichever
        # is UNTICKED is the one that greys out, so a user can always untick
        # what they turned on.
        # The hold slider goes with the step box for the plainer reason that it
        # is the only thing that reads it -- the ramp has no constant to hold
        # and neither does a tempo nobody is steering.
        def _tempo_excl(follow, step):
            return (gr.update(interactive=not step),
                    gr.update(interactive=not follow),
                    gr.update(interactive=bool(step)))

        for _cb in (bpm_follow_cb, bpm_step_cb):
            _cb.change(_tempo_excl, [bpm_follow_cb, bpm_step_cb],
                       [bpm_follow_cb, bpm_step_cb, bpm_hold_sl])

        # The continuation input list, shared by all three buttons because they
        # are one operation at three lengths. `draft` and `guidance_sl` live on
        # Edit and are read from there; the three radios come from Testing,
        # visible or not.
        cont_in = [cur, cont_seed, draft, guidance_sl, s_hist_g, s_ctx_noise,
                   s_hist_freq, s_hist_m, s_hg_mode, s_steer, s_move,
                   s_pace, bpm_follow_cb, bpm_step_cb,
                   bpm_hold_sl, cross_cluster_cb]
        tail(s_cont_btn.click(_snapshot, [cur, hist], hist).then(
            on_simple_continue, cont_in, out_all))
        # Same input list -- it is the same run, repeated -- and one _snapshot
        # for the whole thing, so Undo steps back over the run rather than over
        # its last take.
        tail(s_ff_btn.click(_snapshot, [cur, hist], hist).then(
            on_simple_ff, cont_in, out_all))
        tail(s_undo_btn.click(on_undo, [hist], [hist] + out_all))

        # Held in a variable because it is the one event that has to be
        # cancellable: on_stream never returns on its own (see its docstring),
        # so ⏹ Stop is the only hard way out. `cancels` closes the handler
        # generator, which closes the backend's, which ends the @spaces.GPU
        # call and releases the ZeroGPU reservation. gradio can only stop a
        # sync generator BETWEEN yields, so the stop lands within a second
        # during a wait and within one take mid-leg.
        s_stream_in = cont_in + [pause_req, resume_req]
        s_stream_evt = s_stream_btn.click(
            _radio_on, None, [s_audio, s_live, s_stream_btn, s_stop_btn]).then(
            _snapshot, [cur, hist], hist).then(
            on_simple_stream, s_stream_in, [s_live] + out_all)
        # The relay: the same handler on the same inputs, entered from a .change
        # instead of a click so that it arrives on a new request with a new
        # ZeroGPU token. No _radio_on (the players are already swapped) and no
        # _snapshot (a handover is not an edit, and stacking one undo step per
        # five minutes of listening would bury whatever you actually did).
        s_relay_evt = s_relay.change(
            on_simple_stream, s_stream_in, [s_live] + out_all)
        # Both endings run the same tail, and the swap-back is its own CHAIN
        # rather than a sibling `.then` so that it is settled before the next
        # hop is asked for. Sibling order is not something to rely on when the
        # two siblings read the same flag.
        for ev in (s_stream_evt, s_relay_evt):
            tail(ev)
            ev.then(_radio_off_unless_relaying, [resume_req],
                    [s_audio, s_live, s_stream_btn, s_stop_btn]).then(
                _relay_tick, [resume_req, s_relay], s_relay)
        s_stop_btn.click(_s_stream_stopped, [cur],
                         [s_audio, s_status, audio_out, status],
                         cancels=[s_stream_evt, s_relay_evt])
        # Its own listener, not a `.then` off the line above: a cancel means the
        # chain hanging off s_stream_evt never runs, so the swap back has to be
        # triggered by the Stop press itself.
        s_stop_btn.click(_radio_off, None,
                         [s_audio, s_live, s_stream_btn, s_stop_btn])
        s_stop_btn.click(_relay_cancel, [resume_req], None)
        # Pausing is a REQUEST, not a cancel -- it used to be `cancels=` and
        # that is what killed a run the moment the player caught up with the
        # lead-in on a cold worker. It only records when it happened; on_stream
        # honours it if audio was still buffered then. See PAUSE_MIN_BUFFER_S.
        s_live.pause(_note_pause, [pause_req], None)

        # =====================================================================
        # EDIT
        # =====================================================================
        # --- the selection ----------------------------------------------------
        # `.release`, not `.change`, for the usual reason (see the tempo note
        # above): a Slider's `change` fires per keystroke in its number box and
        # is validated against bounds that move as the song grows. Here it also
        # means one seat per drag instead of one per pixel.
        #
        # The nav buttons move the SELECTION, keeping its length, and the seat
        # follows from it like any other change -- there is no second thing for
        # them to move.
        def _sel_nav(track, a, b, step):
            """Shift the selection by `step` measures, or to the end if None.

            Through `_window_updates`, not as two bare numbers: moving the
            selection past where the sliders' bounds currently sit is the whole
            point of these buttons, and a bare number would leave the bounds
            behind and break every later event. See `_window_updates`."""
            total = N_CHUNKS if track is None else int(track.total_measures)
            n = max(1, min(int(b) - int(a), EDIT_MAX_MEASURES))
            start = total - n if step is None else int(a) + step
            # Clamped HERE rather than left to _sel_window, which is given an
            # absolute pair and would answer a shift off the front of the track
            # by collapsing the selection to one measure. A nav button moves a
            # selection; it does not resize it.
            _, a2, b2 = _sel_window(total, max(0, min(start, total - n)),
                                    max(0, min(start, total - n)) + n)
            return _window_updates(total, a2, b2)

        # A page is EDIT_MAX_MEASURES, not N_CHUNKS: the buttons exist to walk
        # the song a selection at a time, and stepping by the window width would
        # skip the eight measures between one maximum selection and the next --
        # measures you could then only reach by dragging.
        def on_nav_back(track, a, b):
            return _sel_nav(track, a, b, -EDIT_MAX_MEASURES)

        def on_nav_fwd(track, a, b):
            return _sel_nav(track, a, b, EDIT_MAX_MEASURES)

        def on_nav_end(track, a, b):
            return _sel_nav(track, a, b, None)

        sel_in = [cur, sel_from, sel_to]
        for _ev in (sel_from.release(on_seat, sel_in, out_all),
                    sel_to.release(on_seat, sel_in, out_all)):
            tail(_ev)
        for _btn, _fn in ((win_back, on_nav_back), (win_fwd, on_nav_fwd),
                          (win_end, on_nav_end)):
            tail(_btn.click(_fn, sel_in, [sel_from, sel_to])
                     .then(on_seat, sel_in, out_all))

        # --- the rewrite --------------------------------------------------------
        # `on_seat` first, always. It is free when the window is already right,
        # which it normally is -- the sliders seat as they are released -- but
        # the slider's own number box commits on `change`, which nothing is
        # bound to, so a typed selection would otherwise be rewritten against
        # whatever window the last drag left behind.
        #
        # The snippet players are outputs of `on_regen` itself, BEFORE the
        # mirrors (which _mirror always appends last) -- see the note in its
        # docstring: an output is how gradio is told to show the wait.
        tail(regen_btn.click(_snapshot, [cur, hist], hist)
             .then(on_seat, sel_in, out_all)
             .then(on_regen,
                   [cur, sel_from, sel_to, rg_seed, draft, guidance_sl,
                    rg_strength],
                   out_main + [snip_sel, snip_edit] + s_mirror))

        # --- the editor -------------------------------------------------------
        # One button, every edit. The inputs are the union of what the four
        # branches need, because gradio binds an event's inputs once and the
        # picker chooses the branch at press time -- the controls the branch
        # does not read are hidden, not absent, and their values are ignored.
        tail(apply_btn.click(_snapshot, [cur, hist], hist).then(
            on_edit_apply,
            [cur, signal_dd, sel_from, sel_to, shape_dd, mode_rb, wave_dd,
             root_dd, minor_cb, style_dd, style_amt, style_text,
             draft, *pslots],
            out_all))
        signal_dd.change(_target_controls, [signal_dd],
                         [curve_grp, key_grp, morph_grp, text_grp, apply_btn])

        # --- coverage ---------------------------------------------------------
        tail(cov_on.click(_snapshot, [cur, hist], hist).then(
            on_cov_on, [cur, cov_signal], out_all))
        tail(cov_off.click(_snapshot, [cur, hist], hist).then(
            on_cov_off, [cur, cov_signal], out_all))

        # --- undo / A / B -----------------------------------------------------
        # Undo restores a whole Track, so it has to rebuild every A-facing view
        # AND the three live previews -- same tail as the mutating chains above,
        # because the thing it is putting back was produced by one of them.
        tail(undo_btn.click(on_undo, [hist], [hist] + out_all))
        save_btn.click(on_save, [cur, prev], [prev, ab_audio, status])
        # Swap rebuilds the whole A-facing UI, so it needs out_main plus the B
        # slot and the two controls that feed the next render -- and then the
        # mirrors after those, because _mirror always appends last.
        tail(swap_btn.click(
            on_swap, [cur, prev, stage_rb, guidance_sl],
            out_main + [prev, ab_audio, stage_rb, guidance_sl] + s_mirror))

        export_btn.click(on_export, [cur], [export_file, ex_status])
        export_cond_btn.click(on_export_cond, [cur],
                              [export_cond_file, ex_status])

        # =====================================================================
        # TESTING
        # =====================================================================
        tail(stage_rb.change(on_set_stage, [cur, stage_rb], out_all))
        # Second listener on the same event: on_set_stage rebuilds the A-facing
        # views, this one rebuilds the controls. demo.load runs it once so the
        # initial radio position is honoured before anything is generated.
        stage_rb.change(_edit_controls, edit_ctl_in, edit_ctl_out)
        demo.load(_edit_controls, edit_ctl_in, edit_ctl_out)

        # ---- the one setting that exists on both tabs -------------------------
        # Draft belongs to Edit -- it is a render setting -- but it also halves
        # what every Continue costs, and making someone switch tabs to change
        # that mid-session made both tabs worse. So Generate carries a copy.
        # Guidance does not get one: it is a knob you set while listening to a
        # window you are rewriting, which is Edit's whole job, and Generate's
        # buttons read Edit's component directly.
        #
        # Two components for one quantity is a ping-pong risk, and a Checkbox
        # has no `.release` to sidestep it with (that is the trick the slider
        # pairs used to use: pointer-up does not fire on a programmatic write).
        # So the link takes BOTH values and returns gr.skip() when they already
        # agree -- which is exactly what the second hop of a round trip finds,
        # so it stops there rather than bouncing.
        def _link_cb(src, dst):
            return gr.skip() if bool(src) == bool(dst) else bool(src)

        g_draft.change(_link_cb, [g_draft, draft], draft)
        draft.change(_link_cb, [draft, g_draft], g_draft)

        # =====================================================================
        # HELP
        # =====================================================================
        # The button is the only thing that opens or closes the panel. Switching
        # tabs only changes WHAT it says: auto-opening would be the app talking
        # when it was not spoken to, and auto-closing would take the panel away
        # from someone doing the obvious thing while reading it -- going to the
        # tab the paragraph is about to look at what it names.
        def _help_toggle(is_open, tab):
            is_open = not is_open
            return (is_open,
                    gr.update(value=_help_text(tab), visible=is_open),
                    HELP_OPENED if is_open else HELP_CLOSED)

        help_btn.click(_help_toggle, [help_open, help_tab],
                       [help_open, help_md, help_btn])

        # One listener per tab with its name closed over, rather than a single
        # `Tabs.select` reading the name off gr.SelectData: a tab's own event
        # cannot be wrong about which tab it came from, and this keeps working
        # if the tabs are reordered or relabelled.
        #
        # `visible=is_open` rather than leaving visibility alone, because a bare
        # value update is the one thing that cannot be written here -- the panel
        # must come back in the state it was already in, and re-asserting the
        # flag we were handed is how it does that without a second listener.
        def _help_for(name):
            def _help_tab_change(is_open):
                return name, gr.update(value=_help_text(name),
                                       visible=bool(is_open))
            # Set by hand for the reason _mirror does it: gradio reads
            # __name__, and functools.wraps' __wrapped__ breaks its arity
            # detection.
            _help_tab_change.__name__ = f"help_tab_{name.lower()}"
            return _help_tab_change

        for _tab, _name in ((tab_generate, "Generate"),
                            (tab_continue, "Continue"),
                            (tab_select, "Select"),
                            (tab_edit, "Edit"),
                            (tab_export, "Export"),
                            (tab_weather, "Weather"),
                            (tab_testing, "Testing")):
            _tab.select(_help_for(_name), help_open, [help_tab, help_md])

    return demo


if __name__ == "__main__":
    app = build_app(MockBackend())
    # ssr_mode=False: see the note in app_hf.py — SSR's Node proxy is what emits
    # the "Invalid file descriptor: -1" asyncio tracebacks.
    try:
        app.launch(theme=gr.themes.Soft(), ssr_mode=False)
    except TypeError:
        app.launch(ssr_mode=False)