songsheet 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(/usr/bin/git -C /Users/administrator/Projects/songsheet log --oneline --all)",
5
+ "Bash(/usr/bin/git -C /Users/administrator/Projects/songsheet diff v4.0.0..v5.0.0 -- index.js)",
6
+ "Bash(npm install:*)",
7
+ "Bash(npx vitest run:*)",
8
+ "Bash(node -e:*)",
9
+ "WebSearch",
10
+ "WebFetch(domain:tonejs.github.io)",
11
+ "WebFetch(domain:github.com)",
12
+ "WebFetch(domain:www.andronio.me)",
13
+ "WebFetch(domain:www.guitarland.com)",
14
+ "Bash(npm test:*)",
15
+ "Bash(/usr/bin/git diff HEAD~2..HEAD -- package.json)"
16
+ ]
17
+ }
18
+ }
package/CLAUDE.md ADDED
@@ -0,0 +1,180 @@
1
+ # Songsheet
2
+
3
+ A zero-dependency songsheet parser and transposer. Parses plaintext songsheet files into a structured AST with chord-lyric character alignment.
4
+
5
+ ## Quick Start
6
+
7
+ ```js
8
+ import { parse, transpose } from 'songsheet'
9
+
10
+ const song = parse(rawText) // synchronous, returns plain object
11
+ const songInA = transpose(song, 2) // up 2 semitones
12
+ const songInBb = transpose(song, 3, { preferFlats: true })
13
+ ```
14
+
15
+ ## Architecture
16
+
17
+ ```
18
+ index.js — Public API: re-exports parse + transpose
19
+ src/
20
+ lexer.js — scanChordLine(), isChordLine(), lexExpression()
21
+ parser.js — parse(), expression parser, section assembly
22
+ transpose.js — transpose(), note math
23
+ test/
24
+ lexer.test.js — Chord line detection, bar lines, edge cases
25
+ parser.test.js — All 4 song fixture tests + bar line tests
26
+ expression.test.js — Expression parsing and resolution
27
+ transpose.test.js — Semitone math, round-trips, flat/sharp preference
28
+ *.txt — Song fixtures (do not modify)
29
+ ```
30
+
31
+ ## Module Format
32
+
33
+ ESM only (`"type": "module"` in package.json). No CommonJS.
34
+
35
+ ## Dev Commands
36
+
37
+ ```bash
38
+ npm test # vitest run — all tests
39
+ npx vitest # watch mode
40
+ npx vitest run test/parser.test.js # single file
41
+ ```
42
+
43
+ ## AST Shape
44
+
45
+ `parse()` returns:
46
+
47
+ ```js
48
+ {
49
+ title: 'SONG TITLE',
50
+ author: 'AUTHOR NAME',
51
+ sections: {
52
+ verse: {
53
+ count: 4,
54
+ chords: [{ root: 'G', type: '' }, { root: 'F', type: '' }, ...],
55
+ lyrics: ['lyric line 1', ...],
56
+ lines: [
57
+ {
58
+ chords: [{ root: 'G', type: '', column: 0 }, ...],
59
+ barLines: [], // column positions of | markers
60
+ lyrics: 'lyric line 1',
61
+ characters: [
62
+ { character: 'B', chord: { root: 'G', type: '' } },
63
+ { character: 'l' },
64
+ ...
65
+ ]
66
+ }
67
+ ]
68
+ },
69
+ chorus: { ... },
70
+ },
71
+ structure: [
72
+ {
73
+ sectionType: 'verse',
74
+ sectionIndex: 0,
75
+ chords: [...],
76
+ lyrics: [...],
77
+ lines: [...],
78
+ expression: null, // non-null on directive entries
79
+ },
80
+ ...
81
+ ]
82
+ }
83
+ ```
84
+
85
+ ## Key Design Decisions
86
+
87
+ - **Chord line detection**: Exhaustive left-to-right scan — every non-whitespace token must parse as a valid chord or `|`, otherwise the line is lyrics
88
+ - **root includes accidental**: `root: 'Bb'` not `root: 'B', accidental: 'b'`
89
+ - **Synchronous parse**: No Promise wrapper, plain objects (no Immutable.js)
90
+ - **Expression AST preserved**: `(VERSE, CHORUS*2)` stored as tree AND resolved to flat chords
91
+ - **Character alignment includes barLines**: `{ character: 'r', barLine: true }` at `|` column positions
92
+ - **Column preservation**: Chord lines are never trimmed — column positions match the original file
93
+
94
+ ## Songsheet Format
95
+
96
+ ```
97
+ SONG TITLE - AUTHOR NAME
98
+
99
+ G F
100
+ Lyrics aligned under chords...
101
+ C G
102
+ More lyrics here
103
+
104
+ F C D
105
+ Chorus lyrics...
106
+
107
+ Verse lyrics without chords (inherits first verse's chord pattern)
108
+
109
+ CHORUS
110
+ CHORUS*2
111
+
112
+ PRECHORUS:
113
+ D
114
+ Labeled section with chords...
115
+
116
+ INSTRUMENTAL: (VERSE, CHORUS*2)
117
+ FILL: D G D A D
118
+
119
+ BRIDGE
120
+ ```
121
+
122
+ ### Section Type Inference
123
+
124
+ 1. 1st block with chords+lyrics → `verse`
125
+ 2. 2nd block with chords+lyrics → `chorus`
126
+ 3. 3rd block with chords+lyrics → `bridge`
127
+ 4. Subsequent lyric-only blocks → `verse` (inherits first verse's chords)
128
+ 5. `LABEL:` with body → named section (e.g., `prechorus`)
129
+ 6. `LABEL: expression` → directive (e.g., `instrumental`, `fill`)
130
+ 7. `LABEL` or `LABEL*N` → section reference / repeat
131
+
132
+ ### Expression Grammar
133
+
134
+ ```
135
+ Expression = Sequence
136
+ Sequence = Item (',' Item)*
137
+ Item = Atom ('*' Number)?
138
+ Atom = SectionRef | ChordList | '(' Sequence ')'
139
+ ```
140
+
141
+ Examples: `(VERSE, CHORUS*2)`, `(D G D A)*4`, `D G D A D`
142
+
143
+ ## Future Features
144
+
145
+ ### Percentage-based timing (`%`)
146
+ Support `%` notation for specifying timing or duration within a measure:
147
+
148
+ ```
149
+ G%50 C%50 — split measure: 50% G, 50% C
150
+ D%75 G%25 — 3/4 D, 1/4 G
151
+ ```
152
+
153
+ This would add a `duration` or `percent` field to chord objects:
154
+ ```js
155
+ { root: 'G', type: '', percent: 50 }
156
+ ```
157
+
158
+ ### Slash chords
159
+ Parse slash chords like `G/B`, `Am/E`:
160
+ ```js
161
+ { root: 'G', type: '', bass: 'B' }
162
+ ```
163
+
164
+ ### Time signature support
165
+ ```
166
+ TIME: 3/4
167
+ ```
168
+ Stored as `song.timeSignature = { beats: 3, value: 4 }`.
169
+
170
+ ### Key detection
171
+ Auto-detect the song's key from its chord progression. Useful for intelligent transposition suggestions.
172
+
173
+ ### Measure/bar grouping
174
+ Currently `|` markers are tracked by column position. Future: group chords into explicit measures with bar lines as structural delimiters.
175
+
176
+ ### Multi-voice / harmony lines
177
+ Support for parallel harmony notation — multiple chord lines mapped to the same lyric line.
178
+
179
+ ### Nashville number system output
180
+ Convert chord roots to Nashville numbers (1, 2, 3...) relative to the detected or specified key.
package/README.md CHANGED
@@ -1,2 +1,137 @@
1
1
  # songsheet
2
- A songsheet interpreter
2
+
3
+ A zero-dependency songsheet parser and transposer. Parses plaintext songsheet files into a structured AST with chord-lyric character alignment.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install songsheet
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```js
14
+ import { parse, transpose } from 'songsheet'
15
+
16
+ const song = parse(rawText) // synchronous, returns plain object
17
+ const songInA = transpose(song, 2) // up 2 semitones
18
+ const songInF = transpose(song, -2) // down 2 semitones
19
+ const songInBb = transpose(song, 3, { preferFlats: true })
20
+ ```
21
+
22
+ ## Songsheet Format
23
+
24
+ ```
25
+ SONG TITLE - AUTHOR NAME
26
+
27
+ G F
28
+ Lyrics aligned under chords...
29
+ C G
30
+ More lyrics here
31
+
32
+ F C D
33
+ Chorus lyrics...
34
+
35
+ Verse lyrics without chords (inherits first verse's chord pattern)
36
+
37
+ CHORUS
38
+ CHORUS*2
39
+
40
+ PRECHORUS:
41
+ D
42
+ Labeled section with chords...
43
+
44
+ INSTRUMENTAL: (VERSE, CHORUS*2)
45
+ FILL: D G D A D
46
+
47
+ BRIDGE
48
+ ```
49
+
50
+ ### Section Type Inference
51
+
52
+ 1. 1st block with chords+lyrics → `verse`
53
+ 2. 2nd block with chords+lyrics → `chorus`
54
+ 3. 3rd block with chords+lyrics → `bridge`
55
+ 4. Subsequent lyric-only blocks → `verse` (inherits first verse's chords)
56
+ 5. `LABEL:` with body → named section (e.g., `prechorus`)
57
+ 6. `LABEL: expression` → directive (e.g., `instrumental`, `fill`)
58
+ 7. `LABEL` or `LABEL*N` → section reference / repeat
59
+
60
+ ### Expression Grammar
61
+
62
+ ```
63
+ Expression = Sequence
64
+ Sequence = Item (',' Item)*
65
+ Item = Atom ('*' Number)?
66
+ Atom = SectionRef | ChordList | '(' Sequence ')'
67
+ ```
68
+
69
+ Examples: `(VERSE, CHORUS*2)`, `(D G D A)*4`, `D G D A D`
70
+
71
+ ## AST Shape
72
+
73
+ `parse()` returns:
74
+
75
+ ```js
76
+ {
77
+ title: 'SONG TITLE',
78
+ author: 'AUTHOR NAME',
79
+
80
+ // Unique section definitions
81
+ sections: {
82
+ verse: {
83
+ count: 4,
84
+ chords: [{ root: 'G', type: '' }, { root: 'F', type: '' }, ...],
85
+ lyrics: ['lyric line 1', ...],
86
+ lines: [
87
+ {
88
+ chords: [{ root: 'G', type: '', column: 0 }, ...],
89
+ barLines: [], // column positions of | markers
90
+ lyrics: 'lyric line 1',
91
+ characters: [
92
+ { character: 'B', chord: { root: 'G', type: '' } },
93
+ { character: 'l' },
94
+ ...
95
+ ]
96
+ }
97
+ ]
98
+ },
99
+ chorus: { ... },
100
+ },
101
+
102
+ // Ordered playback structure
103
+ structure: [
104
+ {
105
+ sectionType: 'verse',
106
+ sectionIndex: 0,
107
+ chords: [...],
108
+ lyrics: [...],
109
+ lines: [...],
110
+ expression: null, // non-null on directive entries
111
+ },
112
+ ...
113
+ ]
114
+ }
115
+ ```
116
+
117
+ ## Transposition
118
+
119
+ `transpose()` deep-walks the AST and replaces every chord root. It auto-detects whether the song uses flats or sharps, or you can override with `{ preferFlats: true }`.
120
+
121
+ ```js
122
+ const song = parse(rawText)
123
+ const up2 = transpose(song, 2) // G → A
124
+ const down3 = transpose(song, -3, { preferFlats: true }) // G → Eb
125
+ ```
126
+
127
+ ## Development
128
+
129
+ ```bash
130
+ npm test # vitest run — all tests
131
+ npx vitest # watch mode
132
+ npx vitest run test/parser.test.js # single file
133
+ ```
134
+
135
+ ## License
136
+
137
+ ISC
package/index.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ export interface Chord {
2
+ root: string
3
+ type: string
4
+ }
5
+
6
+ export interface PositionedChord extends Chord {
7
+ column: number
8
+ }
9
+
10
+ export interface Character {
11
+ character: string
12
+ chord?: Chord
13
+ barLine?: true
14
+ }
15
+
16
+ export interface Line {
17
+ chords: PositionedChord[]
18
+ barLines: number[]
19
+ lyrics: string
20
+ characters: Character[]
21
+ }
22
+
23
+ export type Expression =
24
+ | { type: 'section_ref'; name: string }
25
+ | { type: 'chord_list'; chords: Chord[] }
26
+ | { type: 'sequence'; items: Expression[] }
27
+ | { type: 'repeat'; body: Expression; count: number }
28
+
29
+ export interface Section {
30
+ count: number
31
+ chords: Chord[]
32
+ lyrics: string[]
33
+ lines: Line[]
34
+ }
35
+
36
+ export interface StructureEntry {
37
+ sectionType: string
38
+ sectionIndex: number
39
+ chords: Chord[]
40
+ lyrics: string[]
41
+ lines: Line[]
42
+ expression: Expression | null
43
+ }
44
+
45
+ export interface Song {
46
+ title: string
47
+ author: string
48
+ bpm: number | null
49
+ sections: Record<string, Section>
50
+ structure: StructureEntry[]
51
+ }
52
+
53
+ export function parse(raw: string): Song
54
+ export function transpose(song: Song, semitones: number, options?: { preferFlats?: boolean }): Song
package/index.js CHANGED
@@ -1,214 +1,2 @@
1
- const { Map, List, fromJS } = require('immutable')
2
-
3
- const isCaps = string => string.toUpperCase() === string
4
-
5
- const chordRegExp = /(([A-G]{1}[b#]{0,1})([\w+]*))/g // http://regexr.com/3eesm
6
- const wordRegExp = /([A-Za-z#\+7])+/g // http://regexr.com/3eep7
7
-
8
- const isChordLine = string => string.match(chordRegExp) && string.match(wordRegExp)
9
- ? string.match(chordRegExp).length === string.match(wordRegExp).length && !string.match(/:/)
10
- : false
11
-
12
- const multiplyRegExp = /([A-Z]*|\((.*)\))\*([0-9])/ // http://regexr.com/3eero
13
-
14
- const multiplyExpand = line => {
15
- const multiplyExecResults = multiplyRegExp.exec(line)
16
- return multiplyExecResults
17
- ? line.replace(multiplyRegExp,
18
- `${multiplyExecResults[2] || multiplyExecResults[1]} `
19
- .repeat(multiplyRegExp.exec(line)[3]))
20
- .replace(/[\(\)]/g, '')
21
- .slice(0, -1)
22
- : line.replace(/[\(\)]/g, '')
23
- }
24
-
25
- const notSectionHeader = (lines) => !(lines.length === 1 && isCaps(lines[0]))
26
-
27
- const getLyrics = ({ rawSection }) => {
28
- const lines = rawSection.split('\n')
29
-
30
- return notSectionHeader(lines)
31
- ? lines.reduce((lyrics, line) => {
32
- if (!isChordLine(line) && !isCaps(line)) {
33
- lyrics.push(line)
34
- }
35
- return lyrics
36
- }, [])
37
- : []
38
- }
39
-
40
- const getChordLines = ({ rawSection }) => {
41
- const lines = rawSection.split('\n')
42
-
43
- return notSectionHeader(lines)
44
- ? lines.reduce((chords, line) => {
45
- if (isChordLine(line)) {
46
- chords.push(line)
47
- }
48
- return chords
49
- }, [])
50
- : []
51
- }
52
-
53
- const getChords = ({ rawSection, song }) => {
54
- const rawSectionLine = /.*[A-Z]:/.test(rawSection)
55
- ? rawSection.match(/.*[A-Z]:(.*)/)[1]
56
- : false
57
-
58
- return rawSectionLine
59
- ? multiplyExpand(rawSectionLine)
60
- .match(wordRegExp)
61
- .map(word => {
62
- const section = song.get('sections').get(word.toLowerCase())
63
- return section
64
- ? section.get('chords').toArray()
65
- : word
66
- })
67
- .reduce((a, b) => a.concat(b), []) // flatten the array
68
- : getChordLines({ rawSection }).reduce((chords, line) => {
69
- let match
70
- while ( ( match = chordRegExp.exec(line) ) != null ) {
71
- const [,, root, type] = match
72
- chords.push({ root, type })
73
- }
74
- return chords
75
- }, [])
76
- }
77
-
78
- const eachCharacterWithChordLine = ({ chordLine }) => (character, index) => {
79
- const chordCheck = chordLine.charAt(index).match(/[A-G]/)
80
-
81
- const chord = chordCheck
82
- ? chordLine.slice(index).match(/[^ ]*/)[0]
83
- : ''
84
-
85
- return chordCheck
86
- ? { chord, character }
87
- : { character }
88
- }
89
-
90
- const eachLyricLineWithChords = ({ chordLines }) => (lyricLine, i) => {
91
- const chordLine = chordLines[i]
92
-
93
- return chordLine
94
- ? lyricLine
95
- .match(/(.)/g)
96
- .map(eachCharacterWithChordLine({ chordLine }))
97
- : []
98
- }
99
-
100
- const getLyricLineCharacters = ({ rawSection }) => {
101
- const lyrics = getLyrics({ rawSection })
102
- const chordLines = getChordLines({ rawSection })
103
- return lyrics && chordLines
104
- ? lyrics.map(eachLyricLineWithChords({ chordLines }))
105
- : []
106
- }
107
-
108
- const getSectionType = ({ rawSection, lyrics, chords, song }) => {
109
- // (1st, 2nd, 3rd, and all remaining ordering from cascading logic of !chorus, !bridge, verses.length >= 1)
110
-
111
- const sections = song.get('sections')
112
- const verse = sections.get('verse')
113
- const chorus = sections.get('chorus')
114
- const bridge = sections.get('bridge')
115
-
116
- // consider the 1st section with chords to be the verse
117
- if (!verse) {
118
- if (lyrics && chords.length > 0) {
119
- return ['verse']
120
- }
121
- // consider the 2nd section with chords to be the chorus
122
- } else if (verse.get('count') === 1 && lyrics.length > 0 && chords.length > 0 && !chorus) {
123
- return ['chorus']
124
- // consider the 3rd section with chords to be a bridge
125
- } else if (verse.get('count') >= 1 && lyrics.length > 0 && chords.length > 0 && chorus && !bridge) {
126
- return ['bridge']
127
- // consider all remaining sections with lyrics and no chords to be verses
128
- } else if (verse.get('count') >= 1 && lyrics.length > 0 && chords.length === 0 && chorus) {
129
- return ['verse']
130
- } else {
131
- // if LABEL: - defines and gives a section a label
132
- if (/.*[A-Z]:/.test(rawSection)) {
133
- const [, sectionTitle] = rawSection.match(/(.*[A-Z]):/)
134
- return [sectionTitle.toLowerCase()]
135
- }
136
-
137
- // CHORUS, CHORUS*N
138
- if (rawSection.substr(0, 6) === 'CHORUS') {
139
- if (rawSection.length === 6) return ['chorus']
140
- if (rawSection[6] === '*') {
141
- const repeat = rawSection.split('*')[1]
142
-
143
- const sectionTypes = []
144
- let i = 0
145
- while (i < repeat) {
146
- sectionTypes.push('chorus')
147
- i++
148
- }
149
-
150
- return sectionTypes
151
- }
152
- }
153
-
154
- // bridge is built in
155
- if (rawSection === 'BRIDGE') return ['bridge']
156
-
157
- // TODO: INSTRUMENTAL (BRIDGE, CHORUS*2)
158
- if (rawSection.split(' ')[0] === 'INSTRUMENTAL') return ['instrumental']
159
-
160
- // if LABEL matches on previously defined LABEL:
161
- if (rawSection.toLowerCase() && sections.get(rawSection.toLowerCase())) return [rawSection.toLowerCase()]
162
-
163
- // otherwise it's a verse
164
- if (lyrics && chords.length === 0) return ['verse']
165
- }
166
- }
167
-
168
- const parse = (rawSongsheet) => new Promise((resolve, reject) => {
169
- const rawSections = rawSongsheet.replace('\r\n', '\n').replace(/^\s*\n/gm, '\n').split('\n\n')
170
- resolve(rawSections.reduce((song, rawSection, structureIndex) => {
171
- const presentLyrics = getLyrics({ rawSection })
172
- const presentChords = getChords({ rawSection, song })
173
- const presentLyricLineCharacters = getLyricLineCharacters({ rawSection })
174
-
175
- const sectionTypes = getSectionType({ rawSection, lyrics: presentLyrics, chords: presentChords, song }) || []
176
-
177
- return sectionTypes.reduce((song, sectionType) => {
178
- const section = song.get('sections').get(sectionType)
179
-
180
- const lyrics = presentLyrics.length === 0 && section && section.get('lyrics')
181
- ? section.get('lyrics')
182
- : fromJS(presentLyrics)
183
-
184
- const chords = presentChords.length === 0 && section && section.get('chords')
185
- ? section.get('chords')
186
- : fromJS(presentChords)
187
-
188
- const lyricLineCharacters = presentLyrics.length === 0 && section && section.get('lyricLineCharacters')
189
- ? section.get('lyricLineCharacters')
190
- : fromJS(presentLyricLineCharacters)
191
-
192
- return (!section
193
- ? song.mergeDeepIn(['sections', sectionType], { lyrics, chords, lyricLineCharacters, count: 0 })
194
- : song)
195
- .updateIn(['sections', sectionType, 'count'], count => count + 1)
196
- .updateIn(['structure'], structure => structure.push(Map({ sectionType, lyrics, chords, lyricLineCharacters, sectionIndex: song.getIn(['sections', sectionType, 'count']) || 0 })))
197
- }, song)
198
- }, Map({
199
- title: rawSections[0].split(' - ')[0],
200
- author: rawSections[0].split(' - ')[1],
201
- structure: List(),
202
- sections: Map({}),
203
- rawSongsheet
204
- })))
205
- })
206
-
207
- module.exports = () => ({
208
- parse,
209
- chordRegExp,
210
- wordRegExp,
211
- isChordLine,
212
- multiplyRegExp,
213
- multiplyExpand
214
- })
1
+ export { parse } from './src/parser.js'
2
+ export { transpose } from './src/transpose.js'
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "songsheet",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "A songsheet interpreter",
5
- "main": "index.js",
5
+ "type": "module",
6
+ "exports": "./index.js",
6
7
  "scripts": {
7
- "test": "node test.js"
8
+ "test": "vitest run"
8
9
  },
9
10
  "repository": {
10
11
  "type": "git",
@@ -24,12 +25,6 @@
24
25
  },
25
26
  "homepage": "https://github.com/williamcotton/songsheet#readme",
26
27
  "devDependencies": {
27
- "tape": "^4.5.1",
28
- "fs-promise": "^0.5.0"
29
- },
30
- "dependencies": {
31
- "immutable": "^3.8.1",
32
- "react-addons-pure-render-mixin": "^15.3.2",
33
- "tokenizer": "^1.1.2"
28
+ "vitest": "^3.0.0"
34
29
  }
35
30
  }
@@ -1,10 +1,11 @@
1
1
  SPENT SOME TIME IN BUFFALO - WILLIE COTTON
2
+ (100 BPM)
2
3
 
3
- D
4
+ D |
4
5
  Sweating bullets in the morning
5
- G
6
+ G |
6
7
  The same old rhyme, the same old reason
7
- A C D
8
+ A C D |
8
9
  Had to get our fill of drinking up the last warm night of this here summer season
9
10
 
10
11
  Riding shotgun down to Dansville
@@ -12,16 +13,19 @@ G
12
13
  Pretty soon this truck'll be kicking up rock salt in everybody's front lawn
13
14
 
14
15
  PRECHORUS:
15
- D
16
+ D |
16
17
  Spent some time in Buffalo
18
+ D |
17
19
  Dug my life out of the snow
20
+ D |
18
21
  And when those lake winds start to blow
22
+ D |
19
23
  I've had my fill it's time to go
20
24
 
21
25
  CHORUS:
22
- C G D
26
+ C G D |
23
27
  Head out west to find the good life but I'm thinking of you now
24
- C G D
28
+ C G D |
25
29
  Can't pay the rent, packing up my tent, picking up my money, I'm headed home
26
30
 
27
31
  Just like Jimmie nobody wants me