From 3bb311ad8e0f10617805c8737d92d13e6a627818 Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Sun, 12 Jul 2026 01:10:41 +0900 Subject: waveファイル名可変対応 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++- doc/Design.md | 6 +++++- src/mml_composemusic_mcp/server.py | 16 +++++++++++++++- tests/test_compose_mml_tool.py | 33 +++++++++++++++++++++++++++++++++ 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) -- cgit v1.2.3