summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md3
-rw-r--r--doc/Design.md6
-rw-r--r--src/mml_composemusic_mcp/server.py16
-rw-r--r--tests/test_compose_mml_tool.py33
4 files changed, 55 insertions, 3 deletions
diff --git a/README.md b/README.md
index 387f5b2..2935a21 100644
--- a/README.md
+++ b/README.md
@@ -185,7 +185,7 @@ uv run mml-composemusic-mcp --transport sse --port 8080 --output-dir ./data
```json
{
"success": true,
- "wav_path": "./data/output.wav",
+ "wav_path": "./data/output_20260711_120000_123.wav",
"duration_sec": 2.0,
"note_sequence": { ... },
"validation": {
@@ -197,6 +197,7 @@ uv run mml-composemusic-mcp --transport sse --port 8080 --output-dir ./data
- `success` が `false` の場合、`wav_path` は `null` になります。
- エラーがある場合は `validation.errors` に詳細なエラー情報が入ります。
+- **WAVファイル名は生成時刻ベースの `output_YYYYMMDD_HHMMSS_mmm.wav` 形式で出力されます。** 毎回異なるファイル名になるため、複数回の `compose` 実行でファイルが上書きされることはありません。
### action=`validate` — 構文チェック
diff --git a/doc/Design.md b/doc/Design.md
index fdaf860..e7196c3 100644
--- a/doc/Design.md
+++ b/doc/Design.md
@@ -134,7 +134,7 @@ MML文字列
```json
{
"success": true,
- "wav_path": "./data/output.wav",
+ "wav_path": "./data/output_20260711_120000_123.wav",
"duration_sec": 2.0,
"note_sequence": { ... },
"validation": {
@@ -154,6 +154,8 @@ MML文字列
| `validation.warnings` | array | 警告のリスト |
> `compose` は必ず検証を内包する。エラーがあればWAVは生成せず、検証結果のみ返す。
+>
+> **WAVファイル名**: 固定名 `output.wav` ではなく、生成時刻を含む `output_YYYYMMDD_HHMMSS_mmm.wav` 形式で出力される。これにより、複数回の `compose` 実行でファイルが上書きされるのを防ぐ。ミリ秒まで含めることで、短時間連続実行時のファイル名衝突も回避する。
**`action: "validate"`** — MMLの構文チェックのみ(音声生成なし)
@@ -917,6 +919,7 @@ PyxelのMML仕様に準拠。Pyxelのサウンドシステムに合わせた記æ
| 7 | ErrorDetail line/column | 1始まり。システム/APIエラー時は0 |
| 8 | pyxel 無限リピート | 省略時は2回で打ち切り、WARNINGを返す |
| 9 | pyxel `@ENV`/`@VIB`/`@GLI` パース | 第1段階は `{...}` ブレースではなくフラット数値列を簡易パース |
+| 10 | WAVファイル名 | 固定名 `output.wav` から、生成時刻ベースの `output_YYYYMMDD_HHMMSS_mmm.wav` 形式に変更。毎回異なるファイル名で上書きを防止 |
---
@@ -960,6 +963,7 @@ PyxelのMML仕様に準拠。Pyxelのサウンドシステムに合わせた記æ
| pyxel `Y`(ディチューン) | note_numberの小数部として保持 | `detune_cents` フィールドとして保持 |
| トランスポート | 設計書に記載なし | `stdio`/`http`/`sse`/`streamable-http` + CLI引数 |
| ErrorDetail line/column | 旧資料で表記揺れあり | 実装は1始まり(システムエラー時は0) |
+| WAVファイル名 | 固定 `output.wav` | 可変 `output_YYYYMMDD_HHMMSS_mmm.wav`。生成時刻ベースで上書き防止 |
---
diff --git a/src/mml_composemusic_mcp/server.py b/src/mml_composemusic_mcp/server.py
index 04b5546..0a09131 100644
--- a/src/mml_composemusic_mcp/server.py
+++ b/src/mml_composemusic_mcp/server.py
@@ -1,6 +1,7 @@
# MCP server exposing compose_mml tool.
import argparse
+from datetime import datetime
from pathlib import Path
from fastmcp import FastMCP
@@ -20,6 +21,19 @@ def _is_error(errors: list[ErrorDetail]) -> bool:
return any(e.severity == "error" for e in errors)
+def _generate_wav_filename() -> str:
+ """Generate a unique WAV filename based on the current timestamp.
+
+ Format: output_YYYYMMDD_HHMMSS_mmm.wav
+ Milliseconds are included to reduce the chance of collisions when
+ multiple compose requests are issued in rapid succession.
+ """
+ now = datetime.now()
+ # %f gives microseconds (6 digits); keep the first 3 for milliseconds.
+ timestamp = now.strftime("%Y%m%d_%H%M%S_") + now.strftime("%f")[:3]
+ return f"output_{timestamp}.wav"
+
+
def _split_errors(errors: list[ErrorDetail]) -> tuple[list[dict], list[dict]]:
errs = [e.to_dict() for e in errors if e.severity == "error"]
warns = [e.to_dict() for e in errors if e.severity == "warning"]
@@ -143,7 +157,7 @@ def compose_mml(
"note_sequence": note_sequence_dict,
"validation": {"errors": errors_list, "warnings": warnings_list},
}
- wav_path = OUTPUT_DIR / "output.wav"
+ wav_path = OUTPUT_DIR / _generate_wav_filename()
wav_errors = write_wav(wav_path, wave_data, sample_rate)
errors.extend(wav_errors)
errors_list, warnings_list = _split_errors(errors)
diff --git a/tests/test_compose_mml_tool.py b/tests/test_compose_mml_tool.py
index eb1b093..3d56115 100644
--- a/tests/test_compose_mml_tool.py
+++ b/tests/test_compose_mml_tool.py
@@ -151,3 +151,36 @@ def test_compose_normalize(compose_mml, tmp_output_dir):
assert on["success"] and off["success"]
assert Path(on["wav_path"]).exists()
assert Path(off["wav_path"]).exists()
+
+
+def test_compose_unique_filename(compose_mml, tmp_output_dir):
+ """Each compose call should produce a distinct WAV filename."""
+ mml = """A t120 l4 o4 v15 q2
+ c4
+"""
+ result1 = compose_mml(action="compose", mml=mml, mode="ppmck")
+ result2 = compose_mml(action="compose", mml=mml, mode="ppmck")
+ assert result1["success"] is True
+ assert result2["success"] is True
+
+ path1 = result1["wav_path"]
+ path2 = result2["wav_path"]
+ assert path1 is not None
+ assert path2 is not None
+ assert path1 != path2, "Two compose calls produced the same wav_path"
+
+ p1 = Path(path1)
+ p2 = Path(path2)
+ assert p1.exists()
+ assert p2.exists()
+ assert p1.name.startswith("output_")
+ assert p1.name.endswith(".wav")
+ assert p2.name.startswith("output_")
+ assert p2.name.endswith(".wav")
+
+ # Ensure the filename follows the expected timestamp pattern.
+ import re
+
+ pattern = re.compile(r"^output_\d{8}_\d{6}_\d{3}\.wav$")
+ assert pattern.match(p1.name)
+ assert pattern.match(p2.name)