Skip to content

Song search: forgiving, multilingual guess matching - #12

Merged
Privex-chat merged 2 commits into
codex/rollback-d5f02f1-working-statefrom
stage-b/song-search
Jul 11, 2026
Merged

Song search: forgiving, multilingual guess matching#12
Privex-chat merged 2 commits into
codex/rollback-d5f02f1-working-statefrom
stage-b/song-search

Conversation

@Privex-chat

@Privex-chat Privex-chat commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Fixes your >one - greater than one example and multilingual titles (Hindi, Spanish, Japanese, Korean).

Problem

The guess dropdown matched raw lowercased strings, so special characters and accents broke discovery — typing one greater than one never surfaced >one - greater than one, and accented/non-latin titles were hard to find.

Fix (frontend-only)

New lib/search.js:

  • normalizeText — lowercase, fold Latin accents (café→cafe), drop punctuation/symbols (>oneone), collapse whitespace. Unicode-aware: it decomposes, strips only the Latin accent range, then recomposes and keeps combining marks, so non-latin scripts survive intact rather than getting mangled (I caught NFKD turning Japanese パ→ハ and gutting Devanagari matras during testing — both fixed).
  • matchesQuery — normalized substring, or all query tokens present in any order.

The dropdown index and filter now run through these.

Not made too lenient

Song correctness is unchanged — still an exact track-id match (you pick the actual song). This only improves surfacing candidates in the dropdown, never scoring. splitArtists/matchesArtist are included and unit-tested for the next PR (artist mode) but not wired into correctness here.

Verified

14 Jest unit tests (npm test) covering the >one case, accent folding, Japanese/Korean/Hindi preservation, token-order surfacing, and artist split/match. Production build compiles clean. One file changed + one new lib + its test; zero backend/contract changes.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved search and guess matching with accent-insensitive, punctuation-tolerant text handling.
    • Search now supports multi-word queries in any order.
    • Artist matching recognizes collaborators and common credit separators.
    • Improved support for non-Latin scripts.
  • Bug Fixes

    • Reduced false negatives when searching for artists or tracks with accents, symbols, or varied formatting.

Typing 'one greater than one' now surfaces '>one - greater than one', and
accented/other-language titles match too. Adds lib/search.js:
- normalizeText: lowercase, fold Latin accents, drop punctuation/symbols,
  collapse whitespace — Unicode-aware. Decomposes → strips only the Latin
  accent range → recomposes, and preserves combining marks (category M), so
  Japanese dakuten (パ) and Devanagari matras (होने) survive instead of being
  mangled/gutted.
- matchesQuery: normalized substring OR all-tokens-present surfacing.

GamePage's guess dropdown builds its index and filters through these. Song
correctness is unchanged (still by track id — the dropdown only surfaces
candidates), so this is purely better discovery, not looser scoring.

splitArtists/matchesArtist are included for the upcoming artist-mode work but
not yet wired into correctness here. 14 unit tests (npm test).

