Skip to content

fix: prefer record url for audio downloads#7726

Open
bugkeep wants to merge 1 commit intoAstrBotDevs:masterfrom
bugkeep:codex/fix-record-url-fallback
Open

fix: prefer record url for audio downloads#7726
bugkeep wants to merge 1 commit intoAstrBotDevs:masterfrom
bugkeep:codex/fix-record-url-fallback

Conversation

@bugkeep
Copy link
Copy Markdown

@bugkeep bugkeep commented Apr 22, 2026

Summary

  • Prefer Record.url over Record.file when resolving audio records, matching the existing Image fallback behavior.
  • Download remote record URLs with the generic file downloader into temp audio files instead of treating them as images.
  • Cover NapCat-style payloads where file is only a filename but url points to the downloadable voice file.

Fixes #7686

Testing

  • uv run pytest tests/unit/test_message_record_component.py -q
  • uv run pytest tests/unit -q
  • uv run ruff format .
  • uv run ruff check .

Summary by Sourcery

Prefer record URLs over file names for resolving audio records and handle remote audio downloads via the generic file downloader.

Bug Fixes:

  • Fix audio record resolution when only the URL points to the downloadable voice file, including NapCat-style payloads where file is just a name.

Enhancements:

  • Add URL-based audio download helper that stores remote audio into temp files with appropriate extensions and reuses it for base64 conversion.
  • Align audio record handling with image behavior by falling back from URL to file while supporting base64 and local paths.

Tests:

  • Add unit tests covering URL-preferred resolution for record file path and base64 conversion with mocked downloads.

@auto-assign auto-assign Bot requested review from LIghtJUNction and Soulter April 22, 2026 07:30
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. area:core The bug / feature is about astrbot's core, backend labels Apr 22, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • Consider refactoring the common url = self.url or self.file resolution and subsequent checks in convert_to_file_path and convert_to_base64 into a shared helper to avoid duplication and keep behavior consistent if the logic evolves.
  • Both _download_audio_url and the conversion methods raise bare Exception with generic messages; using a more specific exception type (or preserving the underlying download error) would make failure modes easier to distinguish and debug.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider refactoring the common `url = self.url or self.file` resolution and subsequent checks in `convert_to_file_path` and `convert_to_base64` into a shared helper to avoid duplication and keep behavior consistent if the logic evolves.
- Both `_download_audio_url` and the conversion methods raise bare `Exception` with generic messages; using a more specific exception type (or preserving the underlying download error) would make failure modes easier to distinguish and debug.

## Individual Comments

### Comment 1
<location path="tests/unit/test_message_record_component.py" line_range="37-46" />
<code_context>
+
+
+@pytest.mark.asyncio
+async def test_record_convert_to_base64_prefers_url_when_file_is_name(
+    monkeypatch,
+    tmp_path,
+):
+    audio_bytes = b"audio-content"
+    audio_url = "http://napcat.local/nt_data/Ptt/2026-04/Ori/voice.amr"
+
+    async def fake_download_file(url: str, path: str, *_, **__) -> None:
+        assert url == audio_url
+        Path(path).write_bytes(audio_bytes)
+
+    monkeypatch.setattr(components, "download_file", fake_download_file)
+    monkeypatch.setattr(components, "get_astrbot_temp_path", lambda: str(tmp_path))
+
+    record = Record(file="voice.amr", url=audio_url)
+
+    assert await record.convert_to_base64() == base64.b64encode(audio_bytes).decode()
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding coverage for `base64://` URLs to ensure the new URL preference applies there as well

Since `convert_to_base64` now prefers `url` over `file` (including for `base64://` URLs), adding tests like `Record(file="voice.amr", url="base64://...")` for both `convert_to_file_path` and `convert_to_base64` would verify that `url` is used for base64 sources and that audio base64 handling (extension and round-tripping) works as intended.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +37 to +46
async def test_record_convert_to_base64_prefers_url_when_file_is_name(
monkeypatch,
tmp_path,
):
audio_bytes = b"audio-content"
audio_url = "http://napcat.local/nt_data/Ptt/2026-04/Ori/voice.amr"

async def fake_download_file(url: str, path: str, *_, **__) -> None:
assert url == audio_url
Path(path).write_bytes(audio_bytes)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Consider adding coverage for base64:// URLs to ensure the new URL preference applies there as well

Since convert_to_base64 now prefers url over file (including for base64:// URLs), adding tests like Record(file="voice.amr", url="base64://...") for both convert_to_file_path and convert_to_base64 would verify that url is used for base64 sources and that audio base64 handling (extension and round-tripping) works as intended.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the Record component to improve audio handling by introducing helper methods for downloading files and determining suffixes. It updates convert_to_file_path and convert_to_base64 to prioritize the url field over file and adds corresponding unit tests. Review feedback suggests refactoring the duplicated URL selection logic into a shared property and using more specific exception types, such as ValueError and RuntimeError, to improve maintainability and error handling.

if os.path.exists(self.file):
return os.path.abspath(self.file)
raise Exception(f"not a valid file: {self.file}")
url = self.url or self.file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The logic to prefer self.url over self.file is duplicated in both convert_to_file_path and convert_to_base64. Refactor this into a shared helper function or property to avoid code duplication. Additionally, ensure this new attachment handling functionality is accompanied by corresponding unit tests.

    @property
    def _source_url(self) -> str | None:
        return self.url or self.file

    async def convert_to_file_path(self) -> str:
        """将这个语音统一转换为本地文件路径。这个方法避免了手动判断语音数据类型,直接返回语音数据的本地路径(如果是网络 URL, 则会自动进行下载)。

        Returns:
            str: 语音的本地路径,以绝对路径表示。

        """
        url = self._source_url
References
  1. When implementing similar functionality for different cases (e.g., direct vs. quoted attachments), refactor the logic into a shared helper function to avoid code duplication.
  2. New functionality, such as handling attachments, should be accompanied by corresponding unit tests.

raise Exception(f"not a valid file: {self.file}")
url = self.url or self.file
if not url:
raise Exception(f"not a valid file: {url}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using a generic Exception is discouraged. Since this indicates an invalid input state, ValueError is more appropriate and consistent with the Image component's implementation.

Suggested change
raise Exception(f"not a valid file: {url}")
if not url:
raise ValueError("No valid file or URL provided")

await download_file(url, str(file_path))
if file_path.exists():
return str(file_path.resolve())
raise Exception(f"download failed: {url}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The download_file utility already raises an exception if the download fails (e.g., non-200 status or network error). This explicit check is largely redundant, but if kept, it should raise a more specific exception like RuntimeError or IOError instead of a generic Exception.

Suggested change
raise Exception(f"download failed: {url}")
raise RuntimeError(f"download failed: {url}")

Comment on lines +194 to +196
url = self.url or self.file
if not url:
raise Exception(f"not a valid file: {url}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

As suggested for convert_to_file_path, using a property for the source URL and a more specific exception type would improve maintainability and consistency by avoiding logic duplication.

Suggested change
url = self.url or self.file
if not url:
raise Exception(f"not a valid file: {url}")
url = self._source_url
if not url:
raise ValueError("No valid file or URL provided")
References
  1. When implementing similar functionality for different cases (e.g., direct vs. quoted attachments), refactor the logic into a shared helper function to avoid code duplication.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core The bug / feature is about astrbot's core, backend size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Docker + NapCat 接收语音消息时报错:Voice processing failed: not a valid file: xxx.amr

1 participant