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}
+42 -83
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,43 +2318,57 @@ 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");
setPendingClick(null); setPendingClick(null);
+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 { narration, speaker, line, lineDelivery }; return beats;
}
// Legacy single-beat fallback
const single = coerceBeatPartial(parsed as Record<string, unknown>);
return [single ?? { narration: "(你停下脚步,环视片刻。)" }];
} }
+15 -18
View File
@@ -196,21 +196,14 @@ 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`).
//
// Exception (Pattern B): speaker = "你" is the player speaking. No
// Character record exists for "你" (intentional — TTS is skipped), so we
// must NOT demote it; the client renders the dialog box correctly.
// directInsertBeat already normalized POV variants to "你" before this
// guard, so a literal "你" here is always Pattern B player dialog.
if ( if (
partial.speaker && partial.speaker &&
partial.speaker !== "你" && partial.speaker !== "你" &&
@@ -219,22 +212,26 @@ export async function requestInsertBeat(
console.warn( console.warn(
`[insert-beat] unregistered speaker "${partial.speaker}" ignored`, `[insert-beat] unregistered speaker "${partial.speaker}" ignored`,
); );
const promotedNarration =
[partial.narration, partial.line].filter(Boolean).join("\n") || undefined;
tlog("[insert-beat] TOTAL", tTotal);
return { return {
partial: { narration:
narration: promotedNarration, [partial.narration, partial.line].filter(Boolean).join("\n") || undefined,
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