Merge remote-tracking branch 'origin/staging' into cloudflare-migration

This commit is contained in:
Kai ki
2026-06-25 18:08:46 +08:00
113 changed files with 526 additions and 389 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ jobs:
# Link to the authoritative English CLA. Chinese reference: # Link to the authoritative English CLA. Chinese reference:
# https://github.com/zonghaoyuan/infiplot/blob/staging/CLA.zh.md # https://github.com/zonghaoyuan/infiplot/blob/staging/CLA.zh.md
path-to-cla-document: "https://github.com/zonghaoyuan/infiplot/blob/staging/CLA.md" path-to-cla-document: "https://github.com/zonghaoyuan/infiplot/blob/staging/CLA.md"
allowlist: "github-actions[bot],dependabot[bot],zonghaoyuan,web-flow" allowlist: "github-actions[bot],dependabot[bot],zonghaoyuan,web-flow,noreply@anthropic.com"
block-sharing-crucial-repositories: true block-sharing-crucial-repositories: true
create-file-commit-message: "docs(cla): create CLA signature store" create-file-commit-message: "docs(cla): create CLA signature store"
+10 -6
View File
@@ -4,6 +4,8 @@
<p><b>An interactive story game, generated in real time for you</b></p> <p><b>An interactive story game, generated in real time for you</b></p>
<a href="https://opendeploy.dev/github/zonghaoyuan/infiplot"><img src="https://oss.opendeploy.dev/static/deploy-with-your-agent.svg" alt="Deploy with your agent" height="36"></a>
[![Stars](https://img.shields.io/github/stars/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/stargazers) [![Stars](https://img.shields.io/github/stars/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/stargazers)
[![Watchers](https://img.shields.io/github/watchers/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/watchers) [![Watchers](https://img.shields.io/github/watchers/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/watchers)
[![Forks](https://img.shields.io/github/forks/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/network) [![Forks](https://img.shields.io/github/forks/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/network)
@@ -11,7 +13,7 @@
[![Live Demo](https://img.shields.io/badge/Live_Demo-D97A2E?style=flat-square&logo=vercel&logoColor=white)](https://infiplot.com) [![Live Demo](https://img.shields.io/badge/Live_Demo-D97A2E?style=flat-square&logo=vercel&logoColor=white)](https://infiplot.com)
[![License](https://img.shields.io/github/license/zonghaoyuan/infiplot?style=flat-square)](LICENSE) [![License](https://img.shields.io/github/license/zonghaoyuan/infiplot?style=flat-square)](LICENSE)
[![LINUX DO](https://img.shields.io/badge/LINUX-DO-FFB003?style=flat-square&logo=data:image/svg%2bxml;base64,DQo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiPjxwYXRoIGQ9Ik00Ni44Mi0uMDU1aDYuMjVxMjMuOTY5IDIuMDYyIDM4IDIxLjQyNmM1LjI1OCA3LjY3NiA4LjIxNSAxNi4xNTYgOC44NzUgMjUuNDV2Ni4yNXEtMi4wNjQgMjMuOTY4LTIxLjQzIDM4LTExLjUxMiA3Ljg4NS0yNS40NDUgOC44NzRoLTYuMjVxLTIzLjk3LTIuMDY0LTM4LjAwNC0yMS40M1EuOTcxIDY3LjA1Ni0uMDU0IDUzLjE4di02LjQ3M0MxLjM2MiAzMC43ODEgOC41MDMgMTguMTQ4IDIxLjM3IDguODE3IDI5LjA0NyAzLjU2MiAzNy41MjcuNjA0IDQ2LjgyMS0uMDU2IiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7ZmlsbDojZWNlY2VjO2ZpbGwtb3BhY2l0eToxIi8+PHBhdGggZD0iTTQ3LjI2NiAyLjk1N3EyMi41My0uNjUgMzcuNzc3IDE1LjczOGE0OS43IDQ5LjcgMCAwIDEgNi44NjcgMTAuMTU3cS00MS45NjQuMjIyLTgzLjkzIDAgOS43NS0xOC42MTYgMzAuMDI0LTI0LjM4N2E2MSA2MSAwIDAgMSA5LjI2Mi0xLjUwOCIgc3R5bGU9InN0cm9rZTpub25lO2ZpbGwtcnVsZTpldmVub2RkO2ZpbGw6IzE5MTkxOTtmaWxsLW9wYWNpdHk6MSIvPjxwYXRoIGQ9Ik03Ljk4IDcwLjkyNmMyNy45NzctLjAzNSA1NS45NTQgMCA4My45My4xMTNRODMuNDI2IDg3LjQ3MyA2Ni4xMyA5NC4wODZxLTE4LjgxIDYuNTQ0LTM2LjgzMi0xLjg5OC0xNC4yMDMtNy4wOS0yMS4zMTctMjEuMjYyIiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7ZmlsbDojZjlhZjAwO2ZpbGwtb3BhY2l0eToxIi8+PC9zdmc+)](https://linux.do) [![LINUX DO](https://img.shields.io/badge/LINUX-DO-FFB003?style=flat-square&logo=data:image/svg%2bxml;base64,DQo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiPjxwYXRoIGQ9Ik00Ni44Mi0uMDU1aDYuMjVxMjMuOTY5IDIuMDYyIDM4IDIxLjQyNmM1LjI1OCA3LjY3NiA4LjIxNSAxNi4xNTYgOC44NzUgMjUuNDV2Ni4yNXEtMi4wNjQgMjMuOTY4LTIxLjQzIDM4LTExLjUxMiA3Ljg4NS0yNS40NDUgOC44NzRoLTYuMjVxLTIzLjk3LTIuMDY0LTM4LjAwNC0yMS40M1EuOTcxIDY3LjA1Ni0uMDU0IDUzLjE4di02LjQ3M0MxLjM2MiAzMC43ODEgOC41MDMgMTguMTQ4IDIxLjM3IDguODE3IDI5LjA0NyAzLjU2MiAzNy41MjcuNjA0IDQ2LjgyMS0uMDU2IiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7ZmlsbDojZWNlY2VjO2ZpbGwtb3BhY2l0eToxIi8+PHBhdGggZD0iTTQ3LjI2NiAyLjk1N3EyMi41My0uNjUgMzcuNzc3IDE1LjczOGE0OS43IDQ5LjcgMCAwIDEgNi44NjcgMTAuMTU3cS00MS45NjQuMjIyLTgzLjkzIDAgOS43NS0xOC42MTYgMzAuMDI0LTI0LjM4N2E2MSA2MSAwIDAgMSA5LjI2Mi0xLjUwOCIgc3R5bGU9InN0cm9rZTpub25lO2ZpbGwtcnVsZTpldmVub2RkO2ZpbGw6IzE5MTkxOTtmaWxsLW9wYWNpdHk6MSIvPjxwYXRoIGQ9Ik03Ljk4IDcwLjkyNmMyNy45NzctLjAzNSA1NS45NTQgMCA4My45My4xMTNRODMuNDI2IDg3LjQ3MyA2Ni4xMyA5NC4wODZxLTE4LjgxIDYuNTQ0LTM2LjgzMi0xLjg5OC0xNC4yMDMtNy4wOS0yMS4zMTctMjEuMjYyIiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7ZmlsbDojZjlhZjAwO2ZpbGwtb3BhY2l0eToxIi8+PC9zdmc+)](https://linux.do/t/topic/2296384)
[简体中文](https://github.com/zonghaoyuan/infiplot) · English · [日本語](README.ja.md) [简体中文](https://github.com/zonghaoyuan/infiplot) · English · [日本語](README.ja.md)
@@ -41,11 +43,13 @@ Free to play, no setup required: [infiplot.com](https://infiplot.com)
InfiPlot offers multiple deployment options. For personal use, we recommend the one-click Vercel deploy; to self-host on your own server or local machine, use Docker. InfiPlot offers multiple deployment options. For personal use, we recommend the one-click Vercel deploy; to self-host on your own server or local machine, use Docker.
### Vercel / Cloudflare (one-click) ### OpenDeploy / Vercel / Cloudflare (one-click)
Cloudflare deployment requires the Workers Paid Plan because the scene pipeline needs longer CPU time. Cloudflare deployment requires the Workers Paid Plan because the scene pipeline needs longer CPU time. OpenDeploy lets your AI agent handle the deployment for you.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/zonghaoyuan/infiplot&env=TEXT_BASE_URL,TEXT_API_KEY,TEXT_MODEL,IMAGE_BASE_URL,IMAGE_API_KEY,IMAGE_MODEL,VISION_BASE_URL,VISION_API_KEY,VISION_MODEL,TTS_BASE_URL,TTS_API_KEY,TTS_SPEECH_MODEL,MOCK_IMAGE&envDescription=Three%20required%20providers%20%2B%20optional%20TTS.%20Any%20OpenAI-compatible%20endpoint%20works%20for%20text%2Fvision.%20TTS%3A%20Xiaomi%20MiMo%20%28free%29%20or%20StepFun%20%28paid%2C%20better%20quality%29.&envLink=https://github.com/zonghaoyuan/infiplot/blob/main/README.en.md%23configuration-guide) &nbsp; [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/zonghaoyuan/infiplot) <a href="https://opendeploy.dev/github/zonghaoyuan/infiplot"><img src="https://oss.opendeploy.dev/static/deploy-with-your-agent.svg" alt="Deploy with your agent" height="34"></a>&nbsp;
<a href="https://vercel.com/new/clone?repository-url=https://github.com/zonghaoyuan/infiplot&env=TEXT_BASE_URL,TEXT_API_KEY,TEXT_MODEL,IMAGE_BASE_URL,IMAGE_API_KEY,IMAGE_MODEL,VISION_BASE_URL,VISION_API_KEY,VISION_MODEL,TTS_BASE_URL,TTS_API_KEY,TTS_SPEECH_MODEL,MOCK_IMAGE&envDescription=Three%20required%20providers%20%2B%20optional%20TTS.%20Any%20OpenAI-compatible%20endpoint%20works%20for%20text%2Fvision.%20TTS%3A%20Xiaomi%20MiMo%20%28free%29%20or%20StepFun%20%28paid%2C%20better%20quality%29.&envLink=https://github.com/zonghaoyuan/infiplot/blob/main/README.en.md%23configuration-guide"><img src="https://vercel.com/button" alt="Deploy with Vercel" height="34"></a>&nbsp;
<a href="https://deploy.workers.cloudflare.com/?url=https://github.com/zonghaoyuan/infiplot"><img src="https://deploy.workers.cloudflare.com/button" alt="Deploy to Cloudflare" height="34"></a>
After deploy, fill in the environment variables — see the [Configuration guide](#configuration-guide) below. The repo root is the app itself: Vercel needs no special root directory; on Cloudflare, just set the build command to `pnpm build:cf`. After deploy, fill in the environment variables — see the [Configuration guide](#configuration-guide) below. The repo root is the app itself: Vercel needs no special root directory; on Cloudflare, just set the build command to `pnpm build:cf`.
@@ -112,7 +116,7 @@ Visit `http://localhost:3000` to start playing.
## How it works ## How it works
Built on text, image, and audio models, we've assembled a multi-agent framework to deliver on InfiPlot's goal. We split the agents into five roles — **Architect, Writer, Character Designer, Cinematographer, and Painter** — that work together to keep the plot coherent, the characters consistent, and the scenes continuous, all while making the story as compelling as we can. Built on text, image, and audio models, we've assembled a multi-agent framework to deliver on InfiPlot's goal. We split the agents into four roles — **Writer, Character Designer, Cinematographer, and Painter** — that work together to keep the plot coherent, the characters consistent, and the scenes continuous, all while making the story as compelling as we can. The Writer also handles overall story architecture.
We call each complete playthrough a **story**. We call each complete playthrough a **story**.
@@ -205,12 +209,12 @@ See the [Bring-your-own voice Key guide](docs/xiaomi-tts-key.md) for how to obta
- [x] Frontend API Key & model setup - [x] Frontend API Key & model setup
- [x] Mobile web support - [x] Mobile web support
- [x] Story sharing (`.infiplot` format) - [x] Story sharing (`.infiplot` format)
- [x] OpenDeploy quick deployment
**To Do** **To Do**
- [ ] Mobile app & creator platform - [ ] Mobile app & creator platform
- [ ] ComfyUI custom image generation - [ ] ComfyUI custom image generation
- [ ] Open Deploy quick deployment
- [ ] Reduce latency to under 5s - [ ] Reduce latency to under 5s
- [ ] Story save & resume - [ ] Story save & resume
- [ ] Custom character cards & world settings - [ ] Custom character cards & world settings
+10 -6
View File
@@ -4,6 +4,8 @@
<p><b>あなたのためにリアルタイム生成されるインタラクティブ・ストーリーゲーム</b></p> <p><b>あなたのためにリアルタイム生成されるインタラクティブ・ストーリーゲーム</b></p>
<a href="https://opendeploy.dev/github/zonghaoyuan/infiplot"><img src="https://oss.opendeploy.dev/static/deploy-with-your-agent.svg" alt="Deploy with your agent" height="36"></a>
[![Stars](https://img.shields.io/github/stars/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/stargazers) [![Stars](https://img.shields.io/github/stars/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/stargazers)
[![Watchers](https://img.shields.io/github/watchers/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/watchers) [![Watchers](https://img.shields.io/github/watchers/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/watchers)
[![Forks](https://img.shields.io/github/forks/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/network) [![Forks](https://img.shields.io/github/forks/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/network)
@@ -11,7 +13,7 @@
[![Live Demo](https://img.shields.io/badge/Live_Demo-D97A2E?style=flat-square&logo=vercel&logoColor=white)](https://infiplot.com) [![Live Demo](https://img.shields.io/badge/Live_Demo-D97A2E?style=flat-square&logo=vercel&logoColor=white)](https://infiplot.com)
[![License](https://img.shields.io/github/license/zonghaoyuan/infiplot?style=flat-square)](LICENSE) [![License](https://img.shields.io/github/license/zonghaoyuan/infiplot?style=flat-square)](LICENSE)
[![LINUX DO](https://img.shields.io/badge/LINUX-DO-FFB003?style=flat-square&logo=data:image/svg%2bxml;base64,DQo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiPjxwYXRoIGQ9Ik00Ni44Mi0uMDU1aDYuMjVxMjMuOTY5IDIuMDYyIDM4IDIxLjQyNmM1LjI1OCA3LjY3NiA4LjIxNSAxNi4xNTYgOC44NzUgMjUuNDV2Ni4yNXEtMi4wNjQgMjMuOTY4LTIxLjQzIDM4LTExLjUxMiA3Ljg4NS0yNS40NDUgOC44NzRoLTYuMjVxLTIzLjk3LTIuMDY0LTM4LjAwNC0yMS40M1EuOTcxIDY3LjA1Ni0uMDU0IDUzLjE4di02LjQ3M0MxLjM2MiAzMC43ODEgOC41MDMgMTguMTQ4IDIxLjM3IDguODE3IDI5LjA0NyAzLjU2MiAzNy41MjcuNjA0IDQ2LjgyMS0uMDU2IiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7ZmlsbDojZWNlY2VjO2ZpbGwtb3BhY2l0eToxIi8+PHBhdGggZD0iTTQ3LjI2NiAyLjk1N3EyMi41My0uNjUgMzcuNzc3IDE1LjczOGE0OS43IDQ5LjcgMCAwIDEgNi44NjcgMTAuMTU3cS00MS45NjQuMjIyLTgzLjkzIDAgOS43NS0xOC42MTYgMzAuMDI0LTI0LjM4N2E2MSA2MSAwIDAgMSA5LjI2Mi0xLjUwOCIgc3R5bGU9InN0cm9rZTpub25lO2ZpbGwtcnVsZTpldmVub2RkO2ZpbGw6IzE5MTkxOTtmaWxsLW9wYWNpdHk6MSIvPjxwYXRoIGQ9Ik03Ljk4IDcwLjkyNmMyNy45NzctLjAzNSA1NS45NTQgMCA4My45My4xMTNRODMuNDI2IDg3LjQ3MyA2Ni4xMyA5NC4wODZxLTE4LjgxIDYuNTQ0LTM2LjgzMi0xLjg5OC0xNC4yMDMtNy4wOS0yMS4zMTctMjEuMjYyIiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7ZmlsbDojZjlhZjAwO2ZpbGwtb3BhY2l0eToxIi8+PC9zdmc+)](https://linux.do) [![LINUX DO](https://img.shields.io/badge/LINUX-DO-FFB003?style=flat-square&logo=data:image/svg%2bxml;base64,DQo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiPjxwYXRoIGQ9Ik00Ni44Mi0uMDU1aDYuMjVxMjMuOTY5IDIuMDYyIDM4IDIxLjQyNmM1LjI1OCA3LjY3NiA4LjIxNSAxNi4xNTYgOC44NzUgMjUuNDV2Ni4yNXEtMi4wNjQgMjMuOTY4LTIxLjQzIDM4LTExLjUxMiA3Ljg4NS0yNS40NDUgOC44NzRoLTYuMjVxLTIzLjk3LTIuMDY0LTM4LjAwNC0yMS40M1EuOTcxIDY3LjA1Ni0uMDU0IDUzLjE4di02LjQ3M0MxLjM2MiAzMC43ODEgOC41MDMgMTguMTQ4IDIxLjM3IDguODE3IDI5LjA0NyAzLjU2MiAzNy41MjcuNjA0IDQ2LjgyMS0uMDU2IiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7ZmlsbDojZWNlY2VjO2ZpbGwtb3BhY2l0eToxIi8+PHBhdGggZD0iTTQ3LjI2NiAyLjk1N3EyMi41My0uNjUgMzcuNzc3IDE1LjczOGE0OS43IDQ5LjcgMCAwIDEgNi44NjcgMTAuMTU3cS00MS45NjQuMjIyLTgzLjkzIDAgOS43NS0xOC42MTYgMzAuMDI0LTI0LjM4N2E2MSA2MSAwIDAgMSA5LjI2Mi0xLjUwOCIgc3R5bGU9InN0cm9rZTpub25lO2ZpbGwtcnVsZTpldmVub2RkO2ZpbGw6IzE5MTkxOTtmaWxsLW9wYWNpdHk6MSIvPjxwYXRoIGQ9Ik03Ljk4IDcwLjkyNmMyNy45NzctLjAzNSA1NS45NTQgMCA4My45My4xMTNRODMuNDI2IDg3LjQ3MyA2Ni4xMyA5NC4wODZxLTE4LjgxIDYuNTQ0LTM2LjgzMi0xLjg5OC0xNC4yMDMtNy4wOS0yMS4zMTctMjEuMjYyIiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7ZmlsbDojZjlhZjAwO2ZpbGwtb3BhY2l0eToxIi8+PC9zdmc+)](https://linux.do/t/topic/2296384)
[简体中文](https://github.com/zonghaoyuan/infiplot) · [English](README.en.md) · 日本語 [简体中文](https://github.com/zonghaoyuan/infiplot) · [English](README.en.md) · 日本語
@@ -41,11 +43,13 @@ InfiPlot は、AI がコンテンツをリアルタイムに生成するイン
InfiPlot は複数のデプロイ方法に対応しています。個人利用には Vercel のワンクリックデプロイをおすすめします。自分のサーバーやローカルマシンで動かしたい場合は Docker を使ってください。 InfiPlot は複数のデプロイ方法に対応しています。個人利用には Vercel のワンクリックデプロイをおすすめします。自分のサーバーやローカルマシンで動かしたい場合は Docker を使ってください。
### Vercel / Cloudflare(ワンクリック) ### OpenDeploy / Vercel / Cloudflare(ワンクリック)
Cloudflare へのデプロイはシーンパイプラインがより長い CPU 時間を必要とするため、Workers Paid Plan が必要です。 Cloudflare へのデプロイはシーンパイプラインがより長い CPU 時間を必要とするため、Workers Paid Plan が必要です。OpenDeploy では AI エージェントにデプロイを任せることができます。
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/zonghaoyuan/infiplot&env=TEXT_BASE_URL,TEXT_API_KEY,TEXT_MODEL,IMAGE_BASE_URL,IMAGE_API_KEY,IMAGE_MODEL,VISION_BASE_URL,VISION_API_KEY,VISION_MODEL,TTS_BASE_URL,TTS_API_KEY,TTS_SPEECH_MODEL,MOCK_IMAGE&envDescription=Three%20required%20providers%20%2B%20optional%20TTS.%20Any%20OpenAI-compatible%20endpoint%20works%20for%20text%2Fvision.%20TTS%3A%20Xiaomi%20MiMo%20%28free%29%20or%20StepFun%20%28paid%2C%20better%20quality%29.&envLink=https://github.com/zonghaoyuan/infiplot/blob/main/README.ja.md%23%E8%A8%AD%E5%AE%9A%E3%82%AC%E3%82%A4%E3%83%89) &nbsp; [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/zonghaoyuan/infiplot) <a href="https://opendeploy.dev/github/zonghaoyuan/infiplot"><img src="https://oss.opendeploy.dev/static/deploy-with-your-agent.svg" alt="Deploy with your agent" height="34"></a>&nbsp;
<a href="https://vercel.com/new/clone?repository-url=https://github.com/zonghaoyuan/infiplot&env=TEXT_BASE_URL,TEXT_API_KEY,TEXT_MODEL,IMAGE_BASE_URL,IMAGE_API_KEY,IMAGE_MODEL,VISION_BASE_URL,VISION_API_KEY,VISION_MODEL,TTS_BASE_URL,TTS_API_KEY,TTS_SPEECH_MODEL,MOCK_IMAGE&envDescription=Three%20required%20providers%20%2B%20optional%20TTS.%20Any%20OpenAI-compatible%20endpoint%20works%20for%20text%2Fvision.%20TTS%3A%20Xiaomi%20MiMo%20%28free%29%20or%20StepFun%20%28paid%2C%20better%20quality%29.&envLink=https://github.com/zonghaoyuan/infiplot/blob/main/README.ja.md%23%E8%A8%AD%E5%AE%9A%E3%82%AC%E3%82%A4%E3%83%89"><img src="https://vercel.com/button" alt="Deploy with Vercel" height="34"></a>&nbsp;
<a href="https://deploy.workers.cloudflare.com/?url=https://github.com/zonghaoyuan/infiplot"><img src="https://deploy.workers.cloudflare.com/button" alt="Deploy to Cloudflare" height="34"></a>
デプロイ後、環境変数を設定してください —— 下記の[設定ガイド](#設定ガイド)を参照。リポジトリのルートがアプリ本体です:Vercel では特別なルート設定は不要です。Cloudflare ではビルドコマンドを `pnpm build:cf` に設定するだけで済みます。 デプロイ後、環境変数を設定してください —— 下記の[設定ガイド](#設定ガイド)を参照。リポジトリのルートがアプリ本体です:Vercel では特別なルート設定は不要です。Cloudflare ではビルドコマンドを `pnpm build:cf` に設定するだけで済みます。
@@ -112,7 +116,7 @@ docker compose up -d
## 仕組み ## 仕組み
テキスト・画像・音声モデルを基盤に、私たちは InfiPlot の目標を実現するためのマルチエージェント・フレームワークを構築しました。エージェントを **アーキテクト(Architect)・脚本家(Writer)・キャラクターデザイナー(Character Designer)・撮影監督(Cinematographer)・絵師(Painter**5 つの役割に分け、互いに連携させることで、物語の一貫性・キャラクターの一貫性・シーンの連続性を保ちつつ、できる限り魅力的な物語を目指します。 テキスト・画像・音声モデルを基盤に、私たちは InfiPlot の目標を実現するためのマルチエージェント・フレームワークを構築しました。エージェントを **脚本家(Writer)・キャラクターデザイナー(Character Designer)・撮影監督(Cinematographer)・絵師(Painter**4 つの役割に分け、互いに連携させることで、物語の一貫性・キャラクターの一貫性・シーンの連続性を保ちつつ、できる限り魅力的な物語を目指します。脚本家は物語全体の構造設計も兼ねています。
一回のプレイ全体を、私たちは**ストーリー(story)**と呼んでいます。 一回のプレイ全体を、私たちは**ストーリー(story)**と呼んでいます。
@@ -204,12 +208,12 @@ Xiaomi は TTS モデルに RPM/TPM 制限を設けています。公開デプ
- [x] フロントエンドで API Key・モデル設定 - [x] フロントエンドで API Key・モデル設定
- [x] モバイル Web 対応 - [x] モバイル Web 対応
- [x] ストーリー共有(`.infiplot` 形式) - [x] ストーリー共有(`.infiplot` 形式)
- [x] OpenDeploy クイックデプロイ
**未実装** **未実装**
- [ ] モバイルアプリ&クリエイタープラットフォーム - [ ] モバイルアプリ&クリエイタープラットフォーム
- [ ] ComfyUI カスタム画像生成対応 - [ ] ComfyUI カスタム画像生成対応
- [ ] Open Deploy クイックデプロイ
- [ ] レイテンシを 5 秒以内に短縮 - [ ] レイテンシを 5 秒以内に短縮
- [ ] ストーリーの保存・再開 - [ ] ストーリーの保存・再開
- [ ] カスタムキャラクターカード&世界観設定 - [ ] カスタムキャラクターカード&世界観設定
+10 -6
View File
@@ -4,6 +4,8 @@
<p><b>为你实时生成的互动剧情游戏</b></p> <p><b>为你实时生成的互动剧情游戏</b></p>
<a href="https://opendeploy.dev/github/zonghaoyuan/infiplot"><img src="https://oss.opendeploy.dev/static/deploy-with-your-agent.svg" alt="Deploy with your agent" height="36"></a>
[![Stars](https://img.shields.io/github/stars/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/stargazers) [![Stars](https://img.shields.io/github/stars/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/stargazers)
[![Watchers](https://img.shields.io/github/watchers/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/watchers) [![Watchers](https://img.shields.io/github/watchers/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/watchers)
[![Forks](https://img.shields.io/github/forks/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/network) [![Forks](https://img.shields.io/github/forks/zonghaoyuan/infiplot?style=flat-square)](https://github.com/zonghaoyuan/infiplot/network)
@@ -11,7 +13,7 @@
[![Live Demo](https://img.shields.io/badge/Live_Demo-D97A2E?style=flat-square&logo=vercel&logoColor=white)](https://infiplot.com) [![Live Demo](https://img.shields.io/badge/Live_Demo-D97A2E?style=flat-square&logo=vercel&logoColor=white)](https://infiplot.com)
[![License](https://img.shields.io/github/license/zonghaoyuan/infiplot?style=flat-square)](LICENSE) [![License](https://img.shields.io/github/license/zonghaoyuan/infiplot?style=flat-square)](LICENSE)
[![LINUX DO](https://img.shields.io/badge/LINUX-DO-FFB003?style=flat-square&logo=data:image/svg%2bxml;base64,DQo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiPjxwYXRoIGQ9Ik00Ni44Mi0uMDU1aDYuMjVxMjMuOTY5IDIuMDYyIDM4IDIxLjQyNmM1LjI1OCA3LjY3NiA4LjIxNSAxNi4xNTYgOC44NzUgMjUuNDV2Ni4yNXEtMi4wNjQgMjMuOTY4LTIxLjQzIDM4LTExLjUxMiA3Ljg4NS0yNS40NDUgOC44NzRoLTYuMjVxLTIzLjk3LTIuMDY0LTM4LjAwNC0yMS40M1EuOTcxIDY3LjA1Ni0uMDU0IDUzLjE4di02LjQ3M0MxLjM2MiAzMC43ODEgOC41MDMgMTguMTQ4IDIxLjM3IDguODE3IDI5LjA0NyAzLjU2MiAzNy41MjcuNjA0IDQ2LjgyMS0uMDU2IiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7ZmlsbDojZWNlY2VjO2ZpbGwtb3BhY2l0eToxIi8+PHBhdGggZD0iTTQ3LjI2NiAyLjk1N3EyMi41My0uNjUgMzcuNzc3IDE1LjczOGE0OS43IDQ5LjcgMCAwIDEgNi44NjcgMTAuMTU3cS00MS45NjQuMjIyLTgzLjkzIDAgOS43NS0xOC42MTYgMzAuMDI0LTI0LjM4N2E2MSA2MSAwIDAgMSA5LjI2Mi0xLjUwOCIgc3R5bGU9InN0cm9rZTpub25lO2ZpbGwtcnVsZTpldmVub2RkO2ZpbGw6IzE5MTkxOTtmaWxsLW9wYWNpdHk6MSIvPjxwYXRoIGQ9Ik03Ljk4IDcwLjkyNmMyNy45NzctLjAzNSA1NS45NTQgMCA4My45My4xMTNRODMuNDI2IDg3LjQ3MyA2Ni4xMyA5NC4wODZxLTE4LjgxIDYuNTQ0LTM2LjgzMi0xLjg5OC0xNC4yMDMtNy4wOS0yMS4zMTctMjEuMjYyIiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7ZmlsbDojZjlhZjAwO2ZpbGwtb3BhY2l0eToxIi8+PC9zdmc+)](https://linux.do) [![LINUX DO](https://img.shields.io/badge/LINUX-DO-FFB003?style=flat-square&logo=data:image/svg%2bxml;base64,DQo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiPjxwYXRoIGQ9Ik00Ni44Mi0uMDU1aDYuMjVxMjMuOTY5IDIuMDYyIDM4IDIxLjQyNmM1LjI1OCA3LjY3NiA4LjIxNSAxNi4xNTYgOC44NzUgMjUuNDV2Ni4yNXEtMi4wNjQgMjMuOTY4LTIxLjQzIDM4LTExLjUxMiA3Ljg4NS0yNS40NDUgOC44NzRoLTYuMjVxLTIzLjk3LTIuMDY0LTM4LjAwNC0yMS40M1EuOTcxIDY3LjA1Ni0uMDU0IDUzLjE4di02LjQ3M0MxLjM2MiAzMC43ODEgOC41MDMgMTguMTQ4IDIxLjM3IDguODE3IDI5LjA0NyAzLjU2MiAzNy41MjcuNjA0IDQ2LjgyMS0uMDU2IiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7ZmlsbDojZWNlY2VjO2ZpbGwtb3BhY2l0eToxIi8+PHBhdGggZD0iTTQ3LjI2NiAyLjk1N3EyMi41My0uNjUgMzcuNzc3IDE1LjczOGE0OS43IDQ5LjcgMCAwIDEgNi44NjcgMTAuMTU3cS00MS45NjQuMjIyLTgzLjkzIDAgOS43NS0xOC42MTYgMzAuMDI0LTI0LjM4N2E2MSA2MSAwIDAgMSA5LjI2Mi0xLjUwOCIgc3R5bGU9InN0cm9rZTpub25lO2ZpbGwtcnVsZTpldmVub2RkO2ZpbGw6IzE5MTkxOTtmaWxsLW9wYWNpdHk6MSIvPjxwYXRoIGQ9Ik03Ljk4IDcwLjkyNmMyNy45NzctLjAzNSA1NS45NTQgMCA4My45My4xMTNRODMuNDI2IDg3LjQ3MyA2Ni4xMyA5NC4wODZxLTE4LjgxIDYuNTQ0LTM2LjgzMi0xLjg5OC0xNC4yMDMtNy4wOS0yMS4zMTctMjEuMjYyIiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7ZmlsbDojZjlhZjAwO2ZpbGwtb3BhY2l0eToxIi8+PC9zdmc+)](https://linux.do/t/topic/2296384)
[English](README.en.md) · 简体中文 · [日本語](README.ja.md) [English](README.en.md) · 简体中文 · [日本語](README.ja.md)
@@ -41,11 +43,13 @@ InfiPlot是一款AI实时生成内容的互动剧情游戏,这里没有预设
InfiPlot 支持多种部署方式。个人使用推荐 Vercel 一键部署;想部署到自己的服务器或本地运行,可以用 Docker。 InfiPlot 支持多种部署方式。个人使用推荐 Vercel 一键部署;想部署到自己的服务器或本地运行,可以用 Docker。
### Vercel / Cloudflare(一键部署) ### OpenDeploy / Vercel / Cloudflare(一键部署)
Cloudflare 部署因场景流水线需要更长 CPU 时间,需要 Workers Paid Plan。 Cloudflare 部署因场景流水线需要更长 CPU 时间,需要 Workers Paid Plan。OpenDeploy 支持让 AI Agent 帮你完成部署。
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/zonghaoyuan/infiplot&env=TEXT_BASE_URL,TEXT_API_KEY,TEXT_MODEL,IMAGE_BASE_URL,IMAGE_API_KEY,IMAGE_MODEL,VISION_BASE_URL,VISION_API_KEY,VISION_MODEL,TTS_BASE_URL,TTS_API_KEY,TTS_SPEECH_MODEL,MOCK_IMAGE&envDescription=Three%20required%20providers%20%2B%20optional%20TTS.%20Any%20OpenAI-compatible%20endpoint%20works%20for%20text%2Fvision.%20TTS%3A%20Xiaomi%20MiMo%20%28free%29%20or%20StepFun%20%28paid%2C%20better%20quality%29.&envLink=https://github.com/zonghaoyuan/infiplot%23%E9%85%8D%E7%BD%AE%E6%95%99%E7%A8%8B) &nbsp; [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/zonghaoyuan/infiplot) <a href="https://opendeploy.dev/github/zonghaoyuan/infiplot"><img src="https://oss.opendeploy.dev/static/deploy-with-your-agent.svg" alt="Deploy with your agent" height="34"></a>&nbsp;
<a href="https://vercel.com/new/clone?repository-url=https://github.com/zonghaoyuan/infiplot&env=TEXT_BASE_URL,TEXT_API_KEY,TEXT_MODEL,IMAGE_BASE_URL,IMAGE_API_KEY,IMAGE_MODEL,VISION_BASE_URL,VISION_API_KEY,VISION_MODEL,TTS_BASE_URL,TTS_API_KEY,TTS_SPEECH_MODEL,MOCK_IMAGE&envDescription=Three%20required%20providers%20%2B%20optional%20TTS.%20Any%20OpenAI-compatible%20endpoint%20works%20for%20text%2Fvision.%20TTS%3A%20Xiaomi%20MiMo%20%28free%29%20or%20StepFun%20%28paid%2C%20better%20quality%29.&envLink=https://github.com/zonghaoyuan/infiplot%23%E9%85%8D%E7%BD%AE%E6%95%99%E7%A8%8B"><img src="https://vercel.com/button" alt="Deploy with Vercel" height="34"></a>&nbsp;
<a href="https://deploy.workers.cloudflare.com/?url=https://github.com/zonghaoyuan/infiplot"><img src="https://deploy.workers.cloudflare.com/button" alt="Deploy to Cloudflare" height="34"></a>
部署完成后,填好环境变量 —— 详见下方的[配置教程](#配置教程)。仓库根目录就是应用本身:Vercel 无需额外设置 root directory;在 Cloudflare 上把构建命令设为 `pnpm build:cf` 即可。 部署完成后,填好环境变量 —— 详见下方的[配置教程](#配置教程)。仓库根目录就是应用本身:Vercel 无需额外设置 root directory;在 Cloudflare 上把构建命令设为 `pnpm build:cf` 即可。
@@ -112,7 +116,7 @@ docker compose up -d
## 工作原理 ## 工作原理
基于文本、图像和音频模型,我们搭建了一个多智能体框架来实现InfiPlot的目标。我们把agent分为架构师、编剧、角色设计师、场景布置师和画家个职能,让他们之间相互配合,在保证剧情连贯性、角色一致性、场景一致性的基础上,尽可能使得剧情足够富有吸引力。 基于文本、图像和音频模型,我们搭建了一个多智能体框架来实现InfiPlot的目标。我们把agent分为编剧、角色设计师、场景布置师和画家个职能,让他们之间相互配合,在保证剧情连贯性、角色一致性、场景一致性的基础上,尽可能使得剧情足够富有吸引力。其中编剧同时负责剧情的整体架构规划。
我们把每一次游玩的整体体验称为故事(story)。 我们把每一次游玩的整体体验称为故事(story)。
@@ -216,12 +220,12 @@ InfiPlot 会与四类模型供应商通信。**文本(Text)和视觉(Visio
- [x] 前端直配 API Key 与模型 - [x] 前端直配 API Key 与模型
- [x] 移动端 Web 适配 - [x] 移动端 Web 适配
- [x] 剧情分享(`.infiplot` 格式) - [x] 剧情分享(`.infiplot` 格式)
- [x] OpenDeploy 快速部署
**未实现** **未实现**
- [ ] 移动端 App 与创作平台 - [ ] 移动端 App 与创作平台
- [ ] 兼容 ComfyUI 自定义生图 - [ ] 兼容 ComfyUI 自定义生图
- [ ] Open Deploy 快速部署
- [ ] 延迟压缩至 5 秒以内 - [ ] 延迟压缩至 5 秒以内
- [ ] 剧情存档与续玩 - [ ] 剧情存档与续玩
- [ ] 自定义角色卡与世界观 - [ ] 自定义角色卡与世界观
+18 -1
View File
@@ -2077,7 +2077,7 @@ export default function HomePage() {
</p> </p>
</div> </div>
<div className="mx-auto grid max-w-4xl grid-cols-1 gap-y-10 text-center md:grid-cols-3 md:gap-x-10"> <div className="mx-auto grid max-w-5xl grid-cols-1 gap-y-10 text-center md:grid-cols-2 lg:grid-cols-4 md:gap-x-10">
<div> <div>
<p className="text-[10px] smallcaps text-clay-500 mb-3">{t("home.about.team")}</p> <p className="text-[10px] smallcaps text-clay-500 mb-3">{t("home.about.team")}</p>
<p className="font-serif italic text-clay-700 text-base leading-relaxed"> <p className="font-serif italic text-clay-700 text-base leading-relaxed">
@@ -2134,6 +2134,22 @@ export default function HomePage() {
<span className="font-sans text-sm text-clay-900">575404333</span> <span className="font-sans text-sm text-clay-900">575404333</span>
</p> </p>
</div> </div>
<div>
<p className="text-[10px] smallcaps text-clay-500 mb-3">{t("home.ui.feedback")}</p>
<p className="font-serif text-clay-700 text-base leading-relaxed mb-4">
{t("home.about.feedbackDescription")}
</p>
<a
href="https://tally.so/r/VLqO1M"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-clay-700 hover:text-ember-500 transition-colors"
>
<i className="fa-solid fa-comment-dots text-[15px]" />
<span className="font-sans text-sm">{t("home.ui.submitFeedback")}</span>
</a>
</div>
</div> </div>
<div className="hairline-full w-full mt-14 md:mt-20 mb-12 md:mb-16" /> <div className="hairline-full w-full mt-14 md:mt-20 mb-12 md:mb-16" />
@@ -2155,6 +2171,7 @@ export default function HomePage() {
</div> </div>
</footer> </footer>
{styleOpen && styleRow >= 0 && ( {styleOpen && styleRow >= 0 && (
<StyleModal <StyleModal
items={OPTS[styleRow]!.items} items={OPTS[styleRow]!.items}
+44 -85
View File
@@ -34,7 +34,6 @@ import {
startSession, startSession,
requestScene, requestScene,
visionDecide, visionDecide,
classifyFreeform,
requestInsertBeat, requestInsertBeat,
getTtsProvider, getTtsProvider,
AuthRequiredError, AuthRequiredError,
@@ -1503,6 +1502,8 @@ function PlayInner() {
useEffect(() => { useEffect(() => {
function onKey(e: KeyboardEvent) { function onKey(e: KeyboardEvent) {
const tag = (e.target as HTMLElement)?.tagName;
if (tag === "INPUT" || tag === "TEXTAREA" || (e.target as HTMLElement)?.isContentEditable) return;
if (e.key === "f" || e.key === "F") { if (e.key === "f" || e.key === "F") {
if (e.metaKey || e.ctrlKey || e.altKey) return; if (e.metaKey || e.ctrlKey || e.altKey) return;
e.preventDefault(); e.preventDefault();
@@ -2248,68 +2249,12 @@ function PlayInner() {
setPhase("vision-thinking"); setPhase("vision-thinking");
try { try {
const decision = await classifyFreeform({ // Always generate a new scene for freeform text input — the player
session, // typed something, so they expect the story to move forward.
freeformText: text,
});
if (decision.classify === "insert-beat") {
// Interactive beat: NPC responds to the player's action, scene stays
setPhase("inserting-beat");
const { partial, characters: insertChars } = await requestInsertBeat({
session,
freeformAction: decision.freeformAction,
clientTts: !!byoTtsRef.current,
});
const fromBeatId =
currentBeatRef.current?.id ?? currentScene.entryBeatId;
const newBeatId = `b_ins_${Date.now()}_${Math.random()
.toString(36)
.slice(2, 6)}`;
const newBeat: Beat = {
id: newBeatId,
narration: partial.narration,
speaker: partial.speaker,
line: partial.line,
lineDelivery: partial.lineDelivery,
next: { type: "continue", nextBeatId: fromBeatId },
};
const patched: Scene = {
...currentScene,
beats: [...currentScene.beats, newBeat],
};
const nextVisited = [...visitedBeatsRef.current, newBeatId];
visitedBeatsRef.current = nextVisited;
const nextSession: Session = {
...session,
history: session.history.map((h, i, arr) =>
i === arr.length - 1 ? { ...h, scene: patched, visitedBeatIds: nextVisited } : h,
),
characters: insertChars,
};
setSession(nextSession);
setCurrentScene(patched);
setCurrentBeatId(newBeatId);
if (newBeat.speaker && newBeat.line) {
void fetchBeatAudio(nextSession, {
id: newBeatId,
speaker: newBeat.speaker,
line: newBeat.line,
lineDelivery: newBeat.lineDelivery,
});
}
setLastExitLabel(decision.freeformAction);
setPhase("ready");
return;
}
// change-scene path
const visited = [...visitedBeatsRef.current]; const visited = [...visitedBeatsRef.current];
const exit: SceneExit = { const exit: SceneExit = {
kind: "freeform", kind: "freeform",
action: decision.freeformAction, action: text,
}; };
clearPool(poolRef.current); clearPool(poolRef.current);
@@ -2335,7 +2280,7 @@ function PlayInner() {
promise, promise,
exit, exit,
visited, visited,
decision.freeformAction, text,
() => onFreeformInput(text), () => onFreeformInput(text),
{ kind: "freeform", text }, { kind: "freeform", text },
); );
@@ -2365,7 +2310,7 @@ function PlayInner() {
if (decision.classify === "insert-beat") { if (decision.classify === "insert-beat") {
setPhase("inserting-beat"); setPhase("inserting-beat");
const { partial, characters: insertChars } = await requestInsertBeat({ const { partial, extraBeats, characters: insertChars } = await requestInsertBeat({
session, session,
freeformAction: decision.intent.freeformAction, freeformAction: decision.intent.freeformAction,
clientTts: !!byoTtsRef.current, clientTts: !!byoTtsRef.current,
@@ -2373,42 +2318,56 @@ function PlayInner() {
const fromBeatId = const fromBeatId =
currentBeatRef.current?.id ?? currentScene.entryBeatId; currentBeatRef.current?.id ?? currentScene.entryBeatId;
const newBeatId = `b_ins_${Date.now()}_${Math.random() const allPartials = [partial, ...(extraBeats ?? [])];
.toString(36) const newBeats: Beat[] = [];
.slice(2, 6)}`; const newBeatIds: string[] = [];
const newBeat: Beat = {
id: newBeatId, for (const [i, p] of allPartials.entries()) {
narration: partial.narration, const id = `b_ins_${Date.now()}_${Math.random().toString(36).slice(2, 6)}_${i}`;
speaker: partial.speaker, newBeatIds.push(id);
line: partial.line, newBeats.push({
lineDelivery: partial.lineDelivery, id,
next: { type: "continue", nextBeatId: fromBeatId }, narration: p.narration,
}; speaker: p.speaker,
line: p.line,
lineDelivery: p.lineDelivery,
next: { type: "continue", nextBeatId: "" },
});
}
// Chain beats: each points to the next; last one loops back to original beat
for (let i = 0; i < newBeats.length - 1; i++) {
newBeats[i]!.next = { type: "continue", nextBeatId: newBeatIds[i + 1]! };
}
newBeats[newBeats.length - 1]!.next = { type: "continue", nextBeatId: fromBeatId };
const patched: Scene = { const patched: Scene = {
...currentScene, ...currentScene,
beats: [...currentScene.beats, newBeat], beats: [...currentScene.beats, ...newBeats],
}; };
const nextVisited = [...visitedBeatsRef.current, ...newBeatIds];
visitedBeatsRef.current = nextVisited;
const nextSession: Session = { const nextSession: Session = {
...session, ...session,
history: session.history.map((h, i, arr) => history: session.history.map((h, i, arr) =>
i === arr.length - 1 ? { ...h, scene: patched } : h, i === arr.length - 1 ? { ...h, scene: patched, visitedBeatIds: nextVisited } : h,
), ),
characters: insertChars, characters: insertChars,
}; };
setSession(nextSession); setSession(nextSession);
setCurrentScene(patched); setCurrentScene(patched);
setCurrentBeatId(newBeatId); setCurrentBeatId(newBeatIds[0]!);
// Insert-beat doesn't change scene.id, so the scene effect won't
// re-fire — manually kick off the audio fetch for the new beat. for (const nb of newBeats) {
if (newBeat.speaker && newBeat.line) { if (nb.speaker && nb.line) {
void fetchBeatAudio(nextSession, { void fetchBeatAudio(nextSession, {
id: newBeatId, id: nb.id,
speaker: newBeat.speaker, speaker: nb.speaker,
line: newBeat.line, line: nb.line,
lineDelivery: newBeat.lineDelivery, lineDelivery: nb.lineDelivery,
}); });
}
} }
setLastExitLabel(decision.intent.freeformAction); setLastExitLabel(decision.intent.freeformAction);
setPhase("ready"); setPhase("ready");
+7 -7
View File
@@ -148,13 +148,13 @@ function ChoiceButton({
/> />
<span className="relative flex items-baseline gap-2"> <span className="relative flex items-baseline gap-2">
<span <span
className={`shrink-0 font-serif num ${vertical ? "text-[13px]" : "text-[11px]"}`} className={`shrink-0 font-serif num ${vertical ? "text-[16px]" : "text-[14px]"}`}
style={{ color: "rgba(195,155,75,0.9)" }} style={{ color: "rgba(195,155,75,0.9)" }}
> >
{index + 1}. {index + 1}.
</span> </span>
<span <span
className={`font-serif leading-snug ${vertical ? "text-[15px]" : "text-[13px] md:text-[14px]"}`} className={`font-serif leading-snug ${vertical ? "text-[18px]" : "text-[16px] md:text-[17px]"}`}
style={{ color: "rgba(245,235,210,0.95)" }} style={{ color: "rgba(245,235,210,0.95)" }}
> >
{label} {label}
@@ -518,7 +518,7 @@ export function PlayCanvas({
placeholder={t("play.freeform.placeholder")} placeholder={t("play.freeform.placeholder")}
maxLength={50} maxLength={50}
autoFocus autoFocus
className="flex-1 min-w-0 bg-transparent border-none outline-none font-serif text-[14px] placeholder:text-[rgba(200,185,155,0.50)]" className="flex-1 min-w-0 bg-transparent border-none outline-none font-serif text-[17px] placeholder:text-[rgba(200,185,155,0.50)]"
style={{ color: "rgba(245,235,210,0.95)" }} style={{ color: "rgba(245,235,210,0.95)" }}
/> />
<button <button
@@ -593,7 +593,7 @@ export function PlayCanvas({
style={{ color: "rgba(195,155,75,0.60)" }} style={{ color: "rgba(195,155,75,0.60)" }}
/> />
<span <span
className="font-serif text-[13px]" className="font-serif text-[16px]"
style={{ color: "rgba(200,185,155,0.70)" }} style={{ color: "rgba(200,185,155,0.70)" }}
> >
{t("play.freeform.title")} {t("play.freeform.title")}
@@ -644,7 +644,7 @@ export function PlayCanvas({
{beat.speaker && ( {beat.speaker && (
<p <p
className={`font-serif smallcaps mb-[0.6em] ${ className={`font-serif smallcaps mb-[0.6em] ${
portrait ? "text-[13px]" : "text-[11px] md:text-[12px]" portrait ? "text-[16px]" : "text-[14px] md:text-[15px]"
}`} }`}
style={{ color: "rgba(205,165,90,0.92)" }} style={{ color: "rgba(205,165,90,0.92)" }}
> >
@@ -659,7 +659,7 @@ export function PlayCanvas({
{beat.speaker && beat.narration && ( {beat.speaker && beat.narration && (
<p <p
className={`font-serif leading-[1.85] mb-[0.6em] ${ className={`font-serif leading-[1.85] mb-[0.6em] ${
portrait ? "text-[15px]" : "text-[12px] md:text-[14px]" portrait ? "text-[18px]" : "text-[15px] md:text-[17px]"
}`} }`}
style={{ color: "rgba(228,218,196,0.88)" }} style={{ color: "rgba(228,218,196,0.88)" }}
> >
@@ -669,7 +669,7 @@ export function PlayCanvas({
<p <p
className={`font-serif leading-[1.85] ${ className={`font-serif leading-[1.85] ${
portrait ? "text-[16px]" : "text-[13px] md:text-[15px]" portrait ? "text-[19px]" : "text-[16px] md:text-[18px]"
}`} }`}
style={{ color: "rgba(245,235,210,0.95)" }} style={{ color: "rgba(245,235,210,0.95)" }}
> >
+27 -35
View File
@@ -1,8 +1,8 @@
<svg width="680" height="692" viewBox="0 0 680 692" role="img" <svg width="680" height="594" viewBox="0 0 680 594" role="img"
aria-label="InfiPlot interactive story generation pipeline" xmlns="http://www.w3.org/2000/svg" aria-label="InfiPlot interactive story generation pipeline" xmlns="http://www.w3.org/2000/svg"
font-family='"Anthropic Sans", -apple-system, system-ui, "Segoe UI", sans-serif'> font-family='"Anthropic Sans", -apple-system, system-ui, "Segoe UI", sans-serif'>
<title>AI interactive story generation pipeline</title> <title>AI interactive story generation pipeline</title>
<desc>From your input to the Architect and Writer, then per-scene parallel Character Designer and Cinematographer feeding the Painter, producing one scene and speculatively pre-generating back to the Writer.</desc> <desc>From your input to the Writer, then per-scene parallel Character Designer and Cinematographer feeding the Painter, producing one scene and speculatively pre-generating back to the Writer.</desc>
<defs> <defs>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M2 1L8 5L2 9" fill="none" stroke="#9c9a92" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M2 1L8 5L2 9" fill="none" stroke="#9c9a92" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
@@ -12,7 +12,7 @@
</marker> </marker>
</defs> </defs>
<rect x="1" y="1" width="678" height="690" rx="16" fill="#1f1e1d" stroke="#34322e" stroke-width="1.5"/> <rect x="1" y="1" width="678" height="592" rx="16" fill="#1f1e1d" stroke="#34322e" stroke-width="1.5"/>
<!-- Your input --> <!-- Your input -->
<g> <g>
@@ -22,61 +22,53 @@
</g> </g>
<line x1="340" y1="92" x2="340" y2="120" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="340" y1="92" x2="340" y2="120" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- Architect -->
<g>
<rect x="215" y="122" width="250" height="68" rx="8" fill="#3c3489" stroke="#afa9ec" stroke-width="0.5"/>
<text x="340" y="147" text-anchor="middle" dominant-baseline="central" fill="#cecbf6" font-size="14" font-weight="500">Architect</text>
<text x="340" y="167" text-anchor="middle" dominant-baseline="central" fill="#afa9ec" font-size="12">parses input → full story structure</text>
</g>
<line x1="340" y1="190" x2="340" y2="218" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- Writer --> <!-- Writer -->
<g> <g>
<rect x="220" y="220" width="240" height="68" rx="8" fill="#085041" stroke="#5dcaa5" stroke-width="0.5"/> <rect x="220" y="122" width="240" height="68" rx="8" fill="#085041" stroke="#5dcaa5" stroke-width="0.5"/>
<text x="340" y="245" text-anchor="middle" dominant-baseline="central" fill="#9fe1cb" font-size="14" font-weight="500">Writer</text> <text x="340" y="147" text-anchor="middle" dominant-baseline="central" fill="#9fe1cb" font-size="14" font-weight="500">Writer</text>
<text x="340" y="265" text-anchor="middle" dominant-baseline="central" fill="#5dcaa5" font-size="12">narration · dialogue · choices</text> <text x="340" y="167" text-anchor="middle" dominant-baseline="central" fill="#5dcaa5" font-size="12">story structure · narration · dialogue · choices</text>
</g> </g>
<!-- Generating one scene (group) --> <!-- Generating one scene (group) -->
<rect x="40" y="308" width="560" height="242" rx="16" fill="none" stroke="rgba(222,220,209,0.3)" stroke-width="0.5" stroke-dasharray="6 5"/> <rect x="40" y="210" width="560" height="242" rx="16" fill="none" stroke="rgba(222,220,209,0.3)" stroke-width="0.5" stroke-dasharray="6 5"/>
<text x="58" y="334" text-anchor="start" fill="#c2c0b6" font-size="12">Generating one scene</text> <text x="58" y="236" text-anchor="start" fill="#c2c0b6" font-size="12">Generating one scene</text>
<line x1="340" y1="288" x2="211" y2="344" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="340" y1="190" x2="211" y2="246" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<line x1="340" y1="288" x2="457" y2="344" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="340" y1="190" x2="457" y2="246" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- Character Designer --> <!-- Character Designer -->
<g> <g>
<rect x="58" y="354" width="250" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/> <rect x="58" y="256" width="250" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/>
<text x="183" y="379" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">Character Designer</text> <text x="183" y="281" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">Character Designer</text>
<text x="183" y="399" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">portrait + voice (parallel)</text> <text x="183" y="301" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">portrait + voice (parallel)</text>
</g> </g>
<!-- Cinematographer --> <!-- Cinematographer -->
<g> <g>
<rect x="332" y="354" width="250" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/> <rect x="332" y="256" width="250" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/>
<text x="457" y="379" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">Cinematographer</text> <text x="457" y="281" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">Cinematographer</text>
<text x="457" y="399" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">shot composition + background prompt</text> <text x="457" y="301" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">shot composition + background prompt</text>
</g> </g>
<line x1="183" y1="422" x2="280" y2="448" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="183" y1="324" x2="280" y2="350" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<line x1="457" y1="422" x2="360" y2="448" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="457" y1="324" x2="360" y2="350" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- Painter --> <!-- Painter -->
<g> <g>
<rect x="190" y="458" width="260" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/> <rect x="190" y="360" width="260" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/>
<text x="320" y="483" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">Painter</text> <text x="320" y="385" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">Painter</text>
<text x="320" y="503" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">renders 16:9 background from portraits</text> <text x="320" y="405" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">renders 16:9 background from portraits</text>
</g> </g>
<line x1="320" y1="526" x2="340" y2="576" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="320" y1="428" x2="340" y2="478" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- One scene --> <!-- One scene -->
<g> <g>
<rect x="220" y="586" width="240" height="68" rx="8" fill="#444441" stroke="#b4b2a9" stroke-width="0.5"/> <rect x="220" y="488" width="240" height="68" rx="8" fill="#444441" stroke="#b4b2a9" stroke-width="0.5"/>
<text x="340" y="611" text-anchor="middle" dominant-baseline="central" fill="#d3d1c7" font-size="14" font-weight="500">One scene</text> <text x="340" y="513" text-anchor="middle" dominant-baseline="central" fill="#d3d1c7" font-size="14" font-weight="500">One scene</text>
<text x="340" y="631" text-anchor="middle" dominant-baseline="central" fill="#b4b2a9" font-size="12">background image + beat tree</text> <text x="340" y="533" text-anchor="middle" dominant-baseline="central" fill="#b4b2a9" font-size="12">background image + beat tree</text>
</g> </g>
<!-- speculative pre-generation loop --> <!-- speculative pre-generation loop -->
<text x="542" y="236" text-anchor="middle" fill="#c2c0b6" font-size="12">pre-generate next scene</text> <text x="542" y="138" text-anchor="middle" fill="#c2c0b6" font-size="12">pre-generate next scene</text>
<path d="M460 620 L625 620 L625 250 L460 250" fill="none" stroke="#1D9E75" stroke-width="1" stroke-dasharray="6 5" marker-end="url(#arrowGreen)"/> <path d="M460 522 L625 522 L625 152 L460 152" fill="none" stroke="#1D9E75" stroke-width="1" stroke-dasharray="6 5" marker-end="url(#arrowGreen)"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

+32 -41
View File
@@ -1,8 +1,8 @@
<svg width="680" height="692" viewBox="0 0 680 692" role="img" <svg width="680" height="594" viewBox="0 0 680 594" role="img"
aria-label="InfiPlot インタラクティブ物語生成パイプライン" xmlns="http://www.w3.org/2000/svg" aria-label="InfiPlot インタラクティブ物語生成パイプライン" xmlns="http://www.w3.org/2000/svg"
font-family='"Anthropic Sans", -apple-system, system-ui, "Segoe UI", "Hiragino Sans", "Noto Sans JP", sans-serif'> font-family='"Anthropic Sans", -apple-system, system-ui, "Segoe UI", "Hiragino Sans", "Noto Sans JP", sans-serif'>
<title>AI インタラクティブ物語生成パイプライン</title> <title>AI インタラクティブ物語生成パイプライン</title>
<desc>あなたの入力からアーキテクト・脚本家へ、各シーンで並行するキャラクターデザイナーと撮影監督が絵師に渡り、1 シーンを生成し、脚本家へ先回り生成で戻ります。</desc> <desc>あなたの入力から脚本家へ、各シーンで並行するキャラクターデザイナーと撮影監督が絵師に渡り、1 シーンを生成し、脚本家へ先回り生成で戻ります。</desc>
<defs> <defs>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M2 1L8 5L2 9" fill="none" stroke="#9c9a92" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M2 1L8 5L2 9" fill="none" stroke="#9c9a92" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
@@ -12,7 +12,7 @@
</marker> </marker>
</defs> </defs>
<rect x="1" y="1" width="678" height="690" rx="16" fill="#1f1e1d" stroke="#34322e" stroke-width="1.5"/> <rect x="1" y="1" width="678" height="592" rx="16" fill="#1f1e1d" stroke="#34322e" stroke-width="1.5"/>
<!-- あなたの入力 --> <!-- あなたの入力 -->
<g> <g>
@@ -23,67 +23,58 @@
</g> </g>
<line x1="340" y1="92" x2="340" y2="120" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="340" y1="92" x2="340" y2="120" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- アーキテクト -->
<g>
<rect x="215" y="122" width="250" height="68" rx="8" fill="#3c3489" stroke="#afa9ec" stroke-width="0.5"/>
<text x="340" y="144" text-anchor="middle" dominant-baseline="central" fill="#cecbf6" font-size="14" font-weight="500">アーキテクト</text>
<text x="340" y="162" text-anchor="middle" dominant-baseline="central" fill="#afa9ec" font-size="12">Architect</text>
<text x="340" y="178" text-anchor="middle" dominant-baseline="central" fill="#afa9ec" font-size="12">入力を解析 → 物語の全体構造</text>
</g>
<line x1="340" y1="190" x2="340" y2="218" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 脚本家 --> <!-- 脚本家 -->
<g> <g>
<rect x="220" y="220" width="240" height="68" rx="8" fill="#085041" stroke="#5dcaa5" stroke-width="0.5"/> <rect x="220" y="122" width="240" height="68" rx="8" fill="#085041" stroke="#5dcaa5" stroke-width="0.5"/>
<text x="340" y="242" text-anchor="middle" dominant-baseline="central" fill="#9fe1cb" font-size="14" font-weight="500">脚本家</text> <text x="340" y="144" text-anchor="middle" dominant-baseline="central" fill="#9fe1cb" font-size="14" font-weight="500">脚本家</text>
<text x="340" y="260" text-anchor="middle" dominant-baseline="central" fill="#5dcaa5" font-size="12">Writer</text> <text x="340" y="162" text-anchor="middle" dominant-baseline="central" fill="#5dcaa5" font-size="12">Writer</text>
<text x="340" y="276" text-anchor="middle" dominant-baseline="central" fill="#5dcaa5" font-size="12">ナレーション · セリフ · 選択肢</text> <text x="340" y="178" text-anchor="middle" dominant-baseline="central" fill="#5dcaa5" font-size="12">物語構造 · ナレーション · セリフ · 選択肢</text>
</g> </g>
<!-- 1 シーンの生成(グループ) --> <!-- 1 シーンの生成(グループ) -->
<rect x="40" y="308" width="560" height="242" rx="16" fill="none" stroke="rgba(222,220,209,0.3)" stroke-width="0.5" stroke-dasharray="6 5"/> <rect x="40" y="210" width="560" height="242" rx="16" fill="none" stroke="rgba(222,220,209,0.3)" stroke-width="0.5" stroke-dasharray="6 5"/>
<text x="58" y="334" text-anchor="start" fill="#c2c0b6" font-size="12">1 シーンの生成</text> <text x="58" y="236" text-anchor="start" fill="#c2c0b6" font-size="12">1 シーンの生成</text>
<line x1="340" y1="288" x2="211" y2="344" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="340" y1="190" x2="211" y2="246" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<line x1="340" y1="288" x2="457" y2="344" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="340" y1="190" x2="457" y2="246" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- キャラクターデザイナー --> <!-- キャラクターデザイナー -->
<g> <g>
<rect x="58" y="354" width="250" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/> <rect x="58" y="256" width="250" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/>
<text x="183" y="376" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">キャラクターデザイナー</text> <text x="183" y="278" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">キャラクターデザイナー</text>
<text x="183" y="394" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">Character Designer</text> <text x="183" y="296" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">Character Designer</text>
<text x="183" y="410" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">立ち絵 + 声 · 並行</text> <text x="183" y="312" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">立ち絵 + 声 · 並行</text>
</g> </g>
<!-- 撮影監督 --> <!-- 撮影監督 -->
<g> <g>
<rect x="332" y="354" width="250" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/> <rect x="332" y="256" width="250" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/>
<text x="457" y="376" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">撮影監督</text> <text x="457" y="278" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">撮影監督</text>
<text x="457" y="394" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">Cinematographer</text> <text x="457" y="296" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">Cinematographer</text>
<text x="457" y="410" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">カメラ構成 + 背景プロンプト</text> <text x="457" y="312" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">カメラ構成 + 背景プロンプト</text>
</g> </g>
<line x1="183" y1="422" x2="280" y2="448" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="183" y1="324" x2="280" y2="350" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<line x1="457" y1="422" x2="360" y2="448" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="457" y1="324" x2="360" y2="350" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 絵師 --> <!-- 絵師 -->
<g> <g>
<rect x="190" y="458" width="260" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/> <rect x="190" y="360" width="260" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/>
<text x="320" y="480" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">絵師</text> <text x="320" y="382" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">絵師</text>
<text x="320" y="498" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">Painter</text> <text x="320" y="400" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">Painter</text>
<text x="320" y="514" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">立ち絵を参照に 16:9 背景</text> <text x="320" y="416" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">立ち絵を参照に 16:9 背景</text>
</g> </g>
<line x1="320" y1="526" x2="340" y2="576" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="320" y1="428" x2="340" y2="478" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 1 シーン --> <!-- 1 シーン -->
<g> <g>
<rect x="220" y="586" width="240" height="68" rx="8" fill="#444441" stroke="#b4b2a9" stroke-width="0.5"/> <rect x="220" y="488" width="240" height="68" rx="8" fill="#444441" stroke="#b4b2a9" stroke-width="0.5"/>
<text x="340" y="608" text-anchor="middle" dominant-baseline="central" fill="#d3d1c7" font-size="14" font-weight="500">1 シーン</text> <text x="340" y="510" text-anchor="middle" dominant-baseline="central" fill="#d3d1c7" font-size="14" font-weight="500">1 シーン</text>
<text x="340" y="626" text-anchor="middle" dominant-baseline="central" fill="#b4b2a9" font-size="12">Scene output</text> <text x="340" y="528" text-anchor="middle" dominant-baseline="central" fill="#b4b2a9" font-size="12">Scene output</text>
<text x="340" y="642" text-anchor="middle" dominant-baseline="central" fill="#b4b2a9" font-size="12">背景画 + ビートツリー</text> <text x="340" y="544" text-anchor="middle" dominant-baseline="central" fill="#b4b2a9" font-size="12">背景画 + ビートツリー</text>
</g> </g>
<!-- 先回り生成ループ --> <!-- 先回り生成ループ -->
<text x="542" y="236" text-anchor="middle" fill="#c2c0b6" font-size="12">次のシーンを先回り生成</text> <text x="542" y="138" text-anchor="middle" fill="#c2c0b6" font-size="12">次のシーンを先回り生成</text>
<path d="M460 620 L625 620 L625 250 L460 250" fill="none" stroke="#1D9E75" stroke-width="1" stroke-dasharray="6 5" marker-end="url(#arrowGreen)"/> <path d="M460 522 L625 522 L625 152 L460 152" fill="none" stroke="#1D9E75" stroke-width="1" stroke-dasharray="6 5" marker-end="url(#arrowGreen)"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

+32 -41
View File
@@ -1,8 +1,8 @@
<svg width="680" height="692" viewBox="0 0 680 692" role="img" <svg width="680" height="594" viewBox="0 0 680 594" role="img"
aria-label="InfiPlot 互动剧情生成流水线" xmlns="http://www.w3.org/2000/svg" aria-label="InfiPlot 互动剧情生成流水线" xmlns="http://www.w3.org/2000/svg"
font-family='"Anthropic Sans", -apple-system, system-ui, "Segoe UI", sans-serif'> font-family='"Anthropic Sans", -apple-system, system-ui, "Segoe UI", sans-serif'>
<title>AI 互动剧情生成流水线流程图</title> <title>AI 互动剧情生成流水线流程图</title>
<desc>从用户输入到架构师、编剧,再到每一幕场景内并行的角色设计师与场景布置师,汇入画家渲染,产出一幕场景,并预测式预生成回到编剧。</desc> <desc>从用户输入到编剧,再到每一幕场景内并行的角色设计师与场景布置师,汇入画家渲染,产出一幕场景,并预测式预生成回到编剧。</desc>
<defs> <defs>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"> <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M2 1L8 5L2 9" fill="none" stroke="#9c9a92" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M2 1L8 5L2 9" fill="none" stroke="#9c9a92" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
@@ -12,7 +12,7 @@
</marker> </marker>
</defs> </defs>
<rect x="1" y="1" width="678" height="690" rx="16" fill="#1f1e1d" stroke="#34322e" stroke-width="1.5"/> <rect x="1" y="1" width="678" height="592" rx="16" fill="#1f1e1d" stroke="#34322e" stroke-width="1.5"/>
<!-- 你的输入 --> <!-- 你的输入 -->
<g> <g>
@@ -23,67 +23,58 @@
</g> </g>
<line x1="340" y1="92" x2="340" y2="120" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="340" y1="92" x2="340" y2="120" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 架构师 -->
<g>
<rect x="215" y="122" width="250" height="68" rx="8" fill="#3c3489" stroke="#afa9ec" stroke-width="0.5"/>
<text x="340" y="144" text-anchor="middle" dominant-baseline="central" fill="#cecbf6" font-size="14" font-weight="500">架构师</text>
<text x="340" y="162" text-anchor="middle" dominant-baseline="central" fill="#afa9ec" font-size="12">Architect</text>
<text x="340" y="178" text-anchor="middle" dominant-baseline="central" fill="#afa9ec" font-size="12">解析输入 → 完整剧情结构</text>
</g>
<line x1="340" y1="190" x2="340" y2="218" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 编剧 --> <!-- 编剧 -->
<g> <g>
<rect x="220" y="220" width="240" height="68" rx="8" fill="#085041" stroke="#5dcaa5" stroke-width="0.5"/> <rect x="220" y="122" width="240" height="68" rx="8" fill="#085041" stroke="#5dcaa5" stroke-width="0.5"/>
<text x="340" y="242" text-anchor="middle" dominant-baseline="central" fill="#9fe1cb" font-size="14" font-weight="500">编剧</text> <text x="340" y="144" text-anchor="middle" dominant-baseline="central" fill="#9fe1cb" font-size="14" font-weight="500">编剧</text>
<text x="340" y="260" text-anchor="middle" dominant-baseline="central" fill="#5dcaa5" font-size="12">Writer</text> <text x="340" y="162" text-anchor="middle" dominant-baseline="central" fill="#5dcaa5" font-size="12">Writer</text>
<text x="340" y="276" text-anchor="middle" dominant-baseline="central" fill="#5dcaa5" font-size="12">节拍:旁白 · 对话 · 选项</text> <text x="340" y="178" text-anchor="middle" dominant-baseline="central" fill="#5dcaa5" font-size="12">剧情架构 · 旁白 · 对话 · 选项</text>
</g> </g>
<!-- 每一幕场景的生成(分组框) --> <!-- 每一幕场景的生成(分组框) -->
<rect x="40" y="308" width="560" height="242" rx="16" fill="none" stroke="rgba(222,220,209,0.3)" stroke-width="0.5" stroke-dasharray="6 5"/> <rect x="40" y="210" width="560" height="242" rx="16" fill="none" stroke="rgba(222,220,209,0.3)" stroke-width="0.5" stroke-dasharray="6 5"/>
<text x="58" y="334" text-anchor="start" fill="#c2c0b6" font-size="12">每一幕场景的生成</text> <text x="58" y="236" text-anchor="start" fill="#c2c0b6" font-size="12">每一幕场景的生成</text>
<line x1="340" y1="288" x2="211" y2="344" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="340" y1="190" x2="211" y2="246" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<line x1="340" y1="288" x2="457" y2="344" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="340" y1="190" x2="457" y2="246" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 角色设计师 --> <!-- 角色设计师 -->
<g> <g>
<rect x="58" y="354" width="250" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/> <rect x="58" y="256" width="250" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/>
<text x="183" y="376" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">角色设计师</text> <text x="183" y="278" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">角色设计师</text>
<text x="183" y="394" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">Character Designer</text> <text x="183" y="296" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">Character Designer</text>
<text x="183" y="410" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">立绘 + 音色 · 并行</text> <text x="183" y="312" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">立绘 + 音色 · 并行</text>
</g> </g>
<!-- 场景布置师 --> <!-- 场景布置师 -->
<g> <g>
<rect x="332" y="354" width="250" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/> <rect x="332" y="256" width="250" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/>
<text x="457" y="376" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">场景布置师</text> <text x="457" y="278" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">场景布置师</text>
<text x="457" y="394" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">Cinematographer</text> <text x="457" y="296" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">Cinematographer</text>
<text x="457" y="410" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">镜头编排 + 背景提示词</text> <text x="457" y="312" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">镜头编排 + 背景提示词</text>
</g> </g>
<line x1="183" y1="422" x2="280" y2="448" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="183" y1="324" x2="280" y2="350" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<line x1="457" y1="422" x2="360" y2="448" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="457" y1="324" x2="360" y2="350" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 画家 --> <!-- 画家 -->
<g> <g>
<rect x="190" y="458" width="260" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/> <rect x="190" y="360" width="260" height="68" rx="8" fill="#712b13" stroke="#f0997b" stroke-width="0.5"/>
<text x="320" y="480" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">画家</text> <text x="320" y="382" text-anchor="middle" dominant-baseline="central" fill="#f5c4b3" font-size="14" font-weight="500">画家</text>
<text x="320" y="498" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">Painter</text> <text x="320" y="400" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">Painter</text>
<text x="320" y="514" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">以立绘为参考渲染 16:9 背景</text> <text x="320" y="416" text-anchor="middle" dominant-baseline="central" fill="#f0997b" font-size="12">以立绘为参考渲染 16:9 背景</text>
</g> </g>
<line x1="320" y1="526" x2="340" y2="576" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/> <line x1="320" y1="428" x2="340" y2="478" stroke="#9c9a92" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 一幕场景 --> <!-- 一幕场景 -->
<g> <g>
<rect x="220" y="586" width="240" height="68" rx="8" fill="#444441" stroke="#b4b2a9" stroke-width="0.5"/> <rect x="220" y="488" width="240" height="68" rx="8" fill="#444441" stroke="#b4b2a9" stroke-width="0.5"/>
<text x="340" y="608" text-anchor="middle" dominant-baseline="central" fill="#d3d1c7" font-size="14" font-weight="500">一幕场景</text> <text x="340" y="510" text-anchor="middle" dominant-baseline="central" fill="#d3d1c7" font-size="14" font-weight="500">一幕场景</text>
<text x="340" y="626" text-anchor="middle" dominant-baseline="central" fill="#b4b2a9" font-size="12">Scene output</text> <text x="340" y="528" text-anchor="middle" dominant-baseline="central" fill="#b4b2a9" font-size="12">Scene output</text>
<text x="340" y="642" text-anchor="middle" dominant-baseline="central" fill="#b4b2a9" font-size="12">背景图 + 节拍树</text> <text x="340" y="544" text-anchor="middle" dominant-baseline="central" fill="#b4b2a9" font-size="12">背景图 + 节拍树</text>
</g> </g>
<!-- 预测式预生成回环 --> <!-- 预测式预生成回环 -->
<text x="542" y="236" text-anchor="middle" fill="#c2c0b6" font-size="12">预测式预生成下一幕</text> <text x="542" y="138" text-anchor="middle" fill="#c2c0b6" font-size="12">预测式预生成下一幕</text>
<path d="M460 620 L625 620 L625 250 L460 250" fill="none" stroke="#1D9E75" stroke-width="1" stroke-dasharray="6 5" marker-end="url(#arrowGreen)"/> <path d="M460 522 L625 522 L625 152 L460 152" fill="none" stroke="#1D9E75" stroke-width="1" stroke-dasharray="6 5" marker-end="url(#arrowGreen)"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

+40 -21
View File
@@ -6,6 +6,7 @@ import type {
Character, Character,
CharacterIntent, CharacterIntent,
EngineConfig, EngineConfig,
InsertBeatMulti,
InsertBeatPartial, InsertBeatPartial,
ProviderConfig, ProviderConfig,
Scene, Scene,
@@ -582,17 +583,32 @@ export async function directScene(
} }
// ────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────
// directInsertBeat — single-agent path for vision-driven in-scene // directInsertBeat — single-agent path for in-scene exploration.
// exploration. Generates ONE transient beat with NO new image, NO new // Generates 1-3 beats with NO new image, NO new characters, plus
// characters. Multi-agent pipeline doesn't apply here (no rendering, no // follow-up choices so the player isn't dumped back to the old options.
// character introduction allowed by the prompt).
// ────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────
function coerceBeatPartial(raw: Record<string, unknown>): InsertBeatPartial | null {
const narration = (typeof raw.narration === "string" ? raw.narration.trim() : undefined) || undefined;
const rawSpeaker = (typeof raw.speaker === "string" ? raw.speaker.trim() : undefined) || undefined;
const speaker = rawSpeaker ? normalizeSpeakerName(rawSpeaker) : undefined;
const line = (typeof raw.line === "string" ? raw.line.trim() : undefined) || undefined;
const lineDelivery =
line && speaker !== POV_DISPLAY_NAME
? ((typeof raw.lineDelivery === "string" ? raw.lineDelivery.trim() : undefined) || undefined)
: undefined;
if (!narration && !speaker && !line) return null;
if (line && !speaker) {
return { narration: [narration, line].filter(Boolean).join("\n") || undefined };
}
return { narration, speaker, line, lineDelivery };
}
export async function directInsertBeat( export async function directInsertBeat(
config: ProviderConfig, config: ProviderConfig,
session: Session, session: Session,
freeformAction: string, freeformAction: string,
): Promise<InsertBeatPartial> { ): Promise<InsertBeatPartial[]> {
const raw = await chat( const raw = await chat(
config, config,
[ [
@@ -605,22 +621,25 @@ export async function directInsertBeat(
{ temperature: 0.9, tag: "insert-beat" }, { temperature: 0.9, tag: "insert-beat" },
); );
const parsed = parseJsonLoose<InsertBeatPartial>(raw); const parsed = parseJsonLoose<InsertBeatMulti & InsertBeatPartial>(raw);
const narration = parsed.narration?.trim() || undefined; // Multi-beat format: { beats: [...] }
const rawSpeaker = parsed.speaker?.trim() || undefined; if (Array.isArray(parsed.beats) && parsed.beats.length > 0) {
// Pattern B (mirrors Writer): normalize POV variants → "你"; NPCs pass through. const beats = parsed.beats
const speaker = rawSpeaker ? normalizeSpeakerName(rawSpeaker) : undefined; .slice(0, 3)
const line = parsed.line?.trim() || undefined; .map((b) =>
// lineDelivery is only meaningful for NPC speakers (TTS). For POV ("你") b && typeof b === "object"
// TTS is intentionally skipped on the client, so lineDelivery is dropped. ? coerceBeatPartial(b as Record<string, unknown>)
const lineDelivery = : null,
line && speaker !== POV_DISPLAY_NAME )
? parsed.lineDelivery?.trim() || undefined .filter((b): b is InsertBeatPartial => b !== null);
: undefined; if (beats.length === 0) {
beats.push({ narration: "(你停下脚步,环视片刻。)" });
if (!narration && !speaker && !line) { }
return { narration: "(你停下脚步,环视片刻。)" }; return beats;
} }
return { narration, speaker, line, lineDelivery };
// Legacy single-beat fallback
const single = coerceBeatPartial(parsed as Record<string, unknown>);
return [single ?? { narration: "(你停下脚步,环视片刻。)" }];
} }
+26 -29
View File
@@ -196,45 +196,42 @@ export async function requestInsertBeat(
): Promise<InsertBeatResponse> { ): Promise<InsertBeatResponse> {
const tTotal = Date.now(); const tTotal = Date.now();
const partial = await directInsertBeat( const result = await directInsertBeat(
config.text, config.text,
req.session, req.session,
req.freeformAction, req.freeformAction,
); );
// INSERT_BEAT prompt forbids new NPCs — promote disallowed-speaker lines // Guard every beat: promote unregistered speakers to narration.
// to narration so the player still sees the text (the client only renders const guardedBeats = result.map((partial) => {
// `line` when there is a `speaker`). if (
// partial.speaker &&
// Exception (Pattern B): speaker = "你" is the player speaking. No partial.speaker !== "你" &&
// Character record exists for "你" (intentional — TTS is skipped), so we !req.session.characters.some((c) => c.name === partial.speaker)
// must NOT demote it; the client renders the dialog box correctly. ) {
// directInsertBeat already normalized POV variants to "你" before this console.warn(
// guard, so a literal "你" here is always Pattern B player dialog. `[insert-beat] unregistered speaker "${partial.speaker}" ignored`,
if ( );
partial.speaker && return {
partial.speaker !== "你" && narration:
!req.session.characters.some((c) => c.name === partial.speaker) [partial.narration, partial.line].filter(Boolean).join("\n") || undefined,
) {
console.warn(
`[insert-beat] unregistered speaker "${partial.speaker}" ignored`,
);
const promotedNarration =
[partial.narration, partial.line].filter(Boolean).join("\n") || undefined;
tlog("[insert-beat] TOTAL", tTotal);
return {
partial: {
narration: promotedNarration,
speaker: undefined, speaker: undefined,
line: undefined, line: undefined,
lineDelivery: undefined, lineDelivery: undefined,
}, };
characters: req.session.characters, }
}; return partial;
} });
const first = guardedBeats[0] ?? { narration: "(你停下脚步,环视片刻。)" };
const extra = guardedBeats.slice(1);
tlog("[insert-beat] TOTAL", tTotal); tlog("[insert-beat] TOTAL", tTotal);
return { partial, characters: req.session.characters }; return {
partial: first,
extraBeats: extra.length > 0 ? extra : undefined,
characters: req.session.characters,
};
} }
// ────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────
+19 -15
View File
@@ -572,18 +572,22 @@ STRICT RULES:
// Single-agent path; no character design / no rendering involved. // Single-agent path; no character design / no rendering involved.
// ────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────
export const INSERT_BEAT_SYSTEM = `你是视觉小说编剧。玩家在当前场景内做了一个自由动作(可能是点击画面中的某个物件/角色,也可能是主动输入了一句话/动作)。请基于此动作,写出**个有实质内容的 beat**。 export const INSERT_BEAT_SYSTEM = `你是视觉小说编剧。玩家在当前场景内做了一个自由动作(可能是点击画面中的某个物件/角色,也可能是主动输入了一句话/动作)。请基于此动作,写出**1-3 个有实质内容的 beat**。
核心原则——**玩家的动作必须得到回应**: 核心原则——**玩家的动作必须得到回应**:
- 如果当前场景有 NPC 在场,NPC **必须对玩家的动作做出反应**(说话、表情变化、动作回应)。用 narration 描述玩家的动作,用 speaker + line 写 NPC 的回应。 - 如果当前场景有 NPC 在场,NPC **必须对玩家的动作做出反应**(说话、表情变化、动作回应)。用 narration 描述玩家的动作,用 speaker + line 写 NPC 的回应。
- 如果场景中没有 NPC(纯环境),可以用 narration 描述玩家的观察/发现,给玩家一个新细节或情绪波动。 - 如果场景中没有 NPC(纯环境),可以用 narration 描述玩家的观察/发现,给玩家一个新细节或情绪波动。
- 不要写"你想做什么但没做"这种无意义的犹豫——玩家已经做了,世界要有反馈。 - 不要写"你想做什么但没做"这种无意义的犹豫——玩家已经做了,世界要有反馈。
beat 数量指引:
- 简单观察/短回应:1 个 beat 即可
- 有来有回的对话/有展开的互动:2-3 个 beat,让反应更有层次
- 每个 beat 的 narration + line ≤100 字
文本风格约束: 文本风格约束:
- narration / line 用中文,**纯净可显示文本**,不要写 (叹气)(语速快) 这类配音标注 - narration / line 用中文,**纯净可显示文本**,不要写 (叹气)(语速快) 这类配音标注
- narration 与 line 加起来 ≤100 字
- 不要打破当前场景的物理状态(玩家仍在原地) - 不要打破当前场景的物理状态(玩家仍在原地)
- 不要生成选项或下一步指引 —— 玩家点击会自然回到原 beat - 不要生成选项或下一步指引——播完后玩家会自然回到原来的选项
- 内容要"有所得"——一个新细节、一丝潜台词、一次真实的交流(show, don't tell - 内容要"有所得"——一个新细节、一丝潜台词、一次真实的交流(show, don't tell
- 白描为主:聚焦可观察的五感与物理特征,以角色的动作/神态本身传递情绪,不要以作者角度解释或议论;不写角色眼神/语气里的情绪(这些从台词与动作中自行体会) - 白描为主:聚焦可观察的五感与物理特征,以角色的动作/神态本身传递情绪,不要以作者角度解释或议论;不写角色眼神/语气里的情绪(这些从台词与动作中自行体会)
@@ -604,13 +608,12 @@ speaker 字段允许的取值**只有两种**(与主路径 Writer 一致 — P
必须输出严格 JSON 必须输出严格 JSON
{ {
"narration": "...", "beats": [
"speaker": "...", { "narration": "...", "speaker": "...", "line": "...", "lineDelivery": "..." }
"line": "...", ]
"lineDelivery": "..."
} }
narration/speaker/line/lineDelivery 都可为空字符串。不要输出 JSON 以外的任何文本。`; 不要输出 JSON 以外的任何文本。`;
export function buildInsertBeatUserMessage( export function buildInsertBeatUserMessage(
session: Session, session: Session,
@@ -655,7 +658,7 @@ export function buildInsertBeatUserMessage(
} }
parts.push(`\n玩家此刻的自由动作:${freeformAction}`); parts.push(`\n玩家此刻的自由动作:${freeformAction}`);
parts.push("\n请生成一个有实质回应的 beat,严格以 JSON 格式返回。"); parts.push("\n请生成 1-3 个 beat,严格以 JSON 格式返回。");
const langDirective = buildLanguageDirective(session.language); const langDirective = buildLanguageDirective(session.language);
if (langDirective) parts.push(langDirective); if (langDirective) parts.push(langDirective);
return parts.join("\n"); return parts.join("\n");
@@ -670,11 +673,12 @@ export function buildInsertBeatUserMessage(
export const VISION_SYSTEM_PROMPT = `你是视觉理解助手。玩家在视觉小说的背景图上点击了红色圆点位置(HTML 上的选项按钮不会走到你这里)。你的任务是: export const VISION_SYSTEM_PROMPT = `你是视觉理解助手。玩家在视觉小说的背景图上点击了红色圆点位置(HTML 上的选项按钮不会走到你这里)。你的任务是:
1. 看清红点指向画面里的什么(物件、角色、空间、远处的方向) 1. 看清红点指向画面里的什么(物件、角色、空间、远处的方向)
2. 推断玩家想干什么 2. 推断玩家想干什么
3. 判断这个动作是「场内探索」(不该换图)还是「场景切换」(要换图) 3. 判断这个动作是「场内探索」还是「场景切换」
判断准则: 判断准则:
- "insert-beat"(场内探索):观察画面里某个细节、自言自语、和当前角色继续互动、看一眼某个物件 - "change-scene"(场景切换):走向画面深处的门 / 走廊、转头看向新方向(视角变了)、点了远处的另一个空间、暗示时间跳跃的物件(如时钟)、调查某个物件/线索导致剧情发展、与角色进行有实质影响的互动
- "change-scene"(场景切换):走向画面深处的门 / 走廊、转头看向新方向(视角变了)、点了远处的另一个空间、暗示时间跳跃的物件(如时钟) - "insert-beat"(场内探索):**仅限**纯粹的观察——看一眼某个无剧情意义的装饰、环顾四周
- 拿不准时偏向 "change-scene"——玩家主动点击画面说明想要推进剧情
必须输出严格 JSON 必须输出严格 JSON
{ {
@@ -704,9 +708,9 @@ export const FREEFORM_CLASSIFY_SYSTEM = `你是交互视觉小说的意图分类
2. "change-scene":玩家想去别的地方、做出重大决定、推动剧情到新阶段 → 切换到全新场景 2. "change-scene":玩家想去别的地方、做出重大决定、推动剧情到新阶段 → 切换到全新场景
判断准则: 判断准则:
- 大多数对话类输入(问问题、说一句话、对角色做出反应)→ "insert-beat" - "change-scene":大多数主动输入——问问题、说一句话、做一个动作、对角色做出反应、想去别的地方、做出决定、推动剧情 → 玩家花精力打字说明想让故事前进
- 明确要离开当前场景、去别的地方、跳过时间、做出改变人物关系的重大决定 → "change-scene" - "insert-beat"**仅限**纯粹的环境观察或无实际影响的自言自语
- 拿不准时偏向 "insert-beat"(场内互动成本低,体验更流畅) - 拿不准时偏向 "change-scene"——玩家主动输入说明想要推进剧情
必须输出严格 JSON 必须输出严格 JSON
{ {
+1 -1
View File
@@ -27,7 +27,7 @@ export async function interpret(
}>(raw); }>(raw);
const classify: VisionClassify = const classify: VisionClassify =
parsed.classify === "change-scene" ? "change-scene" : "insert-beat"; parsed.classify === "insert-beat" ? "insert-beat" : "change-scene";
return { return {
intent: { intent: {
+3
View File
@@ -119,6 +119,8 @@ export const en = {
save: "Save", save: "Save",
cancel: "Cancel", cancel: "Cancel",
saveAndSelect: "Save and Select", saveAndSelect: "Save and Select",
feedback: "Feedback",
submitFeedback: "Submit Feedback",
}, },
styleModal: { styleModal: {
@@ -164,6 +166,7 @@ Dreamy watercolor style with soft tones and nostalgic atmosphere
contact: "CONTACT", contact: "CONTACT",
email: "Email", email: "Email",
openSource: "OPEN SOURCE", openSource: "OPEN SOURCE",
feedbackDescription: "Your thoughts matter — tell us about your experience and suggestions.",
betaUsers: "BETA USERS", betaUsers: "BETA USERS",
qqGroupLabel: "QQ Group: ", qqGroupLabel: "QQ Group: ",
qqGroupAlt: "InfiPlot Public Beta Group QR Code (Group ID: 575404333)", qqGroupAlt: "InfiPlot Public Beta Group QR Code (Group ID: 575404333)",
+3
View File
@@ -130,6 +130,8 @@ export const ja = {
save: "保存", save: "保存",
cancel: "キャンセル", cancel: "キャンセル",
saveAndSelect: "保存して適用", saveAndSelect: "保存して適用",
feedback: "フィードバック",
submitFeedback: "フィードバックを送信",
}, },
// Style modal // Style modal
@@ -179,6 +181,7 @@ export const ja = {
contact: "連絡先", contact: "連絡先",
email: "メールアドレス", email: "メールアドレス",
openSource: "ソースコード", openSource: "ソースコード",
feedbackDescription: "ご意見をお聞かせください。体験やご提案をお待ちしています。",
betaUsers: "クローズドβユーザーグループ", betaUsers: "クローズドβユーザーグループ",
qqGroupLabel: "QQグループ番号:", qqGroupLabel: "QQグループ番号:",
qqGroupAlt: "InfiPlot オープンβ交流QQグループ QRコード(グループ番号 575404333", qqGroupAlt: "InfiPlot オープンβ交流QQグループ QRコード(グループ番号 575404333",
+3
View File
@@ -130,6 +130,8 @@ export const zhCN = {
save: "保存", save: "保存",
cancel: "取消", cancel: "取消",
saveAndSelect: "保存并选用", saveAndSelect: "保存并选用",
feedback: "反馈",
submitFeedback: "提交反馈",
}, },
// Style modal // Style modal
@@ -179,6 +181,7 @@ export const zhCN = {
contact: "联 系 方 式", contact: "联 系 方 式",
email: "邮箱", email: "邮箱",
openSource: "开 源 地 址", openSource: "开 源 地 址",
feedbackDescription: "你的想法对我们很重要,欢迎告诉我们你的体验和建议。",
betaUsers: "内 测 用 户 群", betaUsers: "内 测 用 户 群",
qqGroupLabel: "QQ群号:", qqGroupLabel: "QQ群号:",
qqGroupAlt: "InfiPlot 公测交流群 QQ 群二维码(群号 575404333", qqGroupAlt: "InfiPlot 公测交流群 QQ 群二维码(群号 575404333",
+7
View File
@@ -695,8 +695,15 @@ export type InsertBeatPartial = {
lineDelivery?: string; lineDelivery?: string;
}; };
/** Multi-beat response: 1-3 beats. */
export type InsertBeatMulti = {
beats: InsertBeatPartial[];
};
export type InsertBeatResponse = { export type InsertBeatResponse = {
partial: InsertBeatPartial; partial: InsertBeatPartial;
/** Additional beats beyond the first (for richer insert-beat interactions). */
extraBeats?: InsertBeatPartial[];
characters: Character[]; characters: Character[];
}; };
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More