記念日や誕生日に「棚から買えるものではない」贈り物を探すと、体験設計が難しいことに気づきます。写真やメッセージカードは手軽ですが、相手の名前や二人だけのエピソードを繰り返し聴ける形に残すには、生成パイプライン側の制約整理が必要です。ここでは公開されている日本語の説明をもとに、Once Song のような「質問→曲」フローを実装するときの設計メモをまとめます。コードはすべて説明用のサンプルであり、プロダクトのソースではありません。
プロダクトから見える入出力
日本語版の流れはおおむね次の4段です。
- 贈る相手・シーン・残したい思い出を案内に沿って答える
- ムード・ジャンル・歌声を選ぶ
- まず自分だけで試聴する(非公開プレビュー)
- フル楽曲をアンロックして贈り物として残す
公開コピーにある「サビに名前を織り込む」「テンプレの穴埋めではない」「最短30分」などは、実装上は 必須スロット(名前など)と自由記述スロット(エピソード)を分け、後段の歌詞・作曲モデルへ構造化して渡す 問題に落ちます。ここを曖昧なまま長い自然文プロンプトに連結すると、固有名詞の欠落やトーンのぶれが増えます。
回答を構造化する
フォームをそのまま連結する前に、スキーマを固定します。必須項目と任意項目を型で区別しておくと、バリデーションと再生成の条件が書きやすくなります。
type SongBrief = {
recipientName: string; // サビ必須
occasion: 'anniversary' | 'birthday' | 'wedding' | 'proposal' | 'memorial' | 'other';
memories: string[]; // 短文のエピソード(2〜6件)
insideJokes?: string[];
mood: string; // 例: warm / nostalgic
genre: string; // 例: acoustic / pop
voice: 'female' | 'male' | 'neutral';
language: 'ja' | 'en' | string;
};
function toLyricPrompt(b: SongBrief): string {
const mem = b.memories.map((m, i) => `${i + 1}. ${m}`).join('\n');
return [
`Language: ${b.language}`,
`Recipient name (must appear in chorus): ${b.recipientName}`,
`Occasion: ${b.occasion}`,
`Mood: ${b.mood}; Genre: ${b.genre}; Voice: ${b.voice}`,
'Write original lyrics (no template fill-in). Use memories concretely:',
mem,
'Structure: verse1 / pre-chorus / chorus / verse2 / chorus / bridge / final chorus',
].join('\n');
}
ポイントは 名前を「推奨」ではなく契約にする ことです。生成後にサビ文字列を検査し、名前が含まれなければ再生成か人手レビューに回すと、ギフト体験の失敗率が下がります。エピソードは「短い具体」を複数渡す方が、長い独白を1本渡すより歌詞に残りやすい、という経験則もあります。
試聴とアンロックを状態で分ける
プレビューは「自分だけで確認」、フル版は「贈り物として残す」——公開フローそのものが課金境界です。API設計では次のように状態を分けるのが素直です。
type SongJob = {
id: string;
brief: SongBrief;
status: 'queued' | 'preview_ready' | 'unlocked' | 'failed';
previewUrl?: string; // 短尺 or 透かし
fullUrl?: string; // unlock 後のみ
};
function canDownloadFull(job: SongJob) {
// クライアントの「再生できた」は認可に使わない
return job.status === 'unlocked';
}
プレビューURLを知っていてもフル音源が取れないよう、署名付きURLの権限をサーバ側で分離します。ギフト用途では、完成前に相手へリンクが漏れないことも重要です。キューの待ち時間(「最短30分」をうたうなら)は、ジョブのSLO監視とユーザー向けの進捗表示をセットで設計します。失敗時は「同じ Brief で再試行できるか/クレジットを戻すか」を先に決めておくとサポートが楽です。
品質チェックの最低ライン
生成系プロダクトでは、モデル出力をそのまま返さず、ゲートを通すのが定石です。
- サビに
recipientNameが含まれるか(表記ゆれを正規化してから判定) - 禁止トピックや過度にセンシティブな記述のフィルタ
- ジャンル/ムード指定との粗い一致(タグ不一致なら再生成)
- 歌詞の言語が指定言語と一致しているか
- 生成時間のSLO(キュー滞留・ワーカー死活)
function normalizeName(s: string) {
return s.normalize('NFKC').toLowerCase().replace(/\s+/g, '');
}
function chorusContainsName(lyrics: string, name: string) {
// サンプル: 最初の [Chorus] ブロックだけ見る
const m = lyrics.match(/\[Chorus\]([\s\S]*?)(\[|$)/i);
const chorus = m?.[1] ?? lyrics;
return normalizeName(chorus).includes(normalizeName(name));
}
ゲートを通らなかった場合は、同じ Brief を保ったまま「歌詞のみ再生成」「歌声のみ差し替え」など、段を分けてリトライできるとコストが下がります。全部を最初からやり直す必要はありません。
フロントの役割とやらないこと
- やる: Brief の入力支援、プレビュー再生、アンロック後のダウンロード、相手に渡す前の最終確認
- やらない: ブラウザだけでフル音源URLを推測する、プレビューをSNSに先出しする導線を黙って置く、必須スロット未入力のまま生成APIを叩く
「まずは自分だけで聴く」という公開コピーは、実装ではプレビュー権限と共有UIの初期状態にそのまま写ります。
キューとワーカーの分け方
歌詞生成・音声合成・マスタリングを1リクエストで同期実行すると、タイムアウトと再試行がつらくなります。公開の「最短30分」を守るなら、少なくとも次の段に分けた方が安全です。
- Brief 受理(同期)→
queued - 歌詞生成(非同期)→ ゲート通過で次へ
- 音声合成(非同期)→
preview_ready - アンロック後にフル音源を確定 →
unlocked
各段で失敗理由をコード化し、ユーザー向け文言と内部ログを分けます。「もう一度試す」がどの段から再開できるかを UI に出すと、サポート問い合わせが減ります。ワーカーは冪等キー(jobId + stage)を持ち、二重実行で二重課金しないようにします。
多言語とギフト文脈
Once Song は多言語サイトとして公開されていますが、1曲のなかで言語が混ざるとギフト体験が壊れやすいです。Brief の language を歌詞・読み上げ・メール文面のすべてに伝播させ、プレビュー画面のUI言語と曲の言語が食い違う場合は明示します。記念日・結婚式・追悼など occasion ごとに禁止表現リストを少し変えるのも有効です(祝いの場に重い表現が混ざる事故を防ぐ)。
まとめ
パーソナライズ曲は「いい感じの歌詞を一発で出す」より、必須スロットの保証・試聴とフルの権限分離・再生成ループ の方が体験を左右します。質問の順番、ムード選択、非公開プレビューの見せ方は Once Song の公開ページで確認できます。物語を音にするプロダクトでは、LLMの出力そのものより、フォーム設計と状態機械の方が長く効いてきます。