Frontend-only; no API contract change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
audyn Ready Ready Preview, Comment Jul 11, 2026 12:25pm

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1bcc82ea-cccc-4887-9763-056924f9b5ac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch stage-b/song-search

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/lib/search.js`:
- Around line 37-61: Update ARTIST_SEP so alphabetic collaboration tokens only
match as standalone words, using word-boundary or equivalent whitespace-aware
constraints while preserving punctuation separators and case-insensitive
matching. Keep splitArtists and matchesArtist behavior unchanged otherwise, and
add regression coverage for names such as “Daft Punk,” “Bill Withers,” and
“Xscape” so they remain single artists.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 449f392b-990e-47d7-8355-533477007ea4

📥 Commits

Reviewing files that changed from the base of the PR and between bb3eecc and 008c275.

📒 Files selected for processing (3)
  • frontend/src/lib/search.js
  • frontend/src/lib/search.test.js
  • frontend/src/pages/GamePage.js

Comment on lines +37 to +61
// Separators Spotify (and people) use between collaborating artists.
const ARTIST_SEP = /\s*(?:,|&|\/|\+|feat\.?|ft\.?|featuring|with|x|×)\s*/gi;

// Split a raw artist field into individual normalized artist names.
// "Drake, 21 Savage" → ["drake", "21 savage"]; "A feat. B" → ["a", "b"].
export function splitArtists(rawArtist) {
if (!rawArtist) return [];
const seen = new Set();
const out = [];
for (const part of rawArtist.split(ARTIST_SEP)) {
const n = normalizeText(part);
if (n && !seen.has(n)) { seen.add(n); out.push(n); }
}
return out;
}

// Artist correctness: the guess names ANY credited artist on the track.
export function matchesArtist(guess, trackArtist) {
const g = normalizeText(guess);
if (!g) return false;
const artists = splitArtists(trackArtist);
// exact per-artist match, plus the whole normalized field (covers a track
// credited as a single "A & B" act the player types in full).
return artists.includes(g) || normalizeText(trackArtist) === g;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

ARTIST_SEP splits inside ordinary words — will corrupt real artist names once wired up.

None of the alphabetic separator alternatives (feat, ft, featuring, with, x) are word-boundary anchored, and \s* permits a zero-width match on either side. So these tokens are also matched as bare substrings inside unrelated words:

  • "Daft Punk" contains "ft" → splits into ["da", "punk"]
  • "Bill Withers" contains "with" as a prefix → splits into ["", "ers"]
  • "Xscape" starts with "x" → splits into ["", "scape"]

This breaks splitArtists/matchesArtist for these (and similar) artists. The PR notes these helpers aren't wired into correctness yet, but the bug already violates the tested contract and will surface the moment this is connected. Also consider adding regression tests for these substring cases alongside the fix.

🐛 Proposed fix using word boundaries
-const ARTIST_SEP = /\s*(?:,|&|\/|\+|feat\.?|ft\.?|featuring|with|x|×)\s*/gi;
+// \b is placed before the optional trailing "." (rather than after) because
+// \b cannot match between two non-word characters (e.g. "." followed by a space).
+const ARTIST_SEP = /\s*(?:,|&|\/|\+|×|\bfeat\b\.?|\bft\b\.?|\bfeaturing\b|\bwith\b|\bx\b)\s*/gi;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Separators Spotify (and people) use between collaborating artists.
const ARTIST_SEP = /\s*(?:,|&|\/|\+|feat\.?|ft\.?|featuring|with|x|×)\s*/gi;
// Split a raw artist field into individual normalized artist names.
// "Drake, 21 Savage" → ["drake", "21 savage"]; "A feat. B" → ["a", "b"].
export function splitArtists(rawArtist) {
if (!rawArtist) return [];
const seen = new Set();
const out = [];
for (const part of rawArtist.split(ARTIST_SEP)) {
const n = normalizeText(part);
if (n && !seen.has(n)) { seen.add(n); out.push(n); }
}
return out;
}
// Artist correctness: the guess names ANY credited artist on the track.
export function matchesArtist(guess, trackArtist) {
const g = normalizeText(guess);
if (!g) return false;
const artists = splitArtists(trackArtist);
// exact per-artist match, plus the whole normalized field (covers a track
// credited as a single "A & B" act the player types in full).
return artists.includes(g) || normalizeText(trackArtist) === g;
}
// Separators Spotify (and people) use between collaborating artists.
// \b is placed before the optional trailing "." (rather than after) because
// \b cannot match between two non-word characters (e.g. "." followed by a space).
const ARTIST_SEP = /\s*(?:,|&|\/|\+|×|\bfeat\b\.?|\bft\b\.?|\bfeaturing\b|\bwith\b|\bx\b)\s*/gi;
// Split a raw artist field into individual normalized artist names.
// "Drake, 21 Savage" → ["drake", "21 savage"]; "A feat. B" → ["a", "b"].
export function splitArtists(rawArtist) {
if (!rawArtist) return [];
const seen = new Set();
const out = [];
for (const part of rawArtist.split(ARTIST_SEP)) {
const n = normalizeText(part);
if (n && !seen.has(n)) { seen.add(n); out.push(n); }
}
return out;
}
// Artist correctness: the guess names ANY credited artist on the track.
export function matchesArtist(guess, trackArtist) {
const g = normalizeText(guess);
if (!g) return false;
const artists = splitArtists(trackArtist);
// exact per-artist match, plus the whole normalized field (covers a track
// credited as a single "A & B" act the player types in full).
return artists.includes(g) || normalizeText(trackArtist) === g;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/lib/search.js` around lines 37 - 61, Update ARTIST_SEP so
alphabetic collaboration tokens only match as standalone words, using
word-boundary or equivalent whitespace-aware constraints while preserving
punctuation separators and case-insensitive matching. Keep splitArtists and
matchesArtist behavior unchanged otherwise, and add regression coverage for
names such as “Daft Punk,” “Bill Withers,” and “Xscape” so they remain single
artists.

feat/ft/with/x were matching as substrings inside artist names
(Daft Punk, Bill Withers, Xscape), splitting them incorrectly.
@Privex-chat
Privex-chat merged commit 271208b into codex/rollback-d5f02f1-working-state Jul 11, 2026
3 checks passed
Privex-chat added a commit that referenced this pull request Jul 11, 2026
Song search: forgiving, multilingual guess matching
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant