Skip to content

Commit 0f191f9

Browse files
authored
fix(freesound_music): report the sound's actual license and author (OpenMontageInc#839)
`_search` never asked the Freesound API for the `license` field, so `execute` had nothing to read and hardcoded the generic string "Creative Commons (check individual sound license)" instead. Every Freesound sound carries its own license (CC0, CC BY, CC BY-NC...), and whether attribution is required or the track may be used commercially depends on it, so the agent had no way to decide. The author was only reachable by parsing `freesound_url`. - add `license` to the requested `fields` - return `sound["license"]`, falling back to the generic string when a (partial) response omits it, so behaviour degrades rather than handing the agent a bare `None` - surface `author` as a first-class result field The fallback string moves to a `_GENERIC_LICENSE` class constant so the test and the implementation cannot drift apart. Closes OpenMontageInc#736 Co-authored-by: Amy Zhao <pseudodream@users.noreply.github.com>
1 parent cf22f15 commit 0f191f9

2 files changed

Lines changed: 122 additions & 2 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""``freesound_music`` must report the actual license of the chosen sound (#736).
2+
3+
Every Freesound sound carries its own license (CC0, CC BY, CC BY-NC...) and the
4+
agent reading the result needs to know it: whether attribution is required, and
5+
whether the track may be used commercially, both depend on it. Returning the
6+
generic string "Creative Commons (check individual sound license)" forced the
7+
agent to guess, and left the author reachable only by parsing ``freesound_url``.
8+
9+
Two things have to hold, and they are separate failure modes:
10+
11+
1. ``_search`` must *ask* the API for ``license``. Without it the field is
12+
absent from the payload and every downstream fix is dead code.
13+
2. ``execute`` must *surface* it, falling back to the generic string when the
14+
API omits it, so partial responses keep the previous behaviour rather than
15+
handing the agent a bare ``None``.
16+
17+
The registry is not exercised here; the tool is instantiated directly because
18+
the assertion is about this tool's contract, not about discovery.
19+
"""
20+
21+
from __future__ import annotations
22+
23+
import json
24+
import urllib.parse
25+
26+
import pytest
27+
28+
from tools.audio.freesound_music import FreesoundMusic
29+
30+
GENERIC = "Creative Commons (check individual sound license)"
31+
LICENSE = "http://creativecommons.org/licenses/by/4.0/"
32+
33+
34+
def _sound(**overrides):
35+
sound = {
36+
"id": 424242,
37+
"name": "ambient drone",
38+
"duration": 72.5,
39+
"avg_rating": 4.2,
40+
"tags": ["ambient", "drone"],
41+
"username": "soundauthor",
42+
"license": LICENSE,
43+
"previews": {"preview-hq-mp3": "https://example.invalid/preview.mp3"},
44+
}
45+
sound.update(overrides)
46+
return sound
47+
48+
49+
class _FakeResponse:
50+
"""Minimal stand-in for the object returned by ``urlopen``."""
51+
52+
def __init__(self, payload: bytes) -> None:
53+
self._payload = payload
54+
55+
def __enter__(self) -> _FakeResponse:
56+
return self
57+
58+
def __exit__(self, *exc) -> None:
59+
return None
60+
61+
def read(self) -> bytes:
62+
return self._payload
63+
64+
65+
@pytest.fixture()
66+
def tool(monkeypatch, tmp_path):
67+
monkeypatch.setenv("FREESOUND_API_KEY", "test-key")
68+
instance = FreesoundMusic()
69+
monkeypatch.setattr(
70+
instance, "_download", lambda sound, inputs, api_key: tmp_path / "out.mp3"
71+
)
72+
return instance
73+
74+
75+
def _run(monkeypatch, tool, sound, query="ambient"):
76+
"""A distinct query per test keeps calls independent of the asset cache."""
77+
monkeypatch.setattr(
78+
FreesoundMusic, "_search", lambda self, inputs, api_key: [sound]
79+
)
80+
return tool.execute({"query": query})
81+
82+
83+
def test_search_requests_the_license_field(monkeypatch, tool):
84+
"""Regression: `fields` omitted `license`, so it never came back."""
85+
captured: dict[str, str] = {}
86+
87+
def fake_urlopen(request, *args, **kwargs):
88+
captured["url"] = request.full_url
89+
return _FakeResponse(json.dumps({"results": []}).encode("utf-8"))
90+
91+
monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)
92+
tool._search({"query": "ambient", "min_duration": 30, "max_duration": 120}, "k")
93+
94+
query = urllib.parse.urlparse(captured["url"]).query
95+
fields = urllib.parse.parse_qs(query)["fields"][0]
96+
assert "license" in fields.split(",")
97+
98+
99+
def test_result_carries_the_sound_license(monkeypatch, tool):
100+
result = _run(monkeypatch, tool, _sound(), query="with-license")
101+
assert result.success is True
102+
assert result.data["license"] == LICENSE
103+
104+
105+
def test_falls_back_to_generic_when_license_is_absent(monkeypatch, tool):
106+
"""A payload without `license` must not regress to a bare None."""
107+
sound = _sound()
108+
del sound["license"]
109+
result = _run(monkeypatch, tool, sound, query="no-license")
110+
assert result.success is True
111+
assert result.data["license"] == GENERIC
112+
113+
114+
def test_author_is_surfaced_even_without_a_license(monkeypatch, tool):
115+
sound = _sound()
116+
del sound["license"]
117+
result = _run(monkeypatch, tool, sound, query="author-only")
118+
assert result.data["author"] == "soundauthor"

‎tools/audio/freesound_music.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ class FreesoundMusic(BaseTool):
108108
]
109109

110110
_BASE_URL = "https://freesound.org/apiv2"
111+
_GENERIC_LICENSE = "Creative Commons (check individual sound license)"
111112

112113
def get_status(self) -> ToolStatus:
113114
if os.environ.get("FREESOUND_API_KEY"):
@@ -163,7 +164,8 @@ def execute(self, inputs: dict[str, Any]) -> ToolResult:
163164
"query": inputs["query"],
164165
"output": str(output_path),
165166
"format": "mp3",
166-
"license": "Creative Commons (check individual sound license)",
167+
"license": sound.get("license") or self._GENERIC_LICENSE,
168+
"author": sound.get("username"),
167169
"freesound_url": f"https://freesound.org/people/{sound.get('username', '')}/sounds/{sound.get('id', '')}/",
168170
"results_found": len(search_result),
169171
},
@@ -182,7 +184,7 @@ def _search(self, inputs: dict[str, Any], api_key: str) -> list[dict]:
182184
"query": query,
183185
"filter": f"duration:[{min_dur} TO {max_dur}]",
184186
"sort": "rating_desc",
185-
"fields": "id,name,duration,previews,tags,avg_rating,username",
187+
"fields": "id,name,duration,previews,tags,avg_rating,username,license",
186188
"token": api_key,
187189
"page_size": 15,
188190
})

0 commit comments

Comments
 (0)