All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
- Sidebar: dynamic brand-accent colors, brainstorm section restyled - AI chat general: popup panel with expand/collapse, hides when contextual AI open - AI chat contextual: tabs reordered (Actions first), X close button, height fix - Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.) - Global color cleanup: emerald/orange hardcoded → brand-accent dynamic - Brainstorm page: orange → brand-accent throughout - PageEntry animation component added to key pages - Floating AI button: bg-brand-accent instead of hardcoded black - i18n: all 15 locales updated with new AI/billing keys - Billing: freemium quota tracking, BYOK, stripe subscription scaffolding - Admin: integrated into new design - AGENTS.md + CLAUDE.md project rules added
226 lines
7.9 KiB
Python
226 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
# /// script
|
|
# requires-python = ">=3.10"
|
|
# dependencies = ["pytest>=7.0"]
|
|
# ///
|
|
"""Tests for syllable-counter.py"""
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
SCRIPT = str(Path(__file__).parent.parent / "syllable-counter.py")
|
|
|
|
|
|
def run_script(*args):
|
|
"""Run the script and return parsed JSON output."""
|
|
result = subprocess.run(
|
|
[sys.executable, SCRIPT, *args],
|
|
capture_output=True, text=True
|
|
)
|
|
return json.loads(result.stdout) if result.stdout else None, result.returncode
|
|
|
|
|
|
# Also test the count_syllables function directly
|
|
import importlib.util
|
|
_spec = importlib.util.spec_from_file_location("syllable_counter", Path(__file__).parent.parent / "syllable-counter.py")
|
|
_mod = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(_mod)
|
|
count_syllables = _mod.count_syllables
|
|
estimate_duration = _mod.estimate_duration
|
|
format_duration_range = _mod.format_duration_range
|
|
|
|
|
|
class TestSyllableCounting:
|
|
"""Test individual word syllable counting."""
|
|
|
|
def test_one_syllable_words(self):
|
|
for word in ["cat", "dog", "the", "run", "light", "dream"]:
|
|
assert count_syllables(word) == 1, f"Expected 1 syllable for '{word}', got {count_syllables(word)}"
|
|
|
|
def test_two_syllable_words(self):
|
|
for word in ["hello", "window", "walking", "morning", "shadow"]:
|
|
assert count_syllables(word) == 2, f"Expected 2 syllables for '{word}', got {count_syllables(word)}"
|
|
|
|
def test_three_syllable_words(self):
|
|
for word in ["beautiful", "another", "everyone", "different"]:
|
|
result = count_syllables(word)
|
|
assert result == 3, f"Expected 3 syllables for '{word}', got {result}"
|
|
|
|
def test_contractions(self):
|
|
assert count_syllables("I'm") == 1
|
|
assert count_syllables("don't") == 1
|
|
assert count_syllables("couldn't") == 2
|
|
|
|
def test_empty_string(self):
|
|
assert count_syllables("") == 0
|
|
|
|
|
|
class TestLyricsAnalysis:
|
|
"""Test full lyrics analysis via the script."""
|
|
|
|
def test_basic_analysis(self):
|
|
lyrics = (
|
|
"[Verse 1]\n"
|
|
"Walking through the morning light\n"
|
|
"Counting shadows on the wall\n"
|
|
)
|
|
report, code = run_script("--text", lyrics)
|
|
assert report is not None
|
|
assert report["script"] == "syllable-counter"
|
|
assert report["metrics"]["total_lyric_lines"] == 2
|
|
assert report["metrics"]["total_syllables"] > 0
|
|
|
|
def test_section_grouping(self):
|
|
lyrics = (
|
|
"[Verse 1]\n"
|
|
"Short line here\n"
|
|
"Another short one\n"
|
|
"\n"
|
|
"[Chorus]\n"
|
|
"The chorus comes in strong and bold\n"
|
|
"With longer lines that carry more weight\n"
|
|
)
|
|
report, code = run_script("--text", lyrics)
|
|
assert report is not None
|
|
assert len(report["section_analysis"]) == 2
|
|
section_names = [s["section"] for s in report["section_analysis"]]
|
|
assert "[Verse 1]" in section_names
|
|
assert "[Chorus]" in section_names
|
|
|
|
def test_line_data_includes_syllables(self):
|
|
lyrics = "[Verse 1]\nHello world\n"
|
|
report, code = run_script("--text", lyrics)
|
|
assert report is not None
|
|
assert len(report["line_data"]) == 1
|
|
assert "syllables" in report["line_data"][0]
|
|
assert report["line_data"][0]["syllables"] > 0
|
|
|
|
def test_skips_metatags(self):
|
|
lyrics = "[Mood: haunting]\n[Verse 1]\nWalking through fog\n"
|
|
report, code = run_script("--text", lyrics)
|
|
assert report is not None
|
|
# Only the lyric line should be counted, not metatags
|
|
assert report["metrics"]["total_lyric_lines"] == 1
|
|
|
|
def test_high_variance_warning(self):
|
|
lyrics = (
|
|
"[Verse 1]\n"
|
|
"Hi\n"
|
|
"This is a much longer line with many more syllables than the first\n"
|
|
"Short\n"
|
|
"Another really long line that goes on and on and on\n"
|
|
)
|
|
report, code = run_script("--text", lyrics)
|
|
assert report is not None
|
|
# Should flag high syllable variance
|
|
issues = [f["issue"] for f in report["findings"]]
|
|
assert any("variance" in i.lower() or "syllable" in i.lower() for i in issues)
|
|
|
|
def test_report_structure(self):
|
|
lyrics = "[Verse 1]\nA simple test line\n"
|
|
report, code = run_script("--text", lyrics)
|
|
assert report is not None
|
|
assert "script" in report
|
|
assert "version" in report
|
|
assert "timestamp" in report
|
|
assert "status" in report
|
|
assert "metrics" in report
|
|
assert "line_data" in report
|
|
assert "section_analysis" in report
|
|
assert "findings" in report
|
|
assert "summary" in report
|
|
|
|
def test_help_flag(self):
|
|
result = subprocess.run(
|
|
[sys.executable, SCRIPT, "--help"],
|
|
capture_output=True, text=True
|
|
)
|
|
assert result.returncode == 0
|
|
assert "syllable" in result.stdout.lower()
|
|
|
|
|
|
class TestDurationEstimation:
|
|
"""Test duration estimation function."""
|
|
|
|
def test_zero_lines(self):
|
|
min_s, max_s = estimate_duration(0, 0)
|
|
assert min_s == 0
|
|
assert max_s == 0
|
|
|
|
def test_one_line(self):
|
|
# 7.0 avg syllables = mid range (3.0-4.5 secs/line)
|
|
min_s, max_s = estimate_duration(1, 7.0)
|
|
assert min_s == round(1 * 3.5) # low-density range
|
|
assert max_s == round(1 * 5.5)
|
|
|
|
def test_typical_song(self):
|
|
# 20 lines at 7.0 avg syllables (mid range)
|
|
min_s, max_s = estimate_duration(20, 7.0)
|
|
assert min_s == round(20 * 3.5) # 70
|
|
assert max_s == round(20 * 5.5) # 110
|
|
|
|
def test_high_density_faster(self):
|
|
# High syllable density = faster delivery = less time per line
|
|
min_s, max_s = estimate_duration(20, 12.0)
|
|
assert min_s == round(20 * 2.5) # 50
|
|
assert max_s == round(20 * 4.0) # 80
|
|
|
|
def test_instrumental_sections_add_time(self):
|
|
# Sections with instrumental tags add time
|
|
sections = [
|
|
{"name": "[Intro]", "lines": []},
|
|
{"name": "[Verse]", "lines": [{"syllables": 7}] * 4},
|
|
{"name": "[Guitar Solo]", "lines": []},
|
|
{"name": "[Outro]", "lines": []},
|
|
]
|
|
min_s, max_s = estimate_duration(4, 7.0, sections)
|
|
# 4 lines at mid range + intro (5-15) + guitar solo (10-25) + outro (8-20)
|
|
assert min_s > round(4 * 3.5) # More than just lyrics
|
|
assert max_s > round(4 * 5.5)
|
|
|
|
def test_formatted_range(self):
|
|
formatted = format_duration_range(50, 90)
|
|
assert formatted == "0:50-1:30"
|
|
|
|
def test_formatted_range_zero(self):
|
|
formatted = format_duration_range(0, 0)
|
|
assert formatted == "0:00-0:00"
|
|
|
|
def test_formatted_range_large(self):
|
|
formatted = format_duration_range(120, 240)
|
|
assert formatted == "2:00-4:00"
|
|
|
|
def test_duration_in_report(self):
|
|
lyrics = (
|
|
"[Verse 1]\n"
|
|
"Walking through the morning light\n"
|
|
"Counting shadows on the wall\n"
|
|
"\n"
|
|
"[Chorus]\n"
|
|
"Come undone come undone\n"
|
|
"Let the weight fall where it may\n"
|
|
)
|
|
report, code = run_script("--text", lyrics)
|
|
assert report is not None
|
|
duration = report["metrics"]["estimated_duration"]
|
|
assert "min_seconds" in duration
|
|
assert "max_seconds" in duration
|
|
assert "formatted" in duration
|
|
assert duration["min_seconds"] > 0
|
|
assert duration["max_seconds"] > duration["min_seconds"]
|
|
# Check formatted string pattern M:SS-M:SS
|
|
assert "-" in duration["formatted"]
|
|
|
|
def test_estimate_duration_flag(self):
|
|
lyrics = "[Verse 1]\nHello world\n"
|
|
report, code = run_script("--text", lyrics, "--estimate-duration")
|
|
assert report is not None
|
|
assert "estimated_duration" in report["metrics"]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import pytest
|
|
pytest.main([__file__, "-v"])
|