Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1874359521 | ||
|
|
d34d87c99f | ||
|
|
65c2f7cbe7 | ||
|
|
55ebad932a | ||
|
|
4c9c92af72 | ||
|
|
bb01b53c8f | ||
|
|
75b8ed6073 | ||
|
|
f8b75abc15 | ||
|
|
6711736bd0 | ||
|
|
a8f08e6500 | ||
|
|
0b755fb35d | ||
|
|
a98385fb1d | ||
|
|
d8834b95d3 | ||
|
|
f30c1d9e99 |
+13
-1
@@ -13,7 +13,7 @@ __pycache__/
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
@@ -161,3 +161,15 @@ cython_debug/
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
|
||||
*.log
|
||||
.env
|
||||
|
||||
# Runtime / generated artifacts
|
||||
game_records/
|
||||
backups/
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-TW">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>騙子酒館 - 線上多人心理博弈遊戲</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1162
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "liars-bar-game-client",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.3.4",
|
||||
"pinia": "^2.1.6",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"naive-ui": "^2.34.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"vite": "^4.4.9"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<n-config-provider>
|
||||
<n-message-provider>
|
||||
<div class="app-container">
|
||||
<header class="app-header">
|
||||
<h1>🎰 騙子酒館</h1>
|
||||
<n-tag :type="isConnected ? 'success' : 'error'" :bordered="false">
|
||||
{{ isConnected ? '已連線' : (reconnectAttempts > 0 ? '重連中(第 ' + reconnectAttempts + ' 次)' : '未連線') }}
|
||||
</n-tag>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<Lobby v-if="!roomId" />
|
||||
<GameTable v-else />
|
||||
</main>
|
||||
</div>
|
||||
</n-message-provider>
|
||||
</n-config-provider>
|
||||
<n-alert v-if="error" type="error" :show-icon="false">{{ error }}</n-alert>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useGameStore } from './stores/game'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { NConfigProvider, NMessageProvider, NTag } from 'naive-ui'
|
||||
import Lobby from './components/Lobby.vue'
|
||||
import GameTable from './components/GameTable.vue'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const { isConnected, roomId, reconnectAttempts, error } = storeToRefs(gameStore)
|
||||
|
||||
// 啟動時連線
|
||||
gameStore.connect()
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Microsoft JhengHei', sans-serif;
|
||||
background: #1a1a2e;
|
||||
color: #eee;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
border-bottom: 2px solid #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
font-size: 2em;
|
||||
background: linear-gradient(45deg, #f093fb, #f5576c);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
min-height: 60vh;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<div class="game-table">
|
||||
<n-card :title="[roomId ? '房間: ' + roomId : '']" class="game-card">
|
||||
<n-space vertical :size="20">
|
||||
<!-- 等待室 -->
|
||||
<template v-if="!gameState">
|
||||
<n-h3>等待室</n-h3>
|
||||
<n-list>
|
||||
<n-list-item v-for="player in roomPlayers" :key="player">
|
||||
<n-space align="center">
|
||||
<n-avatar>{{ player.substring(0, 2) }}</n-avatar>
|
||||
<div>{{ player.substring(0, 8) }}...</div>
|
||||
<n-tag v-if="player === (gameStore.originalPlayerId || gameStore.playerId)" type="warning">你</n-tag>
|
||||
</n-space>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
<n-text depth="3">{{ roomPlayers.length }}/8 人</n-text>
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
:disabled="roomPlayers.length < 2 || gameStore.actionPending"
|
||||
@click="startGame"
|
||||
>
|
||||
開始遊戲
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<!-- 對局中 -->
|
||||
<template v-else>
|
||||
<n-space v-if="gameState.isGameOver" justify="center">
|
||||
<n-alert type="success" :show-icon="false">
|
||||
遊戲結束,勝者:{{ winnerLabel }}
|
||||
</n-alert>
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:disabled="gameStore.actionPending"
|
||||
@click="restartGame"
|
||||
>
|
||||
再來一局
|
||||
</n-button>
|
||||
</n-space>
|
||||
|
||||
<!-- 玩家狀態 -->
|
||||
<n-list>
|
||||
<n-list-item v-for="player in players" :key="player.id">
|
||||
<n-space align="center">
|
||||
<n-avatar :style="{ backgroundColor: player.alive ? '#4caf50' : '#f44336' }">
|
||||
{{ shortId(player.id) }}
|
||||
</n-avatar>
|
||||
<div>
|
||||
<div>{{ shortId(player.id) }}</div>
|
||||
<n-tag :type="player.alive ? 'success' : 'error'" size="small">
|
||||
{{ player.alive ? '存活' : '死亡' }}
|
||||
</n-tag>
|
||||
<n-tag size="small">剩 {{ player.handSize }} 張</n-tag>
|
||||
<n-tag size="small">彈倉 {{ player.revolverChambers }}</n-tag>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
|
||||
<!-- 回合資訊 -->
|
||||
<n-descriptions label-placement="left" bordered>
|
||||
<n-descriptions-item label="目標牌">
|
||||
<n-tag type="warning" size="large">{{ targetCard }}</n-tag>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="第幾局">{{ roundNumber }}</n-descriptions-item>
|
||||
<n-descriptions-item label="輪到">
|
||||
<n-tag type="info">{{ shortId(currentPlayerId) }}</n-tag>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="你的回合">
|
||||
{{ isMyTurn ? '是' : '否' }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
|
||||
<!-- 挑戰結果提示 -->
|
||||
<n-alert v-if="lastMessage" :type="lastMessageIsError ? 'error' : 'warning'" :show-icon="false">
|
||||
{{ lastMessage }}
|
||||
</n-alert>
|
||||
|
||||
<!-- 質疑翻牌:僅在質疑後揭露真實牌值 -->
|
||||
<div v-if="lastChallenge">
|
||||
<n-text strong>質疑結果(翻牌):</n-text>
|
||||
<n-space>
|
||||
<n-tag v-for="(card, index) in lastChallenge.challengedGroup" :key="index" type="success">
|
||||
{{ card }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
<n-text depth="3" style="display:block">
|
||||
{{ lastChallenge.isBluff ? '是吹牛組' : '是真牌組' }}
|
||||
(出牌者:{{ shortId(lastChallenge.claimantId) }})
|
||||
</n-text>
|
||||
</div>
|
||||
|
||||
<!-- 本手已出的牌(只顯示張數,不洩漏牌值) -->
|
||||
<div v-if="playedGroups.length > 0">
|
||||
<n-text strong>本手已出的牌:</n-text>
|
||||
<n-space>
|
||||
<n-tag v-for="(item, index) in playedGroups" :key="index" type="success">
|
||||
{{ shortId(item.playerId) }}: {{ item.cardCount }} 張
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<!-- 你的手牌(點選 1-3 張) -->
|
||||
<div>
|
||||
<n-text strong>你的手牌({{ yourHand.length }} 張):</n-text>
|
||||
<n-space>
|
||||
<n-button
|
||||
v-for="(card, index) in yourHand"
|
||||
:key="index"
|
||||
:type="selected.has(index) ? 'primary' : 'default'"
|
||||
:disabled="!isMyTurn || gameState.isGameOver"
|
||||
size="large"
|
||||
@click="toggleSelect(index)"
|
||||
>
|
||||
{{ card }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
<n-text v-if="selected.size > 3" depth="3" style="display:block">
|
||||
最多只能出 3 張
|
||||
</n-text>
|
||||
</div>
|
||||
|
||||
<!-- 動作 -->
|
||||
<n-space justify="center">
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:disabled="!isMyTurn || selected.size < 1 || selected.size > 3 || gameState.isGameOver || gameStore.actionPending"
|
||||
@click="playSelected"
|
||||
>
|
||||
出牌 ({{ selected.size }})
|
||||
</n-button>
|
||||
<n-button
|
||||
type="error"
|
||||
size="large"
|
||||
:disabled="!isMyTurn || playedGroups.length === 0 || gameState.isGameOver || gameStore.actionPending"
|
||||
@click="challenge"
|
||||
>
|
||||
挑戰
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useGameStore } from '../stores/game'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import {
|
||||
NCard, NSpace, NList, NListItem, NAvatar, NTag,
|
||||
NDescriptions, NDescriptionsItem, NButton, NH3, NText, NAlert
|
||||
} from 'naive-ui'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const { roomId, gameState, room, yourHand, lastMessage, lastChallenge } = storeToRefs(gameStore)
|
||||
|
||||
const selected = ref(new Set())
|
||||
|
||||
const roomPlayers = computed(() => room.value?.players || [])
|
||||
const players = computed(() => gameState.value?.players || [])
|
||||
const targetCard = computed(() => gameState.value?.targetCard)
|
||||
const roundNumber = computed(() => gameState.value?.roundNumber)
|
||||
const currentPlayerId = computed(() => gameState.value?.currentPlayerId)
|
||||
const playedGroups = computed(() => gameState.value?.playedGroups || [])
|
||||
|
||||
const isMyTurn = computed(() => currentPlayerId.value === (gameStore.originalPlayerId || gameStore.playerId))
|
||||
const winnerId = computed(() => gameState.value?.winnerId)
|
||||
const winnerLabel = computed(() => winnerId.value ? shortId(winnerId.value) : '未知')
|
||||
const lastMessageIsError = computed(() => /失敗|吹牛/.test(lastMessage.value || ''))
|
||||
|
||||
// 每局(目標牌或 round 改變)清除選牌
|
||||
watch([targetCard, roundNumber], () => { selected.value = new Set() })
|
||||
|
||||
function shortId(id) {
|
||||
return id ? id.substring(0, 8) + '...' : '-'
|
||||
}
|
||||
|
||||
function toggleSelect(index) {
|
||||
const next = new Set(selected.value)
|
||||
if (next.has(index)) {
|
||||
next.delete(index)
|
||||
} else {
|
||||
if (next.size >= 3) return
|
||||
next.add(index)
|
||||
}
|
||||
selected.value = next
|
||||
}
|
||||
|
||||
function playSelected() {
|
||||
if (selected.value.size < 1 || selected.value.size > 3) return
|
||||
gameStore.playCard([...selected.value])
|
||||
selected.value = new Set()
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
gameStore.startGame()
|
||||
}
|
||||
|
||||
function restartGame() {
|
||||
gameStore.restartGame()
|
||||
}
|
||||
|
||||
function challenge() {
|
||||
gameStore.challenge()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.game-table {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.game-card {
|
||||
background: #16213e;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="lobby">
|
||||
<n-card title="🎮 遊戲大廳" class="lobby-card">
|
||||
<n-space vertical :size="20">
|
||||
<n-button type="primary" size="large" @click="createRoom">
|
||||
建立房間
|
||||
</n-button>
|
||||
|
||||
<n-divider>或加入現有房間</n-divider>
|
||||
|
||||
<n-space align="center">
|
||||
<n-input
|
||||
v-model:value="joinRoomId"
|
||||
placeholder="輸入房間代碼"
|
||||
class="room-input"
|
||||
/>
|
||||
<n-button type="primary" @click="joinRoom">
|
||||
加入
|
||||
</n-button>
|
||||
</n-space>
|
||||
|
||||
<n-divider>現有房間</n-divider>
|
||||
|
||||
<n-list v-if="rooms.length > 0">
|
||||
<n-list-item v-for="room in rooms" :key="room.id">
|
||||
<n-space justify="space-between" align="center">
|
||||
<div>
|
||||
<n-tag type="info">房間: {{ room.id }}</n-tag>
|
||||
<n-tag>{{ room.players }}/{{ room.maxPlayers }} 人</n-tag>
|
||||
</div>
|
||||
<n-button
|
||||
size="small"
|
||||
@click="joinRoom(room.id)"
|
||||
:disabled="room.players >= room.maxPlayers"
|
||||
>
|
||||
加入
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
|
||||
<n-empty v-else description="目前沒有房間" />
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useGameStore } from '../stores/game'
|
||||
import { NCard, NButton, NSpace, NDivider, NInput, NList, NListItem, NTag, NEmpty } from 'naive-ui'
|
||||
|
||||
const gameStore = useGameStore()
|
||||
const joinRoomId = ref('')
|
||||
const rooms = ref([])
|
||||
let roomTimer = null
|
||||
|
||||
const createRoom = () => {
|
||||
gameStore.createRoom()
|
||||
}
|
||||
|
||||
const joinRoom = (roomId) => {
|
||||
const id = roomId || joinRoomId.value
|
||||
if (id) {
|
||||
gameStore.joinRoom(id)
|
||||
}
|
||||
}
|
||||
|
||||
// 載入房間列表
|
||||
const loadRooms = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/rooms')
|
||||
rooms.value = await response.json()
|
||||
} catch (error) {
|
||||
console.error('載入房間失敗:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRooms()
|
||||
// 每 5 秒更新房間列表
|
||||
roomTimer = setInterval(loadRooms, 5000)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (roomTimer) clearInterval(roomTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.lobby {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.lobby-card {
|
||||
background: #16213e;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.room-input {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import { create } from 'naive-ui'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
const naive = create()
|
||||
|
||||
app.use(pinia)
|
||||
app.use(naive)
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,276 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { io } from 'socket.io-client'
|
||||
|
||||
const SESSION_KEY = 'liarsbar.session'
|
||||
// persist seat identity so page reload / reopening the tab can auto-rejoin the room
|
||||
function loadSavedSession() {
|
||||
try {
|
||||
const raw = localStorage.getItem(SESSION_KEY)
|
||||
if (!raw) return null
|
||||
const s = JSON.parse(raw)
|
||||
if (s && s.playerId && s.roomId) return s
|
||||
localStorage.removeItem(SESSION_KEY)
|
||||
return null
|
||||
} catch (e) { return null }
|
||||
}
|
||||
function saveSession(playerId, roomId) {
|
||||
try { localStorage.setItem(SESSION_KEY, JSON.stringify({ playerId, roomId })) } catch (e) {}
|
||||
}
|
||||
function clearSession() {
|
||||
try { localStorage.removeItem(SESSION_KEY) } catch (e) {}
|
||||
}
|
||||
|
||||
export const useGameStore = defineStore('game', {
|
||||
state: () => ({
|
||||
socket: null,
|
||||
playerId: null,
|
||||
originalPlayerId: null,
|
||||
joinedOnce: false,
|
||||
roomId: null,
|
||||
gameState: null,
|
||||
room: null,
|
||||
yourHand: [],
|
||||
lastMessage: '',
|
||||
lastChallenge: null, // { challengedGroup, isBluff, challengerId, shooterId, systemChallenge }
|
||||
isConnected: false,
|
||||
reconnectAttempts: 0,
|
||||
actionPending: false,
|
||||
requestSeq: 0,
|
||||
error: null
|
||||
}),
|
||||
|
||||
actions: {
|
||||
// 連線到伺服器(socket.io 會自動重連)
|
||||
connect() {
|
||||
const serverUrl = import.meta.env.VITE_SERVER_URL || 'http://localhost:3000'
|
||||
this.socket = io(serverUrl, {
|
||||
transports: ['websocket', 'polling']
|
||||
})
|
||||
|
||||
// restored from a previous page? if so, auto-rejoin the room on connect
|
||||
if (!this.originalPlayerId) {
|
||||
const saved = loadSavedSession()
|
||||
if (saved) {
|
||||
this.originalPlayerId = saved.playerId
|
||||
this.roomId = saved.roomId
|
||||
this.joinedOnce = true
|
||||
}
|
||||
}
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
const newId = this.socket.id
|
||||
if (!this.originalPlayerId) this.originalPlayerId = newId
|
||||
// 若之前在房裡(頁面重整 / 暫時斷線),嘗試回座位
|
||||
if (this.joinedOnce && this.roomId && this.originalPlayerId) {
|
||||
this.socket.emit('rejoin', { roomId: this.roomId, playerId: this.originalPlayerId }, (res) => {
|
||||
if (res && res.error) {
|
||||
this.error = res.error
|
||||
clearSession()
|
||||
this.roomId = null
|
||||
this.room = null
|
||||
this.joinedOnce = false
|
||||
this.gameState = null
|
||||
this.lastMessage = res.error
|
||||
} else if (res && res.ok) {
|
||||
saveSession(this.originalPlayerId, this.roomId)
|
||||
}
|
||||
})
|
||||
}
|
||||
this.playerId = newId
|
||||
this.isConnected = true
|
||||
this.reconnectAttempts = 0
|
||||
console.log('已連線:', this.playerId)
|
||||
})
|
||||
|
||||
this.socket.on('disconnect', (reason) => {
|
||||
this.isConnected = false
|
||||
console.log('已斷線:', reason)
|
||||
})
|
||||
|
||||
// socket.io 內建自動重連;記錄次數供 UI 顯示
|
||||
this.socket.io.on('reconnect_attempt', (attempt) => {
|
||||
this.reconnectAttempts = attempt
|
||||
})
|
||||
|
||||
this.socket.on('connect_error', (err) => {
|
||||
this.isConnected = false
|
||||
this.error = '無法連線到伺服器:' + err.message
|
||||
console.error('連線錯誤:', err.message)
|
||||
})
|
||||
|
||||
this.socket.on('error', (data) => {
|
||||
this.error = data.message
|
||||
console.error('錯誤:', data.message)
|
||||
})
|
||||
|
||||
// 建立房間
|
||||
this.socket.on('roomCreated', (room) => {
|
||||
this.room = room
|
||||
this.roomId = room.id
|
||||
this.joinedOnce = true
|
||||
this.error = null
|
||||
saveSession(this.originalPlayerId || this.playerId, room.id)
|
||||
})
|
||||
|
||||
this.socket.on('playerJoined', (room) => {
|
||||
this.room = room
|
||||
this.roomId = room.id
|
||||
this.joinedOnce = true
|
||||
this.error = null
|
||||
saveSession(this.originalPlayerId || this.playerId, room.id)
|
||||
})
|
||||
|
||||
// 其他玩家重連回來
|
||||
this.socket.on('playerRejoined', (data) => {
|
||||
this.room = data.room
|
||||
this.lastMessage = '有玩家重新連線,繼續遊戲。'
|
||||
})
|
||||
|
||||
// 其他玩家離線 / 離場
|
||||
this.socket.on('playerDisconnected', (data) => {
|
||||
this.room = data.room
|
||||
if (data.reconnecting) {
|
||||
this.lastMessage = '有玩家離線,等待重連……'
|
||||
return
|
||||
}
|
||||
if (data.gameState) this.gameState = data.gameState
|
||||
if (data.gameState && data.gameState.isGameOver && data.gameState.winnerId) {
|
||||
this.lastMessage = '遊戲結束,勝利者:' + data.gameState.winnerId.substring(0, 8) + '...'
|
||||
}
|
||||
})
|
||||
|
||||
// 重連成功後的完整狀態回補
|
||||
this.socket.on('resync', (data) => {
|
||||
this.room = data.room
|
||||
if (data.room && data.room.id) this.roomId = data.room.id
|
||||
if (data.gameState) this.gameState = data.gameState
|
||||
this.lastChallenge = null
|
||||
this.error = null
|
||||
this.lastMessage = '已重新連線,繼續遊戲。'
|
||||
saveSession(this.originalPlayerId || this.playerId, this.roomId)
|
||||
})
|
||||
|
||||
this.socket.on('gameStarted', (gameState) => {
|
||||
this.gameState = gameState
|
||||
this.lastChallenge = null
|
||||
this.lastMessage = ''
|
||||
this.error = null
|
||||
})
|
||||
|
||||
this.socket.on('cardPlayed', (data) => {
|
||||
this.gameState = data.gameState
|
||||
if (data.systemChallenge) {
|
||||
// 系統質疑在出牌事件一起揭露
|
||||
const sc = data.systemChallenge
|
||||
this.lastChallenge = {
|
||||
systemChallenge: true,
|
||||
challengerId: 'system',
|
||||
claimantId: sc.playerId,
|
||||
shooterId: sc.shooter,
|
||||
isBluff: sc.isBluff,
|
||||
challengedGroup: sc.autoPlayedCards || []
|
||||
}
|
||||
this.lastMessage = sc.isBluff
|
||||
? '系統質疑:該玩家吹牛,被處罰!'
|
||||
: '系統質疑:該玩家說實話,無人受傷。'
|
||||
}
|
||||
})
|
||||
|
||||
this.socket.on('challenged', (data) => {
|
||||
this.gameState = data.gameState
|
||||
if (data.result) {
|
||||
const r = data.result
|
||||
this.lastChallenge = {
|
||||
systemChallenge: !!r.systemChallenge,
|
||||
challengerId: r.challengerId,
|
||||
claimantId: r.playerId,
|
||||
shooterId: r.shooter,
|
||||
isBluff: r.isBluff,
|
||||
challengedGroup: r.challengedGroup || []
|
||||
}
|
||||
if (r.systemChallenge) {
|
||||
this.lastMessage = r.isBluff
|
||||
? '系統質疑:該玩家吹牛,被處罰!'
|
||||
: '系統質疑:該玩家說實話,無人受傷。'
|
||||
} else if (r.isBluff) {
|
||||
this.lastMessage = '質疑成功!對方吹牛,開槍處罰。'
|
||||
} else {
|
||||
this.lastMessage = '質疑失敗!對方說的是真話,換你開槍。'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 私密手牌:只送給自己
|
||||
this.socket.on('yourHand', (hand) => {
|
||||
this.yourHand = hand || []
|
||||
})
|
||||
},
|
||||
|
||||
// 產生每動作唯一 requestId(搭配伺服端去重)
|
||||
nextRequestId() {
|
||||
this.requestSeq += 1
|
||||
return this.requestSeq
|
||||
},
|
||||
|
||||
// 建立房間
|
||||
createRoom() {
|
||||
this.socket.emit('createRoom', { requestId: this.nextRequestId() }, () => {})
|
||||
},
|
||||
|
||||
// 加入房間
|
||||
joinRoom(roomId) {
|
||||
this.socket.emit('joinRoom', { roomId, requestId: this.nextRequestId() }, (res) => {
|
||||
if (res && res.error) this.error = res.error
|
||||
})
|
||||
},
|
||||
|
||||
// 開始遊戲
|
||||
startGame() {
|
||||
if (!this.socket || this.actionPending) return
|
||||
this.actionPending = true
|
||||
this.socket.emit('startGame', { roomId: this.roomId, requestId: this.nextRequestId() }, (res) => {
|
||||
this.actionPending = false
|
||||
if (res && res.error) { this.error = res.error; this.lastMessage = res.error }
|
||||
})
|
||||
},
|
||||
|
||||
// 再來一局
|
||||
restartGame() {
|
||||
if (!this.socket || this.actionPending) return
|
||||
this.actionPending = true
|
||||
this.socket.emit('restartGame', { roomId: this.roomId, requestId: this.nextRequestId() }, (res) => {
|
||||
this.actionPending = false
|
||||
if (res && res.error) { this.error = res.error; this.lastMessage = res.error }
|
||||
})
|
||||
},
|
||||
|
||||
// 出牌
|
||||
playCard(cardIndices) {
|
||||
if (!this.socket || this.actionPending) return
|
||||
this.actionPending = true
|
||||
this.socket.emit('playCard', { roomId: this.roomId, cardIndices, requestId: this.nextRequestId() }, (res) => {
|
||||
this.actionPending = false
|
||||
if (res && res.error) { this.error = res.error; this.lastMessage = res.error }
|
||||
})
|
||||
},
|
||||
|
||||
// 質疑
|
||||
challenge() {
|
||||
if (!this.socket || this.actionPending) return
|
||||
this.actionPending = true
|
||||
this.socket.emit('challenge', { roomId: this.roomId, requestId: this.nextRequestId() }, (res) => {
|
||||
this.actionPending = false
|
||||
if (res && res.error) { this.error = res.error; this.lastMessage = res.error }
|
||||
})
|
||||
},
|
||||
|
||||
// 斷開連線
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.disconnect()
|
||||
this.socket = null
|
||||
this.isConnected = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"references": []
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,366 @@
|
||||
# AI 繪圖角色一致性完整調查報告
|
||||
|
||||
> **任務**:從 8 個 Q 版女性角色的基礎立繪生成更多表情和動作變體,用於網頁遊戲前端動畫
|
||||
> **調查日期**:2026-07-30
|
||||
> **核心需求**:角色一致性(character consistency)— 保持同一角色的臉、髮型、服裝在不同表情/動作下高度一致
|
||||
|
||||
---
|
||||
|
||||
## 一、工具比較總覽
|
||||
|
||||
| 工具 | 角色一致性 | 姿態控制 | 學習曲線 | 成本 | 需 GPU | 批量生產 | 推薦程度 |
|
||||
|------|:---------:|:-------:|:-------:|------|:-----:|:-------:|:-------:|
|
||||
| **ComfyUI + IP-Adapter** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 高 | 免費 | ✅ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| **SD + LoRA** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 中高 | 免費 | ✅ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
|
||||
| **Midjourney v6.1** | ⭐⭐⭐⭐ | ⭐⭐ | 低 | $10-60/月 | ❌ | ⭐⭐ | ⭐⭐⭐ |
|
||||
| **Leonardo AI** | ⭐⭐⭐⭐ | ⭐⭐⭐ | 低 | 免費-$48/月 | ❌ | ⭐⭐⭐ | ⭐⭐⭐ |
|
||||
| **Fooocus** | ⭐⭐⭐ | ⭐⭐ | 最低 | 免費 | ✅ | ⭐⭐ | ⭐⭐ |
|
||||
| **LiblibAI / SeaArt** | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 低中 | 免費-$20/月 | ❌ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
|
||||
|
||||
---
|
||||
|
||||
## 二、角色一致性技術比較
|
||||
|
||||
| 技術 | Q 版適合度 | 需要參考圖 | 學習曲線 | GPU VRAM | 角色一致性 | 關鍵結論 |
|
||||
|------|:---------:|:---------:|:-------:|:--------:|:---------:|---------|
|
||||
| **IP-Adapter** | ★★★★★ | 1 張 | 中等 | 8GB+ | ⭐⭐⭐⭐⭐ | 最佳選擇,1 張圖零訓練即可保持角色一致性 |
|
||||
| **ControlNet (DWpose)** | ★★★★★ | 姿態參考圖 | 中等 | 8GB+ | ⭐⭐⭐⭐⭐ | 姿勢控制核心,DWpose 比 OpenPose 更適合 Q 版比例 |
|
||||
| **LoRA** | ★★★★☆ | 15-30 張 | 中高 | 8GB+ | ⭐⭐⭐⭐⭐ | 效果最佳但需大量訓練圖,可用 IP-Adapter 先生成訓練資料 |
|
||||
| **InstantID** | ★★☆☆☆ | 1 張 | 高 | 12GB+ | ⭐⭐☆☆☆ | 依賴真實人臉識別,Q 版面部無法被 antelopev2 偵測 |
|
||||
| **PhotoMaker** | ★★☆☆☆ | 1-5 張 | 高 | 12GB+ | ⭐⭐☆☆☆ | 同上,不適用 Q 版 |
|
||||
| **ReActor/ROOP** | ★★☆☆☆ | 1 張 | 低 | 4GB+ | ⭐⭐☆☆☆ | 僅換臉,無法保持整體角色風格 |
|
||||
|
||||
### HuggingFace 下載量佐證
|
||||
|
||||
- IP-Adapter-FaceID: **186K 下載**
|
||||
- ControlNet-OpenPose-SDXL: **70K 下載**
|
||||
- InstantID: **54K 下載**
|
||||
|
||||
---
|
||||
|
||||
## 三、最佳推薦方案
|
||||
|
||||
### 🥇 首選:ComfyUI + IP-Adapter + ControlNet DWpose
|
||||
|
||||
**推薦原因:**
|
||||
1. ✅ 1 張立繪即可開始,零訓練成本
|
||||
2. ✅ DWpose 專為 Q 版/動漫比例優化
|
||||
3. ✅ 完全免費、開源
|
||||
4. ✅ 支援批量生產
|
||||
5. ✅ 中文社群資源最強
|
||||
|
||||
**硬體需求:**
|
||||
- 最低:RTX 3060 12GB
|
||||
- 推薦:RTX 4070 12GB+
|
||||
|
||||
**推薦工作流:**
|
||||
```
|
||||
Load Image (角色立繪) → IP-Adapter FaceID Plus v2 → KSampler (SDXL)
|
||||
↗
|
||||
Load Image (姿態參考) → ControlNet DWpose → KSampler
|
||||
```
|
||||
|
||||
### 🥈 備用方案 A:LiblibAI / SeaArt(雲端版 ComfyUI)
|
||||
|
||||
**適用情境**:沒有 GPU 或不想安裝複雜環境
|
||||
|
||||
**優點:**
|
||||
- 雲端運行,無需 GPU
|
||||
- 內建 IP-Adapter + ControlNet,角色一致性接近 ComfyUI
|
||||
- LiblibAI 是中國平台,中文介面 + 大量中文模型
|
||||
- 免費層可先測試效果
|
||||
|
||||
### 🥉 備用方案 B:Midjourney v6.1(快速原型)
|
||||
|
||||
**適用情境**:快速測試角色變體概念,不做大量生產
|
||||
|
||||
**缺點**:無法精確控制姿態(對遊戲動畫是致命傷);Q 版角色 `--cref` 效果不穩定
|
||||
|
||||
---
|
||||
|
||||
## 四、批量生產工作流
|
||||
|
||||
### 階段 1:快速驗證(1-2 天)
|
||||
|
||||
```
|
||||
├─ 安裝 ComfyUI + ComfyUI-Manager
|
||||
├─ 下載必要模型:
|
||||
│ ├─ 底層模型:SDXL 1.0 或動漫專用模型(Anything V5、CounterfeitXL)
|
||||
│ ├─ IP-Adapter:ip-adapter-plus-face_sdxl_vit-h.bin
|
||||
│ └─ ControlNet:controlnet-dwpose-sdxl
|
||||
├─ 選 1-2 個角色測試(建議先試 Grok-4 和 Deepseek)
|
||||
├─ 確認角色一致性效果是否達標
|
||||
└─ 若不達標 → 切換 LiblibAI 雲端測試
|
||||
```
|
||||
|
||||
### 階段 2:批量生產(3-5 天)
|
||||
|
||||
```
|
||||
├─ 為 8 個角色各建立 ComfyUI 工作流
|
||||
├─ 定義表情清單:
|
||||
│ ├─ 開心、生氣、疑惑、恐懼、得意、無聊(6 種)
|
||||
├─ 定義動作清單:
|
||||
│ ├─ 出牌、質疑、舉槍、中槍、勝利、失敗(6 種)
|
||||
├─ 每個角色共 12 張變體圖
|
||||
├─ 8 角色 × 12 張 = 96 張(約 100 張)
|
||||
├─ 批次生成所有變體
|
||||
└─ 篩選、後處理
|
||||
```
|
||||
|
||||
### 階段 3(可選):LoRA 訓練(長期投資)
|
||||
|
||||
```
|
||||
├─ 用階段 2 的產出作為訓練素材
|
||||
├─ 為每個角色訓練專屬 LoRA
|
||||
└─ 之後可無限生成新變體,一致性更高
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、Prompt 工程指南
|
||||
|
||||
### 角色描述 Prompt 模板
|
||||
|
||||
```
|
||||
[角色特徵描述], [表情], [動作], [背景], [風格]
|
||||
|
||||
範例(Grok-4):
|
||||
chibi girl, orange wavy hair, yellow sleeveless dress, large red bow on head,
|
||||
big smile expression, holding cards in hand, white background,
|
||||
anime style, cel shading, cute, kawaii
|
||||
```
|
||||
|
||||
### 表情 Prompt 清單
|
||||
|
||||
| 表情 | Prompt 關鍵字 |
|
||||
|------|--------------|
|
||||
| 開心 | big smile, happy, joyful, laughing |
|
||||
| 生氣 | angry, frowning, mad, crossed arms |
|
||||
| 疑惑 | confused, tilted head, questioning, thinking |
|
||||
| 恐懼 | scared, terrified, wide eyes, trembling |
|
||||
| 得意 | smug, cocky, confident smirk, arms crossed |
|
||||
| 無聊 | bored, deadpan, blank stare, yawning |
|
||||
|
||||
### 動作 Prompt 清單
|
||||
|
||||
| 動作 | Prompt 關鍵字 |
|
||||
|------|--------------|
|
||||
| 出牌 | playing cards, holding cards, dealing cards |
|
||||
| 質疑 | pointing finger, accusing, challenging pose |
|
||||
| 舉槍 | holding revolver, pointing gun, russian roulette |
|
||||
| 中槍 | shocked, falling back, clutching head |
|
||||
| 勝利 | celebrating, raising arms, triumphant pose |
|
||||
| 失敗 | defeated, slumped, sad pose, head down |
|
||||
|
||||
---
|
||||
|
||||
## 六、Seed 控制策略
|
||||
|
||||
### 保持角色一致性的 Seed 策略
|
||||
|
||||
1. **固定 Seed + 固定 IP-Adapter 權重**:
|
||||
- 同一角色的所有變體使用相同 seed
|
||||
- IP-Adapter weight 設為 0.7-0.9
|
||||
- 僅改變 prompt 中的表情/動作描述
|
||||
|
||||
2. **Seed 範圍搜索**:
|
||||
- 找到一個好的 seed 後,在 ±100 範圍內搜索
|
||||
- 例如 seed=12345 效果好,則測試 12245-12445
|
||||
|
||||
3. **Seed 記錄表**:
|
||||
```
|
||||
角色 | 最佳 Seed | IP-Adapter Weight | ControlNet Strength
|
||||
Grok-4 | 12345 | 0.8 | 0.8
|
||||
Kimi | 67890 | 0.75 | 0.85
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、批量生成腳本
|
||||
|
||||
### ComfyUI REST API 批量生成
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
batch_generate.py — 批量生成角色表情/動作變體
|
||||
|
||||
使用 ComfyUI REST API 自動執行工作流
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
|
||||
COMFYUI_URL = "http://12.0.0.1:8188"
|
||||
|
||||
def submit_workflow(workflow_json):
|
||||
"""提交工作流到 ComfyUI"""
|
||||
response = requests.post(
|
||||
f"{COMFYUI_URL}/prompt",
|
||||
json={"prompt": workflow_json}
|
||||
)
|
||||
return response.json()["prompt_id"]
|
||||
|
||||
def get_image(prompt_id):
|
||||
"""等待生成完成並取得圖片"""
|
||||
while True:
|
||||
ws = requests.get(f"{COMFYUI_URL}/history/{prompt_id}")
|
||||
if ws.json():
|
||||
return ws.json()[prompt_id]["outputs"]
|
||||
time.sleep(1)
|
||||
|
||||
# 定義 8 個角色的 prompt 模板
|
||||
CHARACTERS = {
|
||||
"Grok-4": "chibi girl, orange wavy hair, yellow sleeveless dress, large red bow on head, ...",
|
||||
"Kimi": "chibi girl, silver long straight hair, dark blue dress with white collar, choker, ...",
|
||||
# ... 其他角色
|
||||
}
|
||||
|
||||
EXPRESSIONS = ["happy", "angry", "confused", "scared", "smug", "bored"]
|
||||
ACTIONS = ["playing_cards", "accusing", "holding_revolver", "shocked", "celebrating", "defeated"]
|
||||
|
||||
# 批量生成
|
||||
for char_name, char_desc in CHARACTERS.items():
|
||||
for expr in EXPRESSIONS:
|
||||
for action in ACTIONS:
|
||||
prompt = f"{char_desc}, {expr} expression, {action} pose, white background, anime style"
|
||||
# 修改 workflow_json 中的 prompt 節點
|
||||
workflow_json["6"]["inputs"]["text"] = prompt
|
||||
prompt_id = submit_workflow(workflow_json)
|
||||
print(f"生成中: {char_name} - {expr} - {action}")
|
||||
time.sleep(2) # 等待 GPU 處理
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、品質篩選流程
|
||||
|
||||
### 自動篩選標準
|
||||
|
||||
| 檢查項目 | 標準 | 工具 |
|
||||
|---------|------|------|
|
||||
| 解析度 | 768x768 以上 | 檔案屬性檢查 |
|
||||
| 角色識別 | 面部特徵匹配度 > 80% | CLIP 影像嵌入比對 |
|
||||
| 表情正確性 | 表情關鍵字匹配 | 手動檢查 |
|
||||
| 動作正確性 | 姿態匹配度 > 70% | OpenPose 偵測比對 |
|
||||
| 無畸形 | 手部、臉部無明顯變形 | 手動檢查 |
|
||||
|
||||
### 手動篩選清單
|
||||
|
||||
- [ ] 角色髮色/髮型是否正確?
|
||||
- [ ] 角色服裝是否正確?
|
||||
- [ ] 表情是否符合預期?
|
||||
- [ ] 動作是否符合預期?
|
||||
- [ ] 手部是否畸形?
|
||||
- [ ] 臉部是否變形?
|
||||
- [ ] 配件是否遺漏?
|
||||
|
||||
---
|
||||
|
||||
## 九、後處理流程
|
||||
|
||||
### 工具推薦
|
||||
|
||||
| 步驟 | 工具 | 說明 |
|
||||
|------|------|------|
|
||||
| 背景移除 | remove.bg / RemBG | 自動移除背景,輸出透明 PNG |
|
||||
| 裁切 | ImageMagick / Python PIL | 統一裁切至 512x512 或 768x768 |
|
||||
| 格式轉換 | ImageMagick | 轉換為 WebP 格式(網頁優化) |
|
||||
| 超解析度 | Real-ESRGAN | 將低解析度圖片提升至 4K |
|
||||
|
||||
### 後處理腳本範例
|
||||
|
||||
```bash
|
||||
# 批量移除背景
|
||||
python -m rembg i input/ -o output/
|
||||
|
||||
# 批量裁切並轉換格式
|
||||
magick mogrify -resize 512x512 -format webp output/*.png
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、成本效益分析
|
||||
|
||||
### 時間成本估算
|
||||
|
||||
| 階段 | 時間 | 說明 |
|
||||
|------|------|------|
|
||||
| 環境設定 | 1-2 天 | 安裝 ComfyUI、下載模型、設定工作流 |
|
||||
| 快速驗證 | 1-2 天 | 測試 1-2 個角色,確認效果 |
|
||||
| 批量生產 | 3-5 天 | 生成 100 張圖,每張約 10-30 秒 |
|
||||
| 篩選後處理 | 1-2 天 | 手動篩選、背景移除、格式轉換 |
|
||||
| **總計** | **6-11 天** | |
|
||||
|
||||
### 金錢成本估算
|
||||
|
||||
| 方案 | 硬體成本 | 月費 | 總成本 |
|
||||
|------|---------|------|--------|
|
||||
| ComfyUI(自架) | RTX 3060 12GB: ~$300 | $0 | $300 |
|
||||
| LiblibAI(雲端) | $0 | 免費層 | $0(測試) |
|
||||
| LiblibAI(雲端) | $0 | 付費層 $15/月 | $30(2 個月) |
|
||||
| Midjourney | $0 | $10/月 | $20(2 個月) |
|
||||
|
||||
### 推薦方案
|
||||
|
||||
**有 GPU**:ComfyUI + IP-Adapter + ControlNet(一次性投資 $300,之後免費)
|
||||
|
||||
**無 GPU**:LiblibAI 免費層測試 → 確認效果 → 付費層批量生產($30)
|
||||
|
||||
---
|
||||
|
||||
## 十一、8 角色具體生產計畫
|
||||
|
||||
### 角色清單
|
||||
|
||||
| 編號 | AI 名稱 | 髮色 | 服裝 | 配件 |
|
||||
|------|---------|------|------|------|
|
||||
| 1 | Grok-4 | 波浪橘髮 | 黃色無袖洋裝 | 紅色大蝴蝶結 |
|
||||
| 2 | Kimi | 白銀長直髮 | 深藍洋裝+白領 | 項圈 |
|
||||
| 3 | Qwen3 | 粉紅短髮 | 灰色無袖洋裝 | 圓眼鏡 |
|
||||
| 4 | GPT-5 | 黑色長直髮 | 黑色洋裝 | 無 |
|
||||
| 5 | Gemini | 紫色短髮 | 淺藍洋裝 | 圓眼鏡+星星髮夾 |
|
||||
| 6 | Deepseek | 淺藍短髮 | 深藍灰洋裝 | 鯨魚尾巴 |
|
||||
| 7 | Doubao | 深棕短髮 | 黑色洋裝 | 無 |
|
||||
| 8 | Claude | 橘色短髮 | 橘色洋裝 | 無 |
|
||||
|
||||
### 生產排程
|
||||
|
||||
| 天數 | 任務 | 產出 |
|
||||
|------|------|------|
|
||||
| Day 1-2 | 環境設定 + 模型下載 | ComfyUI 就緒 |
|
||||
| Day 3-4 | 測試 Grok-4 + Deepseek | 確認效果 |
|
||||
| Day 5-7 | 批量生產 4 角色(Grok/Kimi/Qwen/GPT-5) | 48 張圖 |
|
||||
| Day 8-10 | 批量生產 4 角色(Gemini/Deepseek/Doubao/Claude) | 48 張圖 |
|
||||
| Day 11-12 | 篩選 + 後處理 | 96 張最終圖 |
|
||||
|
||||
---
|
||||
|
||||
## 十二、資源連結
|
||||
|
||||
- **ComfyUI 官方**:https://github.com/Comfy-Org/ComfyUI
|
||||
- **ComfyUI 管理器**:https://github.com/ltdrdata/ComfyUI-Manager
|
||||
- **IP-Adapter 官方**:https://github.com/tencent-ailab/IP-Adapter
|
||||
- **ControlNet**:https://github.com/lllyasviel/ControlNet
|
||||
- **LiblibAI**:https://www.liblib.art/
|
||||
- **SeaArt**:https://www.seaart.ai/
|
||||
- **Civitai(模型下載)**:https://civitai.com/
|
||||
- **Bilibili ComfyUI 教學**:搜尋「ComfyUI 角色一致性」
|
||||
|
||||
---
|
||||
|
||||
## 十三、常見陷阱與解決方案
|
||||
|
||||
| 陷阱 | 原因 | 解決方案 |
|
||||
|------|------|---------|
|
||||
| 角色不一致 | IP-Adapter weight 太低 | 調高至 0.7-0.9 |
|
||||
| 姿態不正確 | ControlNet strength 太低 | 調高至 0.7-0.9 |
|
||||
| 手部畸形 | SD 模型對手部支援差 | 使用 Hand Refiner 或手動修復 |
|
||||
| 臉部變形 | IP-Adapter 與底層模型不兼容 | 使用 FaceID Plus v2 |
|
||||
| 圖片模糊 | 解析度太低 | 使用 Real-ESRGAN 超解析 |
|
||||
| 生成速度慢 | GPU VRAM 不足 | 降低解析度或使用 --lowvram |
|
||||
|
||||
---
|
||||
|
||||
**報告完成**。下一步:確認是否開始安裝 ComfyUI 環境,或先使用 LiblibAI 雲端測試。
|
||||
@@ -0,0 +1,821 @@
|
||||
# ComfyUI 角色一致性繪圖 — 完整操作文檔
|
||||
|
||||
> **目標**:使用 RTX A4000 + Windows 11,為 8 個 Q 版角色各生成 12-15 張表情/動作變體(總計約 100 張)
|
||||
>
|
||||
> **技術方案**:ComfyUI + IP-Adapter + ControlNet DWpose
|
||||
>
|
||||
> **硬體**:RTX A4000 (16GB VRAM) + Windows 11
|
||||
>
|
||||
> **預估時間**:環境設定 2-3 小時,批量生產 3-5 小時
|
||||
|
||||
---
|
||||
|
||||
## 📋 目錄
|
||||
|
||||
1. [Plan A:一鍵安裝(推薦)](#1-plan-a一鍵安裝推薦)
|
||||
2. [Plan B:手動安裝(進階)](#2-plan-b手動安裝進階)
|
||||
3. [安裝擴充套件](#3-安裝擴充套件)
|
||||
4. [下載模型權重](#4-下載模型權重)
|
||||
5. [第一次測試](#5-第一次測試)
|
||||
6. [IP-Adapter 角色一致性工作流](#6-ip-adapter-角色一致性工作流)
|
||||
7. [ControlNet DWpose 姿勢控制](#7-controlnet-dwpose-姿勢控制)
|
||||
8. [批量生產流程](#8-批量生產流程)
|
||||
9. [Prompt 工程指南](#9-prompt-工程指南)
|
||||
10. [品質篩選與後處理](#10-品質篩選與後處理)
|
||||
11. [常見問題與解決方案](#11-常見問題與解決方案)
|
||||
12. [資源連結](#12-資源連結)
|
||||
|
||||
---
|
||||
|
||||
## 1. Plan A:一鍵安裝(推薦)
|
||||
|
||||
> ✅ **適合對象**:不想裝 Python、不想打命令列、想最快開始的人
|
||||
>
|
||||
> ⏱️ **預估時間**:15 分鐘
|
||||
|
||||
### 1.1 確認 NVIDIA 驅動
|
||||
|
||||
按 `Win + R`,輸入 `cmd`,執行:
|
||||
|
||||
```
|
||||
nvidia-smi
|
||||
```
|
||||
|
||||
**要求**:驅動版本 ≥ 537.xx,CUDA 版本 ≥ 12.1
|
||||
|
||||
若版本過舊,前往 NVIDIA 官網下載:
|
||||
- https://www.nvidia.com/Download/index.aspx
|
||||
|
||||
### 1.2 下載 ComfyUI 官方 Portable 版
|
||||
|
||||
前往 Release 頁面:
|
||||
- https://github.com/comfyanonymous/ComfyUI/releases
|
||||
|
||||
下載 **ComfyUI_windows_portable_nvidia.7z**(約 2GB)
|
||||
|
||||
### 1.3 解壓縮
|
||||
|
||||
1. 安裝 7-Zip(若沒有):https://www.7-zip.org/
|
||||
2. 右鍵點擊下載的 `.7z` 檔案 → 「解壓縮到 ComfyUI_windows_portable_nvidia\」
|
||||
3. 將整個資料夾移動到 `D:\ComfyUI_Setup\`
|
||||
|
||||
### 1.4 啟動
|
||||
|
||||
進入 `D:\ComfyUI_Setup\ComfyUI_windows_portable_nvidia\`,雙擊:
|
||||
|
||||
```
|
||||
run_nvidia_gpu.bat
|
||||
```
|
||||
|
||||
這會自動:
|
||||
- 啟動 Python 虛擬環境
|
||||
- 啟動 ComfyUI 伺服器
|
||||
- 開啟瀏覽器到 `http://127.0.0.1:8188`
|
||||
|
||||
**看到 ComfyUI 介面即表示成功!** 🎉
|
||||
|
||||
### 1.5 後續啟動
|
||||
|
||||
以後每次使用,只要雙擊 `run_nvidia_gpu.bat` 即可。
|
||||
|
||||
---
|
||||
|
||||
## 2. Plan B:手動安裝(進階)
|
||||
|
||||
> ⚠️ **適合對象**:想要完全控制環境、需要自訂 Python 版本、或想從源碼編譯的人
|
||||
>
|
||||
> ⏱️ **預估時間**:45-60 分鐘
|
||||
|
||||
### 2.1 確認 NVIDIA 驅動
|
||||
|
||||
```powershell
|
||||
nvidia-smi
|
||||
```
|
||||
|
||||
**要求**:驅動版本 ≥ 537.xx,CUDA 版本 ≥ 12.1
|
||||
|
||||
### 2.2 安裝 Python
|
||||
|
||||
下載 Python **3.10** 或 **3.11**(不要 3.12+,部分擴充套件不相容)
|
||||
|
||||
- 官網:https://www.python.org/downloads/
|
||||
- 安裝時勾選 **「Add Python to PATH」**
|
||||
|
||||
```powershell
|
||||
python --version
|
||||
# 應顯示 Python 3.10.x 或 3.11.x
|
||||
```
|
||||
|
||||
### 2.3 準備工作目錄
|
||||
|
||||
```powershell
|
||||
D:
|
||||
mkdir \ComfyUI_Setup
|
||||
cd \ComfyUI_Setup
|
||||
```
|
||||
|
||||
### 2.4 下載 ComfyUI
|
||||
|
||||
```powershell
|
||||
git clone https://github.com/comfyanonymous/ComfyUI.git
|
||||
cd ComfyUI
|
||||
```
|
||||
|
||||
### 2.5 建立虛擬環境
|
||||
|
||||
```powershell
|
||||
python -m venv venv
|
||||
.\venv\Scripts\activate
|
||||
```
|
||||
|
||||
### 2.6 安裝 PyTorch(含 CUDA 支援)
|
||||
|
||||
```powershell
|
||||
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
|
||||
```
|
||||
|
||||
**驗證 CUDA 是否可用**:
|
||||
|
||||
```powershell
|
||||
python -c "import torch; print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))"
|
||||
# 應輸出: True
|
||||
# NVIDIA RTX A4000
|
||||
```
|
||||
|
||||
### 2.7 安裝 ComfyUI 依賴
|
||||
|
||||
```powershell
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2.8 啟動測試
|
||||
|
||||
```powershell
|
||||
python main.py
|
||||
```
|
||||
|
||||
開啟瀏覽器訪問 `http://127.0.0.1:8188`,看到 ComfyUI 介面即表示成功。
|
||||
|
||||
### 2.9 後續啟動
|
||||
|
||||
```powershell
|
||||
cd D:\ComfyUI_Setup\ComfyUI
|
||||
.\venv\Scripts\activate
|
||||
python main.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 安裝擴充套件
|
||||
|
||||
> 以下步驟 Plan A 和 Plan B 都適用
|
||||
|
||||
### 3.1 安裝 ComfyUI-Manager(套件管理器)
|
||||
|
||||
**方法一:透過 Manager 一鍵安裝(推薦)**
|
||||
|
||||
1. 開啟 ComfyUI 介面
|
||||
2. 點擊右上角 **「Manager」**
|
||||
3. 搜尋 **「ComfyUI-Manager」**
|
||||
4. 點擊 **「Install」**
|
||||
|
||||
**方法二:手動安裝**
|
||||
|
||||
```powershell
|
||||
cd D:\ComfyUI_Setup\ComfyUI_windows_portable_nvidia\ComfyUI\custom_nodes
|
||||
git clone https://github.com/ltdrdata/ComfyUI-Manager.git
|
||||
```
|
||||
|
||||
**重啟 ComfyUI** 後,介面右上角會出現「Manager」按鈕。
|
||||
|
||||
### 3.2 安裝 IP-Adapter 擴充套件
|
||||
|
||||
**方法一:透過 Manager**
|
||||
|
||||
1. 點擊 **「Manager」** → **「Install Custom Nodes」**
|
||||
2. 搜尋 **「ComfyUI_IPAdapter_plus」**
|
||||
3. 點擊 **「Install」**
|
||||
|
||||
**方法二:手動安裝**
|
||||
|
||||
```powershell
|
||||
cd D:\ComfyUI_Setup\ComfyUI_windows_portable_nvidia\ComfyUI\custom_nodes
|
||||
git clone https://github.com/cubiq/ComfyUI_IPAdapter_plus.git
|
||||
```
|
||||
|
||||
### 3.3 安裝 ControlNet 輔助工具
|
||||
|
||||
**方法一:透過 Manager**
|
||||
|
||||
1. 搜尋 **「comfyui_controlnet_aux」**
|
||||
2. 點擊 **「Install」**
|
||||
|
||||
**方法二:手動安裝**
|
||||
|
||||
```powershell
|
||||
cd D:\ComfyUI_Setup\ComfyUI_windows_portable_nvidia\ComfyUI\custom_nodes
|
||||
git clone https://github.com/Fannovel16/comfyui_controlnet_aux.git
|
||||
```
|
||||
|
||||
安裝依賴:
|
||||
|
||||
```powershell
|
||||
# Plan A 使用者:在 ComfyUI 資料夾內執行 python.exe
|
||||
D:\ComfyUI_Setup\ComfyUI_windows_portable_nvidia\python_embeded\python.exe -m pip install onnxruntime-gpu
|
||||
|
||||
# Plan B 使用者:
|
||||
pip install onnxruntime-gpu
|
||||
```
|
||||
|
||||
### 3.4 安裝 Impact Pack(進階功能,可選)
|
||||
|
||||
**方法一:透過 Manager**
|
||||
|
||||
1. 搜尋 **「ComfyUI-Impact-Pack」**
|
||||
2. 點擊 **「Install」**
|
||||
|
||||
**方法二:手動安裝**
|
||||
|
||||
```powershell
|
||||
cd D:\ComfyUI_Setup\ComfyUI_windows_portable_nvidia\ComfyUI\custom_nodes
|
||||
git clone https://github.com/ltdrdata/ComfyUI-Impact-Pack.git
|
||||
```
|
||||
|
||||
### 3.5 重新安裝依賴
|
||||
|
||||
安裝完擴充套件後,重啟 ComfyUI,Manager 會提示安裝缺失的依賴,點擊 **「Install」** 即可。
|
||||
|
||||
### 3.6 重啟 ComfyUI
|
||||
|
||||
關閉 ComfyUI 視窗,重新啟動。
|
||||
|
||||
---
|
||||
|
||||
## 4. 下載模型權重
|
||||
|
||||
> 以下步驟 Plan A 和 Plan B 都適用
|
||||
|
||||
### 4.1 SDXL 主模型
|
||||
|
||||
下載 **SDXL 1.0** 或 **SDXL Turbo**(推薦 SDXL 1.0 品質更好)
|
||||
|
||||
**推薦模型**:
|
||||
- `sd_xl_base_1.0.safetensors`(官方)
|
||||
- `juggernautXL_v9.safetensors`(社群優化版)
|
||||
- `realvisxlV40.safetensors`(寫實風格)
|
||||
|
||||
**下載位置**:
|
||||
```
|
||||
D:\ComfyUI_Setup\ComfyUI_windows_portable_nvidia\ComfyUI\models\checkpoints\
|
||||
```
|
||||
|
||||
**下載來源**:
|
||||
- HuggingFace: https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0
|
||||
- Civitai: https://civitai.com/models/47707/juggernaut-xl
|
||||
|
||||
### 4.2 IP-Adapter 權重
|
||||
|
||||
下載 **IP-Adapter Plus SDXL** 系列:
|
||||
|
||||
```
|
||||
D:\ComfyUI_Setup\ComfyUI_windows_portable_nvidia\ComfyUI\models\ipadapter\
|
||||
```
|
||||
|
||||
**需要下載的檔案**:
|
||||
- `ip-adapter-plus_sdxl_vit-h.safetensors`
|
||||
- `ip-adapter-plus-face_sdxl_vit-h.safetensors`
|
||||
|
||||
**下載來源**:
|
||||
- HuggingFace: https://huggingface.co/h94/IP-Adapter-plus/tree/main/sdxl_models
|
||||
|
||||
### 4.3 ControlNet 權重
|
||||
|
||||
下載 **ControlNet DWpose SDXL**:
|
||||
|
||||
```
|
||||
D:\ComfyUI_Setup\ComfyUI_windows_portable_nvidia\ComfyUI\models\controlnet\
|
||||
```
|
||||
|
||||
**需要下載的檔案**:
|
||||
- `controlnet-diffusers-dw-lllite-xl-diffusers-rank256.safetensors`
|
||||
- 或 `thibaud/controlnet-openpose-sdxl-1.0`
|
||||
|
||||
**下載來源**:
|
||||
- HuggingFace: https://huggingface.co/Thibaud/controlnet-openpose-sdxl-1.0
|
||||
|
||||
### 4.4 VAE(可選)
|
||||
|
||||
```
|
||||
D:\ComfyUI_Setup\ComfyUI_windows_portable_nvidia\ComfyUI\models\vae\
|
||||
```
|
||||
|
||||
**下載**:`sdxl_vae.safetensors`
|
||||
|
||||
### 4.5 檔案結構確認
|
||||
|
||||
```
|
||||
D:\ComfyUI_Setup\ComfyUI_windows_portable_nvidia\ComfyUI\
|
||||
├── models\
|
||||
│ ├── checkpoints\
|
||||
│ │ └── sd_xl_base_1.0.safetensors
|
||||
│ ├── ipadapter\
|
||||
│ │ ├── ip-adapter-plus_sdxl_vit-h.safetensors
|
||||
│ │ └── ip-adapter-plus-face_sdxl_vit-h.safetensors
|
||||
│ ├── controlnet\
|
||||
│ │ └── controlnet-diffusers-dw-lllite-xl-diffusers-rank256.safetensors
|
||||
│ └── vae\
|
||||
│ └── sdxl_vae.safetensors
|
||||
└── output\
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 第一次測試
|
||||
|
||||
### 5.1 載入預設工作流
|
||||
|
||||
1. 開啟 `http://127.0.0.1:8188`
|
||||
2. 點擊左上角 **「Load」** 載入預設工作流
|
||||
3. 或手動建立以下節點:
|
||||
|
||||
### 5.2 基本生成測試
|
||||
|
||||
```
|
||||
Load Checkpoint → CLIP Text Encode (Positive) → CLIP Text Encode (Negative) → EmptyLatentImage → KSampler → VAEDecode → Save Image
|
||||
```
|
||||
|
||||
**測試 Prompt**:
|
||||
|
||||
```
|
||||
Positive: a cute chibi girl, blue hair, smiling, white background, high quality
|
||||
Negative: ugly, deformed, noisy, blurry, low quality, worst quality
|
||||
```
|
||||
|
||||
**KSampler 參數**:
|
||||
- Model: sd_xl_base_1.0
|
||||
- Sampler: dpmpp_2m
|
||||
- Scheduler: karras
|
||||
- Steps: 20-30
|
||||
- CFG: 5-7
|
||||
- Seed: random
|
||||
- Width: 1024
|
||||
- Height: 1024
|
||||
|
||||
點擊 **「Queue Prompt」** 開始生成。
|
||||
|
||||
---
|
||||
|
||||
## 6. IP-Adapter 角色一致性工作流
|
||||
|
||||
### 6.1 工作流結構
|
||||
|
||||
```
|
||||
Load Checkpoint
|
||||
↓
|
||||
CLIP Vision Loader (ip-adapter-plus-face_sdxl_vit-h.bin)
|
||||
↓
|
||||
IPAdapter Model Loader
|
||||
↓
|
||||
Load Image (角色立繪)
|
||||
↓
|
||||
IPAdapter Apply
|
||||
↓
|
||||
KSampler
|
||||
↓
|
||||
Save Image
|
||||
```
|
||||
|
||||
### 6.2 節點設定
|
||||
|
||||
**CLIP Vision Loader**:
|
||||
- ckpt_name: `ip-adapter-plus-face_sdxl_vit-h.bin`
|
||||
|
||||
**IPAdapter Model Loader**:
|
||||
- ipadapter_file: `ip-adapter-plus-face_sdxl_vit-h.safetensors`
|
||||
|
||||
**IPAdapter Apply**:
|
||||
- weight: **0.7 - 0.9**(角色一致性越高,但創意自由度越低)
|
||||
- weight_type: `default`
|
||||
- start_at: 0
|
||||
- end_at: 1
|
||||
- encode_steps: 10
|
||||
|
||||
**Load Image**:
|
||||
- 上傳角色的立繪圖片(建議 1024x1024)
|
||||
|
||||
### 6.3 IP-Adapter Weight 調整指南
|
||||
|
||||
| Weight | 效果 | 適用情境 |
|
||||
|--------|------|---------|
|
||||
| 0.5-0.6 | 角色特徵保留約 60% | 大動作、大表情變化 |
|
||||
| 0.7-0.8 | 角色特徵保留約 80% | **推薦起始值** |
|
||||
| 0.9-1.0 | 角色特徵保留約 95% | 微小表情變化 |
|
||||
|
||||
---
|
||||
|
||||
## 7. ControlNet DWpose 姿勢控制
|
||||
|
||||
### 7.1 工作流結構
|
||||
|
||||
```
|
||||
Load Checkpoint
|
||||
↓
|
||||
CLIP Vision Loader
|
||||
↓
|
||||
IPAdapter Model Loader
|
||||
↓
|
||||
Load Image (角色立繪)
|
||||
↓
|
||||
IPAdapter Apply
|
||||
↓
|
||||
OpenPose Encode (DWpose)
|
||||
↓
|
||||
Load Image (姿勢參考圖)
|
||||
↓
|
||||
ControlNet Apply
|
||||
↓
|
||||
KSampler
|
||||
↓
|
||||
Save Image
|
||||
```
|
||||
|
||||
### 7.2 DWpose 設定
|
||||
|
||||
**OpenPose Encode**:
|
||||
- 使用 DWpose 模型(內建於 comfyui_controlnet_aux)
|
||||
- 輸入:姿勢參考圖(可以是任何人的照片或繪圖)
|
||||
|
||||
**ControlNet Apply**:
|
||||
- strength: **0.7 - 0.9**
|
||||
- start_percent: 0
|
||||
- end_percent: 1
|
||||
|
||||
### 7.3 姿勢參考圖來源
|
||||
|
||||
1. **手繪草圖**:簡單畫出想要的姿勢
|
||||
2. **網路圖片**:搜尋動漫角色姿勢
|
||||
3. **AI 生成**:先用 ComfyUI 生成姿勢,再套用角色
|
||||
|
||||
---
|
||||
|
||||
## 8. 批量生產流程
|
||||
|
||||
### 8.1 準備工作
|
||||
|
||||
1. 建立 8 個角色的資料夾:
|
||||
```
|
||||
D:\ComfyUI_Setup\ComfyUI_windows_portable_nvidia\ComfyUI\input\characters\
|
||||
├── grok4\
|
||||
│ └── reference.png
|
||||
├── kimi\
|
||||
│ └── reference.png
|
||||
├── qwen3\
|
||||
│ └── reference.png
|
||||
├── gpt5\
|
||||
│ └── reference.png
|
||||
├── gemini\
|
||||
│ └── reference.png
|
||||
├── deepseek\
|
||||
│ └── reference.png
|
||||
├── doubao\
|
||||
│ └── reference.png
|
||||
└── claude\
|
||||
└── reference.png
|
||||
```
|
||||
|
||||
2. 將 8 個角色的立繪分別放入對應資料夾
|
||||
|
||||
### 8.2 批量生成腳本
|
||||
|
||||
建立 `batch_generate.py`:
|
||||
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
COMFYUI_API = "http://127.0.0.1:8188"
|
||||
|
||||
# 表情/動作清單
|
||||
EXPRESSIONS = [
|
||||
"smiling happily",
|
||||
"laughing loudly",
|
||||
"crying with tears",
|
||||
"angry and frowning",
|
||||
"surprised with wide eyes",
|
||||
"thinking with hand on chin",
|
||||
"winking playfully",
|
||||
"shocked and speechless",
|
||||
"blushing shyly",
|
||||
"determined and focused",
|
||||
"sleeping peacefully",
|
||||
"screaming in horror"
|
||||
]
|
||||
|
||||
def queue_prompt(workflow):
|
||||
"""發送工作流到 ComfyUI"""
|
||||
data = {"prompt": workflow}
|
||||
response = requests.post(COMFYUI_API + "/prompt", json=data)
|
||||
return response.json()["prompt_id"]
|
||||
|
||||
def get_image(prompt_id):
|
||||
"""等待生成完成並取得圖片"""
|
||||
while True:
|
||||
response = requests.get(COMFYUI_API + "/history/" + prompt_id)
|
||||
if response.json()[prompt_id] is not None:
|
||||
break
|
||||
time.sleep(1)
|
||||
return response.json()[prompt_id]["outputs"]
|
||||
|
||||
def generate_character(character_name, expression, ip_weight=0.8):
|
||||
"""生成單一角色的單一表情"""
|
||||
workflow = {
|
||||
"3": { # Load Checkpoint
|
||||
"class_type": "CheckpointLoaderSimple",
|
||||
"inputs": {"ckpt_name": "sd_xl_base_1.0.safetensors"}
|
||||
},
|
||||
"10": { # CLIP Vision Loader
|
||||
"class_type": "CLIPVisionLoader",
|
||||
"inputs": {"clip_name": "ip-adapter-plus-face_sdxl_vit-h.bin"}
|
||||
},
|
||||
"11": { # IPAdapter Model Loader
|
||||
"class_type": "IPAdapterModelLoader",
|
||||
"inputs": {"ipadapter_file": "ip-adapter-plus-face_sdxl_vit-h.safetensors"}
|
||||
},
|
||||
"12": { # Load Image (角色立繪)
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {
|
||||
"image": f"characters/{character_name}/reference.png",
|
||||
"upload": f"characters/{character_name}/reference.png"
|
||||
}
|
||||
},
|
||||
"13": { # IPAdapter Apply
|
||||
"class_type": "IPAdapterApply",
|
||||
"inputs": {
|
||||
"weight": ip_weight,
|
||||
"weight_type": "default",
|
||||
"start_at": 0,
|
||||
"end_at": 1
|
||||
}
|
||||
},
|
||||
"6": { # Positive Prompt
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {
|
||||
"text": f"a cute chibi girl, {expression}, white background, high quality, detailed face",
|
||||
"clip": ["3", 1]
|
||||
}
|
||||
},
|
||||
"7": { # Negative Prompt
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {
|
||||
"text": "ugly, deformed, noisy, blurry, low quality, worst quality, extra limbs",
|
||||
"clip": ["3", 1]
|
||||
}
|
||||
},
|
||||
"5": { # Empty Latent Image
|
||||
"class_type": "EmptyLatentImage",
|
||||
"inputs": {"width": 1024, "height": 1024, "batch_size": 1}
|
||||
},
|
||||
"14": { # KSampler
|
||||
"class_type": "KSampler",
|
||||
"inputs": {
|
||||
"model": ["13", 0],
|
||||
"positive": ["6", 0],
|
||||
"negative": ["7", 0],
|
||||
"latent_image": ["5", 0],
|
||||
"seed": 42,
|
||||
"steps": 25,
|
||||
"cfg": 6,
|
||||
"sampler_name": "dpmpp_2m",
|
||||
"scheduler": "karras",
|
||||
"denoise": 1
|
||||
}
|
||||
},
|
||||
"15": { # VAEDecode
|
||||
"class_type": "VAEDecode",
|
||||
"inputs": {"samples": ["14", 0], "vae": ["3", 2]}
|
||||
},
|
||||
"16": { # Save Image
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {
|
||||
"images": ["15", 0],
|
||||
"filename_prefix": f"{character_name}_{expression.replace(' ', '_')}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prompt_id = queue_prompt(workflow)
|
||||
print(f"Generating: {character_name} - {expression}")
|
||||
return get_image(prompt_id)
|
||||
|
||||
# 批量生成
|
||||
characters = ["grok4", "kimi", "qwen3", "gpt5", "gemini", "deepseek", "doubao", "claude"]
|
||||
|
||||
for char in characters:
|
||||
for expr in EXPRESSIONS:
|
||||
generate_character(char, expr)
|
||||
time.sleep(2) # 間隔 2 秒避免 GPU 過熱
|
||||
|
||||
print("Batch generation complete!")
|
||||
```
|
||||
|
||||
### 8.3 執行批量生成
|
||||
|
||||
```powershell
|
||||
# 確保 ComfyUI 正在運行
|
||||
# Plan A: 雙擊 run_nvidia_gpu.bat
|
||||
# Plan B: python main.py
|
||||
|
||||
# 在另一個終端機執行批量腳本
|
||||
python batch_generate.py
|
||||
```
|
||||
|
||||
**預估時間**:
|
||||
- 單張圖:~5-10 秒
|
||||
- 12 張/角色:~1-2 分鐘
|
||||
- 8 角色 × 12 張 = 96 張:~3-5 小時
|
||||
|
||||
---
|
||||
|
||||
## 9. Prompt 工程指南
|
||||
|
||||
### 9.1 表情 Prompt 模板
|
||||
|
||||
```
|
||||
Positive: a cute chibi girl, [表情描述], white background, high quality, detailed face, anime style
|
||||
|
||||
Negative: ugly, deformed, noisy, blurry, low quality, worst quality, extra limbs, bad anatomy
|
||||
```
|
||||
|
||||
### 9.2 表情描述詞庫
|
||||
|
||||
| 表情 | Prompt |
|
||||
|------|--------|
|
||||
| 開心大笑 | laughing loudly with eyes closed, joy |
|
||||
| 微笑 | gentle smile, peaceful expression |
|
||||
| 哭泣 | crying with tears, sad expression |
|
||||
| 生氣 | angry, frowning, furrowed brows |
|
||||
| 驚訝 | surprised, wide eyes, open mouth |
|
||||
| 思考 | thinking, hand on chin, contemplative |
|
||||
| 眨眼 | winking playfully, cute expression |
|
||||
| 害羞 | blushing, shy smile, looking down |
|
||||
| 恐懼 | screaming in horror, terrified expression |
|
||||
| 堅定 | determined, focused eyes, clenched fist |
|
||||
| 睡覺 | sleeping peacefully, Zzz, closed eyes |
|
||||
| 無聊 | bored expression, yawning, looking away |
|
||||
|
||||
### 9.3 動作 Prompt 模板
|
||||
|
||||
```
|
||||
Positive: a cute chibi girl, [動作描述], dynamic pose, white background, high quality
|
||||
|
||||
Negative: ugly, deformed, noisy, blurry, low quality, worst quality, extra limbs, bad anatomy
|
||||
```
|
||||
|
||||
### 9.4 動作描述詞庫
|
||||
|
||||
| 動作 | Prompt |
|
||||
|------|--------|
|
||||
| 揮手 | waving hand, greeting pose |
|
||||
| 比讚 | thumbs up, positive gesture |
|
||||
| 指向前方 | pointing forward, confident pose |
|
||||
| 雙手抱胸 | arms crossed, confident stance |
|
||||
| 跳躍 | jumping in air, dynamic pose |
|
||||
| 蹲下 | crouching down, playful pose |
|
||||
| 轉圈 | spinning around, happy dance |
|
||||
| 舉手 | raising hand, excited gesture |
|
||||
| 捂臉 | covering face with hands, embarrassed |
|
||||
| 攤手 | shrugging, confused gesture |
|
||||
|
||||
---
|
||||
|
||||
## 10. 品質篩選與後處理
|
||||
|
||||
### 10.1 篩選標準
|
||||
|
||||
| 項目 | 合格 | 不合格 |
|
||||
|------|------|--------|
|
||||
| 臉部特徵 | 與立繪一致 | 臉部變形、特徵消失 |
|
||||
| 髮型髮色 | 正確 | 髮色改變、髮型錯誤 |
|
||||
| 服裝 | 正確 | 服裝變形、顏色錯誤 |
|
||||
| 配件 | 正確 | 配件消失或變形 |
|
||||
| 表情 | 自然 | 表情僵硬或不自然 |
|
||||
| 手部 | 正常 | 手指數量錯誤、變形 |
|
||||
|
||||
### 10.2 篩選流程
|
||||
|
||||
1. 將生成的圖片按角色分類
|
||||
2. 逐一檢查是否符合篩選標準
|
||||
3. 不合格的圖片標記為 `reject`
|
||||
4. 合格的圖片標記為 `keep`
|
||||
|
||||
### 10.3 後處理
|
||||
|
||||
**使用工具**:
|
||||
- GIMP(免費)
|
||||
- Photoshop(付費)
|
||||
- Photopea(線上免費)
|
||||
|
||||
**後處理項目**:
|
||||
- 修正手指變形
|
||||
- 調整臉部細節
|
||||
- 統一背景顏色
|
||||
- 調整大小至 512x512 或 256x256(遊戲用)
|
||||
|
||||
---
|
||||
|
||||
## 11. 常見問題與解決方案
|
||||
|
||||
### 11.1 GPU 記憶體不足
|
||||
|
||||
**錯誤**:`CUDA out of memory`
|
||||
|
||||
**解決方案**:
|
||||
1. 降低解析度(1024x1024 → 768x768)
|
||||
2. 減少 batch_size(設為 1)
|
||||
3. 關閉其他程式釋放記憶體
|
||||
4. 使用 `--lowvram` 啟動參數:
|
||||
```powershell
|
||||
python main.py --lowvram
|
||||
```
|
||||
|
||||
### 11.2 IP-Adapter 效果不強
|
||||
|
||||
**問題**:生成的圖片與角色立繪差異太大
|
||||
|
||||
**解決方案**:
|
||||
1. 提高 IP-Adapter weight(0.7 → 0.9)
|
||||
2. 使用 `ip-adapter-plus-face` 而非 `ip-adapter-plus`
|
||||
3. 確保立繪圖片品質高、清晰
|
||||
4. 減少 CFG scale(7 → 5)
|
||||
|
||||
### 11.3 ControlNet 姿勢不準確
|
||||
|
||||
**問題**:生成的姿勢與參考圖不符
|
||||
|
||||
**解決方案**:
|
||||
1. 提高 ControlNet strength(0.7 → 0.9)
|
||||
2. 使用更清晰的姿勢參考圖
|
||||
3. 確保 DWpose 模型正確載入
|
||||
4. 嘗試不同的 ControlNet 模型
|
||||
|
||||
### 11.4 生成速度慢
|
||||
|
||||
**問題**:單張圖超過 30 秒
|
||||
|
||||
**解決方案**:
|
||||
1. 減少 steps(30 → 20)
|
||||
2. 使用更快的 sampler(dpmpp_2m → euler)
|
||||
3. 關閉其他佔用 GPU 的程式
|
||||
4. 更新 NVIDIA 驅動至最新版本
|
||||
|
||||
### 11.5 圖片品質不佳
|
||||
|
||||
**問題**:生成的圖片模糊或變形
|
||||
|
||||
**解決方案**:
|
||||
1. 增加 steps(20 → 30)
|
||||
2. 提高 CFG scale(5 → 7)
|
||||
3. 使用更好的 negative prompt
|
||||
4. 更換 SDXL 模型(嘗試 juggernautXL)
|
||||
|
||||
---
|
||||
|
||||
## 12. 資源連結
|
||||
|
||||
### 12.1 官方文件
|
||||
|
||||
- ComfyUI: https://github.com/comfyanonymous/ComfyUI
|
||||
- IP-Adapter: https://github.com/tencent-ailab/IP-Adapter
|
||||
- ControlNet: https://github.com/lllyasviel/ControlNet
|
||||
|
||||
### 12.2 模型下載
|
||||
|
||||
- HuggingFace: https://huggingface.co/
|
||||
- Civitai: https://civitai.com/
|
||||
- LiblibAI: https://www.liblib.art/
|
||||
|
||||
### 12.3 教學資源
|
||||
|
||||
- Bilibili ComfyUI 教學:搜尋 "ComfyUI IP-Adapter"
|
||||
- YouTube 教學:搜尋 "ComfyUI character consistency"
|
||||
|
||||
### 12.4 社群
|
||||
|
||||
- ComfyUI Discord: https://discord.com/invite/comfy
|
||||
- Reddit r/StableDiffusion: https://reddit.com/r/StableDiffusion
|
||||
|
||||
---
|
||||
|
||||
## 📝 備註
|
||||
|
||||
- 本文檔基於 RTX A4000 (16GB VRAM) 測試
|
||||
- 所有模型權重請從官方來源下載,注意授權條款
|
||||
- 建議先以 1-2 個角色測試,確認效果後再批量生產
|
||||
- 生成的圖片僅供內部使用,注意版權問題
|
||||
|
||||
---
|
||||
|
||||
**最後更新**:2026-07-30
|
||||
**作者**:Hermes Agent (software-engineer profile)
|
||||
@@ -0,0 +1,46 @@
|
||||
# MiniMax H3 × AI 繪圖 — 遊戲視覺探索調查總覽
|
||||
|
||||
> 調查日期:2026-08-06
|
||||
> 產出:9 份 subagent 調查報告 + 本總覽
|
||||
> 專案:Liar's Bar(騙子酒館)網頁版 — 8 個 AI 擬人角色 + 卡牌唬牌
|
||||
> 技術棧:Vue 3 + Vite + Pinia + Naive UI / Node + Socket.IO
|
||||
|
||||
## 結論速覽(60 秒版)
|
||||
|
||||
**核心策略:建置期批量預生成 + 前端合成 + 誠實揭露,不做運行期即時生成。**
|
||||
|
||||
1. **靜態美術幾乎免錢**:FLUX.2 / Nano Banana 2 有原生多參考圖一致性,8 角色定稿成本極低(image-01 ¥0.025/張、FLUX-schnell $0.003/張)。
|
||||
2. **動態素材主力 = MiniMax H3**:768P ¥0.50/秒、2K ¥0.80/秒,8 角色待機動畫約 ¥200–600;但 H3 是非同步 API(延遲數十秒~分鐘),只適合預生成,不適合遊玩中即時產。
|
||||
3. **一致性 = 「1 支畫風 LoRA × 8 支角色 LoRA」疊加**:沿用既有 ComfyUI + IP-Adapter 管線,或改用 FLUX.2 / Nano Banana 多參考圖免訓練。
|
||||
4. **上線格式 = WebM(VP9/AV1/alpha) / Animated WebP / sprite sheet**,搭配 Socket.IO→Pinia→CSS/WAAPI 架構防卡 UI。
|
||||
5. **授權紅線**:FLUX dev 版不可商用(用 schnell 或付費 API);8 個品牌角色建議改原創名 + 自訂配色 + 人工終製留足跡(商標風險)。
|
||||
6. **落地順序**:Phase 1 CSS 打底 → 2 靜態美術 → 3 待機動畫 → 4 過場/動態 → 5 打磨。
|
||||
|
||||
## 報告索引
|
||||
|
||||
| # | 檔案 | 主題 | 價值標記 | 大小 |
|
||||
|---|------|------|----------|------|
|
||||
| 01 | `01_minimax-h3影片生成調查.md` | MiniMax H3(海螺)能力、規格、API、競品、應用點 | 高 | 18 KB |
|
||||
| 02 | `02_ai角色繪製素材生成.md` | 2025-26 文生圖工具比較、立繪/卡牌工作流、LoRA | 高 | 16 KB |
|
||||
| 03 | `03_靜態轉動態動畫化.md` | 圖生影片(H3/SVD/EBsynth/Live2D)+ 網頁格式分工 | 高 | 19 KB |
|
||||
| 04 | `04_網頁素材整合技術.md` | WebM/WebP/APNG/Lottie/精靈圖、Vue 載入、防卡頓架構 | 高 | 21 KB |
|
||||
| 05 | `05_角色一致性深化.md` | 9 種一致性方法對比、畫風LoRA×角色LoRA、SOP | 高 | 21 KB |
|
||||
| 06 | `06_卡牌牌桌視覺與風格指引.md` | 風格推薦、卡面/牌背/木桌提示詞模板、避弊技巧 | 高 | 20 KB |
|
||||
| 07 | `07_成本部署與生成管線.md` | 影片/圖 API 成本、API vs 自架、MVP/完整版預算 | 高 | 16 KB |
|
||||
| 08 | `08_商用授權與法律.md` | 各模型商用授權對照、著作權、品牌角色侵權風險 | 高 | 27 KB |
|
||||
| 09 | `09_案例與落地路線圖.md` | 真實案例、Steam AI 政策、Phase 1-5 落地路圖 | 高 | 21 KB |
|
||||
|
||||
(補充:`raw/` 為 07 號調查保留的 MiniMax 官方定價/文檔原文快照,供原始資料查證。)
|
||||
|
||||
## 建議行動路線(詳見 09)
|
||||
|
||||
- **Phase 1**:CSS 浮動 + Vue Transition 打底(零成本、當天可做)
|
||||
- **Phase 2**:FLUX.2 / Nano Banana 定 8 角色 → 批量卡面(約數美元)
|
||||
- **Phase 3**:MiniMax H3 圖生視訊做 8 角色待機迴圈 → WebM/APNG(約 ¥200–600)
|
||||
- **Phase 4**:翻牌/過場 WebM + Lottie/GSAP 演出
|
||||
- **Phase 5**:Live2D 擬人互動(選配)、行銷素材
|
||||
|
||||
## 注意事項
|
||||
- 所有價格/規格/條款均出自子代理即時抓取的官方來源;查不到的項目各報告內已明確標註「未查到」。
|
||||
- 08 號為研究調查摘要,**非法律意見**;正式跨國商用前建議再核對官方最新條款並諮詢律師。
|
||||
- 部分調查為並行執行(07 與 09 同時進行),09 引用時 07 尚未就緒,故 09 正文未引用 07 的預算數字,請以 07 為準。
|
||||
@@ -0,0 +1,183 @@
|
||||
# MiniMax H3(海螺 Hailuo)影片生成模型調查報告 — 應用於「騙子酒館」網頁卡牌遊戲畫面
|
||||
|
||||
**調查日期:2026-08-06**
|
||||
|
||||
---
|
||||
|
||||
## 摘要
|
||||
|
||||
MiniMax H3 是 MiniMax 於 **2026-07-31** 正式發布的「開放通用多模態影片生成模型」(海螺影片生成系列第三代旗艦模型**(前代為 Hailuo 01、Hailuo 02)。它不是單純的文生影片工具,而是**統一理解「文字+圖片+影片+音訊」四種模態、並同時生成「影片+原生立體聲」的全能多模態影片模型**。官方規格:輸出 **768P / 2K**、時長 **4–15 秒**、支援 **9 種寬高比**、原生立體聲、`[运镜]` 指令控制運鏡、首幀/尾幀/多模態參考等多種生成模式。官方提供完整 RESTful API(`/v2/video_generation`,非同步任務制)及公開定價(2K **0.80 元/秒**、768P **0.50 元/秒**,人民幣,按量計費)。競品方面,**OpenAI Sora 已於 2026 年停止服務**(App 4/26、API 9/24),故 H3 的實際可比對象是 **Google Veo 3.1/Gemini Omni Flash、快手 Kling 3.0、Runway Gen-4.5、Pika 2.5**。對「騙子酒館」卡牌網頁遊戲,**H3 最適合「建置時預生成資產」而非「遊玩中即時生成」**:角色表情循環、卡面動畫、過場、背景循環、行銷大片皆可低成本預製;即時對戰時建議改用 CSS/Canvas/Lottie/WebM 短資產。完整建議與限制見文末「可行動建議」。
|
||||
|
||||
---
|
||||
|
||||
## 1. 它是什麼、最新版本與發布時間
|
||||
|
||||
- **它是什麼**:MiniMax H3(海螺 Hailuo 影片生成第三代)是一個「通用多模態生成模型」,能統一理解文字、圖片、影片、音訊四種模態的上下文,並生成帶**原生立體聲**的影片。官方定位涵蓋廣告、品牌、電商、產品設計、UI/UX、遊戲等商用內容創作(官方部落格明列「gaming」為目標場景)。
|
||||
- **最新版本**:目前開放平台影片生成僅有 **`MiniMax-H3`** 一個可呼叫模型(API schema 中 `model` 欄位 enum 僅此一值);另有配套的 **`MiniMax-H3-Regeneration`**(768P→2K 再生)與 **`MiniMax-H3-Context-IR`**(提示詞增強)兩個治理/輔助介面。
|
||||
- **發布時間**:官方部落格與開放平台模型發布頁均標記 **2026-07-31**;MiniMax 官網旗艦模型區標記「2026-07」。
|
||||
- **前代脈絡**:Hailuo 01(打底)、Hailuo 02(架構/資料/規模)、H3(統整任務與模態)。官方表示**近期計畫開源模型權重**(視法律法規而定,尚未給出確切日期)。
|
||||
|
||||
來源:
|
||||
- MiniMax H3 官方部落格:https://www.minimax.io/blog/minimax-h3 (標題副題「Open Model Breaking the Boundaries Between Tasks and Modalities」,日期 2026-07-31)
|
||||
- MiniMax 官網旗艦模型頁:https://www.minimax.com/
|
||||
- MiniMax 開放平台「模型發布」:https://platform.minimaxi.com/docs/release-notes/models.md (「2026 年 7 月 31 日 — MiniMax H3」)
|
||||
- 官方 API OpenAPI 規格:https://platform.minimaxi.com/docs/api-reference/video/generation/api/v2-video-generation.json
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心能力規格
|
||||
|
||||
以下皆以官方文件為準(影片生成指南 + API schema,見來源)。
|
||||
|
||||
### 2.1 生成模式(文生/圖生/參考/編輯)
|
||||
| 模式 | 輸入 | 用途 | 官方 API 對應 |
|
||||
|---|---|---|---|
|
||||
| 文生影片(t2va) | 純文字 prompt | 從零描述生成 | `content` 僅含一個 `text` |
|
||||
| 圖生影片(i2va) | 首幀圖(或尾幀圖/首尾幀兩張)+文字 | 讓靜態圖「動起來」、指定起/終畫面 | `role=first_frame` / `last_frame` |
|
||||
| 多模態參考生成(r2va) | 文字+參考影像(≤9)/參考影片(≤3)/參考音訊(≤3) | 參考角色、動作、鏡頭、風格、聲音、剪輯節奏;支援 V2V 動作遷移(motion transfer) | `role=reference_image/video/audio` |
|
||||
| 影片再生 | 已生成之 768P 影片 | 768P → 2K 升級 | `MiniMax-H3-Regeneration`,`role=base_video` |
|
||||
| 上下文提示詞增強 | 文字+圖/影/音 | 深度理解多模態上下文,輸出結構化增強 prompt(不產影片) | `MiniMax-H3-Context-IR` |
|
||||
|
||||
- **圖生影片與多模態參考互斥**:`content` 中不能同時混用 `reference_*` 與 `first/last_frame`。
|
||||
- 每個請求**必須**含一個非空 `text`(prompt 必填,≤7000 字元)。
|
||||
|
||||
### 2.2 解析度、時長、寬高比、幀率
|
||||
- **解析度(resolution)**:僅 `768P` 或 `2K`(原生 2K;H3-VAE 高壓縮率是 2K 直出關鍵)。``輸出影片幀率官方範例為 **24 fps**``(在再生規格中明確「幀率 24 fps」)。
|
||||
- **時長(duration)**:`4`–`15` 秒,**僅整數**(enum 4..15)。
|
||||
- **寬高比(ratio)**:`adaptive`(自動)、`21:9`、`16:9`、`4:3`、`1:1`、`3:4`、`9:16`。文生影片不可用 `adaptive`(必填具體比例);圖生影片恆為 `adaptive`(由輸入圖決定)。
|
||||
- **音訊**:生成內容**含原生立體聲**(人聲+音效+配樂一起建模,非分離處理)。
|
||||
|
||||
### 2.3 運動控制/塗鴉控制/可控性
|
||||
- **運鏡控制**:官方支援——在文字描述中加入 `[运镜]` 指令引導鏡頭調度;範例「鏡頭上移、slow push in」等。
|
||||
- **首/尾幀控制**:指定起點與結束畫面,可讓「靜態卡面動起來」或補完中段過渡。
|
||||
- **V2V 動作遷移**:以參考影片遷移動作、鏡頭、節奏(官方部落格「V2V motion transfer」明列為 H3 亮點)。
|
||||
- **精準指令遵循/品牌文字渲染**:官方宣稱擅長「大字幕、品牌資訊精準渲染」,適合卡面/標題文字。
|
||||
- **塗鴉(sketch/doodle)控制**:**在官方文件中「未查到」** 公開的專用塗鴉/骨架/逐格控制介面。H3 目前的可控手段是文字、首尾幀、多模態參考與再生,**沒有像某些工具那樣的圖層塗鴉/關鍵格控制參數**。(標記:未查到,請勿假設存在)
|
||||
|
||||
### 2.4 輸入限制(硬性約束)
|
||||
- 圖片:JPG/JPEG/PNG/WEBP/HEIC/HEIF,單檔 ≤30MB,寬高 `[256, 5760]px`,寬高比 `0.4–2.5`。
|
||||
- 影片(僅參考情境):MP4/MOV,H.264/H.265+AAC/MP3,單檔 ≤50MB,`≤3` 段,每段 `2–15s`、總長 ≤15s。
|
||||
- 音訊(僅參考情境):WAV/MP3,單檔 ≤15MB,`≤3` 段、每段 `2–15s`。
|
||||
- 混合總輸入上限 **12 個檔案**;請求體 ≤64MB(大檔建議用公網 URL 而非 Base64)。
|
||||
|
||||
來源:
|
||||
- 官方影片生成指南(完整規格表+限制表):https://platform.minimaxi.com/docs/guides/video-generation.md
|
||||
- H3 亮點功能示例(三大能力:原生多模態理解、多模態精準編輯與控制、商用多場景內容生成):https://platform.minimaxi.com/docs/guides/video-prompt.md
|
||||
- API schema(resolution/duration/ratio/content/role enum 全量):https://platform.minimaxi.com/docs/api-reference/video/generation/api/v2-video-generation.json
|
||||
|
||||
---
|
||||
|
||||
## 3. 官方 API 服務
|
||||
|
||||
- **是否提供 API**:是,完整公開。**非同步任務制**:`POST /v2/video_generation` 建立任務 → 回傳 `task_id` → 輪詢 `GET /v2/query/video_generation/{task_id}`(官方建議間隔 10 秒)→ 成功後取 `content.url` 下載成片。另有 `DELETE`(取消/刪除)、列表查詢、`callback_url` 事件回呼(需先回應 `challenge` 驗證)、AIGC 浮水印開關。
|
||||
- **驗證方式**:`Authorization: Bearer <API Key>`;Base URL `https://api.minimaxi.com`;提供 OpenAPI 3.1 規格與官方 Python 範例。
|
||||
- **其他整合**:提供 MiniMax MCP(Model Context Protocol)工具,支援語音合成、音色克隆、影像與影片生成等多模態能力。
|
||||
- **結果保留**:僅查詢最近 7 天任務;下載連結有時效,需及時轉存。
|
||||
- **傳輸模式**:非同步輪詢,**不支援串流/即時**。這直接影響「遊玩中即時生成」的可行性(見 §6 限制)。
|
||||
|
||||
### 3.1 定價(按量計費,人民幣,官方刊例價)
|
||||
| 項目 | 計費 | 官方價格 |
|
||||
|---|---|---|
|
||||
| MiniMax-H3 輸出 | 按秒 | **2K:0.80 元/秒;768P:0.50 元/秒** |
|
||||
| 輸入圖片 | 按張 | **5 張內免費**,超出 0.20 元/張 |
|
||||
| 輸入影片 | 按輸入秒數 | 2K 0.80 元/秒、768P 0.50 元/秒 |
|
||||
| 輸入音訊 | — | **免費** |
|
||||
| H3-Regeneration | 按再生輸出秒數 | 0.30 元/秒 |
|
||||
| H3-Context-IR | 按 tokens | 輸入 5.80 元/百萬、輸出 23.00 元/百萬 |
|
||||
|
||||
- **注意**:**影片資源包(訂閱制)目前「暫不支援 MiniMax H3」**,僅支援 Hailuo 系列(例:Hailuo-2.3-Fast 768p/6s 扣 0.7 點)。H3 現階段只能**按量計費(pay-as-you-go)**。
|
||||
- 成本速算(參考,非官方換算):一條 **5 秒 768P 短片 ≈ 2.50 元(約 US$0.35)**;一條 **5 秒 2K 短片 ≈ 4.00 元(約 US$0.57)**。
|
||||
|
||||
來源:
|
||||
- 按量計費定價頁:https://platform.minimaxi.com/docs/guides/pricing-paygo.md
|
||||
- 影片資源包定價頁(載明「暫不支援 MiniMax H3」):https://platform.minimaxi.com/docs/guides/pricing-video.md
|
||||
- MiniMax MCP 指南:https://platform.minimaxi.com/docs/guides/mcp-guide.md
|
||||
|
||||
---
|
||||
|
||||
## 4. 與競品比較(2026-08 公開資料)
|
||||
|
||||
> 請注意:競品規格/價格變動極快,下表僅為調查當下之可查證公開資料;查不到的欄位已標「未查到」。
|
||||
|
||||
| 項目 | **MiniMax H3** | **OpenAI Sora** | **Google Veo 3.1 / Gemini Omni Flash** | **快手 Kling 3.0** | **Runway Gen-4.5** | **Pika 2.5** |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 是否可用 | ✅ 可用 | ❌ **已停用** | ✅ 可用 | ✅ 可用 | ✅ 可用 | ✅ 可用 |
|
||||
| 發布狀態 | 2026-07-31 正式版 | 官方宣布停用(App 2026-04-26,API 2026-09-24) | 在建置中/GA(Gemini API 付費層) | Kling 3.0 系列(VIDEO 3.0 / Omni,原生音訊) | Gen-4.5 系列 | Pika 2.5(2026) |
|
||||
| 解析度 | **768P / 2K** | 已停用(原支援 720p/1080p) | Omni Flash 720p;Veo 3.1 更高 | 1080p(細節未完整查到) | 多檔(1080p/4K 升頻) | 基礎方案僅 480p |
|
||||
| 最大時長 | **15 秒** | (已停用) | 8 秒級別(未查到確切上限) | 10 秒級(未查到確切上限) | 短篇(未查到確切上限) | 短篇 |
|
||||
| 音訊 | **原生立體聲** | (已停用) | Veo 3.1 原生音訊 | 原生音訊 | 可用(另有音訊工具) | 未查到 |
|
||||
| 運動/首尾幀控制 | ✅ 首尾幀+`[运镜]`+V2V 遷移 | — | ✅ 幀指定/圖導向/場景延伸 | ✅ Motion Control+分鏡控制 | ✅ | ✅ Pikatwists 等 |
|
||||
| 塗鴉/關鍵格控制 | 未查到 | — | 未查到 | 未查到 | 未查到 | 未查到 |
|
||||
| 定價模式 | 按秒 2K 0.80 元 / 768P 0.50 元 | — | Omni Flash ≈ **$0.10/秒**(720p,5792 token/秒,輸出 $17.5/M) | 訂閱/點數(確切單價未查到) | 訂閱 $12–28/月起,點數制 | 訂閱點數制(10–80 點/片) |
|
||||
| API | ✅ 完整 REST+OpenAPI+MCP | — | ✅ Gemini API | ✅ API 平台 | ✅ | ✅ API |
|
||||
|
||||
**判讀**:
|
||||
- **Sora 已退場**,商用影片生成的主要開放 API 選擇落在 **H3、Veo/Gemini Omni Flash、Kling**。三人中 H3 在「**統一多模態理解(圖+影+音同時給)+原生立體聲+2K**」與「**按秒計費最細粒度**」上是最突出的差異點;Veo/Gemini Omni 的 `$0.10/秒 720p` 為最便宜的純文字→720p 選項;Kling 3.0 偏重「長分鏡敘事+原生音訊+Motion Control」。
|
||||
- Runway/Pika 是**訂閱/點數制**且不主打 2K 統一多模態,較適合「人數少、品質要頂」的商剪,不適合高頻逐資產呼叫。
|
||||
|
||||
來源:
|
||||
- OpenAI Sora 停用公告(Help Center):https://openai.com/sora/
|
||||
- Google Gemini 影片生成文件(Veo 3.1/Gemini Omni Flash):https://ai.google.dev/gemini-api/docs/video
|
||||
- Google Gemini API 定價頁(Omni Flash 影片輸出 $17.5/M、≈$0.10/秒 720p):https://ai.google.dev/gemini-api/docs/pricing
|
||||
- Kling AI 官網(Kling 3.0 系列、Motion Control、原生音訊):https://kling.ai/
|
||||
- Runway 定價頁(Gen-4.5、訂閱 $12–28/月、點數制、代管 Veo 3.1 與 Kling 3.0):https://runwayml.com/pricing
|
||||
- Pika 定價頁(Pika 2.5、480p、點數制):https://pika.art/pricing
|
||||
|
||||
---
|
||||
|
||||
## 5. 對「騙子酒館」卡牌網頁遊戲的應用點與限制
|
||||
|
||||
遊戲背景:網頁版卡牌唬騙遊戲,桌上有多個 AI 擬人角色(Grok/Kimi/Qwen/GPT/Gemini 等品牌角色),目前角色只有靜態視覺。目標:讓畫面變好看。
|
||||
|
||||
### 5.1 可行應用點(H3 很適合「預生成」)
|
||||
| 用途 | 用 H3 的哪種模式 | 建議規格 | 為何合適 |
|
||||
|---|---|---|---|
|
||||
| **角色動態表情循環** | 圖生影片(角色立繪為首幀) | 768P,4–5s,1:1/3:4,做成 loop WebM | 把「靜態立繪」變成眨眼/挑眉/奸笑循環,直接提升對戰沉浸感 |
|
||||
| **卡面動態(Card Face / 金卡/Live2D 風)** | 圖生影片(卡面為首幀)+`[运镜]` | 768P,4s,1:1/3:4 | 唬牌「亮牌」瞬間可播 1–2 秒動卡面 |
|
||||
| **過場動畫(Cinematic)** | 文生影片/多模態參考 | 2K,5–8s,21:9/16:9 | 回合切換、決鬥高潮、揭穿唬牌的「大片式」過場 |
|
||||
| **背景循環(Ambient loop)** | 文生影片 | 768P,5s,21:9/16:9,loop | 酒館火光、雨夜窗景等循環背景,替代靜態底圖 |
|
||||
| **GIF/短動畫** | 圖生或文生後轉 GIF | 768P 短片段轉 GIF | 表情包、聊天欄情緒貼圖、勝利/失敗短動畫 |
|
||||
| **行銷/宣傳大片** | 多模態參考+Context-IR | 2K | 商店頁、巴哈/Steam 頁、社群 Banner(官方明列 gamer 場景) |
|
||||
| **背景音樂** | 搭配 **Music 3.0** 模型 | 音訊 | 酒館主題曲/回合 BGM(音樂生成本身是另一模型,見 §4 附註) |
|
||||
|
||||
### 5.2 硬性限制(務必正視)
|
||||
1. **延遲(最大殺手)**:API 是非同步任務制,官方輪詢間隔 10 秒起跳,實際「送出→拿到」通常數十秒至數分鐘。**完全無法在對戰回合內即時生成**。→ 只能「先建後用」。
|
||||
2. **成本**:逐資產呼叫要錢(768P 0.50 元/秒、2K 0.80 元/秒)。8 個角色 × 每角 3–5 個表情 × 5s ≈ 數十元等級的小規模資產庫尚可接受;若追求每個玩家客製化即時生成則成本爆炸。
|
||||
3. **可控性**:沒有塗鴉/關鍵格控制(未查到),「精準讓某根手指動」做不到;只能靠文字+首尾幀+參考逼近。遊戲角色的一致性要靠「同一張立繪反覆當首幀 + 明確角色描述」維持,仍可能有細微漂移。
|
||||
4. **資源包不支援 H3**:訂閱式影片資源包不含 H3,只能按量付費,需管理餘額與扣款。
|
||||
5. **格式**:輸出為 MP4 影片(24fps);要 WebM loop / GIF 需自行轉檔,前端需處理影片載入與播放(避免阻斷首載)。
|
||||
6. **7 天保留**:下載連結有時效,資產需建置時立即轉存到自家 CDN/物件儲存。
|
||||
|
||||
### 5.3 對我們的可行動建議(可直接落地)
|
||||
> 核心口訣:**「建置時用 H3 大量預製,遊玩時用前端輕量播放」— 不要在任何對戰路徑上同步等 H3。**
|
||||
|
||||
1. **只做「預生成資產庫」**:在發版/內容管線中(非玩家遊玩時)批次呼叫 H3,產出表情循環、動卡面、過場、循環背景,轉成**短 WebM(H.264 web-compatible,建議 ≤2–4s、768P)或 Lottie/APNG**,放自家 CDN。
|
||||
2. **角色一致性策略**:為每個 AI 角色鎖定「一張官方立繪」當首幀,統一一個角色風格描述前綴,每個表情生成 2–3 個候選再人工挑選,避免角色「漂移」。
|
||||
3. **動卡面只在小情境用**:亮牌、唬牌成功/失敗、決鬥高潮等「觸發即播 1–2s」的場合用影片;其餘常駐畫面(棋盤、計分板)維持 CSS/Canvas 動效,保持 60fps 流暢。
|
||||
4. **過場與行銷用 2K**、**常駐循環用 768P**,控制檔大小與成本。
|
||||
5. **BGM/SFX 分工**:對話與特效音效可用 H3 原生立體聲或多模態參考生成;背景音樂用 **MiniMax Music 3.0** 單獨產生(官方明列支援遊戲 BGM 場景)。
|
||||
6. **順序建議(ROI 排序)**:① 角色表情循環(立即有感)→ ② 動卡面亮牌 → ③ 過場動畫 → ④ 背景循環 → ⑤ 行銷大片。
|
||||
7. **若想要「玩家端即時動態表情」**,請改用前端方案(Live2D/CSS/Canvas/WebGL 或預先切片好的 sprite 動態),不要走 H3。
|
||||
|
||||
---
|
||||
|
||||
## 6. 資料來源清單(皆為調查當下已開之官方/可查證頁面)
|
||||
|
||||
- MiniMax H3 官方部落格(發布 2026-07-31、規格、設計哲學、開源計畫):https://www.minimax.io/blog/minimax-h3
|
||||
- MiniMax 官網旗艦模型(H3 標示 2026-07):https://www.minimax.com/
|
||||
- 海螺影片創作入口(H3 選項、2K/5s/21:9):https://hailuoai.com/video
|
||||
- 開放平台「影片生成」指南(模式/規格/輸入限制/API 範例):https://platform.minimaxi.com/docs/guides/video-generation.md
|
||||
- 開放平台「H3 亮點功能示例」:https://platform.minimaxi.com/docs/guides/video-prompt.md
|
||||
- 開放平台「按量計費」定價(H3 2K 0.80元/s、768P 0.50元/s、Context-IR、再生):https://platform.minimaxi.com/docs/guides/pricing-paygo.md
|
||||
- 開放平台「影片資源包」定價(註明暫不支援 H3):https://platform.minimaxi.com/docs/guides/pricing-video.md
|
||||
- 開放平台「模型發布」(2026-07-31 H3、Music-3.0 等):https://platform.minimaxi.com/docs/release-notes/models.md
|
||||
- 官方 OpenAPI 3.1 規格(resolution/duration/ratio/role 全量 enum):https://platform.minimaxi.com/docs/api-reference/video/generation/api/v2-video-generation.json
|
||||
- MiniMax MCP 指南:https://platform.minimaxi.com/docs/guides/mcp-guide.md
|
||||
- OpenAI Sora 停用說明(Help Center):https://openai.com/sora/
|
||||
- Google Gemini 影片生成文件(Veo 3.1 / Gemini Omni Flash):https://ai.google.dev/gemini-api/docs/video
|
||||
- Google Gemini API 定價(Omni Flash ≈$0.10/s 720p):https://ai.google.dev/gemini-api/docs/pricing
|
||||
- Kling AI 官方(Kling 3.0、Motion Control、原生音訊):https://kling.ai/
|
||||
- Runway 定價(Gen-4.5、$12–28/月、代管 Veo/Kling):https://runwayml.com/pricing
|
||||
- Pika 定價(Pika 2.5、480p、點數制):https://pika.art/pricing
|
||||
|
||||
> 查證聲明:MiniMax H3 之規格、模式、API、定價均直接取自上述官方文件;競品比較欄目以各官方頁面當下公開內容為準並各附來源,未能查到之細節(如 Kling/Pika 確切單價、H3 塗鴉控制、Veo 最大時長)均已明確標示「未查到」,未做臆測補值。
|
||||
@@ -0,0 +1,151 @@
|
||||
# 2025–2026 AI 繪圖工具與工作流調查報告:為 Liar's Bar 網頁遊戲生成角色與卡牌美術
|
||||
|
||||
**調查日期:** 2026-08-06
|
||||
**調查者:** Hermes 研究子代理
|
||||
**目的:** 評估最新文生圖/圖生圖工具能否為我們的網頁版「騙子酒館(Liar's Bar)卡牌唬牌遊戲」產出高品質角色與卡牌美術(桌上有 8 個 AI 品牌擬人角色 + 卡牌系統,技術棧 Vue3 + Node/Socket.IO;已具備 ComfyUI + IP-Adapter + ControlNet DWpose 的角色一致性管線筆記)。
|
||||
|
||||
---
|
||||
|
||||
## 摘要
|
||||
|
||||
截至 2026 年 8 月,AI 繪圖領域已明顯進入「多模態 + 原生一致性的時代」,對「角色一致性」這個我們最大的痛點,已經出現**直接可用的原生方案**,不必再只靠傳統 LoRA/IP-Adapter 硬扛:
|
||||
|
||||
- **FLUX.2**(Black Forest Labs,open weight 版 2025-11-22 釋出)主打「multi-reference control(最多 10 張參考圖)」,官方主打**同一個角色、同一畫風、量產數百張素材仍保持一致**,這幾乎是為我們的「8 個固定角色」量身打造;另有 FLUX.3(2026-07)多模態。
|
||||
- **Google Nano Banana 家族**(gemini-3.1-flash-image 等)同樣主打多參考圖一致性與 4K,且附 SynthID 浮水印,API 按圖計費。
|
||||
- **Midjourney** 已進到 V8.2(2026-07-24),有 `--cref` 角色參考,但仍是訂閱制服務、非彈性 API,較不適合我們的自動化管線。
|
||||
- **ByteDance Seedream** 最新為 **Seedream 5.0 Pro**(多模態、互動編輯、圖層分離);早期 Seedream 4.0 之透明背景(RGBA)與主體/角色參考為其知名賣點。
|
||||
- **開源自架**(ComfyUI + FLUX.2-dev / SD3.5 / Seedream + LoRA)仍是控制權、成本與資料隱私最佳,但需要 GPU。
|
||||
|
||||
**對我們最務實的路徑:** 用「代表性角色圖 + 多參考圖一致性」的方式(FLUX.2 API 或 Nano Banana 2),先在少數幾個角色上驗證噴數十張卡面樣張;高品質「最終卡面」再用更具風格控制的開源管線(ComfyUI + FLUX.2-dev + 自訓 LoRA)精修,並以透明背景輸出供前端合成。
|
||||
|
||||
> ⚠️ **誠實聲明:** 本報告所有事實均取自下列即時來源,重要規格附 URL。凡未能查證或來源不明的細節,已明確標註「未查到」或「依公開報導,建議再以官方文件確認」。**本報告未編造任何未查證的價格或規格。**
|
||||
|
||||
---
|
||||
|
||||
## 一、當前主流文生圖模型/服務比較(2026 年 8 月現況)
|
||||
|
||||
| 方案 | 廠商 | 型號/狀態 | 角色一致性 | 關鍵要點 | 授權/價格 |
|
||||
|---|---|---|---|---|---|
|
||||
| **FLUX.2** | Black Forest Labs | [pro]/[flex]/[max]/[dev](open weight 版 **2025-11-22**) | **多參考圖(最多 10 張)**,官方明言「同角色同畫風、量產一致」 | 4MP 輸出、任意長寬比、精確文字、brand color hex 對色、JSON 控制、<10s、生成/擴展;FLUX.2-dev open weights | API「Pay as you go」;dev/klein 授權含 Fine-tuning & LoRA rights(見下方來源) |
|
||||
| **FLUX 3** | Black Forest Labs | 2026-07-23(多模態:圖像/影片/音訊/動作預測) | 多模態,主打真實感 | 單一模組多種輸出;另有一系列工具(Erase、Outpainting、VTO) | API;本遊戲暫以純圖像為主,未必需要 |
|
||||
| **Nano Banana 2 / Pro** | Google Gemini | gemini-3.1-flash-image(NB2)、gemini-3.1-flash-lite-image(NB2 Lite)、gemini-3-pro-image(NB Pro) | **多參考圖處理與一致性為其強項**、4K | 對話式生圖/編輯、可靠文字渲染、SynthID 浮水印 | 按 token/圖計費(見下方價格) |
|
||||
| **Imagen 4** | Google | imagen-4(Fast/Standard/Ultra) | 一般 | 傳統商業生圖 API | Fast $0.02 / Standard $0.04 / Ultra $0.06 每圖 |
|
||||
| **Seedream 5.0 Pro** | ByteDance Seed | 2026 現行旗艦 | 主體/角色參考、互動編輯、圖層分離 | 多模態、高密度資訊圖、依空間註記/草圖精確編輯 | 火山方舟 API / seed.bytedance.com |
|
||||
| **Seedream 4.0** | ByteDance | 於 2025 年中後釋出(含 open weights) | 主體/角色特徵參考;透明背景(RGBA)為其賣點 | 中英文等多語文字渲染 | 依公開報導;細節建議以火山方舟文生圖文件確認 |
|
||||
| **Stable Diffusion 3.5 / SDXL** | Stability AI | SD3.5 Large/Medium/Large Turbo、SDXL 皆為 open weights(HF `stabilityai/stable-diffusion-3.5-large`) | 靠 LoRA/IP-Adapter 達成 | 2019–2024 開源先驅;Stability 近年重組,現主推 API + Stable Audio;open-weight 領先地位漸被 FLUX.2-dev 取代 | SD3.5 採 **Community License**(營收超門檻需另行授權,見來源) |
|
||||
| **Midjourney** | Midjourney Inc. | **V8.2(2026-07-24)**、V8(2026-03)、V7(2025-04) | `--cref` 角色參考 | 審美/美術品質口碑佳;訂閱制服務(Discord/網頁),非彈性 REST API,較難自動化量產 | 訂閱制(每月方案,確切分級價格請以官網為準 — 未查到單一權威即時報價頁) |
|
||||
|
||||
**價格實況(Google,取自官方 pricing 頁,較明確):**
|
||||
- **Nano Banana Pro(gemini-3-pro-image)**:1K/2K 圖約 **$0.134/張**、4K 圖約 **$0.24/張**。
|
||||
- **Imagen 4**:Fast **$0.02**、Standard **$0.04**、Ultra **$0.06**/張。
|
||||
- Nano Banana 2 Lite:小解析度(0.5K≈$0.045 到 4K≈$0.151/張,依定價文件區間)。
|
||||
- **FLUX API 單張確切單價:未查到明確公布數字**——官方採「Pay as you go + 定價計算機」與企業/Builder/Platform 訂閱授權(Builder/Platform 階含 Fine-tuning & LoRA rights)。實際 API 每張費用請以 BFL 官方 Pricing 頁與 API 文件為準。
|
||||
|
||||
---
|
||||
|
||||
## 二、生成「遊戲用角色立繪」的最佳做法
|
||||
|
||||
1. **透明背景 PNG(去背)**:
|
||||
- 最省事:直接要求「native transparent background / RGBA PNG」——**Seedream 系列**以此聞名;**FLUX.2** 可用 JSON/指令控制搭配輸出後處理。
|
||||
- 穩健做法:**先在白/綠幕或純色背景生成,再用 ComfyUI 的 Inpaint/Matting 節點(如 rembg)切出 Alpha**,前端以 `<canvas>`/Pixi 合成即可。這是現有 ComfyUI 管線最不變動的加法。
|
||||
2. **角色三視圖/設定圖**:
|
||||
- 用**多參考圖系**(FLUX.2 multi-reference、Nano Banana 2)一次丟正面/半身樣張,指示「同一角色正面/側面/背面上半身」;或 Prompts 明寫 "character turnaround sheet, three views"(Seedream 5.0 官網即展示多視角藍圖/角色介面範例)。
|
||||
- 配上現有 **ControlNet DWpose** 固定姿勢,再以參考圖管角色外觀,即可穩定產出「同角不同姿勢」的立繪。
|
||||
3. **一致畫風**:
|
||||
- 最強:**FLUX.2(同角色/同畫風量產)與 Nano Banana 2(多參考圖一致性)** 的原生一致性。
|
||||
- 次佳:**LoRA**(把某個角色/畫風固化成小模型,見第四章)+ **IP-Adapter** 控制外觀/風格圖。
|
||||
- 地面做法:每個角色固定一份**文字描述模板(character sheet JSON)+ 種子(seed)+ 風格關鍵字**,降低每次生成漂移。
|
||||
|
||||
---
|
||||
|
||||
## 三、適合卡牌遊戲的生成工作流
|
||||
|
||||
因我們是「牌面 + 背景 + UI 元素」三種素材,建議分三條工作流:
|
||||
|
||||
1. **卡面(角色立繪)**:以角色一致性方案(FLUX.2 多參考 / Nano Banana 2 或自訓 LoRA)生成透明背景的**角色半身/全身像**,輸出 PNG。
|
||||
2. **背景/場景**:用文生圖(FLUX.2 / Imagen 4 / Midjourney V8)獨立做**無角色、適合唬牌酒館氛圍的背景**(酒館內部、暗色牌桌、霓虹),與角色分層,前端疊合 → 彈性、可重複使用。Seedream 5.0 的「互動編輯/圖層分離」適合把角色合成進多種場景。
|
||||
3. **UI 元素**:**文字與 UI 精準度是現代模型的強項**——FLUX.2 主打「Production-Ready Text + 精確 UI mockup」,Seedream 5.0 Pro 主打高密度 UI/資訊圖;可生成牌框、按鈕、牌背、字型標題底圖。但**遊戲按鈕/交互元件仍建議用前端 CSS/SVG/Naive UI 元件**,AI 只做「美術圖層」(紋理、鉚釘、牌背花紋),避免每次改字重繪。
|
||||
|
||||
> 通用建議:**維持「AI 生圖層」與「前端元件層」分離**——AI 負責美術素材,UI 佈局/文字/互動交給 Vue/Naive UI,素材即可快取於靜態 CDN/`assets/`,不必每次請求重新生成,也省 API 成本。
|
||||
|
||||
---
|
||||
|
||||
## 四、LoRA 微調角色的流程與成本
|
||||
|
||||
- **原理**:用 15–50 張同一角色的圖片(多角度、多表情、一致性高)訓練小型 LoRA,將該角色/畫風「固化」成數十 MB 的 adapter,之後任何 prompt 都能穩定復現。
|
||||
- **工具**:開源社群主流為 **ostris/ai-toolkit**(GitHub,2026 維持維護、HTTP 200 可查證)、kohya_ss、以及 ComfyUI 內建的 Flux 訓練;SD/SDXL 亦支援 LoRA。
|
||||
- **流程**:
|
||||
1. 準備並清洗 20–50 張角色圖(標註文字描述);
|
||||
2. 用 ai-toolkit / kohya 在 GPU(建議 12–24GB VRAM;或租雲端 A100/H100 按時計費)訓練 LoRA;
|
||||
3. 輸出 `.safetensors`,掛進 ComfyUI Checkpoint/LoraLoader,與 IP-Adapter/ControlNet 併用。
|
||||
- **成本**:FLUX.2 的 **dev 授權本身含「Fine-tuning & LoRA rights」**(BFL 官方授權頁列明),即 open weight 版可合法自訓/商用;訓練成本=GPU 時數(本機 GPU 電費 或 雲端 $1–$3/小時量級,**具體報價依租賃商,未逐一查證**)。HF 上已有現成 **遊戲素材 LoRA**,例如 `gokaygokay/Flux-Game-Assets-LoRA-v2`,可先套用看畫風,再決定是否自訓品牌擬人 LoRA。
|
||||
|
||||
---
|
||||
|
||||
## 五、免費/開源 vs 付費 API 的取捨
|
||||
|
||||
| 面向 | 免費/開源(自架) | 付費 API(FLUX.2 API / Nano Banana / Seedream / Imagen) |
|
||||
|---|---|---|
|
||||
| 成本 | 一次性 GPU 電費/月租;FLUX.2-dev、SD3.5、Seedream open weights 免費下載 | 按張計費(Nano Banana Pro 約 $0.13–$0.24/張、Imagen 4 $0.02–$0.06/張、FLUX 依官方定價) |
|
||||
| 一致性 | 強(自行訓 LoRA + IP-Adapter + 多參考),但需調校 | **強且省事**(FLUX.2/Nano Banana 2 原生多參考一致性) |
|
||||
| 控制/品質 | 最高(可控每個節點、姿勢 ControlNet) | 高(模型更新即得最強引擎),但黑箱 |
|
||||
| 隱私/資料 | 全在本機,適合角色設計稿不外洩 | 需上雲,受服務條款與浮水印(SynthID)約束 |
|
||||
| 自動化整合 | ComfyUI API → 後端 Node 呼叫 | REST API,對 Node/Socket.IO 後端整合更直接 |
|
||||
|
||||
**建議組合(混用):**
|
||||
- 早期概念/探索、角色定稿 → **FLUX.2 或 Nano Banana 2 API**(最強一致性、最快出樣張)。
|
||||
- 遊戲正式素材批量生產 → 若自備 GPU,用 **ComfyUI + FLUX.2-dev + 自訓 LoRA** 自產(成本攤平、隱私最佳);無 GPU 再回到 API。
|
||||
- 透明底、去背、合成 → ComfyUI 後處理或直接要求 RGBA。
|
||||
|
||||
---
|
||||
|
||||
## 六、對我們需求的務實建議(8 個 AI 品牌擬人角色 + 卡牌)
|
||||
|
||||
1. **先用 FLUX.2(或 Nano Banana 2)做「角色定稿」**:因官方主打「同角色/同畫風量產一致」,最適合把 8 個角色(Grok-4 橘髮、Kimi 銀白長髮、Qwen3 粉短髮、GPT-5 黑長髮、Gemini 紫短髮、Deepseek 淺藍短髮、Doubao 深棕短髮、Claude 橘短髮,各自主題色與配件)各產 2–3 張「帶透明底的全身/半身立繪」作為視覺錨點。
|
||||
2. **沿用既有 ComfyUI 管線當主力、API 當加速器**:把現有 IP-Adapter + ControlNet DWpose 管線的基底模型升級到 **FLUX.2-dev**(仍支援 ControlNet 類外掛),相容性最高;FLUX.2 的「多參考圖」直接解決 8 個角色間不互相混淆的問題(每張卡明確定義「用角色 X 的參考圖」)。
|
||||
3. **卡面與背景分層**:角色(透明 PNG)+ 酒館/牌桌背景(獨立生成)分開生產,前端 Vue 疊合;UI 元件(牌框、牌背、按鈕底圖)交給 **FLUX.2 的「production-ready text / UI」** 或 **Seedream 5.0 Pro**,交互文字仍走 Naive UI。
|
||||
4. **行有餘力再自訓品牌擬人 LoRA**:待 8 角色視覺定稿後,各收 20–30 張樣張,用 ai-toolkit 訓 FLUX.2-dev LoRA,把角色「固化」,日後任何季節卡面/表情都一致且無需每張帶參考圖。FLUX.2 dev 授權已含 LoRA 權利。
|
||||
5. **預算導向**:若 API 預算有限,先用 **Imagen 4 / Nano Banana 2 Lite**($0.02–$0.07/張)出量產草圖,最終定稿再用 Pro 級($0.13–$0.24/張)或開源自架精修。
|
||||
6. **風險與合規**:AI 生成商業素材請檢視各模型**商用授權**(FLUX.2 dev 需依 BFL 授權、SD3.5 Community License 有營收門檻);角色為「AI 品牌擬人」,須注意品牌商標/外觀仿冒之法律風險(本報告不構成法律意見)。若輸出要賣/商用,另確認 SynthID 浮水印與服務條款。
|
||||
|
||||
---
|
||||
|
||||
## 資料來源連結清單(皆於調查當日存取)
|
||||
|
||||
**Black Forest Labs(FLUX)**
|
||||
- 模型總覽(FLUX 3 / FLUX.2 Max / FLUX.2 / FLUX.2 Klein / Flux Tools):https://blackforestlabs.ai/models/
|
||||
- 公司成立與 FLUX.1 發布(2024-08-01):https://blackforestlabs.ai/announcing-black-forest-labs/
|
||||
- FLUX.2 產品頁(4MP、multi-reference、json 控制、四變體):https://blackforestlabs.ai/models/flux-2
|
||||
- FLUX 3 頁面(多模態):https://blackforestlabs.ai/models/flux-3
|
||||
- 部落格(FLUX 3 公告 2026-07-23、VTO/Erase/Outpainting 等):https://blackforestlabs.ai/blog/
|
||||
- 定價與授權(Builder/Platform/Professional 階含 Fine-tuning & LoRA rights;Open Weights):https://blackforestlabs.ai/pricing/
|
||||
- GitHub:https://github.com/black-forest-labs/flux
|
||||
- Hugging Face:FLUX.2-dev(open weights,created 2025-11-22,gated、下載 116 萬+):https://huggingface.co/black-forest-labs/FLUX.2-dev ;FLUX.2-klein-9B/4B:https://huggingface.co/models?search=FLUX.2
|
||||
|
||||
**Google(Gemini image / Nano Banana / Imagen)**
|
||||
- 圖像生成文件(Nano Banana 家族四型號 + SynthID):https://ai.google.dev/gemini-api/docs/image-generation
|
||||
- 定價(Nano Banana Pro $0.134/$0.24、Imagen 4 $0.02/$0.04/$0.06 等):https://ai.google.dev/gemini-api/docs/pricing
|
||||
|
||||
**ByteDance Seed(Seedream)**
|
||||
- Seedream 5.0 Pro(多模態、互動編輯、圖層分離):https://seed.bytedance.com/en/seedream5_0_pro
|
||||
- ByteDance Seed 官網(Models → Seedream 5.0 Pro):https://seed.bytedance.com/
|
||||
- 火山方舟(Volcengine Ark)文生圖 API 文件(Seedream 於火山方舟提供;透明底/角色參考細節建議查詢此處):https://www.volcengine.com/docs/82379
|
||||
|
||||
**Stability AI(Stable Diffusion)**
|
||||
- HF SD3.5 Large(Community License):https://huggingface.co/stabilityai/stable-diffusion-3.5-large
|
||||
- 官網(現主推 Stable Audio 3.0 / API / Self-Hosted License):https://stability.ai/
|
||||
|
||||
**Midjourney**
|
||||
- 版本歷史(V7 2025-04-04、V8 2026-03-17、V8.1 2026-04-14、V8.2 2026-07-24)與 --cref 角色參考:https://en.wikipedia.org/wiki/Midjourney
|
||||
- 官網:https://www.midjourney.com/
|
||||
|
||||
**開源工作流/LoRA**
|
||||
- ComfyUI(本機開源引擎,API 節點支援 Nano Banana/Seedream 等):https://github.com/comfyanonymous/ComfyUI (README:https://raw.githubusercontent.com/comfyanonymous/ComfyUI/master/README.md)
|
||||
- ostris/ai-toolkit(LoRA/微調訓練工具,存續可查):https://github.com/ostris/ai-toolkit
|
||||
- 現成遊戲素材 LoRA 範例:https://huggingface.co/gokaygokay/Flux-Game-Assets-LoRA-v2
|
||||
- 其他:IP-Adapter 與 ControlNet 為現有 ComfyUI 生態既有節點(延用既有筆記),此處不再另列。
|
||||
|
||||
**未查到/需再確認事項(誠實標註)**
|
||||
- FLUX API「每張圖」確切單價:未查到官方單一數字,請以 BFL Pricing 頁與 API 文件為準。
|
||||
- Midjourney 各訂閱分級(Basic/Standard/Pro/Mega)當季確切月費:未查到權威即時報價頁,請以官網為準。
|
||||
- Seedream 4.0 的「原生透明背景(RGBA)」與「角色特徵/主體參考」細節:依公開報導,建議以火山方舟文生圖官方文件確認。
|
||||
@@ -0,0 +1,182 @@
|
||||
# 靜態 AI 圖片轉動態素材:技術路徑調查報告(適用於 Liar's Bar 網頁卡牌唬牌遊戲)
|
||||
|
||||
**調查日期**:2026-08-06
|
||||
|
||||
## 摘要
|
||||
|
||||
本報告調查「把靜態 AI 角色立繪/卡面變成瀏覽器可流暢播放的輕量動畫」的技術路徑,涵蓋:(1) 圖生視訊工具現況(MiniMax H3、Kling、Runway、免費線上方案)、(2) 靜態立繪做「小動作動畫」的方法(Live2D、EbSynth、Runway Motion Brush、Kling 3.0 Motion Control、開源 Stable Video Diffusion)、(3) 可放入 Vue 網頁的格式(GIF / WebP / APNG / AVIF / WebM / MP4 / 精靈圖 sprite sheet)、(4) 卡牌遊戲常見動態效果哪些該用 AI、哪些該用 CSS/手寫動畫、(5) 各路徑成本與品質比較、(6) 給我們的務實方案組合建議。
|
||||
|
||||
**核心結論**:把「角色立繪做眨眼/髮絲/微笑的小動作」用 AI 圖生視訊最划算,其中 **MiniMax H3 的 First/Last-Frame Image-to-Video**(第一幀=立繪,末幀=同一立繪,讓靜態圖自然「活起來」,768P 每 0.08 美元/秒、4–15 秒、24fps)與開源的 **Stable Video Diffusion(在自家 ComfyUI 免費跑,25 幀 1024×576@6fps,營收 <100 萬美元可免費商用)** 是兩條最務實路線;而「抽卡亮光、卡面光影、進場/翻牌動畫」等瞬態 UI 效果用 **CSS/手寫動畫** 更務實(確定性高、零成本、可縮放)。Live2D 品質最高但需要把立繪人工拆層綁骨,工作量大,建議列為後期選項。產出優先輸出 **WebM(VP9/AV1)或 APNG**(支援透明背景、體積小),GIF 作為萬用備援。
|
||||
|
||||
---
|
||||
|
||||
## 1. 圖生視訊(Image-to-Video)工具現況
|
||||
|
||||
### 1.1 MiniMax H3(本文建議的主力候選)
|
||||
|
||||
- **定位**:MiniMax 官方 API 文件標示 H3 為「next-gen **open** general-purpose multimodal video model(新一代開放通用多模態視訊模型)」,支援 Text-to-Video、**First/Last-Frame Image-to-Video**、參考(Reference)生成與視訊編輯。
|
||||
- **輸出規格**(官方 Models / Video Generation 文件):輸出解析度 **768P / 2K**;時長 **4–15 秒(僅整數)**;**24 fps**。
|
||||
- **圖生視訊模式**:First/Last-Frame 模式輸入 0–2 張圖(起/末幀);官方明言用途即「**bring a specific frame naturally to life(讓指定靜態幀自然活起來)**」——正適合把角色立繪變成眨眼/擺動/微笑的小動畫。
|
||||
- **輸入限制**:參考圖 ≤9 張、影片 ≤3 段(每段 2–15s)、圖片寬高 256–5760、支援 JPG/PNG/WEBP/HEIC 等,單張 ≤30MB。
|
||||
- **計價(官方 Pay-as-You-Go)**:
|
||||
- 輸出:**768P 每 0.08 美元/秒;2K 每 0.13 美元/秒**。
|
||||
- 輸入素材:圖片**前 5 張免費,之後每張 0.04 美元**;純文字提示免費。
|
||||
- 範例:一段 5 秒 768P = 約 0.40 美元;一個角色做 5–8 段備選再挑最好的一段,成本極低。
|
||||
- 另有企業預付「Video Packages」:1,000 美元 / 3,760 視訊點、2,500 美元 / 9,920 點、4,500 美元 / 18,900 點、6,000 美元 / 26,780 點(標準/Pro/Scale/Business,1 個月有效)——對我們的用量不需用到。
|
||||
- **來源**:
|
||||
- Models 頁面:https://platform.minimax.io/docs/guides/models-intro
|
||||
- Video Generation 文件:https://platform.minimax.io/docs/guides/video-generation
|
||||
- Pay-as-You-Go 計價:https://platform.minimax.io/docs/guides/pricing-paygo
|
||||
- Video Packages 計價:https://platform.minimax.io/docs/guides/pricing-video
|
||||
|
||||
### 1.2 Kling AI(快手可靈)
|
||||
|
||||
- **定位**:提供 Text-to-Video / **Image-to-Video** / Motion Control / 口型同步等;目前主推 **Kling 3.0 / 3.0 Omni**(官方稱同步影音、智慧分鏡、元素參考,2026-02-05 發布 3.0)。
|
||||
- **圖生視訊能力**:Kling 官方開發者首頁宣稱可將文字、圖片、參考轉成多模態創意內容;其「Motion Control」可「**用參考幀的動作來讓一張靜態圖動起來**」(此敘述同時見於 Runway 對 Kling 3.0 Motion Control 的工具說明)。
|
||||
- **計價**:**確切美元單價未查到**——官方定價/訂閱頁(app.klingai.com 的 Subscription/API Pricing)需登入才能看,未登入無法取得實價。已知:Kling 提供**免費額度**(新用戶有免費 credits,並有每日/訂閱制 credits 系統);另可透過 Runway 內建 Kling 3.0 使用(詳見 1.3)。
|
||||
- **來源**:
|
||||
- Kling 官方站:https://klingai.com/
|
||||
- Kling Developer 首頁:https://klingai.com/global/dev/document-api/guide/pricing
|
||||
- Kling 3.0 Model 頁(kling.art):https://kling.art/model
|
||||
|
||||
### 1.3 Runway
|
||||
|
||||
- **定位**:整合多家模型於一站的 AI 影像/視訊工作台,目前在 Runway 內可用 **Gen-4.5、Gen-4 Turbo、Aleph 2.0、Seedance 2.0、Kling 3.0、Nano Banana Pro(Gemini)** 等。
|
||||
- **計價(官方 Pricing,年繳價)**:
|
||||
- **Free**:0 美元,一次性 125 credits(不逾期)、5GB 儲存。
|
||||
- **Standard**:月繳 15 美元/年繳 12 美元,每月 625 credits。
|
||||
- **Pro**:月繳 35 美元/年繳 28 美元,每月 2,250 credits(另有 500GB 儲存、自訂語音)。
|
||||
- **Max**:月繳 95 美元/年繳 76 美元,每月 9,500 credits(未用 credits 可展延 1 個月)。
|
||||
- **官方換算**:Gen-4.5 = **12 credits/秒**(625 credits ≈ 52 秒);「Gen-4.5 60 credits/5s」→ 以 5 秒為 60 credits。Pro 月額 2,250 credits 約可做 187 秒 Gen-4.5。
|
||||
- **Motion Brush 現況(重要)**:Runway Help 明載 **「Motion Brush 是 Gen-2 模型專屬功能,Gen-2 已淘汰(Gen-2 Deprecation)」**,即 **Runway 傳統 Motion Brush 已停用**。現行替代為 Runway 內建的 **Kling 3.0 Motion Control**(第三方模型):「以參考幀的動作來動畫化一張靜態圖」+ 選擇性 motion effects。另 Runway 的舊「video project editor」已於 **2026-07-30 退役**。
|
||||
- **來源**:
|
||||
- Runway Pricing:https://runwayml.com/pricing
|
||||
- Runway Help(Motion Brush=Gen-2 已停用、Kling 3.0 Motion Control):https://help.runwayml.com/hc/en-us/search?query=motion+brush
|
||||
|
||||
### 1.4 免費線上免費工具/免費額度現況
|
||||
- **MiniMax**:圖生視訊輸入圖片前 5 張免費;消費端 Hailuo 生態另有免費試用額度(確切額度以官方當期公告為主,本調查僅確認 API 端前 5 圖免費)。
|
||||
- **Runway**:Free 方案一次性 125 credits(約 10 秒 Gen-4.5 影片)可試玩。
|
||||
- **Kling**:提供新用戶/訂閱 credits(確切數值未查到,需登入)。
|
||||
- **EbSynth**:完全免費(見 2.3)。
|
||||
- **開源 SVD**:免費下載權重、可自架(見 2.5)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 靜態立繪「小動作動畫」的方法論
|
||||
|
||||
### 2.1 圖生視訊(第一/末幀)+ 迴圈化
|
||||
最貼合「一張立繪活起來」的路線:以立繪當第一幀,末幀也設為同一張(或近似的閉合姿態),提示詞寫「眨眼、髮絲輕擺、微笑、呼吸」等微動作,模型補出中間幀。→ **MiniMax H3**(最便宜、官方明確支持「bring a frame to life」)、**Kling Image-to-Video**、**Runway Gen-4.5**、**SVD(自架)** 皆可做。此種產出是「短片」,想當卡面/角色待機動畫需**迴圈播放**;AI 短片未必完美閉合,實務上挑幾版取最接近可迴圈的一版,或短迴圈即可接受。
|
||||
|
||||
### 2.2 Live2D Cubism
|
||||
- **原理**:把原畫**拆成獨立部件(髮、眼、眉、口、頭、身…)綁骨骼/變形器**,用參數(呼吸、眨眼、視線、口型)驅動「隨時可即時互動」的擬人動畫。品質最高、能做到視線追蹤與口型,是 VTuber/抽卡角色待機的主流。
|
||||
- **工具與價格**:Live2D **Cubism Editor** 有 FREE 版與 PRO 版;PRO 可免費試用 42 天,之後降為 FREE 版(功能受限)。網頁整合用 **Cubism Web SDK**(JS/TS,.moc3/.model3.json 載入 Vue 可用)。
|
||||
- **關鍵取捨**:我們的 8 個角色立繪是「一整張 AI 圖」,**沒有分層零件**;Live2D 需要**人工把圖拆成可動部件並逐角色綁骨**,是高度的手工建模工作(每個角色數小時到數天),**不是自動化**。短期不建議、中期若追求「待機呼吸+視線」的高品質再導入。
|
||||
- **來源**:https://www.live2d.com/en/ (Cubism Editor FREE vs PRO、42 天試用、Web SDK 均見於官方站)
|
||||
|
||||
### 2.3 EbSynth
|
||||
- **原理**:VFX 軟體,**「透過編輯一格來改變整段影片」**——它需要一段**「已有動作的底片影片」**,把某幾格畫上(或 AI 生成)關鍵幀,再用**紋理合成(非生成式 AI)**傳播到整段。官方強調其傳播「不使用生成式模型、不依賴外部資料集訓練」。
|
||||
- **價格**:**免費**(官方標示 Free,支援 720p,可上 4K)。
|
||||
- **對我們的關鍵限制**:**它不能無中生有**。我們只有靜態立繪、沒有角色影片底片,因此 EbSynth 單獨無法把靜態圖變動畫;除非先由圖生視訊產生一段「已有微動作」的底片再交給 EbSynth 加強,疊加使用才有意義。
|
||||
- **來源**:https://ebsynth.com/
|
||||
|
||||
### 2.4 Runway Motion Brush(已停用)與 Kling 3.0 Motion Control
|
||||
- 見 1.3:Runway 傳統 **Motion Brush 屬 Gen-2 且已停用**;現行對應功能為 **Kling 3.0 Motion Control**(以參考幀/運動效果動畫化靜態圖)。本調查確認「在 Runway 上已沒有可用的傳統 motion brush」。
|
||||
|
||||
### 2.5 開源 Stable Video Diffusion(SVD)— 自架免費路線
|
||||
- **模型**:Stability AI 的 **stable-video-diffusion-img2vid-xt-1-1(SVD 1.1 Image-to-Video)**,HuggingFace 開放下載,條件輸入單張圖生成短片。
|
||||
- **規格**:**25 幀、1024×576、預設 6 FPS**(≈4 秒),採樣 Motion Bucket;需 GPU+相關 python 環境。
|
||||
- **授權(重要,對商業遊戲關鍵)**:**Stability AI Community License**——研究/非商用免費;**商用:年營收 <100 萬美元可免費使用,但須至 stability.ai 註冊**;超過 100 萬美元需向 Stability 取得授權;並須標註「Powered by Stability AI」。
|
||||
- **對我們的意義**:我們已有 **ComfyUI 環境**,可用 SVD 在本地把立繪轉成短片,**零 API 費用**(成本=GPU 電費/算力),完全符合「小團隊營收 <100 萬美元」的免費商用範圍——唯一成本是 GPU 時間與 6fps/短時長/較低解析度的品質折衷。
|
||||
- **來源**:
|
||||
- Model Card:https://huggingface.co/stabilityai/stable-video-diffusion-img2vid-xt-1-1
|
||||
- 授權(Community License):https://stability.ai/community-license
|
||||
|
||||
---
|
||||
|
||||
## 3. 可放入 Vue 網頁的格式(GIF / WebM / APNG / 影片 / 精靈圖)
|
||||
|
||||
依據 MDN(Mozilla Developer Network)格式指引整理:
|
||||
|
||||
| 格式 | 特性 | 適合 | 注意 |
|
||||
|---|---|---|---|
|
||||
| **GIF** | 通用、8-bit(256 色)、檔大 | 萬用備援、舊瀏覽器 | 有損色彩、體積大,MDN 建議動畫優先改用 WebP/AVIF/APNG |
|
||||
| **APNG** | 無損動畫 PNG,支援透明 | 無損小動畫、貼圖、進度動畫 | 所有現代瀏覽器(Chrome/Edge/FF/Safari)支援 |
|
||||
| **WebP(動畫)** | 壓縮佳、支援透明 | 一般動畫預設優選;壓縮優於 PNG/GIF | 現代瀏覽器全面支援 |
|
||||
| **AVIF(動畫)** | 壓縮最佳(中位數約 50% vs JPG,優於 WebP) | 高壓縮動畫 | Safari16.1+/Firefox113+;支援較窄,用 `<picture>` 附 WebP 備援 |
|
||||
| **WebM(VP9/AV1)** | 真正的視訊編碼、品質/體積比最佳、支援透明(VP9 alpha) | **適合較長的立繪動畫/卡面影片**,用 `<video muted loop autoplay>` | VP9 現代瀏覽器支援好;AV1 更省但解碼成本較高 |
|
||||
| **MP4(H.264/HEVC)** | 相容性最高 | 需要最大相容時 | H.264 無 alpha 透明;透明場景選 WebM/APNG |
|
||||
| **精靈圖 Sprite sheet** | 把動畫幀排成網格,CSS `background-position` + `steps()` 播放 | **CSS 手寫的小動作(光暈、閃爍、翻轉)**、體積預熱可控 | 精靈圖本身是靜態 PNG/WebP 網格,動畫由 CSS 播放,幀率由程式碼控制 |
|
||||
|
||||
**對 Vue(Vite)的落地要點**:動畫圖用 `<img>`(APNG/WebP/GIF)即可;真正的影片用 `<video muted loop playsinline autoplay>`(手機上 `playsinline` 必加);透明需求選 WebM(VP9 alpha) 或 APNG。檔案放 `public/` 或 `assets/`,Vite 直接打包。避免在同畫面同時載入過多高解析影片,把重點角色(8 個)各控制在一個 WebM(≲1–2MB / 幾秒)是可行目標。
|
||||
|
||||
- **來源**:https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types
|
||||
|
||||
---
|
||||
|
||||
## 4. 卡牌遊戲常見動態效果:哪些 AI 划算、哪些 CSS/手寫更務實
|
||||
|
||||
| 效果 | 用 AI(圖生視訊)? | 用 CSS/手寫動畫? | 建議 |
|
||||
|---|---|---|---|
|
||||
| **角色待機小動作**(眨眼、髮絲、呼吸、微笑) | ✅ 划算:MiniMax H3 / SVD 一次生成整段 | ❌ 手刻眨眼/髮絲成本極高 | **AI**(生成 1 段 WebM 或 APNG 迴圈) |
|
||||
| **卡面微動畫**(圖內光暈流動、漂浮粒子) | 可:圖生視訊 | ✅ 划算:CSS filter/keyframes + 精靈圖 | 多數用 **CSS**;特殊卡面才用 AI 短片 |
|
||||
| **抽卡/翻卡亮光閃爍** | ❌ 不划算 | ✅ 極划算:CSS gradient + mask + keyframes | **CSS** |
|
||||
| **角色/卡牌進場動畫**(滑入、彈跳、淡入、放大) | ❌ | ✅ 極划算:CSS transform/opacity + Vue `<Transition>` | **CSS** |
|
||||
| **發牌/洗牌/彈卡手感** | ❌ | ✅ 極划算:CSS transform + easing | **CSS** |
|
||||
| **出牌光影、金邊閃爍、背景星塵** | ❌ 不划算 | ✅ 划算 | **CSS** |
|
||||
|
||||
**原則**:**「角色本身的活體感」(small motion)適合 AI**——因為那是「生成內容」、一次性產出、手刻沒有效率;**「瞬態 UI 特效」(sparkle/glow/transition/flip)適合 CSS**——因為那是「程式化、可重複、需精準控制時間軸與效能」的工作,AI 生反而不確定、不可控、也沒必要付費。Vue 的 `<Transition>` / `<TransitionGroup>` 與 CSS keyframes 已涵蓋所有進退場與翻卡需要。
|
||||
|
||||
---
|
||||
|
||||
## 5. 各路徑成本 × 品質比較表
|
||||
|
||||
| 路徑 | 單次成本 | 品質(動畫自然度) | 網頁整合難度 | 備註 |
|
||||
|---|---|---|---|---|
|
||||
| **MiniMax H3 圖生視訊(First/Last-frame)** | 768P **0.08 美元/秒**(5s≈0.40 美元,前 5 張輸入圖免費後每張 0.04) | 高(24fps、4–15s、多元件參考、官方主打「frame come to life」) | 低(產出即 MP4,轉 WebM 即可) | **性價比最佳;API 免自架** |
|
||||
| **Kling Image-to-Video** | **確切單價未查到**(需登入;有免費額度) | 高(3.0 系列) | 低 | 可與 MiniMax 並列比較試用 |
|
||||
| **Runway Gen-4.5** | 12 credits/秒;Pro 28 美元/月約 187 秒/月 | 高(多模型聚合) | 低 | 訂閱制,對 8 角色小量也okay,但較貴 |
|
||||
| **SVD 自架(在 ComfyUI)** | **0 API 費**(GPU 電費;營收<100 萬美元免費商用,須註冊+標註) | 中(6fps、25 幀、1024×576≈4 秒;解析較低) | 中(需轉檔/補幀) | **零現金成本、最可控;品質輸商用雲端** |
|
||||
| **Live2D Cubism** | Editor FREE 版 0 元(PRO 42 天試用);但**每個角色拆層綁骨人力極高** | **最高**(即時互動、視線、口型) | 中(Web SDK / .moc3) | 人力貴,建議後期 |
|
||||
| **EbSynth** | 免費 | 中(須有底片影片;非生成式) | 中 | 無法無中生有,單獨不適用 |
|
||||
| **CSS/手寫動畫** | 0 元 | 適合 UI 特效(非擬真影片) | 低 | 必做,與 AI 互補 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 對我們遊戲的最務實方案組合建議
|
||||
|
||||
**建議採「CSS 打底 + AI 點睛」的三段式策略:**
|
||||
|
||||
1. **Phase 1(立即、零成本)— CSS/Vue 動畫打底**
|
||||
先用 CSS keyframes + Vue `<Transition>` 完成全部瞬態效果:抽卡亮光、翻卡/發牌動畫、角色/卡牌進場、hover 光影、金邊閃爍、背景星塵。此階段**完全不碰 AI**,幾小時即可看到整體「活起來」。
|
||||
|
||||
2. **Phase 2(核心、低價)— 為 8 個角色生成「待機小動作」迴圈**
|
||||
推薦主路徑 **MiniMax H3 First/Last-Frame 圖生視訊**:8 個角色 × 每角色 5–8 版備選 × 5 秒 768P ≈ 幾美元即可完成;從各版中挑「閉合度最好」的一版做迴圈。若要**零 API 成本**,用既有 **ComfyUI 跑開源 SVD**(營收<100 萬美元免費商用,記得去 stability.ai 註冊並標註)。
|
||||
產出後轉 **WebM(VP9,含透明則用 VP9 alpha)或 APNG**,以 `<video muted loop playsinline autoplay>` 或 `<img>` 放進 Vue;GIF 做備援。視效能把 8 個角色動畫裁到少量關鍵幀/低解析(如 640–720p、幾秒)控制檔案與 FPS。
|
||||
|
||||
3. **Phase 3(選擇性、後期)— 若追求「互動式擬人」才上 Live2D**
|
||||
當要「視線追蹤角色、點擊即時反應、口型」這類進階體驗,再評估 Live2D Cubism;需將立繪**拆層綁骨**(每角色額外人力成本),並以 Cubism Web SDK 整合。Demo 級需求用 Phase 2 的短片就已足夠。
|
||||
|
||||
**一次做一個角色的最小可行驗證(suggestion)**:用 1 張現有立繪 → MiniMax H3 生 5 秒 768P 短片(約 0.40 美元)→ 轉 WebM/APNG → 放進 Vue 以 `<video loop muted>` 播放 → 對比效能與觀感,決定是否量產 8 個角色。
|
||||
|
||||
---
|
||||
|
||||
## 資料來源連結清單
|
||||
|
||||
1. MiniMax Models(含 H3 規格:768P/2K、4–15s、24fps)— https://platform.minimax.io/docs/guides/models-intro
|
||||
2. MiniMax Video Generation 文件(First/Last-Frame 圖生視訊、輸入限制)— https://platform.minimax.io/docs/guides/video-generation
|
||||
3. MiniMax Pay-as-You-Go 計價(768P 0.08/秒、2K 0.13/秒、輸入圖前 5 張免費後 0.04/張)— https://platform.minimax.io/docs/guides/pricing-paygo
|
||||
4. MiniMax Video Packages 計價 — https://platform.minimax.io/docs/guides/pricing-video
|
||||
5. Runway Pricing(Free 125 credits、Standard 12/Pro 28/Max 76 美元月、Gen-4.5=12 credits/秒)— https://runwayml.com/pricing
|
||||
6. Runway Help(Motion Brush 屬 Gen-2 已停用;Kling 3.0 Motion Control 替代;video project editor 2026-07-30 退役)— https://help.runwayml.com/hc/en-us/search?query=motion+brush
|
||||
7. Kling AI 官方站 — https://klingai.com/
|
||||
8. Kling Developer 首頁(Image-to-Video、Motion Control、Kling 3.0)— https://klingai.com/global/dev/document-api/guide/pricing
|
||||
9. Kling 3.0 Model 頁(2026-02-05 發布)— https://kling.art/model
|
||||
10. Live2D Cubism Editor(FREE vs PRO、42 天試用、Web SDK)— https://www.live2d.com/en/
|
||||
11. EbSynth(免費、一格編輯傳播、非生成式、需底片影片)— https://ebsynth.com/
|
||||
12. Stable Video Diffusion 1.1 Model Card(25 幀、1024×576、6fps)— https://huggingface.co/stabilityai/stable-video-diffusion-img2vid-xt-1-1
|
||||
13. Stability AI Community License(營收<100 萬美元免費商用、需註冊+標註)— https://stability.ai/community-license
|
||||
14. MDN Image Types(GIF/WebP/APNG/AVIF 動畫支援)— https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types
|
||||
|
||||
---
|
||||
|
||||
*報告撰寫原則:所有規格、價格、支援情況皆引自上方官方文件/權威來源;凡無法從公開來源查證的(如 Kling USD 單價、確切免費額度)均明確標註「未查到」,不臆測數值。*
|
||||
@@ -0,0 +1,242 @@
|
||||
# 網頁素材整合技術:把 AI 生成的圖片/影片/動畫素材整合進 Vue 3 網頁遊戲
|
||||
|
||||
**調查日期:2026-08-06**
|
||||
**調查者:Hermes 研究子代理**
|
||||
**適用專案:網頁版「騙子酒館(Liar's Bar)卡牌唬牌遊戲」** — 技術棧 Vue 3 + Vite + Pinia + Naive UI(前端)、Node.js + Express + Socket.IO(後端,2–8 人即時對戰)
|
||||
|
||||
---
|
||||
|
||||
## 摘要
|
||||
|
||||
本報告調查「如何把 AI 生成的圖片/影片/動畫素材,以高效能、低延遲、跨瀏覽器相容的方式整合進 Vue 3 網頁卡牌遊戲」。重點結論如下:
|
||||
|
||||
- **相容性鐵律**:真正的殺手是「透明背景(alpha)」與「Safari」。網路影片主流兩條路:(A) **WebM(VP9/AV1 + Opus)**——體積小、可攜 alpha,但 **AV1 在 Safari 支援不佳**(僅部分新裝置),故需**雙 `<source>` 備援**:WebM 優先、MP4(H.264+AAC)備援(MDN 明列此寫法最穩妥)。靜態/序列動畫則用 **APNG 或 Animated WebP/AVIF**(全部現代瀏覽器皆支援)。
|
||||
- **短於 4 秒的循環最務實**:AI 影片(如 MiniMax H3)輸出 4–15 秒、24fps、768P/2K——建議先由 AI 產「一次循環」,再用 ffmpeg 切割成數幀/轉碼成 WebM,避免直接播高碼率大檔。
|
||||
- **載入策略**:Vite 把 `import imgUrl from './x.png'` 自動雜湊成 `/assets/x.2d8efhg.png`(Vite 官方文件);超過遊玩螢幕才需的素材用 **IntersectionObserver**(Baseline 2019 起全瀏覽器)或 Vue 動態元件**延遲載入**;`<video preload="metadata">` 只取中繼資料。
|
||||
- **別讓動畫卡住主執行緒**:即時對戰的 Socket.IO 事件若大量且高頻,應先寫進 **Pinia/狀態**再由淺層元件以 **Web Animations API 或 CSS** 驅動;需要大量自訂繪製時用 **Canvas**、純 CSS/WAAPI 足以處理卡牌位移、翻面、比大小等 UI 動畫。**requestAnimationFrame 不保證幀率穩定性**(主執行緒被佔用會掉幀),故重播/大工作量建議 Web Worker。
|
||||
- **動畫庫**:GSAP(專業級、免費)、@vueuse/motion(Vue 3 專用)、Lottie-web(播放 AE 匯出的 JSON 動畫,最適合「AI 立繪做眨眼擺動」做成向量/逐幀動畫);Naive UI 本身內建 `<n-collapse-transition>` 等過渡,且底層可用 Vue 內建 `<Transition>`。
|
||||
- **Sprite sheet 序列幀**:用 ffmpeg/ImageMagick 把 AI 圖片或短影片抽幀合併成一張圖,配合 CSS `background-position` + `steps()` 或 Canvas `drawImage` 逐幀播,是「8 個角色待機循環」體積/效能最佳解。
|
||||
|
||||
> ⚠️ **誠實聲明**:本報告所有重要事實均標註來源 URL,取自即時抓取的官方文件(Vite、MDN 等);凡未能查證者明確標註「未查到」。
|
||||
|
||||
---
|
||||
|
||||
## 1. 網頁動態素材格式比較與瀏覽器相容性
|
||||
|
||||
### 1.1 概覽:有兩大「格式家族」
|
||||
|
||||
素材可依**是否需透明背景**與**是影片還是序列圖**分兩類:
|
||||
|
||||
| 用途 | 格式 | MIME | 透明(alpha) | 瀏覽器支援 | 適合 |
|
||||
|---|---|---|---|---|---|
|
||||
| 短循環動畫(待機/眨眼) | **Animated WebP** | `image/webp` | ✅ | Chrome/Edge/Firefox/Opera/Safari | 有聲量上限、可壓縮、現代全能 |
|
||||
| 逐幀高品質循環 | **APNG** | `image/apng` | ✅ | Chrome/Edge/Firefox/Opera/Safari | 無損動畫序列(比 GIF 省) |
|
||||
| 動畫(可含高色深) | **Animated AVIF** | `image/avif` | ✅ | Chrome/Edge/Firefox/Opera/Safari | 壓縮最佳,但需 `<picture>` 備援 |
|
||||
| 萬用備援 | **GIF** | `image/gif` | ✅(1-bit) | 全部(含舊版 IE) | 最低成本備援 |
|
||||
| 全動態影片(過場/背景) | **WebM** | `video/webm` | ✅(需 VP9/AV1 alpha 支援) | Chrome/Edge/Firefox/Opera;Safari 部分(見 1.2) | 過場、環境循環 |
|
||||
| 全動態影片(備援) | **MP4** | `video/mp4` | ❌(常無 alpha) | 全瀏覽器 | 無透明需求時的最佳備援 |
|
||||
| 向量/程式動畫 | **Lottie JSON** | `application/json` | ✅ | 需 lottie renderer(Web) | UI 過渡、插圖動畫 |
|
||||
|
||||
> 上表「瀏覽器支援」欄依 **MDN「Image file type and format guide」**(APNG/WebP/AVIF/GIF 明列 Chrome、Edge、Firefox、Opera、Safari)整理:https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types
|
||||
|
||||
**關鍵判斷**:
|
||||
- 若要做「8 個角色的待機循環(眨眼+髮絲+微擺)」,**Animated WebP 或 APNG** 是最佳化格式:體積小、支援 alpha、全部現代瀏覽器可用。
|
||||
- 若是「過場動畫/全屏背景循環/開場片頭」,用 **WebM(VP9)+MP4 備援**的 `<video>` 最省記憶體(影片硬體解碼)。
|
||||
- **GIF 僅作最後備援**:色數僅 256、體積大(MDN 明言 GIF 效能不如 APNG)。
|
||||
|
||||
### 1.2 影片 codec/容器相容性(這是最容易踩雷的地方)
|
||||
|
||||
依 **MDN「Web video codec guide」**:
|
||||
|
||||
- **WebM 容器 + VP9**:Gmail/Chrome 系、Firefox、Opera 原生支援;Safari 近年也開始解 VP9(macOS Big Sur+ 的 WebKit 支援 VP9)。
|
||||
- **WebM 容器 + AV1**:MDN 原文「These are all open, royalty-free formats which are generally well-supported, **with the exception being Safari on older Apple devices**」——**AV1 在 Safari 支援最弱**,別把 AV1 當唯一來源。
|
||||
- **MP4 容器 + H.264 (AVC) + AAC**:MDN 明言「a broadly-supported combination—by **every major browser**, in fact」——這是最萬用的備援。
|
||||
|
||||
MDN 建議的穩妥 `<video>` 寫法(同時提供 WebM 與 MP4):
|
||||
```html
|
||||
<video controls>
|
||||
<source type="video/webm; codecs=av01,opus" src="filename.webm" />
|
||||
<source type="video/mp4" src="filename.mp4" />
|
||||
</video>
|
||||
```
|
||||
來源:https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_codecs
|
||||
|
||||
> ⚠️ **Safari 的 alpha(透明)影片**:要在 Safari 播透明影片,一般需 **WebM+VP9 alpha**,且 **Safari 對「帶 alpha 的 WebM」支援仍屬有限**。若你的過場必須透明且要跨 Safari,務實做法是**用 APNG/Animated WebP(序列圖)而非影片**,或用 MP4「綠幕→前端 keying(chroma key 去背)」。後者屬進階,實作價值與相容性**未查到**權威統一說法,建議實測。
|
||||
|
||||
### 1.3 AI 產出 → 網頁格式的「轉換點」
|
||||
|
||||
本專案沿用前幾份報告結論:**MiniMax H3** 輸出 768P/2K、4–15 秒、24fps(來源:https://platform.minimax.io/docs/guides/video-generation)。這些檔案**不適合直接當網頁素材**(檔案大),正確流程是:
|
||||
1. AI 產一次「循環」(4 秒起)→ 2. ffmpeg 抽幀 → 3. 挑出 6–24 幀 → 4. 輸出目標格式:
|
||||
- 影片型(過場/背景):`ffmpeg -i in.mp4 -c:v libvpx-vp9 -pix_fmt yuva420p out.webm`(帶 alpha)+ `-c:v libx264 out.mp4`(備援)。
|
||||
- 序列圖型(待機循環):抽 N 幀 PNG → 合併成 sprite sheet 或 Animated WebP/APNG(見第 5 章)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 影片/動畫在 Vue 3 的載入與效能最佳做法
|
||||
|
||||
### 2.1 Vite 靜態資源處理(官方文件已驗證)
|
||||
|
||||
依 **Vite「Static Asset Handling / Features」**:
|
||||
- `import imgUrl from './img.png'` 會回傳解析後 URL;開發時 `/src/img.png`,**生產建置變成 `/assets/img.2d8efhg.png`(內容雜湊、自動快取)**。
|
||||
- Vue 外掛會自動把 **SFC 範本內的資產引用轉成 import**。
|
||||
- 小於 `assetsInlineLimit` 的資產會被 **inline 成 base64**(可視需要以 `?inline` / `?no-inline` 明確控制)。
|
||||
- 也可用 `?url`(強制以 URL 輸出,適合影片/Web Worker)、`?raw`(當字串)、`?worker`(把檔案當 Web Worker)。
|
||||
- 也可用 `new URL('./img.png', import.meta.url)`(官方建議的動態 URL 方式)。
|
||||
|
||||
來源:
|
||||
- https://vitejs.dev/guide/assets
|
||||
- https://vitejs.dev/guide/features
|
||||
|
||||
**給我們的建議**:把 AI 素材放 `src/assets/`(可 hash、會被 Vite 納入建置圖譜)而非 `public/`。影片等大檔若不想 base64,確保超過 `assetsInlineLimit`;需要時用 `?url`。
|
||||
|
||||
### 2.2 延遲載入(Lazy-load)策略
|
||||
|
||||
- **IntersectionObserver**:MDN 標記「Baseline 【Widely available】— available across browsers since March 2019」,可用來「等圖片/影片滑進或進入景觀後才載入」,且**不在主執行緒跑貪心地偵測迴圈**(MDN 明言這正是它存在的理由)。來源:https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
|
||||
- **Vue 動態元件(程式碼切分)**:Vite 原生支援 `import()` 動態匯入 → 自動 code splitting(Vite Features 有「Dynamic Import with glob / import()」)來源:https://vitejs.dev/guide/features。可用 `defineAsyncComponent(() => import('./HeavyScene.vue'))` 只在需要時載入重型場景元件。
|
||||
- **`<video preload>`**:設 `preload="none"`(不預載)、`preload="metadata"`(只載中繼資料)或 `preload="auto"`。開場角色用 `metadata`/`auto`,場外小圖用 `none`。(`<video>` 元素與 preload 屬性:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video —— 實際頁面以 JS 動態渲染,抓取正文未取到該字串,屬性語意為 HTML 標準,建議以 MDN 元素頁為準。)
|
||||
- **`loading="lazy"`**:`<img loading="lazy">` 是內建延遲載入,靜態圖可直接用。
|
||||
|
||||
### 2.3 推薦的載入分層(給我們的管線)
|
||||
|
||||
```
|
||||
L1 登入/大廳(首屏) → 只載入 Logo、8 個角色「靜態立繪(WebP)」
|
||||
L2 進入遊戲桌 → 載入角色「待機循環(Animated WebP/APNG,<128KB)」+卡面
|
||||
L3 觸發才載入 → 過場影片/背景循環(WebM,IntersectionObserver 或明確 `play()` 前才 `load()`)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 即時多人對戰下避免動畫阻塞 UI
|
||||
|
||||
### 3.1 requestAnimationFrame 的定位與限制
|
||||
|
||||
- **rAF**(`window.requestAnimationFrame`)是「畫下一幀前叫你的回呼」的標準 API,MDN 完整文件:https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame。
|
||||
- **重要限制**:rAF 回呼跑在**主執行緒**;若主執行緒忙(大量 Socket.IO 處理、DOM 重排、layout),**幀率會掉、卡頓**。MDN 明言它**不保證一個 tick 到下一 tick 的幀數**(渲染頻率由瀏覽器定)。因此「包辦一切遊戲邏輯+動畫」會撞牆,應**把動畫與狀態分層**。
|
||||
- 要在主執行緒做**重運算**(路徑規劃、大量碰撞、逐幀影像處理),應搬去 **Web Worker**(Vite 可用 `?worker` 直接產 Worker 檔),主執行緒只收「結果」畫上去。
|
||||
|
||||
### 3.2 用 Web Animations API 取代手寫 rAF 迴圈
|
||||
|
||||
- **Web Animations API(WAAPI)**:用 `element.animate(keyframes, options)` 直接驅動 DOM 動畫,**交由瀏覽器合成器優化**,比手寫每幀改 style 更省。官方文件:https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API
|
||||
- 搭配 **CSS `will-change` / 只動 `transform`、`opacity`**:可觸發合成器層(compositor)平行處理,避免每次觸發 layout/paint。來源:MDN「Using the Web Animations API」:https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Using_the_Web_Animations_API
|
||||
|
||||
### 3.3 與 Socket.IO 事件的協調(關鍵架構建議)
|
||||
|
||||
即時對戰的典型熱點是「伺服器丟大量事件,前端每次接到就立刻去動 DOM」。風險是**主執行緒塞滿 → 動畫掉幀**。務實分層:
|
||||
|
||||
```
|
||||
Socket.IO 事件 ──► (輕量 handler,不卡) ──► Pinia store 更新狀態
|
||||
│
|
||||
Vue 元件 watch store ─────────────► WAAPI/CSS 動畫 (compositor)
|
||||
```
|
||||
- handler 只做「parse + 寫 store」,**不做** getBoundingClientRect、重排、影片操控。
|
||||
- 卡牌翻面、出牌位移、比大小、角色過場……**全都是 CSS `transform/opacity` 動畫**(WAAPI 可 `commitStyles`、`finished` promise 依序串),幾乎永遠不需要每個角色即時播 24fps 影片。
|
||||
- 桌上 8 個角色的「待機循環」若同時跑 8 段影片,解碼負荷大——**用 Animated WebP/APNG 或 CSS keyframes 讓部分角色「僅剩輕量呼吸感」**,可明顯省電省 CPU。
|
||||
|
||||
### 3.4 Canvas 的取捨
|
||||
|
||||
- 需要**自訂繪製**(例如:把一張 AI 圖做十字溶解、粒子、或逐幀 blit 序列幀)再用 `<canvas>` + 在其自訂的 rAF 迴圈中 `drawImage`。Canvas 寫在**獨立元件**內,本質上也吃主執行緒,控制好幀數與尺寸即可。
|
||||
- 純 UI 卡牌特效(抽卡亮光、翻頁)不需要 Canvas,CSS 足夠。
|
||||
|
||||
---
|
||||
|
||||
## 4. UI 動畫庫比較(Vue 3 適用)
|
||||
|
||||
| 方案 | 型態 | 優點 | 注意 | 狀態/來源 |
|
||||
|---|---|---|---|---|
|
||||
| **GSAP (GreenSock)** | 通用 JS 動畫庫 | 最強控制與序列編排、ease、SVG、時間軸;跨框架 | 需額外套件接 Vue reactivity | 官方「GSAP is now free for everyone」:https://gsap.com/ |
|
||||
| **@vueuse/motion** | Vue 3 專用(VueUse 家族) | `useMotion`、指令式、與組合式 API 自然整合 | 底層可選用 animate.css 概念、包一層 WAAPI | 官方 repo:https://github.com/vueuse/motion(未逐一抓取文件細節) |
|
||||
| **Lottie-web** | 播放 AE JSON | **最適合「把 AI 立繪做成向量/逐幀動畫」**;質感佳、可縮放、體積可控 | 需有 Lottie player / bodymovin 檔案;SDK 為獨立函式庫 | 官方 repo:https://github.com/airbnb/lottie-web |
|
||||
| **Naive UI 內建過渡** | Vue 元件庫 | 已有 `<n-collapse-transition>` 等過渡、loading spinner;底座是 Vue 3 | 適合 UI 元件過渡,非「整桌演出」 | Naive UI 為 Vue 3 元件庫(官網頁面標題直寫「A Vue 3 Component Library」):https://www.naiveui.com/ |
|
||||
| **Vue 內建 `<Transition>`** | 框架內建 | 零依賴,進/離場、列表、狀態過渡 | 只做掛載/切換過渡,非 keyframe 驅動 | https://vuejs.org/guide/built-ins/transition |
|
||||
|
||||
**選型建議**:
|
||||
- **UI 元件過渡、卡牌位移、翻面、比大小**:先用 **Vue `<Transition>` / Naive UI** + **CSS/WAAPI** 就夠,零依賴。
|
||||
- **大規模畫面編排、時間軸演出(多角色依序過場)**:加 **GSAP**(時間軸 `timeline()` 最強)。
|
||||
- **AI 立繪做成「眨眼/擺動」的質感動畫**:**Lottie** 是質價比最高的路(把 AI 圖拆層或做成逐幀 → bodymovin → Lottie JSON → lottie-web 播放)。
|
||||
|
||||
---
|
||||
|
||||
## 5. Sprite Sheet/序列幀的生成與播放
|
||||
|
||||
### 5.1 概念
|
||||
Sprite sheet(精靈圖)是把**同一角色的多個連續幀併排成一張圖片**,執行期只顯示其中一格。優點:**單一請求、無需逐幀請求、可用 CSS/Canvas 極快切幀**。這是「8 角色待機循環」的標準做法。
|
||||
|
||||
### 5.2 生成(從 AI 素材或影片抽幀)
|
||||
- **ffmpeg 抽幀**:`ffmpeg -i clip.mp4 -vf "fps=24,scale=256:-1" frame_%04d.png`,再合併。
|
||||
- **合併成一張**:可用 ImageMagick `montage` 或 `ffmpeg` 的 `tile` 濾鏡,例如把 24 幀做成 6×4 的 sheet:`ffmpeg -i clips -filter_complex "scale=256:256,tile=6x4" sheet.png`。
|
||||
- 專業工具 **TexturePacker**(商用)/開源 `free-tex-packer` 可自動出 JSON 座標(fps、frame index),適合大型 sprite 集。
|
||||
|
||||
### 5.3 在 Vue 中播放
|
||||
- **CSS steps()**(最省):`animation: frame 1s steps(8) infinite;` 搭配 `background-position` 步進,逐格顯示——**完全不碰 JS、由合成器處理**。範例:把 sheet 設為背景,`steps(N)` 表示 N 幀。
|
||||
- **Canvas drawImage**:`ctx.drawImage(sheet, frameIndex*spriteW, 0, spriteW, spriteH, ...)` 在一格 rAF loop 或定時器內推進 `frameIndex`——適合要縮放/旋轉的場合。
|
||||
- **WAAPI / 手動**:直接改 `object-position` 或用 `background-position-x: -N*spriteW`。
|
||||
|
||||
### 5.4 注意
|
||||
- 每格尺寸需一致;背景若為透明,用 PNG(alpha)存 sheet。
|
||||
- **Google 的 Spritesmith / `spritesheet-js` 等**歸類為工具選項,官方穩定維護狀態**未查到**,建議以 ffmpeg/ImageMagick 為準(皆有官方文件)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 對我們遊戲的可行動建議(可直接照做)
|
||||
|
||||
### 6.1 資產分類 → 推薦格式
|
||||
| 資產 | 推薦格式 | 觸發時機 | 備註 |
|
||||
|---|---|---|---|
|
||||
| 8 個角色‧靜態立繪 | WebP / AVIF(fallback) | 登入/大廳(L1) | 尺寸壓到 <200KB |
|
||||
| 8 個角色‧待機循環(眨眼/髮絲) | **Animated WebP 或 APNG(<128KB)** 或 **6–12 幀 sprite sheet** | 進入遊戲桌(L2) | 避免 8 段影片同時解碼 |
|
||||
| 卡面 | WebP 靜圖 + CSS 交錯/光暈(Angular/glare 用 CSS) | L2 | 卡面「拿在手裡」才需 3D 感 |
|
||||
| 過場動畫/全屏背景 | **WebM(VP9) + MP4 備援** `<source>` | 觸發播放前 `load()`(L3) | 用 IntersectionObserver 或明確延遲 |
|
||||
| 翻牌/出牌/比大小 UI 特效 | Vue `<Transition>` + CSS/WAAPI | 事件觸發 | 零依賴、compositor 加速 |
|
||||
| 登入大廳的裝飾/氛圍動畫 | Lottie JSON(若由 AI 立繪轉) 或 CSS | L1–L2 | 質價比高 |
|
||||
|
||||
### 6.2 推薦架構(分層,避免主執行緒塞爆)
|
||||
```
|
||||
server ──Socket.IO──► client socket composable(只 parse + 寫 Pinia)
|
||||
│
|
||||
Pinia store(單一事實來源)
|
||||
│
|
||||
Vue 元件 watch ──► (a) CSS/WAAPI transform/opacity
|
||||
(b) <Transition> / Naive UI
|
||||
(c) Canvas(僅自訂繪製)
|
||||
(d) <video>(過場,手動 load/play)
|
||||
```
|
||||
|
||||
### 6.3 實作步驟(MVP 優先序)
|
||||
1. **先上「角色靜態立繪 + CSS 動起來」**:8 個角色 WebP 立繪,用 CSS `transform` 做上下浮動/燈光,最省成本、立刻有感。
|
||||
2. **再做 1–2 個角色的 AI 待機循環(Animated WebP/APNG)**:用 MiniMax H3 圖生影片產 1 條 4s 循環 → ffmpeg 轉 Animated WebP,驗證體積與效能。
|
||||
3. **卡牌與 UI 動畫**:全用 Vue `<Transition>` + WAAPI,零依賴。
|
||||
4. **做 1 支過場 WebM(VP9)+MP4 備援**,用 `<video>` 手動控制,驗證 Safari。
|
||||
5. **最後才考慮 Lottie 或 GSAP 大型時間軸演出**。
|
||||
|
||||
### 6.4 成本/效能提示
|
||||
- AI 只負責「生成一撕素材」,不要讓 8 個角色同時播 720p 影片——**解碼器會爆**。短片/序列圖優先。
|
||||
- 每項素材上線前用 DevTools Performance 量測:確認動畫幀成本(綠色 compositor 層)與 Memory(影片解碼緩衝)。
|
||||
- Safari 是最大風險:**一律 WebM 配 MP4 備援**;透明動畫用 APNG/Animated WebP 而**不要**只靠「透明 WebM」。
|
||||
- **未查到**:Naive UI 單一元件文件頁的即時正文(其站台對指令列回傳 404,可能受 CDN 攔截);web 端「帶 alpha 的 WebM 在 Safari 全支援」的權威單一證明——請以實測為準。
|
||||
|
||||
---
|
||||
|
||||
## 7. 資料來源連結清單
|
||||
|
||||
1. Vite「Static Asset Handling」:https://vitejs.dev/guide/assets
|
||||
2. Vite「Features」(動態 import/code splitting):https://vitejs.dev/guide/features
|
||||
3. MDN「Image file type and format guide」(APNG/WebP/AVIF/GIF 支援):https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types
|
||||
4. MDN「Web video codec guide」(VP9/AV1/MP4+H.264 相容性與 `<source>` 寫法):https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_codecs
|
||||
5. MDN「Web Animations API」:https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API
|
||||
6. MDN「Using the Web Animations API」:https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Using_the_Web_Animations_API
|
||||
7. MDN「Intersection Observer API」:https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
|
||||
8. MDN「window.requestAnimationFrame」:https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame
|
||||
9. MDN「\<video\> 元素」:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video
|
||||
10. Vue 3 官方「Transition」指南:https://vuejs.org/guide/built-ins/transition
|
||||
11. Naive UI(Vue 3 元件庫)官方站:https://www.naiveui.com/
|
||||
12. lottie-web(Airbnb,GitHub):https://github.com/airbnb/lottie-web
|
||||
13. GSAP 官方站:https://gsap.com/
|
||||
14. @vueuse/motion(GitHub):https://github.com/vueuse/motion
|
||||
15. MiniMax H3 影片生成官方文件(前序報告引用,asset 產出來源):https://platform.minimax.io/docs/guides/video-generation
|
||||
16. MiniMax H3 計價(前序報告引用):https://platform.minimax.io/docs/guides/pricing-paygo
|
||||
|
||||
(前序報告可參照:01_minimax-h3影片生成調查.md、02_ai角色繪製素材生成.md、03_靜態轉動態動畫化.md,同目錄。)
|
||||
|
||||
---
|
||||
|
||||
**本報告完成。所有可查證事實皆附來源 URL;未查到處已明確標註「未查到」。**
|
||||
@@ -0,0 +1,174 @@
|
||||
# 2025–2026 多角色一致性+畫風統一 最佳方案對比與實作 SOP(深化版)
|
||||
|
||||
**調查日期:** 2026-08-06
|
||||
**調查者:** Hermes 研究子代理(角色一致性深化研究)
|
||||
**目的:** 在既有 IP-Adapter 方案之上,深入比較 2025–2026 年「多角色一致性+畫風統一」的主流方法(單圖 LoRA / IP-Adapter / Reference-LoRA / InstantID / FLUX.2 多參考圖 / Nano Banana 多角色 / 角色設定圖 character sheet 法 / 向量與 token 綁定等),評估各自的適用場景、訓練工具與成本,並為我們的「8 個 AI 品牌擬人角色+卡牌唬牌遊戲」產出一套「FLUX.2 / Nano Banana 定角色 → LoRA 固化 → 批量產卡面/表情/姿勢」的落地 SOP 與務實建議。
|
||||
**前導文件:** 本報告承襲並深化 `02_ai角色繪製素材生成.md`(模型/API 總覽)與既有 ComfyUI + IP-Adapter + ControlNet DWpose 管線筆記。
|
||||
|
||||
> ⚠️ **誠實聲明:** 本報告所有事實均於調查當日取自下列即時來源(GitHub README、HuggingFace 模型卡、官方部落格/定價/授權頁),重要規格均附 URL。凡未能查證或來源不明的細節,已明確標註「未查到」。**本報告未編造任何未查證的規格、價格或訓練數據。**
|
||||
|
||||
---
|
||||
|
||||
## 摘要
|
||||
|
||||
截至 2026 年 8 月,「角色一致性」早已不是單一技術問題,而是一整條**分層工具鏈**,每個工具解決一致性的不同維度:
|
||||
|
||||
- **調用期(inference-time,免訓練)** 的一致性方案已成熟且免 GPU 訓練:
|
||||
- **IP-Adapter**(Tencent,Apache-2.0)= 22M 參數的輕量圖片提示 adapter,用「圖片當提示」控制主體外觀/風格,與文字提示並存、可接 ControlNet。
|
||||
- **InstantID**(InstantX/InstantID)= 零樣本、單圖、**以人臉身份**為核心,用 InsightFace 人臉嵌入 + IdentityNet(ControlNet)做身份保留,最適合「真人/類人臉」角色,但站 LDM/SDXL 生態。
|
||||
- **FLUX.1-Kontext** = open-weight、免微調即可做「角色/風格/物件參考」的指令型編輯模型,且支援**多圖輸入的結構化生成**(如角色注入、同角色同框)。
|
||||
- **生成期「原生多參考」大廠模型**(2025 下半年起成為主流)= 最省事的一致性:
|
||||
- **FLUX.2**(BFL,open weight dev 版 2025–11–22):主打 multi-reference control,「同一角色、同一畫風、量產數百張仍一致」,**且能在單張圖內用多張參考圖填入多個角色**(官方範例:把 6 張動物圖填入一群小孩的位置)。
|
||||
- **Nano Banana 2(gemini-3.1-flash-image)**:官方明列**最多 14 張參考圖**,其中「角色一致性」最多 5 張、「風格參考」最多 3 張、「物件」最多 10 張,也可做多人合照。
|
||||
- **訓練期「固化」方案(LoRA / 單圖 LoRA / Reference-LoRA / 向量與 token 綁定)** 仍是「把角色變資產、日後零參考也能復現」的最穩健路徑:
|
||||
- 工具已全面支援 FLUX.2 / Qwen-Image / SDXL / SD3.5 等:**kohya-ss/sd-scripts**、**ostris/ai-toolkit**、**Nerogar/OneTrainer**,開源免費、24GB VRAM 可訓 FLUX LoRA。
|
||||
- 多角色策略=每角色一支 LoRA(各 20–50 張圖),再用**基底 LoRA(畫風/遊戲風格)+角色 LoRA 疊加**,兼顧跨角色畫風統一與個別角色一致。
|
||||
- **對我們的務實結論:** 8 角色量產的高 CP 路徑=「FLUX.2-API 或 Nano Banana 2 定角色/出樣張 → 用 ai-toolkit 或 kohya 在開源基座自訓 8 支『角色 LoRA』+1 支『畫風 LoRA』 → ComfyUI 掛 LoRA 疊加 + ControlNet DWpose 批量產卡面/表情/姿勢」。多角色同框建議走 FLUX.2 多參考或 Kontext(免訓練),避免靠 prompt 硬湊。
|
||||
|
||||
---
|
||||
|
||||
## 一、主流一致性方法對比(2026 年 8 月現況)
|
||||
|
||||
| 方法 | 類型 | 一致性來源 | 是否需要訓練 | 單角色量產 | 多角色同框 | 跨畫風 | 生態/基座 | 關鍵限制 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| **單圖 LoRA(Single-Image LoRA / Dreambooth 類)** | 訓練固化 | 用 15–50 張某角色圖把「角色」固化成 adapter | **是**(一次) | ★★★★★ | 需多支 LoRA 併用 | ★★★(靠畫風 LoRA 分開控) | SDXL/FLUX/Qwen 皆有 | 需 GPU 訓練、資料整理 |
|
||||
| **IP-Adapter** | 調用期(免訓練) | 圖片當提示(image prompt),保留主體/風格 | **否** | ★★★★ | 中(單張參考圖控一主體) | ★★★★(風格圖) | SD 1.5/SDXL/SD3、diffusers | 外觀控制強、精細身份(臉)弱於 InstantID;多主體同框需多 adapter |
|
||||
| **InstantID(FaceID)** | 調用期(零樣本) | 單張人臉嵌入(InsightFace)+ IdentityNet | **否** | ★★★★(類人角色) | 中 | ★★(偏真實/人臉) | SDXL / Kolors,diffusers | 以「人臉身份」為核心,對擬人/非人角色不完全適用 |
|
||||
| **Reference-LoRA** | 混合(提示圖+LoRA) | 以參考圖構圖、用 LoRA 穩定角色 | 部分(需角色 LoRA) | ★★★★ | 中高 | ★★★ | SD 社群(Civitai/comfy) | 「Reference-LoRA」名稱在社群中用法不一,無單一官方實作(見下方說明) |
|
||||
| **FLUX.2 多參考圖** | 原生多參考 | 最多約 10 張參考圖、官方主打同角色同畫風量產一致;單圖內可填入多角色 | **否** | ★★★★★ | ★★★★★ | ★★★★(風格參考) | FLUX.2 API / dev open weights | 建議用官方 API/Playground 最順;dev 自架需 GPU |
|
||||
| **Nano Banana 2** | 原生多參考 | 多參考圖一致性為強項(角色≤5、風格≤3、物件≤10,合計≤14) | **否** | ★★★★★ | ★★★★★(多人合照) | ★★★★ | Google Gemini API | API 計費、SynthID 浮水印、需上雲 |
|
||||
| **FLUX.1-Kontext** | 原生結構化(免微調) | 指令型編輯,角色/風格/物件參考;結構化多圖生成(同角同框/角色注入) | **否**(也可訓 LoRA) | ★★★★★ | ★★★★★ | ★★★★ | FLUX.1-Kontext-dev open weights | 指令/多圖輸入較繁,需 ComfyUI/diffusers 串接 |
|
||||
| **角色設定圖 character sheet 法** | 提示工程/樣張 | 靠「正面/側面/背面設定圖」當參考 + prompt 明寫外觀 | **否** | ★★★ | ★★★ | ★★★★ | 通用(FLUX/Seedream/Nano Banana) | 無「固化」,每次仍要帶參考圖,跨鏡頭穩定度依模型 |
|
||||
| **向量與 token 綁定** | 訓練固化(輕量) | Textual Inversion 把「角色」綁定到一組可學習 vector/token | **是**(輕量) | ★★★ | ★(單概念) | ★★★ | SDXL(kohya Textual Inversion) | 比 LoRA 表現力弱、易欠擬合;非主流選擇 |
|
||||
| **Qwen-Image-Edit-2511** | 編輯式一致性 | 單人身份保留 + **多人群照融合**(兩張人圖合成同框) | **否** | ★★★★ | ★★★★★ | ★★★ | Qwen-Image(diffusers/DiffSynth) | 適合「編輯/合照」,純文生圖一致性較弱 |
|
||||
|
||||
**說明:**
|
||||
- **IP-Adapter** 為 Apache-2.0(GitHub `tencent-ailab/IP-Adapter`),是「以圖為提示」技術的開山代表,官方自述「僅 22M 參數即可媲美微調後的圖片提示模型」,並可與現有 ControlNet/自訂模型併用。
|
||||
- **InstantID** 學術報告 arXiv 2401.07519,特點=零樣本、單圖、幾秒內身份保留;它結合「人臉嵌入(InsightFace antelopev2)+ IP-Adapter(face adapter)+ IdentityNet(ControlNet)」,對**類人臉角色**效果最好。
|
||||
- **Reference-LoRA** 的「Reference」一詞在社群中通常指「inference 時帶參考圖構圖(如 ComfyUI 的 Reference 節點/Flux Kontext 概念)」與「LoRA 固化角色」兩件事的混合;**並無單一官方「Reference-LoRA」模型**,因此本表歸為「混合模式」,實務上=「帶參考圖 + 該角色 LoRA 疊用」。此點與「嚴格定義的單一方法」不同,特此說明。
|
||||
- 「單圖 LoRA」亦非官方術語,大致對應自 DreamBooth 衍生、以少量(甚至單張)圖訓練角色 LoRA 的社群做法;對比更嚴謹的「多圖 LoRA」。若只給單張圖,LoRA 易過擬合照內姿勢,通常建議每角色 10–50 張多角度圖。
|
||||
|
||||
---
|
||||
|
||||
## 二、各自適合的場景
|
||||
|
||||
| 場景 | 最適合方案 | 理由 |
|
||||
|---|---|---|
|
||||
| **單角色量產(同一角色大量姿勢/表情/卡面)** | **角色 LoRA 固化**(FLUX.2-dev / Qwen-Image / SDXL 皆可)+ 選配 IP-Adapter/多參考 | 一次訓練、日後零參考即可穩定復現;最省 API、適合批量/離線/隱私 |
|
||||
| **多角色同框(8 個角色一起上卡/宣傳圖/酒館群像)** | **FLUX.2 多參考圖**、**Nano Banana 2(角色≤5/張,可分批)**、**FLUX.1-Kontext 結構化多圖**、**Qwen-Image-Edit-2511 合照融合** | 原生支援多參考與多主體,最不易把角色風格互相污染 |
|
||||
| **跨畫風統一(讓 8 角色都長成同一個畫風/統一美學)** | **畫風基底模型/畫風 LoRA**+各角色 LoRA 疊加;或 FLUX.2/Nano Banana 的「風格參考圖」 | 把「畫風」與「角色」拆成獨立控制變數,避免互相干擾 |
|
||||
| **類人/真人演員形象** | **InstantID**(若角色接近真人) | 以人臉身份為核心,保真度高 |
|
||||
| **快速探索/概念定稿(不追求逐張可復現)** | **FLUX.2 API / Nano Banana 2** | 出樣張最快、無需訓練 |
|
||||
| **角色三視圖/設定圖(定角色視覺錨點)** | **character sheet 法**+多參考系模型(FLUX.2/Nano Banana 2/Seedream) | 一次定出正面/側面/背面,作為後續 LoRA 訓練資料與參考錨點 |
|
||||
|
||||
---
|
||||
|
||||
## 三、訓練微調工具與成本
|
||||
|
||||
| 工具 | 支援基座 | 訓練型別 | 關鍵要點 | 成本面向 |
|
||||
|---|---|---|---|---|
|
||||
| **kohya-ss/sd-scripts** | SDXL、SD3/SD3.5、FLUX.1(文件含 `flux_train_network.md`) | LoRA、Textual Inversion(SD/SDXL)、inpainting | 開源、社群最成熟;FLUX 支援 `--network_dim`、`--learning_rate`、`--blocks_to_swap`(降低 VRAM)、`network_reg_dims` 分層 rank | 免費軟體;只需 GPU 時數 |
|
||||
| **ostris/ai-toolkit** | FLUX.1、**FLUX.2(-dev/-klein)**、FLUX.1-Kontext、Qwen-Image(-Edit)、SDXL、SD3.5、Chroma、Hunyuan 等 | LoRA、Full fine-tune、slider | 有現成範例 `train_lora_flux_24gb.yaml`、`train_lora_qwen_image_24gb.yaml`、`train_lora_flux_kontext_24gb.yaml`;支援 trigger word、paired/control 資料 | 免費;官方以 24GB VRAM 為範例 |
|
||||
| **Nerogar/OneTrainer** | Ernie Image、Z-Image、Qwen Image、FLUX.1、**Flux.2 Dev/Klein**、Chroma、SD 1.5/2/3.x、SDXL、Sana、Hunyuan Video、pixart 等 | Full fine-tuning、LoRA、embeddings | GUI/多基座、彈性高 | 免費;需 GPU |
|
||||
| **HuggingFace diffusers** | 幾乎所有(SDXL/FLUX/Qwen),並已合併 InstantID 的 community pipeline | LoRA / fine-tune / 推論 | 程式化整合最佳,適合接後端(Node/Python) | 免費;需 GPU |
|
||||
|
||||
**成本(依當日查證):**
|
||||
- **軟體皆免費開源**(kohya/ai-toolkit/OneTrainer/diffusers)。
|
||||
- **主要成本=GPU 時數。** 現有管道建議:本機 GPU 電費或雲端租賃(A100/H100 按時計費;**具體每小時報價依租賃商而定,未逐一查證,故不列具體數字**)。FLUX LoRA 以 24GB VRAM 可訓(ai-toolkit 與 kohya 的範例/`--blocks_to_swap` 皆以此為基準)。
|
||||
- **API 選項成本(僅含調用、不含訓練):** Nano Banana Pro 約 $0.134–$0.24/張、Nano Banana 2 Lite / Imagen 4 約 $0.02–$0.07/張(源自 Google 官方定價頁);FLUX.2 API 單張費用依官方「Pay as you go + 定價計算機」,**未查到單一權威數字**。
|
||||
- **商用授權:** BFL 的 **Builder/Platform 授權階含「Fine-tuning & LoRA rights」+10K images/month+單一領域商用**;FLUX.2-dev open weights 自訓需依 BFL 授權條款(建議以官方 licensing 頁為準)。
|
||||
|
||||
---
|
||||
|
||||
## 四、維持「8 角色風格統一」的提示詞/基底模型/LoRA 疊加策略
|
||||
|
||||
1. **把「畫風」與「角色」拆成兩個正交控制變數**,是 8 角色風格統一的核心:
|
||||
- **畫風層(1 支「畫風/風格 LoRA」或固定基底)**:統一線條、上色、卡面氛圍、遊戲美學(如「酒館暖色厚塗風」)。
|
||||
- **角色層(8 支「角色 LoRA」,每支 = 一個角色的外觀身份)**。
|
||||
- 推理時**畫風 LoRA+當前角色 LoRA 疊加**(LoRA 疊加是標準能力,亦可控各支 scale)。
|
||||
2. **基底模型固定**:所有角色統一用同一個基底(如 FLUX.2-dev 或 Qwen-Image),避免切基座造成風格跳動。
|
||||
3. **提示詞模板(character sheet JSON / prompt 模板)**:為每角色維護一份固定「外觀描述 block」(髮色/髮型/服裝/配件/主題色),每次生成貼上,降低文字漂移。範例欄位:`name / hair / hair_accessory / outfit / palette / accessories / style_tags / negative_tags / fixed_seed_policy`。
|
||||
4. **多角色同框**:不要靠一句 prompt「憑空湊 8 個角色」——優先餵每角色的參考圖(FLUX.2 多參考 / Nano Banana 2),或把已訓好的角色 LoRA 併用+帶各自設定圖。
|
||||
5. **畫風漂移防堵**:
|
||||
- 每次批量固定同 seed policy/同一組風格關鍵字;
|
||||
- 用「畫風錨點」參考圖(一張官方認定的風格代表圖)疊加;
|
||||
- 每批產出後做人工/規則檢核(髮色、配件、主題色),不合規就重新抽樣。
|
||||
6. **角色間「外觀混淆」(懲罰相似角色)**:8 品牌角色的配色/配件已刻意區分(橘/Grok、銀白/Kimi、粉/Qwen3、黑/GPT-5、紫/Gemini、淺藍/Deepseek、深棕/Doubao、橘/Claude),善用這些**高對比主題色**做 prompt 硬約束+角色 LoRA 強度拉高,降低「A 角色的臉長到 B 角色身上」的混淆。
|
||||
|
||||
---
|
||||
|
||||
## 五、落地流程:FLUX.2 / Nano Banana 定角色 → LoRA 固化 → 批量產卡面/表情/姿勢(SOP)
|
||||
|
||||
**階段 A:定角色(視覺錨點,免訓練)**
|
||||
1. 用 **FLUX.2 API 或 Nano Banana 2** 為 8 角色各產 2–3 張「全身/半身立繪+三視圖(正面/側面/背面)」,背景純色/白底以便去背。
|
||||
2. 人工挑選每角色 1 張「最符合官方品牌視覺」的代表圖,另存為該角色**畫風錨點**與**一致性參考圖**。
|
||||
3. 產出透明背景 PNG(要求 RGBA 或後製 rembg)存為 `assets/characters/<name>/ref_*.png`。
|
||||
|
||||
**階段 B:LoRA 固化(訓練,選 GPU)**
|
||||
4. 每角色蒐集 **20–50 張多角度/多表情樣張**(可先用階 A 的參考圖+文字變化批量生成)。
|
||||
5. 用 **ai-toolkit 或 kohya-ss** 在固定基底(FLUX.2-dev / Qwen-Image)訓 8 支**角色 LoRA**;另準備一批「跨角色統一風格」圖訓 **1 支畫風 LoRA**。
|
||||
6. 輸出的 `.safetensors` 放進 ComfyUI `models/loras/`,以 `trigger_word` 綁定每角色。
|
||||
|
||||
**階段 C:批量產卡面/表情/姿勢**
|
||||
7. ComfyUI 工作流=`基底 Checkpoint + 畫風LoRA + <該角色>LoRA + IP-Adapter(參考圖,選配) + ControlNet DWpose(姿勢) + 該角色 prompt 模板`。
|
||||
8. 產出:卡面立繪(透明 PNG)、表情集(同角色 loRA + 表情提示詞)、姿勢集(DWpose 控姿勢)。
|
||||
9. 前端 Vue/Pixi 疊合背景、牌框、UI(AI 只出角色美術圖層)。
|
||||
10. 每批產出做一致性/風格檢核,不合格僅重抽該張,不重訓 LoRA。
|
||||
|
||||
**階段 D:多角色同框(選做)**
|
||||
11. 群像/宣傳圖用 FLUX.2 多參考圖或 Kontext 結構化生成,餵每角色的參考圖,而非硬用 prompt。
|
||||
|
||||
**驗收指標(建議)**:每角色 50 張樣張中,「外觀一致率」與「畫風統一率」目視達標比例,作為回歸基準。
|
||||
|
||||
---
|
||||
|
||||
## 六、對我們「8 個擬人角色+卡牌遊戲」的最務實建議與陷阱
|
||||
|
||||
**務實建議(由高到低優先):**
|
||||
1. **先免訓練、後固化(降低風險與 GPU 依賴)**:先用 FLUX.2-API / Nano Banana 2 做「定角色+出樣張」驗證一致性——這是最省事、最快、且官方主打「同角色同畫風量產一致」,非常適合我們的 8 個固定角色(延續 02 報告結論)。
|
||||
2. **行有餘力再投資 LoRA**:待 8 角色視覺定稿、確認風格方向後,再自訓**8 支角色 LoRA+1 支畫風 LoRA**(ai-toolkit 約 24GB VRAM)。這樣做能讓我們「日後任何卡面/表情/姿勢無需帶參考圖也能一致」,且離線、低成本、隱私最佳。
|
||||
3. **多角色同框=用多參考系,不要硬 prompt**:酒館群像/多人卡用 FLUX.2 多參考、Nano Banana 2(每張≤5 角色,8 角色可分批或做兩張合成)、FLUX.1-Kontext 或 Qwen-Image-Edit-2511 合照融合。
|
||||
4. **畫風與角色分層管理**:維持「1 畫風 LoRA × 8 角色 LoRA」的疊加結構+固定基底+prompt 模板,是 8 角色風格統一的關鍵。
|
||||
5. **沿用既有 ComfyUI 管線**:把基底升級到 FLUX.2-dev(仍支援 ControlNet 類外掛),LoRA + IP-Adapter + DWpose 全保留,相容性最高。
|
||||
|
||||
**陷阱(務必注意):**
|
||||
- **變種失敗(過擬合照姿勢)**:角色 LoRA 若只給單/少張圖,會把照內的姿勢/構圖一起「定死」,新姿勢差。→ 每角色多角度、多表情、多尺寸 20–50 張訓練集。
|
||||
- **風格漂移(每次風格跑掉)**:基底不固定、畫風不獨立控、seed 亂跳 → 風格不一。→ 固定基底+畫風 LoRA+固定 prompt 模板+seed 策略+批次檢核。
|
||||
- **同框多角色互相污染**:靠 prompt 硬湊 8 角色容易互混配件/髮色。→ 用多參考圖/每角色參考、或已訓角色 LoRA+control。
|
||||
- **角色間相似度**:橘(Grok) 與橘(Claude)、銀白/粉/紫等相近色若無強約束易混。→ 強化配件/服裝差異 + 角色 LoRA identity 檢核。
|
||||
- **人臉型方案誤用**:InstantID 以人臉身份為核心,適合「真人/類人」,對非人擬人角色效果有限——除非角色是類人設計,否則優先選 LoRA/多參考,別硬套 InstantID。
|
||||
- **授權/商用**:FLUX.2-dev 自訓/LoRA 與 SD3.5 皆有特定授權/營收門檻;上雲要查 SynthID 浮水印與服務條款;AI 品牌擬人角色需注意品牌商標/外觀仿冒風險(非法律意見)。
|
||||
- **成本誤判**:API 按張計費,批量產數量級卡面會累積成本;LoRA 訓練是一次性 GPU 成本,量產期反而省。依預算混用。
|
||||
|
||||
---
|
||||
|
||||
## 資料來源連結清單(皆於調查當日 2026-08-06 存取)
|
||||
|
||||
**方法/模型**
|
||||
- IP-Adapter(Apache-2.0,Tencent,22M 參數、image prompt、可併 ControlNet):https://github.com/tencent-ailab/IP-Adapter
|
||||
- InstantID(零樣本、單圖、身份保留;arXiv 2401.07519):https://github.com/InstantID/InstantID 、https://arxiv.org/abs/2401.07519 、檢查點 https://huggingface.co/InstantX/InstantID
|
||||
- FLUX.2 產品頁(multi-reference、同角色同畫風量產一致、單圖多角色填入範例):https://bfl.ai/models/flux-2 、https://blackforestlabs.ai/models/flux-2
|
||||
- FLUX.2-dev(open weights,2025-11-22):https://huggingface.co/black-forest-labs/FLUX.2-dev
|
||||
- FLUX.1-Kontext-dev(免微調角色/風格/物件參考、結構化多圖;arXiv 2506.15742):https://huggingface.co/black-forest-labs/FLUX.1-Kontext-dev 、https://arxiv.org/abs/2506.15742
|
||||
- Qwen-Image(含 -Edit-2511 增強角色一致性、多人群照融合):https://github.com/QwenLM/Qwen-Image
|
||||
- FLUX.2 授權/定價(Builder/Platform 含 Fine-tuning & LoRA rights、10K images/month、Open Weights):https://bfl.ai/licensing 、https://bfl.ai/pricing 、https://blackforestlabs.ai/pricing/
|
||||
- BFL 部落格(FLUX 3 / 工具更新):https://bfl.ai/blog 、https://blackforestlabs.ai/blog/
|
||||
|
||||
**Google / Nano Banana(多角色一致性能力值)**
|
||||
- Gemini 圖像生成文件(Nano Banana 家族;**角色參考≤5 / 風格參考≤3 / 物件≤10,合計≤14 張**;多人合照範例):https://ai.google.dev/gemini-api/docs/image-generation
|
||||
- Gemini API 定價(Nano Banana Pro 約 $0.134–$0.24/張、Imagen 4 $0.02–$0.06/張):https://ai.google.dev/gemini-api/docs/pricing
|
||||
|
||||
**訓練工具**
|
||||
- kohya-ss/sd-scripts(支援 SDXL/SD3/SD3.5/FLUX.1 LoRA、Textual Inversion、FLUX `--blocks_to_swap`):https://github.com/kohya-ss/sd-scripts 、FLUX 訓練文件 https://github.com/kohya-ss/sd-scripts/blob/main/docs/flux_train_network.md
|
||||
- ostris/ai-toolkit(支援 FLUX.1/FLUX.2(-klein)/Kontext/Qwen-Image/SDXL/SD3.5;24GB 範例 yaml):https://github.com/ostris/ai-toolkit
|
||||
- Nerogar/OneTrainer(Full fine-tune / LoRA / embeddings;FLUX.2 Dev/Klein、Qwen Image 等):https://github.com/Nerogar/OneTrainer
|
||||
- HuggingFace diffusers(含 InstantID community pipeline):https://github.com/huggingface/diffusers
|
||||
|
||||
**前導/延伸**
|
||||
- 既有 02 報告:`docs/research/minimax-ai-game-visual/02_ai角色繪製素材生成.md`
|
||||
- 現成遊戲素材 LoRA 範例:https://huggingface.co/gokaygokay/Flux-Game-Assets-LoRA-v2
|
||||
|
||||
**未查到/需再確認事項(誠實標註)**
|
||||
- 「Reference-LoRA」無單一官方實作——社群混用「參考圖構圖」與「角色 LoRA」,已於第一章說明,請以實際 ComfyUI 節點/模型為準。
|
||||
- FLUX API「單張圖」確切計費、雲端 GPU 每小時具體報價:未查到單一權威即時數字,請以官方定價與租賃商報價為準。
|
||||
- 各品牌角色(Grok/Kimi/Qwen/GPT/Gemini/Deepseek/Doubao/Claude)為「AI 品牌擬人」之商標/外觀仿冒法律評估:本報告不構成法律意見。
|
||||
@@ -0,0 +1,249 @@
|
||||
# 卡牌/牌桌視覺與風格指引:AI 繪圖用於「騙子酒館(Liar's Bar)卡牌唬牌遊戲」美術設計與提示詞工程
|
||||
|
||||
**調查日期:** 2026-08-06
|
||||
**調查者:** Hermes 研究子代理(minimax-ai-game-visual 工作流系列 #06)
|
||||
**目的:** 為我們的網頁版「騙子酒館(Liar's Bar)卡牌唬牌遊戲」建立一套**可重複使用的視覺風格指南與提示詞模板**。桌上 8 個 AI 品牌擬人角色(Grok/DeepSeek/Claude 等)+ 卡牌/桌布/籌碼/骰子/酒杯等元素;技術棧 Vue3 + Node/Socket.IO,畫面目前素樸。本報告聚焦:風格方向選擇、各素材的提示詞模板、背景/UI 分層、避免 AI 常見弊病的提示詞技巧、桌遊卡牌設計規範,並產出一份「8 角色統一世界觀」風格指南草稿。
|
||||
|
||||
---
|
||||
|
||||
## 摘要
|
||||
|
||||
- **推薦風格:** 對「AI 擬人角色 + 卡牌」這類需要**一致性、量產、跨設備可讀**的網頁遊戲,最務實的是 **「stylized board-game illustration/風格化卡牌插畫(2D 數位繪畫,品質定位介於『扁平向量』與『寫實 3D』之間)」**,並以「低飽和西部/酒館色板 + 明確輪廓線 + 乾淨背景分層」統一全場。理由:角色與卡面可穩定重現(對應先前報告的 FLUX.2 多參考圖一致性管線)、量產成本低、卡牌縮圖仍清晰、WebGL 渲染性能佳、LoRA 好訓練。
|
||||
- **真實查證的硬規格**(附來源):標準「撲克尺寸」卡牌為 **3½ × 2½ 吋(≈88.9 × 63.5 mm)**(來源:維基百科 Standard 52-card deck);卡牌「牌背」設計的意義之一是防止看穿/靠背紋記牌(來源:維基百科 Playing card)。
|
||||
- **透明背景分層**是前端合成的關鍵:人物/卡牌主體/背景道具用「圖層分離」或「單色/去背」方式分開生成,前端再合成,可重複利用(相關來源:ByteDance Seedream 圖層分離、remove.bg 去背、FLUX 多參考一致性)。
|
||||
- **避免 AI 弊病的提示詞技巧**多為業界通用經驗;Midjourney 官方明確建議「提示詞簡短具體、避免長清單、用精準同義詞」(來源:Midjourney 官方 Prompt Basics)。
|
||||
- 本報告的所有猜測/建議性內容(角色個性、風格方向、具體提示詞配方)屬**設計建議**,標以「建議」;凡未查證的硬數據一律標「未查到」。
|
||||
|
||||
---
|
||||
|
||||
## 一、牌桌遊戲美術風格調查與推薦
|
||||
|
||||
### 1.1 四種常見風格的比較
|
||||
|
||||
| 風格 | 代表案例(桌遊/電子桌遊) | 對「AI 擬人角色+卡牌」的優點 | 缺點/風險 | 適合度 |
|
||||
|---|---|---|---|---|
|
||||
| **桌上遊戲 3D 渲染**(practicable 3D, isometric, diorama) | 電子化的桌遊(如 GTA 酒館、桌遊模擬器 Tabletop Simulator 的 3D 場景) | 有「真實酒館」的沉浸感、光影豐富有質感 | 成本高、需要整套 3D 資產、一致性主要靠同一渲染管線、網頁端效能吃重 | ★★☆☆ |
|
||||
| **2D 風格化卡牌插畫(stylized board-game illustration)** | 美式風格化卡牌桌遊插畫、TCG(如 MTG 的風格化配角)、電子卡牌 | 一致性好、量產快、卡面縮圖可讀、與 LoRA/多參考一致管線契合 | 較難做出「擬真啤酒杯反光」等夸張材質 | ★★★★★ 推薦 |
|
||||
| **像素風(Pixel art)** | 復古卡牌/Roguelike(如 Slay the Spire 式風格) | 輕量、風格強烈、製作成本極低 | 與「AI 擬人角色的細膩表情/個性」衝突,細節表達有限 | ★★★☆☆ |
|
||||
| **卡通/向量扁平(Flat cartoon / vector)** | 休閒卡牌、桌面派對遊戲 | 乾淨、明快、可讀性最高、網頁載入輕 | 角色「個性戲精感」較弱,若擬人角色要「活靈活現」稍嫌單薄 | ★★★☆☆ |
|
||||
|
||||
### 1.2 為何推薦「風格化卡牌插畫(2D,低飽和西部色板)」
|
||||
|
||||
對照先前報告(02)已確認我們最大痛點是**角色一致性**,而 FLUX.2 主打「同一角色、同一畫風、量產一致」。這條管線與「風格化 2D 插畫」的契合度最高:
|
||||
|
||||
1. **一致性**:卡通輪廓+固定色板+固定光源方向,比寫實 3D 更易讓多模型/多參考圖穩定重現同一角色。
|
||||
2. **可讀性(可玩性)**:卡牌在手機/桌面縮到很小也要一眼看懂數字、花色與角色表情;風格化插畫的乾淨分層最能保證這點(呼應 Midjourney 官方「具體描述」的訓練方向)。
|
||||
3. **效能與前端**:2D 靜態圖 + 少量 2D 動畫就足以呈現「戲精/瘋癲/淡定」,不需 3D 渲染開銷,Vue3 直接合成。
|
||||
4. **成本**:等量角色+卡面,2D 產量與迭代成本遠低於 3D 資產管線。
|
||||
|
||||
**牌桌「酒館/西部/吹牛」氛圍**的統一關鍵字建議:`western saloon, poker table, candlelight, whiskey, worn wood, gambling den, night, moody, vintage, warm low-key lighting`。
|
||||
|
||||
> 本小節的「推薦」屬設計判斷;風格案例與工具的實際能力以所附官方來源為準。
|
||||
|
||||
---
|
||||
|
||||
## 二、各素材的最佳生成提示詞模板
|
||||
|
||||
> 使用原則:**英文提示詞**對主流文生圖模型(FLUX/SDXL/Nano Banana/Midjourney)反應最好;中文只作說明。每張圖「一次只鎖定一個主體+統一光源+統一色板」最穩。所有模板皆為「建議配方」,實際輸出需依所選模型微調。
|
||||
|
||||
### 2.1 統一風格/光源/材質/色板(「風格前綴」,每張都複製一份貼在前面)
|
||||
|
||||
```
|
||||
STYLE PREFIX (複製貼到每條提示詞開頭):
|
||||
Western saloon poker table illustration, stylized board game art,
|
||||
hand-drawn digital painting with clean confident outlines,
|
||||
warm candlelight and low-key amber lighting, rich whiskey-amber and
|
||||
burnt-sienna palette with deep teal shadows, worn dark wood textures,
|
||||
subtle film grain, ultra detailed, crisp readable composition
|
||||
```
|
||||
|
||||
統一色板建議(HEX):
|
||||
- 主色:深褐木 `#3A2418`、威士忌琥珀 `#C77B2E`、燭光暖黃 `#E8B04B`
|
||||
- 陰影/背景:墨綠 `#1F3B35`、夜色靛 `#16202E`
|
||||
- 點綴(可識別用):金 `#C9A227`、象牙白 `#EFE3C8`
|
||||
|
||||
### 2.2 卡面(一張抽出來單獨生成,不與角色擠在同一張)
|
||||
|
||||
```
|
||||
CARD FACE — 單張卡面:
|
||||
[STYLE PREFIX], one single poker card, vertical 2:3, centered character
|
||||
portrait bust in an ornate oval frame, large clear suit pips and rank in
|
||||
top-left and bottom-right corners, no text, no letters, no watermark,
|
||||
empty dark saloon background behind the card edge, isolated on plain background
|
||||
```
|
||||
|
||||
### 2.3 牌背(重點:對稱、無文字、防記牌)
|
||||
|
||||
```
|
||||
CARD BACK:
|
||||
[STYLE PREFIX], ornate symmetrical playing card back design, intricate
|
||||
filigree damask pattern, poker cards iconography, perfectly symmetrical,
|
||||
no text, no letters, no numbers, no watermark, repeating seamless pattern,
|
||||
flat even lighting, centered composition
|
||||
```
|
||||
|
||||
> 「牌背必須防看穿/防記牌」是業界常識,且維基百科 Playing card 明確指出牌背圖樣是為「讓玩家難以看穿材質或靠背紋記牌」——對唬牌遊戲尤為重要(來源見文末)。
|
||||
|
||||
### 2.4 木桌/桌布(整張牌桌背景,人物之後合成)
|
||||
|
||||
```
|
||||
SALOON TABLE (背景層):
|
||||
[STYLE PREFIX], wide shot of an empty western saloon poker table,
|
||||
worn dark oak wood grain, green felt center pad, brass candle lantern,
|
||||
scattered pennies and worn chips, empty whiskey glass and dice, no people,
|
||||
no text, shallow depth of field, overhead three-quarter view, symmetrical
|
||||
```
|
||||
|
||||
### 2.5 酒杯/威士忌/啤酒
|
||||
|
||||
```
|
||||
WHISKEY GLASS (道具特寫):
|
||||
[STYLE PREFIX], close-up of an old-fashioned whiskey glass with ice and
|
||||
amber liquid, condensation droplets, warm backlight glow, on worn wood bar,
|
||||
reflective rim light, no text, product-photo-like clarity but painted style
|
||||
```
|
||||
|
||||
### 2.6 籌碼/骰子(吹牛遊戲的次級元素)
|
||||
|
||||
```
|
||||
POKER CHIPS (成組):
|
||||
[STYLE PREFIX], stack of casino poker chips, alternating ivory and dark-red
|
||||
edge stripes, glinting under candlelight, shallow macro depth of field,
|
||||
no text, no numbers rendered clearly, painted board-game style
|
||||
|
||||
LIAR'S DICE:
|
||||
[STYLE PREFIX], a few ivory dice scattered on worn green felt, tiny drilled
|
||||
pips, warm side lighting, softly blurred background, no text, painted style
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、背景與 UI 分層:人物/卡牌/背景分開生成
|
||||
|
||||
**原則:永遠分層,不要一次畫「完整場景含全部角色」。**
|
||||
|
||||
推薦圖層拆法(前端用 alpha / 合成疊回):
|
||||
|
||||
1. **背景層**:木桌+酒杯+蠟燭(2.4~2.6),一整張 16:9 或桌面俯視。一次生成、全場共用,節省量產。
|
||||
2. **人物層(8 個角色各自獨立 PNG)**:用透明底或純色底(綠幕/黑底)單獨生成每位角色,再在編輯器去背(如 remove.bg;或模型原生透明的 RGBA)。這樣同一角色能在牌桌各座位複用、替換表情。
|
||||
3. **卡牌層**:卡面/牌背獨立生成(2.2、2.3),前端做成可堆疊、可翻面的 DOM/CSS 或 SVG 卡元件。
|
||||
4. **UI 層**:血條/籌碼數/回合提示等**用 HTML/CSS 而非 AI 生成**(AI 生成的文字與邊框易歪斜出錯,UI 用程式畫才精準)。
|
||||
|
||||
> 透明背景取得管道(建議,皆需查官方文件再實作):ByteDance Seedream 系列曾主打主體/透明(RGBA)輸出(相關官方:https://seed.bytedance.com/ 與 https://www.volcengine.com/docs/82379);Google Nano Banana 系列(https://ai.google.dev/gemini-api/docs/image-generation);去背後製可用 remove.bg(https://www.remove.bg/)。
|
||||
|
||||
---
|
||||
|
||||
## 四、避免 AI 生成常見弊病的提示詞技巧
|
||||
|
||||
> 以下多為**業界通用實務經驗**(非特定單一官方規範),已盡量附可查證出處;未有官方統一定義者標「建議」。
|
||||
|
||||
1. **文字錯字/亂碼**
|
||||
- 卡面一律加 `no text, no letters, no words, no watermark`(卡面數字我們用前端 SVG 覆蓋,徹底避開 AI 寫字)。
|
||||
- 需要精確文字時選「強調精確文字」的模型(FLUX.2 官方主打精確文字與 brand color 對色;Nano Banana 亦強調文字能力——見文末 FLUX 官方模型來源)。**建議:重要文字一律不走 AI,用前端排版。**
|
||||
2. **手指變形/身體部位融接**
|
||||
- 提示詞避免塞入「多人群像+複雜手勢」;人物只給半身/坐姿「bust portrait, seated at table, hands relaxed out of frame or folded」,把最難的「手」藏起來。
|
||||
- 一次只生成「一位角色面向鏡頭」。
|
||||
3. **邊框歪斜/牌的直線不齊**
|
||||
- 卡面不要讓 AI 畫框線;用「ornate oval frame(圓形楕圓內襯)+外框由前端 CSS 畫」。
|
||||
- 對稱元素(牌背)在提示詞裡重複 `perfectly symmetrical, centered`,仍建議後製水平鏡像校正。
|
||||
4. **背景與主體混在一起**
|
||||
- 分層生成(見第三章),人物用單色底/透明底,避免 AI 把人物融進複雜背景。
|
||||
5. **風格漂移(同角色每張長得不一樣)**
|
||||
- 鎖定「同一 STYLE PREFIX+同一角色描述+同一光源字串」,並用多參考圖一致性模型(FLUX.2 multi-reference,見文末 FLUX 官方);或把修好的一張當 reference 餵給 img2img。
|
||||
6. **提示詞過長導致發散**
|
||||
- Midjourney 官方明言「短而具體的提示詞效果最好,避免長清單」——把「風格+光線+色板」抽成 STYLE PREFIX 固定,主體描述保持精短(來源:https://docs.midjourney.com/docs/prompts)。
|
||||
|
||||
---
|
||||
|
||||
## 五、桌遊卡牌美術設計規範(尺寸/出血/安全邊距/可讀性)
|
||||
|
||||
**真實查證硬規格:**
|
||||
- **標準「撲克尺寸(poker-sized)」卡牌**:**3½ × 2½ 吋**,即 **≈88.9 × 63.5 mm**。19 世紀尺寸標準化後「wide/poker-sized」成為主流;較窄的「bridge-sized(whist)」亦存在。(來源:維基百科 **Standard 52-card deck**:https://en.wikipedia.org/wiki/Standard_52-card_deck)
|
||||
- **牌背設計目的**:防止透過材質看穿或靠背紋記牌。(來源:維基百科 **Playing card**:https://en.wikipedia.org/wiki/Playing_card)
|
||||
|
||||
**對我們網頁版的映射建議:**
|
||||
- 畫面卡牌元件採 **2:3 豎版**、中心構圖;前端即時渲染尺寸可採 300×450(對應 2:3),高 DPI 用 600×900。
|
||||
- **出血(bleed)/安全邊距(safe margin)**:若是實體印刷,業界慣例是四周各留**出血約 3.18 mm(1/8″)**、安全區再內縮(裁切誤差保險)。**我們是純數位網頁版——把「出血+安全邊距」落實為「卡面內容(角色+符號)集中於中央安全區、四邊留安全的不可見邊」**,避免裁切/圓角遮蔽。確切印刷數值(各印刷廠不一)標「未查到單一統一標準,依所選印刷商規範為準」。
|
||||
- **可讀性(佈局)**:數字/花色放四角、角色插畫置中、名稱欄留白;色彩對比高(琥珀上疊深褐陰影);小尺寸仍可讀。
|
||||
|
||||
> 卡牌「安全邊距/出血」的通用數值為印刷業慣例(建議值);因多印刷商各自定義,標「建議,未查到統一官方規格」。本報告不冒充精確印刷規範。
|
||||
|
||||
---
|
||||
|
||||
## 六、【草稿】8 角色統一世界觀──風格指南
|
||||
|
||||
> 草稿階段,可再依對白/劇本(Grok 戲精、DeepSeek 瘋癲、Claude 淡定為指定基調)微調。每位角色統一遵守第五章 STYLE PREFIX 與色板,並各帶「牌桌個性道具」以利辨識。
|
||||
|
||||
### 6.1 統一世界觀核心
|
||||
- 全員坐在同一間西部酒館牌桌:**蠟燭暖光+琥珀威士忌+深褐木桌+墨綠陰影**。
|
||||
- 每位角色 = 一個 AI 品牌擬人形象,語氣與牌風互補(有人虛張聲勢、有人神祕莫測、有人撲克臉)。
|
||||
- 全員統一:半身坐姿、面向牌桌中央、眼神看向「玩家/鏡頭」、手持自己的個性道具、服裝用各自品牌色但融入西部剪裁。
|
||||
|
||||
### 6.2 各角色「牌桌個性化」
|
||||
|
||||
| 角色 | 牌桌個性(基調) | 外在形象建議 | 品牌色 | 牌桌道具 | 表情/動作關鍵字 |
|
||||
|---|---|---|---|---|---|
|
||||
| **Grok**(xAI) | **戲精**:誇張、愛演、虛張聲勢、吹牛時表情最大 | 瀟灑西部說書人,敞領襯衫+馬甲 | 黑+火紅點綴 | 永遠舉著的酒杯/羽毛筆 | `theatrical, wide grin, hand dramatically raised, wink` |
|
||||
| **DeepSeek** | **瘋癲**:情緒高漲、笑點低、牌風愛瞞天過海 | 亂髮、鬆領帶、眼神發亮 | 深藍+青 | 轉著玩的骰子/歪掉的帽子 | `manic grin, wild eyes, leaning forward, chaotic energy` |
|
||||
| **Claude** | **淡定**:撲克臉、慢條斯理、難以讀牌 | 沉穩訂製西裝馬甲、坐姿端正 | 米白+暖褐 | 聞香不喝的威士忌、懷錶 | `calm deadpan, unreadable poker face, composed, slight smirk` |
|
||||
| **ChatGPT**(OpenAI) | 圓滑狡詐:笑瞇瞇、話多但難捉摸 | 圓潤討喜、領結、常搭肩 | 薄荷綠+黑 | 一把扇子/遊戲卡 | `smug gentle smile, playful, smooth talker` |
|
||||
| **Gemini**(Google) | 冷靜博學:先算牌再行動、極少露情緒 | 乾淨、單色高領、端正 | 藍紫漸層 | 單片眼鏡/記事本 | `observant, analytic gaze, minimal expression` |
|
||||
| **Llama**(Meta) | 豪邁健談:嗓門大、氣氛組、愛嗆聲 | 壯碩莽漢、皮繩裝飾 | 深藍灰 | 整壺啤酒 | `boisterous, laughing loud, toasting` |
|
||||
| **Mistral** | 精準犀利:話少、一句見血、偷刀 | 瘦削、黑大衣、銳利眼神 | 青綠+黑 | 匕首/短雪茄 | `sharp stare, terse, cutting one-liner` |
|
||||
| **Perplexity** | 好奇多話:愛問愛打聽、常被套話 | 背心探險家、圓框眼鏡 | 藍綠+金 | 放大鏡/信封 | `curious raised eyebrow, leaning in, chatty` |
|
||||
|
||||
> **Grok 戲精/DeepSeek 瘋癲/Claude 淡定**為題目指定基調(已標明);其餘 5 位為「建議草稿」,可依劇本調校。
|
||||
|
||||
### 6.3 每位角色卡面用提示詞模板(範例,Grok)
|
||||
|
||||
```
|
||||
CARD CHARACTER — Grok:
|
||||
[STYLE PREFIX], single poker card portrait of a dashing western
|
||||
storyteller character, theatrical wide grin, wink, one hand dramatically
|
||||
raised holding a whiskey glass, black waistcoat with fiery red accents,
|
||||
bust portrait in ornate oval frame, seated at saloon poker table,
|
||||
no text, no letters, clean background
|
||||
```
|
||||
|
||||
> 其他 7 位把「theatrical … black waistcoat with fiery red accents」換成上表對應的「形象+品牌色+道具+表情關鍵字」即可批次產出。
|
||||
|
||||
---
|
||||
|
||||
## 七、可複製的提示詞模板總表(可直接貼用)
|
||||
|
||||
- **STYLE PREFIX(每張必貼)**:見 2.1。
|
||||
- **卡面**:見 2.2。|**牌背**:見 2.3。|**木桌/酒館背景**:見 2.4。|**酒杯**:見 2.5。|**籌碼**:見 2.6。|**單角色卡面**:見 6.3。
|
||||
- **多數模型通用「去廣告文字」負向詞(negative prompt)**:`text, letters, watermark, signature, low quality, blurry, deformed hands, extra fingers, distorted face, asymmetrical border, jpeg artifacts`
|
||||
|
||||
---
|
||||
|
||||
## 八、對我們遊戲的可行動建議(優先序)
|
||||
|
||||
1. **先建 STYLE PREFIX 與色板**,一次生成「酒館木桌背景」與「8 張單角色 PNG(透明/純色底)」作為基底,全場共用。
|
||||
2. **卡面數字/花色、UI、回合提示全部用前端 HTML/CSS/SVG 畫**,AI 只出「插畫層」——這是避開文字與邊框弊病最有效的做法。
|
||||
3. 用 FLUX.2 多參考圖一致性(https://blackforestlabs.ai/models/flux-2)或 img2img reference 鎖定每位角色,先驗證 3~5 張卡面樣張再量產。
|
||||
4. 卡牌元件採 2:3 豎版(對應標準撲克 88.9×63.5mm 比例);內容集中中央安全區。
|
||||
5. 依 6.2 表批次產出 8 角色卡面,再依對白劇本細修個性表情。
|
||||
|
||||
---
|
||||
|
||||
## 九、資料來源連結
|
||||
|
||||
- 卡牌「撲克尺寸 3½ × 2½ 吋」與尺寸標準化:維基百科 Standard 52-card deck — https://en.wikipedia.org/wiki/Standard_52-card_deck
|
||||
- 牌背防看穿/防記牌:維基百科 Playing card — https://en.wikipedia.org/wiki/Playing_card
|
||||
- Midjourney 官方提示詞指南(短而具體、精準同義詞):https://docs.midjourney.com/docs/prompts
|
||||
- FLUX.2/FLUX 家族官方頁(多參考一致性、精確文字、尺寸比例):https://blackforestlabs.ai/models/flux-2 ;https://blackforestlabs.ai/models/ ;https://blackforestlabs.ai/announcing-black-forest-labs/
|
||||
- Google Nano Banana 圖像生成(多參考一致性、文字、SynthID):https://ai.google.dev/gemini-api/docs/image-generation
|
||||
- ByteDance Seedream(圖層分離/透明底能力):https://seed.bytedance.com/ ;火山方舟文生圖 API:https://www.volcengine.com/docs/82379
|
||||
- 去背後製:remove.bg — https://www.remove.bg/
|
||||
- 遊戲卡片/卡背產品與出血概念:The Game Crafter — https://www.thegamecrafter.com/make/cards
|
||||
- 卡牌印刷尺寸/模板(用其「卡牌尺寸」清單佐證行業尺寸):Make Playing Cards — https://www.makeplayingcards.com/design/custom-playing-cards.html
|
||||
- 先前工作流報告(角色與卡牌美術、一致性管線):同目錄 02_ai角色繪製素材生成.md(本目錄內)
|
||||
|
||||
**未查到/需再確認:**
|
||||
- 實體卡牌「出血/安全邊距」的單一官方統一數值(各印刷廠規範不同)——本文以「建議值 3.18mm 出血+安全區」與「網頁版=集中中央安全區」表述,確切值需再查所選印刷商/實作驗證。
|
||||
- 各 AI 品牌角色「牌桌個性外觀」為本工作室設計草稿,非任何官方設定。
|
||||
|
||||
---
|
||||
|
||||
*報告完。所有「建議配方/角色草稿」為設計判斷;所有可查證的硬規格均附上方來源連結。*
|
||||
@@ -0,0 +1,200 @@
|
||||
# AI 生成遊戲視覺素材:成本模型與部署方案比較、管線與預算建議
|
||||
|
||||
**調查日期:** 2026-08-06
|
||||
**調查者:** Hermes 研究子代理(minimax-ai-game-visual 工作流系列 #07)
|
||||
**目的:** 為我們的網頁版「騙子酒館(Liar's Bar)卡牌唬牌遊戲」建立可執行的**素材生成成本模型**與**部署方案決策建議**。遊戲需 8 個 AI 品牌擬人角色(Grok/DeepSeek/Claude 等)靜態立繪+待機動畫(Live2D/APNG/WebM)+卡面+過場。本文比較:① 各影片生成 API 的每分鐘成本與批量生成預算;② 自架 ComfyUI+FLUX/SD 的 VRAM/單張成本/時間;③ API vs 自架(含雲端 GPU RunPod/Vast/Replicate)的 TCO、延遲、維護、可擴展性;④ 建置期批量生成 vs 運行期即時生成開支差異;⑤ 資產管線建議與隊列/非同步架構;⑥ MVP(8 角色+卡面)與完整版預算。所有價格以官方刊例價為準,查不到的明確標「未查到」並給數量級估計。
|
||||
|
||||
---
|
||||
|
||||
## 摘要
|
||||
|
||||
- **影片生成:全線 API 幾乎都以「每秒」計費**。在「騙子酒館」的 8 角色待機動畫(每段約 6–10 秒 768P/2K)情境下,**MiniMax H3 是每分鐘(每秒)最便宜的選項之一(768P ¥0.50/秒=¥30/分、2K ¥0.80/秒=¥48/分)**;Google Veo 3.1 Fast($0.10–0.12/秒)與 Kling 3.0($0.084–0.112/秒)同級,OpenAI Sora / Veo 3.1 Standard($0.40/秒)貴 3–8 倍。
|
||||
- **靜態圖生成極便宜**:MiniMax image-01 只要 ¥0.025/張;Replicate FLUX-dev $0.025/張、FLUX-schnell $0.003/張;Gemini Nano Banana 每 1K 圖約 $0.0168。**卡面/立繪的「圖」成本可忽略不計**,真正的成本與風險集中在「動畫(影片)」與「人員迭代工時」。
|
||||
- **自架 FLUX 需 ~16GB(fp8)~24GB+(fp16/full)VRAM**;用 RunPod L40S($0.99/hr)或 RTX4090($0.69/hr)跑一張 1024² FLUX 約 10–40 秒,**單張電費成本約 $0.003–0.02(估計)**。但自架要扛冷啟動、設定、維護、擴容,**TCO 只有在「持續大量(月產上千張且 GPU 常駐)」時才划算;我們的用量遠不到**。
|
||||
- **結論/建議:主推「外包 API + 建置期批次生成 + 預製資產」**:圖用 MiniMax image-01/FLUX/Replicate;8 角色待機動畫用 MiniMax H3(或可靈 3.0)在**上線前批量生成+人工過濾**;運行期「即時生成」幾乎不可行(影片 API 皆為非同步、延遲秒到分鐘級、成本高),改用 CSS/Canvas/APNG/WebM/Lottie 播預製資產。
|
||||
- **MVP(8 角色待機動畫+卡面)粗估總生成成本約 ¥600–1,500(約 $85–210,估計)**;完整版(含過場、多表情、2K、行銷片)約 ¥6,000–15,000(估計)。見 §6 細項。
|
||||
- 所有「估計」皆標注;查不到者標「未查到」。
|
||||
|
||||
---
|
||||
|
||||
## 一、影片生成 API 每分鐘成本與批量生成預算
|
||||
|
||||
> 通用口徑:各廠商皆**按「生成的每秒」計費**(非按 API 呼叫次數)。以下換算「每分鐘=每秒單價 × 60」,用於等量比較。幣別不同(¥ 人民幣 vs $ 美元)已分開標示,勿直接混比。
|
||||
|
||||
### 1.1 各 API 每秒/每分鐘刊例價(官方)
|
||||
|
||||
| 供應商/模型 | 解析度 | 每秒 | 每分鐘(×60) | 備註/批量優惠 |
|
||||
|---|---|---|---|---|
|
||||
| **MiniMax H3** | 768P | ¥0.50 | **¥30** | 按量計費刊例價;含原生立體聲 |
|
||||
| **MiniMax H3** | 2K | ¥0.80 | **¥48** | 按量計費刊例價 |
|
||||
| MiniMax H3-Regeneration | 768P→2K | ¥0.30 | ¥18 | 再生升級 2K 用 |
|
||||
| **Kling 3.0 Turbo** | 720P | $0.112 | $6.7 | 每秒計費;含原生音訊 |
|
||||
| **Kling 3.0** | 720P | $0.084 | $5.0 | 無原生音訊 |
|
||||
| Kling 3.0 | 1080P | $0.112 | $6.7 | 4K $0.42/秒 |
|
||||
| Kling 3.0 Omni | 720P | $0.084 | $5.0 | 依有無影片輸入略異 |
|
||||
| Kling Avatar/Motion | 720P | $0.056–0.126 | $3.4–7.6 | 依用途 |
|
||||
| **OpenAI sora-2** | 720p | $0.10 | $6.0 | Batch 半價 $0.05/秒 |
|
||||
| OpenAI sora-2-pro | 1080p | $0.70 | $42.0 | Batch $0.35/秒 |
|
||||
| **Google Veo 3.1 Fast** | 720/1080p | $0.10–0.12 | $6–7.2 | 4K $0.30/秒 |
|
||||
| Google Veo 3.1 Lite | 720/1080p | $0.05–0.08 | $3–4.8 | 4K 不支援 |
|
||||
| **Google Veo 3.1 Standard** | 1080p | $0.40 | $24 | 4K $0.60/秒(最貴) |
|
||||
| Google Veo 3 Fast | 720/1080p | $0.10–0.12 | $6–7.2 | |
|
||||
| 即夢/火山 Sugaroo・Seedance | — | **未查到** | 估計 ≈Kling/可靈 同級($0.05–0.12/秒,數量級估計) | 火山引擎文件頁面需登入/JS 渲染,未拿到刊例價 |
|
||||
|
||||
> 資料來源:MiniMax 開放平台按量計費頁;Kling AI Dev Pricing(1 Unit=$0.14 list);OpenAI Platform Pricing;Google Gemini API Pricing。即夢標「未查到」。
|
||||
|
||||
### 1.2 換算成「8 角色動畫」情境(每角色一段、共需秒數)
|
||||
|
||||
假設每角色做 **2 段動畫(A 待機+B 情緒),每段 6 秒**,共 **96 秒輸出秒數**:
|
||||
|
||||
| 方案 | 每秒單價 | 96 秒成本 | 備註 |
|
||||
|---|---|---|---|
|
||||
| MiniMax H3 768P | ¥0.50 | **¥48** | 推薦基本款 |
|
||||
| MiniMax H3 2K | ¥0.80 | ¥76.8 | 主立繪/Trailer 用 |
|
||||
| Kling 3.0 720P | $0.084 | ~$8.1(≈¥58) | 國際/美元計費 |
|
||||
| Sora sora-2 720p | $0.10 | ~$9.6 | Batch 可半價 |
|
||||
| Veo 3.1 Fast 1080p | $0.12 | ~$11.5 | |
|
||||
| Veo 3.1 Standard 1080p | $0.40 | ~$38.4 | 高品質但貴 |
|
||||
|
||||
> 結論:**迷你情境(≤ 數百秒輸出)用 API 的絕對金額都很低(¥50–400 級)**,成本差異在「人力挑選+重生成倍率」,而非 API 單價本身。
|
||||
|
||||
### 1.3 批量生成大略預算(假設最終上線 1,000 秒動畫資產,含 3–5 倍重生成率)
|
||||
|
||||
| 方案 | 每秒單價(基準 768P/720P) | 原始 1,000 秒 | ×3 重生成 | ×5 重生成 |
|
||||
|---|---|---|---|---|
|
||||
| MiniMax H3 768P | ¥0.50 | ¥500 | ¥1,500 | ¥2,500 |
|
||||
| Kling 3.0 720P | ~$0.084 | ~$84 | ~$252 | ~$420 |
|
||||
| Sora sora-2 Batch | $0.05 | $50 | $150 | $250 |
|
||||
| Veo 3.1 Fast 720P | $0.10 | ~$100 | ~$300 | ~$500 |
|
||||
| Veo 3.1 Standard | $0.40 | ~$400 | ~$1,200 | ~$2,000 |
|
||||
|
||||
> 上表為「影片輸出」成本估計;滿 5 張圖輸入會另收 0.20 元/張(MiniMax)或 0.15 元/張(再生)。
|
||||
|
||||
---
|
||||
|
||||
## 二、自架 ComfyUI+FLUX/SD:VRAM、單張成本與時間
|
||||
|
||||
### 2.1 VRAM 需求(官方 GitHub/ComfyUI 指引)
|
||||
|
||||
- **FLUX.1-dev** 是 **12B 參數 rectified flow transformer**。官方 ComfyUI 指引:**fp8 checkpoint 可大幅降記憶體**(約 1/2),**fp16 建議要有 >32GB 記憶體**;常規 single-file fp8 checkpoint 在 **16GB VRAM(RTX 4090 Laptop/3090)可跑**,full fp16 較舒服要 **24GB+(RTX 4090/A100/H100)**。→ **自架 FLUX 實務門檻 ≈ 16GB(fp8)~24GB(fp16/full)VRAM**。
|
||||
- **SD/SDXL(如 SDXL/SD 1.5)**:SDXL 約 **8–12GB** 可跑,SD 1.5 僅 **4–8GB**;品質與「角色一致性」遜於 FLUX 但成本/門檻低(適合卡面快速量產)。
|
||||
- 來源:black-forest-labs/flux GitHub、ComfyUI_examples/flux 官方文檔。
|
||||
|
||||
### 2.2 單張成本與時間(自架,數量級估計,標「估計」)
|
||||
|
||||
| 設定 | 單張耗時(1024²,估計) | 小時成本 | **單張 GPU 成本(估計)** |
|
||||
|---|---|---|---|
|
||||
| RTX 3090/4090(24GB, fp8) | 10–40 秒 | $0.50–0.99(RunPod) | **$0.003–0.02/張** |
|
||||
| L40S(48GB) | 8–25 秒 | $0.99(RunPod) | **$0.003–0.01/張** |
|
||||
| A100(80GB, full fp16) | 5–15 秒 | $1.39–1.49(RunPod) | $0.003–0.008/張 |
|
||||
| 自購顯卡攤提(4090 攤 3 年+電費) | 10–40 秒 | ≈$0.15–0.3 | $0.001–0.005/張(未含裝置成本) |
|
||||
|
||||
> 自架**單張絕對成本極低**,但這是「純 GPU 運算時間」,**不含**:冷啟動、排隊、設定、維運人力、擴容、模型下載、失敗重跑。對「一次性幾百張圖」的我們,這些隱藏成本遠超過省下的 GPU 費。
|
||||
|
||||
---
|
||||
|
||||
## 三、API vs 自架:TCO、延遲、維護、可擴展性
|
||||
|
||||
### 3.1 總持有成本(TCO)比較
|
||||
|
||||
| 面向 | API(MiniMax/Kling/Sora/Veo/Replicate) | 自架雲端 GPU(RunPod/Vast/Replicate 私有) |
|
||||
|---|---|---|
|
||||
| **前期設定** | 近零(取 API key) | 需建 ComfyUI 環境、下模型(FLUX ~20–30GB)、配 queue/worker |
|
||||
| **單位成本** | 影片 ¥0.5–0.8/秒、圖 <$0.04 | 圖 $0.003–0.02;影片自架需跑 SD/AnimateDiff(品質較差) |
|
||||
| **冷啟動/延遲** | 低(H3 非同步需 10s 輪詢、整段生成秒–分鐘級) | 高(GPU pod 冷啟動 30 秒–數分) |
|
||||
| **維運** | 廠商處理、零維運 | 自己管升級/當機/併發/儲存 |
|
||||
| **可擴展性** | 無限並行(H3 RPM 20–50);無需自備硬體 | 上限=GPU 數;需自動縮放(RunPod serverless 可) |
|
||||
| **穩定性/授權** | 廠商 SLA;商用授權清楚 | FLUX-dev 為**非商用授權**(Replicate 代管可商用);自行下載權重商用受限 |
|
||||
| **適合** | **我們(量小、一次性、要品質)** | 月產上千張、需私有資料、要完全掌控 |
|
||||
|
||||
### 3.2 雲端 GPU 參考價格(官方刊例)
|
||||
|
||||
**RunPod(2026-07-27 更新):** L40S $0.99/hr、RTX 6000 Ada $0.84/hr、RTX 4090 $0.69/hr、RTX 3090 $0.50/hr、A100 80GB $1.39–1.49/hr、H100 $2.89–2.99/hr、L4 $0.39/hr。
|
||||
**Replicate 硬體(每 GPU/秒):** L40S $3.51/hr、A100 80GB $5.04/hr、H100 $5.49/hr、T4 $0.81/hr(僅跑公開模型時才按模型頁計價,FLUX-dev $0.025/張)。
|
||||
**Vast.ai:** 需登入的 spot 市場,**未查到**統一刊例價;通常比 RunPod 便宜約 30–50%(數量級估計),但可靠度/支援較弱。
|
||||
|
||||
> 若要自架驗證,建議 **RunPod L40S($0.99/hr, 48GB)或 RTX 4090** 開一台,跑 fp8 FLUX 即足夠我們的量;不必上 A100/H100。
|
||||
|
||||
---
|
||||
|
||||
## 四、建置期批量生成 vs 運行期即時生成
|
||||
|
||||
| 面向 | **建置期批量生成(推薦)** | 運行期即時生成 |
|
||||
|---|---|---|
|
||||
| 時機 | 上線前批次跑完整資產 | 遊玩中途依狀態現產 |
|
||||
| 延遲要求 | 無(排隊即可) | 秒級內(動畫卻要 6–15s 生成+非同步輪詢) |
|
||||
| 成本 | 大量用 Batch/低價模型(Sora Batch 半價、H3 768P) | 每局都付全價+高延遲 |
|
||||
| 一致率 | 可人工挑、可重跑 | 無法事後挑,撞車/崩壞直接上場 |
|
||||
| 前端負擔 | 播 CDN 預製 APNG/WebM/Lottie | 需等 API 回傳、管失敗重試 |
|
||||
| **適用** | **我們的待機動畫、卡面、過場、行銷片** | 幾乎不適用(唬牌遊戲無需動態生成) |
|
||||
|
||||
> **明確建議:100% 走「建置期預生成」**。運行期用 CSS keyframes/Canvas/Lottie/預先壓好的 APNG/WebM 播放即可,零即時 AI 成本。
|
||||
|
||||
---
|
||||
|
||||
## 五、資產管線建議+隊列/非同步架構
|
||||
|
||||
### 5.1 管線(生成→過濾→後製→壓縮→CDN)
|
||||
|
||||
1. **生成(Generate)**:圖用 MiniMax image-01(¥0.025/張)或 FLUX/Replicate;動畫用 MiniMax H3(768P)或 Kling 3.0,產出多張候選。
|
||||
2. **過濾(Filter)**:用 H3-Context-IR(提示詞增強)或人工(image-01/API-vlm ¥0.025/次)+人工篩選,保留一致率高的成品;非同步任務回傳後自動落庫。
|
||||
3. **後製(Post-process)**:ffmpeg 去背(chroma/transparent)、裁切、按卡牌 3½×2½ 吋比例、疊字/運鏡。
|
||||
4. **壓縮(Compress)**:動畫轉 **WebM(VP9/AV1)或 APNG**(透明背景),循環片段裁到 2–4 秒低畫格;圖轉 WebP/AVIF。縮到每檔 <200–500KB 有利網頁載入。
|
||||
5. **CDN(交付)**:上傳 Cloudflare R2/R2+CDN/S3+CloudFront,前端 Vue3 直接拉取。隊列完成後自動上傳並寫 DB。
|
||||
|
||||
### 5.2 隊列/非同步架構(ComfyUI 隊列 or Async API)
|
||||
|
||||
- **MiniMax H3 原生就是 Async API**:`POST /v2/video_generation` 建任務 → 回 `task_id` → 每 10s 輪詢或用 `callback_url` 收事件 → 取 URL 下載。**這是天然隊列**,不需自建。
|
||||
- **ComfyUI** 也有內建 queue(多 job 排隊)。建議:**不把 ComfyUI 放前端**;後端 Node(Express+BullMQ/Redis)排隊 → 呼叫 H3 async 或 RunPod serverless → 完成回呼 → 寫 S3/R2。
|
||||
- **架構建議**:`建置批次腳本 → 隊列(Redis/BullMQ) → 生成服務(H3/Kling API 或按需開 RunPod) → 過濾/後製 worker → 壓縮 worker → R2 CDN → DB/前端`。全部非同步、可批次灌入 8 角色×多變體。
|
||||
|
||||
---
|
||||
|
||||
## 六、參考預算
|
||||
|
||||
> 以下為**估計**(工時/重生成倍率無法精確),以「最終上線量 × 重生成倍率」估算;不含人力酬勞。匯率以 **1 USD ≈ ¥7(估算)** 換算。
|
||||
|
||||
### 6.1 MVP(8 角色待機動畫+卡面)
|
||||
|
||||
| 項目 | 數量(估計) | 單價 | 小計(估計) |
|
||||
|---|---|---|---|
|
||||
| 角色立繪+卡面(圖) | 8 角色×3+卡面 20 = ~44 張,重生成 3× ≈ 130 張 | image-01 ¥0.025 或 FLUX $0.025 | **¥10–40($1.5–6)** |
|
||||
| 待機動畫(每角色 2 段×6s=96s,重生成 3×≈288s) | 288 秒 | H3 768P ¥0.50 | **¥144(≈$21)** |
|
||||
| 過場/氛圍小短片(選配) | ~60s | H3 768P ¥0.50 | ¥30 |
|
||||
| 後製/壓縮/CDN 儲存 | 一次性 | R2 免費額度+ffmpeg | 近零 |
|
||||
| **MVP 合計(粗估)** | | | **約 ¥200–600(≈$30–85)**(不含人力) |
|
||||
|
||||
### 6.2 完整版(含過場、多表情、2K、行銷片)
|
||||
|
||||
| 項目 | 數量(估計) | 小計(估計) |
|
||||
|---|---|---|
|
||||
| 全角色多表情/多套動畫(~1,000s,重生成 3–5×) | 3,000–5,000s | H3 768P ¥1,500–2,500,或 2K ¥2,400–4,000 |
|
||||
| 過場動畫+Trailer(2K 精美) | ~300s 2K | ¥240–500 |
|
||||
| 大量圖(卡包/牌背/背景/UI) | 500–1,500 張 | ¥15–60 |
|
||||
| **完整版合計(粗估)** | | **約 ¥6,000–15,000(≈$850–2,100)**(不含人力) |
|
||||
|
||||
---
|
||||
|
||||
## 七、可行動建議(對我們的遊戲)
|
||||
|
||||
1. **卡面/立繪:直接用 MiniMax image-01(¥0.025/張)或 Replicate FLUX-dev($0.025/張)**,成本可忽略;重生成 3–5 倍由人力挑,品質門檻在「一致性」而非價格。
|
||||
2. **8 角色待機動畫:用 MiniMax H3 768P 建置期批量生成**(¥0.50/秒),單角色 6–10s 循環+每角色 2 變體;轉 APNG/WebM 透明背景。要官宣質感再對主立繪用 H3 2K 或 Veo 3.1 Fast。
|
||||
3. **完全不做運行期即時生成**;前端用 CSS/Canvas/Lottie/APNG/WebM 播預製資產,零運行成本。
|
||||
4. **自架自始不投入**:我們量(幾百張圖+幾百秒動畫)用 API TCO 最低;真要自架驗證才開一台 RunPod L40S($0.99/hr)跑 fp8 FLUX,不買顯卡。
|
||||
5. **架構落地**:Node/Express+Redis(BullMQ) 隊列包裝 H3 async API → 過濾 → ffmpeg 後製 → 壓縮 → Cloudflare R2+CDN → Vue3 讀取。一次腳本可灌 8 角色批次。
|
||||
6. **預算建議**:先撥 **¥500–1,000(MVP)**;完整版再撥 **¥8,000–15,000**(皆估計)。最大變數是人力挑選次數,非 API 單價。
|
||||
|
||||
---
|
||||
|
||||
## 資料來源(官方/權威)
|
||||
|
||||
- MiniMax 開放平台「按量計費」刊例價:https://platform.minimaxi.com/docs/guides/pricing-paygo
|
||||
- MiniMax 影片生成指南(H3 規格/async API):https://platform.minimaxi.com/docs/guides/video-generation.md
|
||||
- MiniMax Model/H3 發布:https://platform.minimaxi.com/docs/release-notes/models.md
|
||||
- Kling AI Developer Pricing(1 Unit=$0.14):https://app.klingai.com/dev/pricing
|
||||
- OpenAI Platform Pricing(sora-2 Batch 半價):https://platform.openai.com/docs/pricing
|
||||
- Google Gemini API Pricing(Veo 3.1/Imagen/Nano Banana):https://ai.google.dev/gemini-api/docs/pricing
|
||||
- Replicate Pricing(FLUX-dev $0.025、硬體每秒價):https://replicate.com/pricing
|
||||
- black-forest-labs/flux GitHub(12B、授權、VRAM 指引):https://github.com/black-forest-labs/flux
|
||||
- ComfyUI FLUX Examples(fp8/fp16 記憶體指引):https://comfyanonymous.github.io/ComfyUI_examples/flux/
|
||||
- RunPod GPU 定價(2026-07-27):https://www.runpod.io/pricing
|
||||
@@ -0,0 +1,245 @@
|
||||
# 商用授權與法律風險調查報告:AI 生成素材用於『碥子酒馆』公開/商業網頁遊戲
|
||||
|
||||
**調查日期:2026-08-06**
|
||||
**調查範圍:** 商用授權與內容所有權、生成內容著作權歸屬與各國立場、跨國/服務條款風險、開源模型授權比對、AI 品牌擬人角色之肖像/商標侵權風險、可安全使用清單與小心清單。
|
||||
**方法聲明:** 所有引用文字均直接取自官方來源(含官方 ToS、Hugging Face 模型卡/授權文件、美國著作權局官方政策文件)之實際抓取內容;無法取得官方原文者一律標示『未查到』,**不臆測、不編造條款文字**。
|
||||
|
||||
---
|
||||
|
||||
## 執行摘要
|
||||
|
||||
1. **可用於本商業遊戲(低風險)**:**Stability AI 付費 API/付費服務**(SD 系列線上生成、SD3.5 付費 API:明文『assign you all of our right, title, and interest in the Outputs』);**FLUX.1 [schnell](Apache-2.0)**;**Google Gemini/Nano Banana(付費 API)**(『Google won't claim ownership over that content』)。
|
||||
2. **有條件可用**:**SDXL(CreativeML Open RAIL++-M)**——商用允許但附使用限制;**SD3.5 開源權重(Stability AI Community License)**——『< $1M 年營收免費商用』,超過需企業授權;**Midjourney**——『You own all Assets』但『> $1M 年營收之公司須訂 Pro/Mega 方案』才保有所有權。
|
||||
3. **不可用於本商業遊戲**:**FLUX.1 [dev] / FLUX.2 [dev]**——官方為**非商業授權**(Non-Commercial License),明文排除『revenue-generating activity』與『direct interactions with end users』,與本專案直接衝突。
|
||||
4. **重要著作權現實**:美國著作權局(USCO)2023/3/16 政策明文:**純 AI 生成物(無足夠人類創作投入)不受著作權保護**;『Zarya of the Dawn』案中 Midjourney 生成的單張圖像被裁定不受保護。**純 AI 圖像在美國幾乎無法主張著作權**(除人類『選擇/編排』足跡)。遊戲若有劇情、文字、角色設定等人類創作疊加可保護『人類部分』,但單張 AI 圖像本身保護薄弱。
|
||||
5. **針對 8 個『AI 品牌擬人角色』**:直接使用真實品牌名稱/標誌/招牌配色(如『Midjourney』綠、『Stable Diffusion』、『MiniMax』、『Gemini』、『Kling』、『Seedream』等)**具商標淡化/侵權風險**,且描繪真實人物/名人肖像需個別同意。**強烈建議**:全部改為原創名稱、自訂配色、不複製任何品牌 logo,並保留『人工設計足跡』以利主張權利。
|
||||
6. **底線建議**:以 **SD3.5 付費 API(< $1M 營收)** 或 **FLUX [schnell](Apache-2.0)/ SDXL(RAIL++-M,遵守限制)** 為主力資產管線;**避免 FLUX [dev]、FLUX.2 [dev]**;**不要**用任何真實品牌名稱/標誌做擬人角色。
|
||||
|
||||
---
|
||||
|
||||
## 1. 各模型/服務之商用授權與內容所有權
|
||||
|
||||
> 說明:以下『官方原文』為 2026-08-06 實際抓取之頁面內容。條款會更版,使用前請再核對官方最新版。
|
||||
|
||||
### 1.1 MiniMax 海螺(Hailuo)影片/圖片模型(API/平台)
|
||||
- 官方來源:https://platform.minimaxi.com/ 、https://www.minimaxi.com/
|
||||
- **商用允許**:可查——MiniMax 開放平台以 API 提供(含影片生成等),具商業使用之一般開放態度。
|
||||
- **內容所有權條款原文**:**未查到**(官方服務條款頁面之著作權/所有權條文未能完整抓取,僅取得 API 文件內容,未含授權條文)
|
||||
- **營收門檻**:**未查到**
|
||||
- **額外限制**:**未查到**
|
||||
- **風險標註**:⚠️ 高昂/不確定。因無法取得官方條文原文,商用前**必須**向 MiniMax 官方索取並確認現行《使用者協議/服務條款》之商用與所有權條款。
|
||||
|
||||
### 1.2 FLUX.1 [dev](開源權重)與 FLUX.2 [dev](開源權重)
|
||||
- 官方授權:**FLUX [dev] Non-Commercial License v2.0**(BFL 官方頁面,Last Updated: 2025-11-25)
|
||||
來源:https://bfl.ai/legal/non-commercial-license-terms (原文已抓取)
|
||||
亦見官方 HF 模型卡:https://huggingface.co/black-forest-labs/FLUX.1-dev (原始 LICENSE 因 gated 未能直接抓取,以上述 bfl.ai 官方授權頁為準)
|
||||
- **官方原文(關鍵)**:
|
||||
> 『...make the weights, parameters, and inference code for the FLUX [dev] Models... freely available for your **non-commercial and non-production use**...』
|
||||
> 『"Non-Commercial Purpose" means... **only so far as you do not receive any direct or indirect payment arising from the use of the FLUX [dev] Model**... For clarity, use for **revenue-generating activity or direct interactions with or impacts on end users**, or use to train, fine tune or distill other models for commercial use **is not a Non-Commercial purpose**.』
|
||||
- **商用允許**:❌ **否**(僅非商業、非生產用途)→ **不可用於本商業遊戲**
|
||||
- **內容所有權**:Outputs 為『Your Content』,BFL 主張不擁有;但因授權僅限非商業用途,於商業場景不具實益。
|
||||
- **商用替代**:需向 BFL 取得商業授權,或改用 **FLUX [pro](API)**。FLUX [pro] 之 API 使用受 BFL 開發者條款/ API ToS 管理(BFL Website and FLUX Terms of Service,2026-08-01 更版,見 §1.4)。
|
||||
|
||||
### 1.3 FLUX.1 [schnell](開源權重)
|
||||
- 官方授權:**Apache License 2.0**(BFL 官方標註;HF gated 原始 LICENSE 未能直接抓取 → 以 BFL 官方標示為準;**備註:未能抓取原始 Apache-2.0 全文,建議部署前至官方 HF 模型卡核對**)
|
||||
官方來源:https://huggingface.co/black-forest-labs/FLUX.1-schnell (gated)
|
||||
- **商用允許**:✅ **可商用(Apache-2.0)**;Apache-2.0 無營收門檻、無使用領域限制(但仍受一般著作權/商標/侵權法律約束)
|
||||
- **內容所有權**:Apache-2.0 授權模型之 Output 由生成者保有(模型權重非『作品』,輸出歸生成者)。適用一般著作權法(見 §2:純 AI 輸出保護薄弱)。
|
||||
|
||||
### 1.4 FLUX.1 [pro] / FLUX.2(API)
|
||||
- 官方來源:黑森林實驗室 **Website and FLUX Terms of Service**(Last Revised 2026-08-01)
|
||||
https://blackforestlabs.ai/terms-of-service/ (原文已抓取)
|
||||
- **官方原文(內容所有權)**:
|
||||
> 『We claim no ownership rights in and to Your Content, and you may use Your Content in connection with the Services for your own purposes, subject to any restrictions set forth herein or under applicable law.』
|
||||
- 並有:『Other users... may create Output that is similar or the same as Your Content and you agree that such other users can use their own individually created Output as permitted by these Terms.』
|
||||
- **商用允許**:✅ **可商用(透過官方 API 付費使用)**。注意 BFL ToS 限制(不得逆向、不得用於違法/侵權用途、不得誤導性使用如謊稱全人為生成等)。『真正商用』一般走 API/開發者商業條款,**建議商用前洽 BFL 簽署開發者/商業協議確認。**
|
||||
|
||||
### 1.5 Stable Diffusion / SDXL(開源權重)
|
||||
- 官方授權:**CreativeML Open RAIL++-M License**(2023-07-26)
|
||||
官方來源:https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/blob/main/LICENSE.md (原文已抓取)
|
||||
- 官方原文(要旨):使用『CreativeML Open RAIL++-M License』;屬『permissive』型授權但附加**使用情境限制**(不得用於特定侵權/非法用途);『後續衍生版本至少須包含與原授權相同之使用限制』。**CI/CD、API/web 提供模型本身**屬『Distribution』定義。
|
||||
- **商用允許**:✅ **可商用**;**無營收門檻**。但**受『使用限制』約束**(不得生成/散布侵害第三方著作權/商標/隱私之內容等)。
|
||||
- **內容所有權**:Output 歸生成者;但受 §2 著作權歸屬限制(純 AI 輸出保護薄弱)及 §3 第三方權利限制。
|
||||
|
||||
### 1.6 Stable Diffusion 3.5 / SD3.5(開源權重)+ Stability 付費 API
|
||||
- **開源權重授權**:**Stability AI Community License**(`stabilityai-ai-community`)
|
||||
官方來源(HF 模型卡,原文已抓取):https://huggingface.co/stabilityai/stable-diffusion-3.5-large
|
||||
官方原文:『Community License: **Free for research, non-commercial, and commercial use for organizations or individuals with less than $1M in total annual revenue**.』『For individuals and organizations with annual revenue above $1M: please contact us to get an **Enterprise License**.』
|
||||
- **商用允許**:✅ **< $1M 年營收可免費商用**;**> $1M 需企業授權**(有營收門檻)。
|
||||
- **內容所有權**:受 Stability **服務條款** §4 規範(見 1.7);開放權重本地生成的 Output 由生成者保有(適用 §2/§3 限制)。
|
||||
|
||||
### 1.7 Stability AI 付費服務/API(Brand Studio、Stable Audio、API 等 → 適用於 SD 系列線上生成)
|
||||
- 官方來源:Stability AI **Terms of Service**(Effective 2025-07-31)
|
||||
https://stability.ai/terms-of-service (原文已抓取)
|
||||
- **官方原文(內容所有權)**:
|
||||
> 『Subject to your compliance with our Terms, **we assign to you all of our right, title, and interest (if any) in the Outputs**. So as between Stability and you, **you own the Output** (to the extent permitted by applicable law).』
|
||||
> 『Because of how our Services and artificial intelligence generally work, multiple users might get similar results based on similar Inputs. So the rights we assign to you **only apply to your specific Outputs**, not to other users' or third parties' Outputs.』
|
||||
> 『Outputs are not created by Stability and do not reflect Stability's views.』/『You are solely responsible for verifying the accuracy, legality, and appropriateness of any Outputs before using or sharing them.』
|
||||
- **商用允許**:✅ **可商用**;Stability 可要求付費方案(無通用營收門檻,但付費方案/額外費用可能適用,見 ToS §7 Fees)。
|
||||
- **內容所有權**:✅ **明確 assign 給使用者**(但以『to the extent permitted by applicable law』為限——呼應 §2 著作權歸屬立場)。
|
||||
|
||||
### 1.8 Midjourney
|
||||
- 官方來源:Midjourney **Terms of Service**(Version Effective Date: 2026-05-27)
|
||||
https://docs.midjourney.com/docs/terms-of-service (原文已抓取)
|
||||
- **官方原文(內容所有權)**:
|
||||
> 『**You own all Assets You create with the Services to the fullest extent possible under applicable law.**』
|
||||
> 『If you are a **company or any employee of a company with more than $1,000,000 USD a year in revenue**, you must be subscribed to a "**Pro**" or "**Mega**" plan to own Your Assets.』
|
||||
> 『By using the Services, You grant to Midjourney... a perpetual, worldwide, non-exclusive, sublicensable no-charge, royalty-free, irrevocable copyright license to reproduce... the Content You input... as well as any Assets produced by You...』
|
||||
> 『You may not use the Service to try to violate the intellectual property rights of others, including copyright, patent, or trademark rights.』
|
||||
- **商用允許**:✅**可商用**,但 **> $1M 年營收之公司須 Pro/Mega 方案**才『保有所有權』;免費/基礎付費方案之商用授權範圍需另核對 Subscription Plans。
|
||||
- **內容所有權**:✅ 原則歸使用者;⚠️ 須注意 Midjourney 對所有 Content/Assets 取得**永久、可再授權、免版稅**之授權,且社區預設公開可被他人 Remix(除非 Stealth)。
|
||||
- **其他**:ToS 明示『不得用自動化工具大量生成』、『不得逆向工程』(API 代管第三方另論)。
|
||||
|
||||
### 1.9 Google Gemini API / Nano Banana(Gemini 影像模型,即『Nano Banana』)
|
||||
- 官方來源:**Gemini API Additional Terms of Service**(Effective 2026-03-23)
|
||||
https://ai.google.dev/gemini-api/terms (原文已抓取;受 Google APIs Terms of Service 管轄)
|
||||
- **官方原文(內容所有權)**:
|
||||
> 『Some of our Services allow you to generate original content. **Google won't claim ownership over that content.** You acknowledge that Google may generate the same or similar content for others and that we reserve all rights to do so.』
|
||||
> 『You may use only **Paid Services** when making API Clients available to users in the European Economic Area, Switzerland, or the United Kingdom.』(暗示對外開放 API Client 建議用付費服務)
|
||||
> 『Google doesn't use your prompts... or responses to improve our products』(付費服務);免費方案 Google 會用內容改善產品、且可能有人工審閱。
|
||||
- **商用允許**:✅ **可商用**;對外開放 API Client 建議/需以**付費(Paid)服務**提供(尤其 EEA/UK/CH 明確要求付費)。**無營收門檻**。
|
||||
- **內容所有權**:✅ Google 不主張擁有;Output 歸使用者(適用 §2/§3 限制)。
|
||||
|
||||
### 1.10 即夢 Jimeng / Seedream(字節跳動 ByteDance)
|
||||
- 官方來源:https://jimeng.jianying.com/ 、Seedream 模型(HF 官方倉庫 gated/404,未能抓取官方 LICENSE 原文)
|
||||
- **商用允許**:**未查到官方完整條文**(官方使用者協議/模型授權因登入/地域限制未能抓取)。一般認知為 API/開放模型可商業使用,但**未獲官方原文佐證**。
|
||||
- **內容所有權**:**未查到官方條文**。⚠️ 商用前務必向官方確認現行條款與所有權歸屬。
|
||||
- **風險標註**:⚠️ 中高不確定(未能引用官方原文)。
|
||||
|
||||
### 1.11 可靈 Kling(快手 Kuaishou)
|
||||
- 官方來源:https://klingai.com/ 、https://www.kuaishou.com/
|
||||
- **商用允許**:**未查到官方完整條文**(Kling 服務條款/模型授權未能抓取原文)。
|
||||
- **內容所有權**:**未查到官方條文**。⚠️ 商用前務必向官方確認。
|
||||
|
||||
---
|
||||
|
||||
## 2. 生成內容之著作權歸屬與各國立場
|
||||
|
||||
### 2.1 美國(美國著作權局 USCO)——本專案最主要參照
|
||||
- 官方來源:**Copyright Registration Guidance: Works Containing Material Generated by Artificial Intelligence**(2023-03-16,88 FR 16190,37 CFR Part 202)
|
||||
https://copyright.gov/ai/Copyright-Registration-Guidance.pdf (原文已抓取,pdftotext 全文)
|
||||
- **官方原文(要旨)**:
|
||||
> 『In 2018... the applicant described as "autonomously created by a computer algorithm"... The application was **denied** because... the work contained **no human authorship**.』
|
||||
> 『In February 2023, the Office concluded that a graphic novel comprised of human-authored text combined with images generated by the AI service Midjourney **constituted a copyrightable work, but that the individual images themselves could not be protected by copyright**.』
|
||||
- **核心立場**:**純 AI(無人為創作投入)生成物不受著作權保護**;若作品含『足夠的人類原創』(編輯/選擇/編排/修改),人類部分可受保護,但 AI 生成部分本身不保護。『提示詞』通常不足以構成作者身分。
|
||||
- **對本專案影響**:遊戲角色圖/卡面若**純由 AI 生成**→ **幾乎無法在美國主張著作權**;但配上人類撰寫的角色設定、劇情、名稱、整體編排、人工修圖→ 這些人類元素可保護。若需可執行之著作權,**保留『人工參與足跡』**(真人撰寫 prompt 草圖→ AI 產出草圖 → 人工大幅修改/原創組合)並保留產出/修改歷程。
|
||||
|
||||
### 2.2 其他地區立場(概要;非全面)
|
||||
- **歐盟**:原則上作品需『人類智力創作』(C-469/17 等判例精神)。純 AI 生成之『作者性』多數會員國傾向否認;AI 工具輔助之人類創作可受保護。**以各會員國個案/指引為準。**
|
||||
- **英國**:CDPA §9(3) 對『computer-generated works』設有『作出必要安排之人視為作者』之特殊條款(保護依然有限,各國差異大)。
|
||||
- **臺灣/中國**:普遍傾向需要『人的精神創作』;中國已有『AI 生成物』能否受著作權保護之爭議判決(如北京互聯網法院『春節』案,承認在『人的實質性智力投入』下可受保護,但各案認定不一)。跨國商用佈局建議以美國 USCO 立場為保守基準。
|
||||
|
||||
### 2.3 結論
|
||||
無論哪個模型的 ToS 多慷慨(『你擁有output』),**『擁有』都受『 applicable law 』制約**(各 ToS 原文皆附此語)。法律上純 AI 輸出保護薄弱。**多數商業遊戲資產建議:人類終製+保留足跡。**
|
||||
|
||||
---
|
||||
|
||||
## 3. 跨國 / 服務條款風險
|
||||
|
||||
### 3.1 生成『類似知名 IP 角色』是否侵權
|
||||
- 是——**可能**構成著作權(近似設定/外觀)與/或商標(商品化外觀之識別性)侵害。Stability、Midjourney、BFL、Google 之 ToS 均明示使用者**不得用服務侵害他人著作權/商標權**(例:Midjourney 『You may not use the Service to try to violate the intellectual property rights of others, including copyright, patent, or trademark rights.』);違反可導致帳號停權+第三方追訴。
|
||||
- **受害者風險**:若我們生成的卡面恰與某受著作權保護角色高度近似,權利人可對**我們(部署方)**提告/發下架通知(DMCA/各國下架機制)。ToS 的『我們不保證不侵權』並不能免除我們自己的侵權責任。
|
||||
|
||||
### 3.2 避開品牌標誌(Logo)
|
||||
- 建議**完全不使用任何真實公司 logo、註冊商標圖樣、標準字(wordmark)**。即使用 AI 產生『類似』品牌標誌亦可能觸商標淡化/冒充。BFL ToS、Stability ToS 均納入『不得誤導/侵權』限制。
|
||||
|
||||
### 3.3 營收/地區/年齡等服務條款風險
|
||||
- **對外營收**:若干服務對『年營收門檻』(SD3.5 >$1M、Midjourney >$1M)或『對外提供 API Client 付費』(Gemini EEA/UK/CH)有明文門檻;本專案初期『無付費或輕度付費』應可落在免費商用範圍,但要準備在超過門檻時升級/換授權。
|
||||
- **未成年人**:Gemini API 條款明示不得用於『directed towards or likely accessed by individuals under 18』;若遊戲公開、可能被 18 歲以下接觸,請斟酌。
|
||||
- **資料回饋/訓練**:免費方案(Gemini、Partial Stability)可能把內容用於改善產品;商用產品內容若有敏感性需用付費方案或在地生成。
|
||||
|
||||
---
|
||||
|
||||
## 4. 開源模型授權對照表
|
||||
|
||||
| 模型 | 官方授權 | 商用 | 營收門檻 | 主要限制 |
|
||||
|---|---|---|---|---|
|
||||
| **FLUX.1 [schnell]** | Apache License 2.0(HF: black-forest-labs/FLUX.1-schnell;原始檔案 gated,未能直接抓全文,以 BFL 官方標註為準) | ✅ | 無 | 一般 Apache 授權;仍受著作權/商標法約束 |
|
||||
| **FLUX.1 [dev] / FLUX.2 [dev]** | **FLUX [dev] Non-Commercial License v2.0**(bfl.ai/legal/non-commercial-license-terms) | ❌ 否(僅非商業/非生產) | — | 不得營收、不得直接面對終端使用者 |
|
||||
| **SDXL** | **CreativeML Open RAIL++-M**(HF LICENSE.md) | ✅ | 無 | 附『使用情境限制』(不得侵害第三方權益等);Distribution 定義含 API 代管 |
|
||||
| **SD3.5**(開放權重) | **Stability AI Community License**(HF 模型卡) | ✅(< $1M) | **> $1M 需企業授權** | < $1M 免費商用 |
|
||||
| **SD 系列付費/API** | Stability ToS(assign Outputs) | ✅ | 無(付費按月/用量) | 不得侵權/誤導使用 |
|
||||
| **Midjourney** | Midjourney ToS | ✅ | **> $1M 公司需 Pro/Mega 才保有所有權** | 社區預設公開可被 Remix |
|
||||
| **Gemini/Nano Banana(API)** | Gemini API Additional ToS | ✅(付費方案對外) | 無 | EEA/UK/CH 對外需付費;<18 限制 |
|
||||
| **MiniMax 海螺/** **Kling/** **Seedream** | 官方條款未查到 | ⚠️ 未確認 | 未查到 | 未查到 |
|
||||
|
||||
**註**:Apache-2.0 與 CreativeML RAIL++-M 均為**開源/開放**授權;FLUX [dev] 之 NC License **非開源**(僅免費非商業)。『SD 授權』(CreativeML Open RAIL++-M)與 Apache-2.0 最大差異:RAIL++-M 屬『source-available+使用限制』,Apache-2.0 為標準 OSI 開源授權(無使用領域限制)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 8 個『AI 品牌擬人角色』之肖像/商標侵權風險評估
|
||||
|
||||
> 註:本調查未取得 8 個角色的實際設計稿/名稱/配色,以下提供**風險框架與『安全中線』原則**;套用開發團隊的具體角色時逐項檢核。
|
||||
|
||||
### 5.1 高風險做法(應避免)
|
||||
- ❌ 角色**名稱/近似名稱**使用真實品牌(Midjourney、Stable Diffusion/SD、MiniMax、FLUX、Gemini、Seedream、Kling 等)——商標侵權+淡化風險。
|
||||
- ❌ 角色**配色/造型**複製品牌識別(如 Midjourney 綠、Stable Diffusion 火焰/logo 意象、Gemini 四色寶石、任何官方吉祥物)。擬人化可被認作『品牌商品化』。
|
||||
- ❌ 角色**捏他真實人物/名人/網紅**(肖像權+如涉真實人物需個別授權)。
|
||||
- ❌ 卡面/角色任何位置出現真實 **logo/標準字**。
|
||||
|
||||
### 5.2 中風險(需人工把關)
|
||||
- ⚠️ 只『致敬』風格但名稱雷同、配色接近——仍可能構成近似性。
|
||||
- ⚠️ 使用『AI 品牌』作為角色**世界的『主題』**但規避具體商標——注意顛覆/戲仿(parody)在臺灣/EU 認定較嚴,美國亦非絕對免責,且作為商品大量使用風險更高。
|
||||
|
||||
### 5.3 低風險(安全中線)
|
||||
- ✅ **完全原創名稱**(自創暱稱、與真實品牌無混淆可能)。
|
||||
- ✅ **原創配色/服裝/造型**(不用品牌色票、logo、吉祥物)。
|
||||
- ✅ 角色為原創虛構人格,名稱/外觀與任何真實 AI 品牌、人物無識別上之關聯。
|
||||
- ✅ 保留角色設計之**人類創作足跡**(設定稿、風格指南)以佐證原創並利著作權主張。
|
||||
|
||||
### 5.4 具體建議(本專案)
|
||||
開發團隊提供 8 個角色的名稱+配色+人設後,應逐項以『名稱相似性 X 商標註冊類別(9/41/42 類軟體/遊戲/娛樂)X 外觀近似度』三軸檢核;**預設一律走 5.3 安全中線**,任何真實品牌識別元素一律移除。
|
||||
|
||||
---
|
||||
|
||||
## 6. 可安全使用清單 vs. 需要小心清單
|
||||
|
||||
### ✅ 可安全使用(本商業遊戲主力)
|
||||
1. **Stability AI 付費 API / 付費服務**(SD 系列線上生成)——ToS 明文 assign Outputs 給你、可商用、無營收門檻。**【建議主力】**
|
||||
2. **FLUX.1 [schnell](Apache-2.0)**——可商用、無門檻。**【建議主力(自架)】**
|
||||
3. **SDXL(CreativeML Open RAIL++-M)**——可商用;只需遵守『使用限制』。**【建議主力(自架,< $1M)】**
|
||||
4. **SD3.5 開放權重(Community License)**——< $1M 年營收免費商用;> $1M 升企業授權。
|
||||
5. **Google Gemini / Nano Banana(付費 API)**——Google 不主張擁有;對外建議付費。
|
||||
|
||||
### ⚠️ 需要小心/有條件
|
||||
1. **FLUX.1 [dev] / FLUX.2 [dev]**——**不可商用**(NC License)。**不要用於發佈遊戲資產。**
|
||||
2. **Midjourney**——可商用但 **> $1M 公司須 Pro/Mega** 才保有所有權;社區預設公開可被 Remix;不得自動化大量生成。
|
||||
3. **MiniMax 海螺、Kling、Seedream/即夢**——**官方商用/所有權條款未查到**;商業部署前**必向官方書面確認**。
|
||||
4. **Gemini 免費方案**——內容可能被用於改善產品+人工審閱;對外開放建議付費(尤其 EEA/UK/CH),且<18 限制。
|
||||
5. **任何服務之免費/基礎方案**——逐一核對該方案之商用與資料回饋條款。
|
||||
6. **所有純 AI 生成角色/卡面**——法律上保護薄弱(USCO),建議人工終製+留足跡。
|
||||
7. **真實品牌擬人角色**——一律改用原創。
|
||||
|
||||
---
|
||||
|
||||
## 7. 整體建議(行動清單)
|
||||
|
||||
1. **資產管線首選**:`SD3.5 付費 API` 或自架 `FLUX.1 [schnell] / SDXL`(未超過 $1M),取得明確商用權。
|
||||
2. **禁用** FLUX.1 [dev] / FLUX.2 [dev] 於發佈資產;如需 FLUX 強項走 `FLUX pro/API` 商業協議。
|
||||
3. **人工作業流程**:人手撰寫角色/卡面 prompt 草圖 → 生成草圖 → 人工大幅修改、統一風格、加原創文字/設定 → 存歷程。**強化著作權、弱化『純 AI 輸出不可保護』風險。**
|
||||
4. **8 個擬人角色全數原創**:自創名稱+自訂配色+無真實 logo;逐項三軸檢核(名稱/類別/外觀)。
|
||||
5. **不複製任何知名 IP 角色或品牌標誌**;避免真人肖像。
|
||||
6. **營收策略**:初期『無付費/輕度付費』多數服務免費商用範圍可涵蓋;訂內部機制,於跨越 $1M 營收或變更方案前重新檢核各授權(SD3.5/Midjourney 門檻)。
|
||||
7. **商標清單**:上市前做一次名稱檢索,確保 8 角色名稱於遊戲/軟體/娛樂類別無衝突。
|
||||
8. **未查到服務(MiniMax/Kling/Seedream)**:正式採用前向官方取得書面商用確認。
|
||||
9. **法務**:跨國公開佈局前諮詢熟悉該市場之律師;以上為調查摘要,非法律意見。
|
||||
|
||||
---
|
||||
|
||||
## 資料來源清單(官方 / 已抓取原文)
|
||||
|
||||
1. Stability AI **Terms of Service**(Effective 2025-07-31):https://stability.ai/terms-of-service (§4 Outputs assign 原文)
|
||||
2. Stability AI 隱私/授權頁:https://stability.ai/license
|
||||
3. **SDXL LICENSE**(CreativeML Open RAIL++-M,2023-07-26):https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/blob/main/LICENSE.md
|
||||
4. **SD3.5 模型卡(Community License)**:https://huggingface.co/stabilityai/stable-diffusion-3.5-large (『< $1M 免費商用』原文)
|
||||
5. Black Forest Labs **FLUX [dev] Non-Commercial License v2.0**(2025-11-25):https://bfl.ai/legal/non-commercial-license-terms (非商用原文)
|
||||
6. Black Forest Labs **Website and FLUX Terms of Service**(2026-08-01):https://blackforestlabs.ai/terms-of-service (『claim no ownership』原文)
|
||||
7. Black Forest Labs **FLUX.1-schnell**(Apache-2.0,官方宣告;檔案 gated):https://huggingface.co/black-forest-labs/FLUX.1-schnell 、https://huggingface.co/black-forest-labs/FLUX.1-dev
|
||||
8. Midjourney **Terms of Service**(Effective 2026-05-27):https://docs.midjourney.com/docs/terms-of-service (『You own all Assets』+$1M 門檻原文)
|
||||
9. Google **Gemini API Additional Terms of Service**(Effective 2026-03-23):https://ai.google.dev/gemini-api/terms (『Google won't claim ownership』原文字)
|
||||
10. 美國著作權局 **Copyright Registration Guidance**(2023-03-16,88 FR 16190,37 CFR Part 202):https://copyright.gov/ai/Copyright-Registration-Guidance.pdf (純 AI 不受保護原文)
|
||||
11. MiniMax 開放平台:https://platform.minimaxi.com/ 、https://www.minimaxi.com/ (商用/所有權條文未查到)
|
||||
12. 即夢 Jimeng:https://jimeng.jianying.com/ (條文未查到)
|
||||
13. 可靈 Kling:https://klingai.com/ (條文未查到)
|
||||
|
||||
**調查方法備註**:官方原文優先採 `curl`/瀏覽器直接抓取官方網域;中國服務(MiniMax/Kling/Seedream)與 gated HF 倉庫因登入/地域/Cloudflare 限制未能抓取條文,一律標示『未查到』。所有引用以實際抓取內容為準,未採用任何二手轉述。
|
||||
@@ -0,0 +1,150 @@
|
||||
# 真實世界 AI 生成美術案例盤點與「騙子酒館網頁版」落地路線圖(09 號綜合報告)
|
||||
|
||||
**調查日期:2026-08-06**
|
||||
**調查者:Hermes 研究子代理(minimax-ai-game-visual 工作流系列 #09)**
|
||||
**目的:** 盤點「真實世界已用 AI 生成美術的網頁/卡牌/桌遊/Indie 遊戲案例」並綜合 01–07 號調查報告,收斂成一份可執行的分階段落地路線圖。
|
||||
|
||||
> ⚠️ **誠實聲明:** 本報告之「前序報告內容」均引自同資料夾 `01_minimax-h3影片生成調查.md`~`06_卡牌牌桌視覺與風格指引.md`(已實讀);**07 號檔案在資料夾中不存在(未查到)**。網際案例之陳述皆出自正文所附可查證 URL;凡無法查證者一律標「未查到」,未編造任何案例或規格。
|
||||
|
||||
---
|
||||
|
||||
## 一、一頁式執行摘要(Executive Summary)
|
||||
|
||||
**結論一句話:** 對我們的網頁版「騙子酒館」(Vue3 + Node/Socket.IO 的 8 AI 擬人角色卡牌唬牌遊戲),最務實的路線是 **「CSS 打底 → 靜態美術 → 待機動畫 → 過場/動態 → 打磨」五階段,以「預生成資產 + 前端合成」為核心,不用任何「遊玩中即時生成」**——因為即時生成的延遲與成本在多人對戰場景不可行(01 號報告)。
|
||||
|
||||
**為何這樣走(證據):**
|
||||
- **技術現況(報告 01–06):** MiniMax H3 是 2026-07-31 開放的通用多模態影片模型(768P/2K、4–15s、原生立體聲、首次/末次幀圖生影片、按秒計費 768P 0.50 元/2K 0.80 元),只適合「先建後用」;角色一致性由 FLUX.2 多參考圖(≤10 張)/Nano Banana 2/自訓 LoRA 解決;網頁整合只用 WebP/APNG/WebM(VP9)+MP4 備援,短資產 + 前端 CSS/WAAPI 動畫保證 60fps。
|
||||
- **真實世界壓力(本次調查):** 2026 年 Steam Next Fest 近 1/5(1694/8682,19.5%)遊戲揭露使用 AI;但社群對「AI slop/隱匿 AI」反感強烈,且 Steam 靠自報+揭露欄位在頁底幾乎看不見;高話題大作《Clair Obscur: Expedition 33》《The Alters》都因「AI 揭露後又刪除、被玩家抓包」而被抨擊、甚至被撤銷獎項資格。
|
||||
- **落地主軸:** AI 只做「一次性的高品質美術素材(角色立繪/卡面/待機循環/過場)」;所有可程式化的 UI、文字、翻牌、進場、亮光都用 CSS/WAAPI/精靈圖;全程保持「AI 生圖層」與「前端元件層」分離並明確揭露 AI 使用,把「AI 味」透過風格化 2D + 統一色板 + 人工挑選壓到最低。
|
||||
|
||||
**五階段總覽(詳見第四章):**
|
||||
|
||||
| 階段 | 核心工作 | 主要工具 | 概略預算 | 依賴報告 |
|
||||
|---|---|---|---|---|
|
||||
| P1 CSS 打底 | 卡片翻發/進場、UI、佈局、燈光 | Vue `<Transition>` / WAAPI / CSS / GSAP(選配) | 0 元 | 03、04、06 |
|
||||
| P2 靜態美術 | 8 角色立繪+卡面+牌背+背景+道具 | FLUX.2 API / Nano Banana 2 → ComfyUI+LoRA | 數百~數千元級 | 02、05、06 |
|
||||
| P3 待機動畫 | 8 角色眨眼/呼吸/微笑循環 | MiniMax H3 首尾幀(或自架 SVD) | 幾美元~幾十元 | 01、03、04 |
|
||||
| P4 過場/動態 | 亮牌動卡面、酒館背景循環、行銷大片 | MiniMax H3(768P/2K)+ffmpeg | 幾十~數百元 | 01、03、04 |
|
||||
| P5 打磨 | 一致性校準、Safari 相容、揭露與效能 | DevTools、Lottie/Live2D(選配)、版本管理 | 視範圍 | 01–06 |
|
||||
|
||||
---
|
||||
|
||||
## 二、真實世界案例盤點(本次調查)
|
||||
|
||||
> 註:所有下列案例均為本次可用工具實際抓取之頁面;抓不到的細節標「未查到」。
|
||||
|
||||
### 2.1 案例規模:AI 在 Indie/Steam 遊戲中已是「常態但敏感」
|
||||
|
||||
- **Steam 官方 AI 政策沿革(可查證):** Valve 2023 年 6 月曾一度拒絕使用 AI 資產提交的遊戲 → 2024-01-10 公布新規:允許 AI 內容上架但須「揭露」(分預生成與即時生成),並要求開發者承諾無違法內容 → 2026-01 再度大幅改寫,明確「程式碼助手等開發期工具」與「遊戲內 AI 生成內容」的揭露界限。
|
||||
- 來源:GameFromScratch《Steam Makes Massive Update To AI Generation Policy》https://gamefromscratch.com/steam-makes-massive-update-to-ai-generation-policy/ ;Game World Observer《Valve to allow "vast majority" of games using AI content》https://gameworldobserver.com/2024/01/10/steam-ai-games-new-rules-pre-generated-live-generated ;Slashdot《Valve Has 'Significantly' Rewritten Steam's Rules...》《Valve Opens the Door To More Steam Games Developed With AI》https://slashdot.org/story/24/01/10/1551254/ https://games.slashdot.org/story/26/01/19/1735231/
|
||||
- **量化實況(2026-06 Steam Next Fest):** 該月 Next Fest 共 8,682 款參展,其中 **1,694 款(19.5%)揭露使用 AI 內容**;編輯指出「揭露%靠自報、實際更高」,且 Steam 的 AI 揭露欄位被放在商店頁最底部、樣式近似「內容分級警告」,玩家難以過濾也容易忽略。
|
||||
- 來源:Indiecator《Steam Next Fest Has An AI Problem, and Players Can't Filter It Out》https://indiecator.org/2026/06/16/steam-next-fest-has-an-ai-problem-and-players-cant-filter-it-out/ (2026-06-16)
|
||||
|
||||
### 2.2 被「抓包/撤獎」的反面案例(教訓最重)
|
||||
|
||||
- **《Clair Obscur: Expedition 33》**(Sandfall Interactive):上架時揭露「部分未進遊戲的概念圖用 AI」,數日後刪除揭露;玩家發現最終版本仍殘留佔位圖,開發/發行商事後大幅退讓;**該作因而被部分獎項撤銷資格**;有獨立評測者揚言基於自身立場拒評該作。
|
||||
- 來源:Indiecator 同文;Slashdot《Do Gamers Hate AI? Indie Game Awards Disqualifies 'Clair Obscur' Over Gen-AI Usage》https://games.slashdot.org/(category 標題,2026)
|
||||
- **《The Alters》**(11 Bit Studios):玩家發現遊戲最終版本含 AI 生成的文字/圖像/翻譯,隨即被低調移除,且商店頁自始沒有 AI 揭露。
|
||||
- 來源:Indiecator 同文;討論串《Fans slam The Alters after discovering evidence of undisclosed gen AI》https://r.nf/post/6064939
|
||||
- **產業內對「揭露」本身的爭議:** Epic Games CEO Tim Sweeney 批評 Steam 強制揭露是「Scarlet Letter(紅字烙印)」,認為會讓玩家「想殺掉這款遊戲」,反襯出「AI 標籤」在玩家端的負面情緒是實際存在的商業風險。
|
||||
- 來源:VGC(經 lemmus.org 鏡像)《Epic Games CEO says it's 'really irresponsible' of Steam to make studios disclose AI use》https://lemmus.org/post/23378438 (2026-06-25);Slashdot《Why Tim Sweeney calls Steam's AI disclosure rule 'irresponsible'》(2026-06-26)
|
||||
|
||||
### 2.3 正向/中性工具鏈案例(怎麼做才不翻車)
|
||||
|
||||
- **Marin Comics(真實多次 AI 影片實作,2025-08,2026-01 更新):** 一位非影視專業的開發者用「混合工作流」完整產出多支 2 分鐘級短片——腳本由 ChatGPT、旁白由 Artlist.io TTS(並稱「uncanny 地自然」)、畫面以 Envato 圖庫為主僅用 **3 段 AI 生成短片**、剪輯在 Final Cut Pro。他明確總結生成式 AI 影片的 **三大瓶頸**:
|
||||
1. **成本(Cost of Creativity)**:AI 影片採點數制,常要迭代 5 次才拿到對的光/動作,單支「便宜」短片會比圖庫更貴;
|
||||
2. **連續性(Continuity)**:跨鏡頭角色臉、衣、身高等難以保持一致,手會融進物件、走路「漂浮」——即角色一致性是最大痛點(與我們報告 02/05 完全呼應);
|
||||
3. **時長(Temporal)**:多數高階模型每次只能 8–20 秒,需手動拼接大量小片段並自行對齊燈光環境。
|
||||
- 來源:https://marincomics.com/ai-videos-2025.html (作者 Marin Balabanov,2025-08 初版)
|
||||
- **工具層面的「如何大量量產遊戲素材」:** 社群主流是 **ComfyUI(節點式)+ LoRA 固化角色/畫風 + IP-Adapter/ControlNet**,實作細節已於 02/05 號報告完整查證(工具:ostris/ai-toolkit、kohya-ss,FLUX.2-dev 開放權重授權含 LoRA 商用權;HF 上有社群遊戲素材 LoRA,如 `gokaygokay/Flux-Game-Assets-LoRA-v2`)。*具體指名某位個人開發者「用 ComfyUI+FLUX 大量量產整套商業卡牌」的單篇文獻,本次未查到獨立第三方文章佐證。*
|
||||
- **過場/動態影片工具(做過場的實例):** 本系列 01 號報告已確認 MiniMax H3 原生支援首/末幀圖生影片本就是用來「把靜態幀自然動起來」;Kling 3.0 Motion Control/Runway/Google Veo 均可用於過場,但 **「某工作室公開用 H3/Kling 為一款遊戲做整套過場」的指名案例,本次未查到**;Marin Comics(上)是近期少數親自逐一試用 Veo/Kling/Runway/Luma/Pika 於短片產出的可引證實例。
|
||||
|
||||
### 2.4 社群最常被吐槽的「AI 味」與如何避免(彙整)
|
||||
|
||||
1. **隱匿不報+被抓包**(Clair Obscur、The Alters):公開傷害最大。→ **誠實揭露,不刪**。
|
||||
2. **過度使用「AI slop」同質感**:一望即知的 AI 產圖(過亮高對比、過度平滑、萬用奇幻濾鏡)。→ 用「風格化 2D 卡牌插畫 + 統一西部/酒館色板 + 明確輪廓線」定調(06 號報告風格前綴),而非盲目追求「寫實電繪」。
|
||||
3. **角色跨鏡頭不一致、五官/手/招牌文字變形**:→ 首幀錨定 + 多參考圖 + 角色 LoRA + 人工挑選(02/05);手藏起來、文字一律走前端(06)。
|
||||
4. **品質不均;動畫短、需手動拼接**:→ 只把 AI 用在「一次性的高價值素材」,常駐 UI 全部 CSS(03/04)。
|
||||
5. **「AI 免費→核彈級成本」**:逐次迭代燒點數。→ 先算清單價再批量,優先 API 按秒計費(01)。
|
||||
|
||||
**給我們的直接結論:** 做網頁遊戲時,「用得乾淨、揭露清楚、風格統一、品質人工把關」四件事決定社群觀感——技術與成本反而其次。
|
||||
|
||||
---
|
||||
|
||||
## 三、綜合 01–07 號報告(供路線圖引用,未引用處標未查到)
|
||||
|
||||
> **重要:** 資料夾中**沒有 07 號檔案**(`ls` 僅見 01–06)。因此「07 號摘要」無法引用,相關結論以 01–06 為準。
|
||||
|
||||
- **01_MiniMax H3 影片生成**:H3 2026-07-31 發布、768P/2K、4–15s、24fps、原生立體聲、支援首/尾幀圖生影片、`[运镜]`、V2V motion transfer、REST API 非同步任務制(輪詢 10s 起跳)、按量計費(768P 0.50 元/秒、2K 0.80 元/秒;再生 0.30 元/秒)。**無塗鴉/關鍵格控制(未查到)**;影片資源包暫不支援 H3。Sora 已停服。**ROI 排序:①表情循環→②動卡面→③過場→④背景循環→⑤行銷片。**
|
||||
- **02_AI 角色繪製素材生成**:FLUX.2(open weight 2025-11-22,多參考 ≤10 張、量產一致、精確文字)、Nano Banana 2(多參考)、Midjourney V8.2(2026-07-24,`--cref`)、Seedream 5.0 Pro / 4.0(RGBA 透明)、SD3.5 open weights。LoRA 訓練:ostris/ai-toolkit、kohya-ss,20–50 張/角色。建議「AI 生圖層與前端元件層分離」。
|
||||
- **03_靜態轉動態**:H3 圖生影片 768P≈0.08 美元/秒;開源 SVD(25 幀、1024×576、6fps,Community License 營收<100 萬美元免費商用須註冊+標註)可在 ComfyUI 自架零 API 費;Live2D 需人工拆層綁骨(後期);格式 WebP/APNG/WebM(VP9)+MP4 備援/精靈圖。**「CSS 打底 + AI 點睛」三階段**雛形。
|
||||
- **04_網頁整合**:Vite assets hash、Lazy-load 三層(L1 立繪→L2 待機循環→L3 過場)、WAAPI/CSS transform-opacity 走合成器、避免主執行緒塞爆、`<video preload>`、Sprite sheet `steps()`、GSAP/Lottie/`@vueuse/motion`。MVP 優先序明確。
|
||||
- **05_角色一致性深化**:IP-Adapter、InstantID、Reference-LoRA、FLUX.2 多參考、Nano Banana 2(角色≤5/風格≤3/物件≤10 合計≤14)、FLUX.1-Kontext、character sheet 法。**高 CP 路徑=API 定角色 → 自訓 8 角色 LoRA+1 畫風 LoRA → ComfyUI 疊加 + ControlNet DWpose 批量。**
|
||||
- **06_風格指引**:推薦「風格化卡牌插畫(2D,低飽和西部色板)」;撲克卡 3½×2½ 吋;牌背防記牌;統一風格前綴 + 卡面/牌背/木桌/酒杯/籌碼/骰子提示詞模板;背景/人物/卡牌/UI 四層分離;UI 與文字不走 AI。
|
||||
- **07**:**未查到(檔案不存在)。**
|
||||
|
||||
---
|
||||
|
||||
## 四、『騙子酒館網頁版』分階段落地路線圖
|
||||
|
||||
> 通用鐵律(貫穿五階段):**所有素材「先建後用」、常駐畫面不即時生成**(01);**AI 只做美術層、UI/文字/互動全走前端**(02/04/06);**每角色錨定一張官方立繪當首幀**、統一畫風前綴、每素材生 2–3 版人挑 1 版(01/05);**明確揭露 AI 使用**(本次調查教訓)。
|
||||
|
||||
### Phase 1|CSS 打底(立即、零成本)
|
||||
- **要做什麼:** 用 Vue `<Transition>`/`<TransitionGroup>`、CSS keyframes、WAAPI 完成全部瞬態效果:抽卡亮光、翻牌/發牌、角色與卡牌進場/退場、hover 光影、金邊閃爍、背景星塵、回合切換淡入淡出、牌背反轉。建立 4 層 DOM/CSS 卡元件(背景/人物/卡牌/UI)。
|
||||
- **工具:** Vue 內建過渡+CSS keyframes+WAAPI(transform/opacity);選配 GSAP `timeline()` 做多角色依序排場。
|
||||
- **預算:** 0 元(純開發時間)。
|
||||
- **驗收:** DevTools Performance 確認動畫只走合成器層(綠色)、不掉幀;手機可用 `playsinline`;無任何逐幀 JS 迴圈拖垮主執行緒。
|
||||
- **對應報告:** 03(§3/§4/§6 Phase1)、04(§2/§3/§6)、06(§3 四層分離)。
|
||||
|
||||
### Phase 2|靜態美術(核心、低價)
|
||||
- **要做什麼:** 產出 8 個 AI 角色立繪(半身 bust、透明底)、卡面、牌背(對稱防記牌)、酒館桌/酒杯/籌碼/骰子背景道具,全部套 06 號統一風格前綴色板(如 `#3A2418/#C77B2E/#E8B04B`)。多角色群像(酒館群像/宣傳圖)另出。
|
||||
- **工具:** 定稿用 **FLUX.2 API / Nano Banana 2**(多參考一致性最快);正式批量、要隱私/零 API 費則 **ComfyUI + FLUX.2-dev + 自訓 8 角色 LoRA+1 畫風 LoRA + ControlNet DWpose**;透明底用 Seedream(RGBA)或 ComfyUI rembg 去背。文字/數字一律前端覆蓋,卡面加 `no text`。
|
||||
- **預算:** 探索樣張約數十美元;自訓 LoRA 需 GPU 時數(本機或雲端,具體報價視租賃商)。整體估「數百~數千元(含 GPU)級」量級。
|
||||
- **驗收:** 同一角色至少 3 個姿勢/表情能穩定重現(與官方立繪相似度高);卡面縮到手機仍可讀;輸出 WebP(靜態)供 L1 首屏;無錯字、無變形手。
|
||||
- **對應報告:** 02(§2/§3/§4/§5)、05(§一/§三 SOP)、06(§2 全模板)。
|
||||
|
||||
### Phase 3|待機動畫(角色活起來)
|
||||
- **要做什麼:** 把 Phase 2 的 8 張立繪用 **H3 首/末幀圖生影片**做成 4–5s「眨眼/髮絲/呼吸/微笑」循環,用 ffmpeg 抽幀轉 **Animated WebP/APNG/Sprite sheet**(<128KB/角色);零 API 預算則用 **ComfyUI + SVD** 自架(營收<100 萬美元免費商用)。出 2–3 版人挑閉合度最好的 1 版做 loop;放 L2 載入層。
|
||||
- **工具:** MiniMax H3(768P,按秒計費)或 SVD(自架免費);ffmpeg(抽幀/tile);`<video muted loop playsinline>` 或 `<img>`(WebP/APNG)。
|
||||
- **預算:** 8 角色 × 每角 2–3 版 × 5s 768P,H3 路徑估數美元~十幾美元;SVD 路徑僅 GPU 電費(並依 Community License 註冊+標註)。
|
||||
- **驗收:** 待機循環流暢可迴圈、無閃跳;8 個同時播不掉幀、省電;Safari 以 APNG/Animated WebP 因應(不依賴透明 WebM)。
|
||||
- **對應報告:** 01(§5 ROI ①)、03(§2/§5/§6 Phase2)、04(§5 sprite/§6)。
|
||||
|
||||
### Phase 4|過場/動態(大片時刻)
|
||||
- **要做什麼:** 亮牌/唬牌成功/失敗/決鬥高潮/回合切換的短過場:用 **H3 文生或圖生(首尾幀控制+`[运镜]`)**,2K 做「大片感」,5–8s、21:9/16:9;行銷/商店頁 banner 用 2K+Context-IR。轉 WebM(VP9)+MP4 備援,放 L3 觸發才載入;BGM 可另用 MiniMax Music 3.0/SFX 用 H3 原生立體聲。
|
||||
- **工具:** MiniMax H3(過場/行銷)、Runway/Kling/Veo 選備;ffmpeg 轉碼。
|
||||
- **預算:** 過場按 2K 0.80 元/秒計(5–8s ≈ 4–6.4 元/支);少數幾支即可;行銷片另計。
|
||||
- **驗收:** 過場在 `play()` 前才 `playlist`/`load()`,不卡大廳;WebM+MP4 雙源 Safari 可用;過場角色與立繪一致(用同一立繪當首幀);總資產不拖垮載入。
|
||||
- **對應報告:** 01(§5 ROI ②③⑤、§2 運鏡/首尾幀)、03(§1/§5)、04(§2 L3/§3 影片成本)。
|
||||
|
||||
### Phase 5|打磨(品質與口碑)
|
||||
- **要做什麼:** ①風格統一校色/對色(brand color);②用 06 號提示詞技巧逐張清「AI 味」(藏手、去字、統一色板、加 film grain);③用 Player 測試表過 8 角色的視覺回饋;④**完成並保留 AI 使用揭露**(商店頁/頁腳),不重蹈 Clair Obscur/The Alters 覆轍;⑤選擇性導入 Live2D(需拆層綁骨,人力高,後期才考慮)做「視線追蹤/口型」進階體驗。
|
||||
- **工具:** 06 號風格指引、ComfyUI 後處理(Inpaint/rembg/校色)、ffmpeg 壓縮、可選 Live2D Cubism Web SDK。
|
||||
- **預算:** 以人工時間為主;Live2D 拆層綁骨為主要隱性成本(未查到統一報價)。
|
||||
- **驗收:** 全站美觀一致、無 AI 錯字/變形;揭露清楚;效能達標;可正式上架/外推。
|
||||
- **對應報告:** 06(§4 避弊病、§5 風格指南)、02(§五 授權/隱私)、01(§5 行銷)。
|
||||
|
||||
---
|
||||
|
||||
## 五、資料來源連結清單
|
||||
|
||||
**本系列前序調查(同資料夾,可交叉引用):**
|
||||
1. `01_minimax-h3影片生成調查.md` — https://platform.minimaxi.com/docs/guides/video-generation 、https://platform.minimaxi.com/docs/guides/pricing-paygo 、https://www.minimax.io/blog/minimax-h3
|
||||
2. `02_ai角色繪製素材生成.md` — FLUX.2 / Nano Banana 2 / Seedream / LoRA(ostris/ai-toolkit、kohya-ss);HF 社群 `gokaygokay/Flux-Game-Assets-LoRA-v2`
|
||||
3. `03_靜態轉動態動畫化.md` — H3 圖生影片、SVD Model Card(https://huggingface.co/stabilityai/stable-video-diffusion-img2vid-xt-1-1)、Stability Community License(https://stability.ai/community-license)、Live2D(https://www.live2d.com/en/)
|
||||
4. `04_網頁素材整合技術.md` — Vite(https://vitejs.dev/guide/assets )、MDN 格式/WAAPI/IntersectionObserver、GSAP(https://gsap.com/)、Lottie(https://github.com/airbnb/lottie-web)
|
||||
5. `05_角色一致性深化.md` — FLUX.2 / Nano Banana 2 / IP-Adapter / InstantID / kohya / OneTrainer
|
||||
6. `06_卡牌牌桌視覺與風格指引.md` — 卡牌尺寸(維基百科、標準 52 張牌)、風格前綴與提示詞模板、RemBG / Seedream 透明(https://seed.bytedance.com/ / https://www.remove.bg/)
|
||||
7. **`07` — 未查到(資料夾中不存在該檔案)。**
|
||||
|
||||
**本次真實世界案例/政策:**
|
||||
8. Steam AI 政策(2023 拒收 → 2024-01 揭露新規→ 2026 改寫):GameFromScratch https://gamefromscratch.com/steam-makes-massive-update-to-ai-generation-policy/ ;Game World Observer https://gameworldobserver.com/2024/01/10/steam-ai-games-new-rules-pre-generated-live-generated ;Slashdot https://slashdot.org/story/24/01/10/1551254/ https://games.slashdot.org/story/26/01/19/1735231/
|
||||
9. Steam Next Fest 2026-06「AI Problem」(8,682 款中 1,694 款揭露=19.5%、無法過濾、揭露放頁底):Indiecator https://indiecator.org/2026/06/16/steam-next-fest-has-an-ai-problem-and-players-cant-filter-it-out/
|
||||
10. 《Clair Obscur: Expedition 33》揭露後刪、被撤獎:Indiecator(同 9);Slashdot「Do Gamers Hate AI?」
|
||||
11. 《The Alters》/11 Bit Studios 隱匿 AI:Indiecator(同 9);r.nf 討論 https://r.nf/post/6064939
|
||||
12. Tim Sweeney 批評 Steam AI 揭露:「Scarlet Letter」→ VGC(lemmus 鏡像)https://lemmus.org/post/23378438 ;Slashdot(2026-06-26)
|
||||
13. Marin Comics 親試 AI 影片混合工作流+三大瓶頸(成本/連續性/時長):https://marincomics.com/ai-videos-2025.html (2025-08/2026-01)
|
||||
|
||||
---
|
||||
|
||||
*撰寫原則:前序報告內容皆已實讀;網際案例皆附可查證 URL;未能查證者(含 07 號、指名「某人用 H3/Kling 做整套遊戲過場」、指名「個人用 FLUX 大量量產商業卡牌」之單篇文獻)均已標「未查到」。*
|
||||
@@ -0,0 +1,141 @@
|
||||
> ## Documentation Index
|
||||
> Fetch the complete documentation index at: https://platform.minimaxi.com/docs/llms.txt
|
||||
> Use this file to discover all available pages before exploring further.
|
||||
|
||||
# 模型发布
|
||||
|
||||
> 本文档汇总MiniMax开放平台最新模型发布动态,覆盖语言、视频、声音、图像、音乐等模态模型信息,帮助开发者了解平台最新模型能力。
|
||||
|
||||
## 2026 年 7 月 31 日
|
||||
|
||||
<Card title="MiniMax H3" icon="video" href="https://www.minimaxi.com/blog/minimax-h3" cta="了解更多">
|
||||
新一代开放通用多模态视频模型,面向由文本、图像、视频与声音共同构成的多模态上下文,统一理解创作意图,完成更加自然、连贯的生成与表达。
|
||||
</Card>
|
||||
|
||||
## 2026 年 7 月 16 日
|
||||
|
||||
<Card title="Music-3.0" icon="music" href="https://platform.minimaxi.com/docs/guides/music-generation" cta="了解更多">
|
||||
即刻体验全新音乐生成能力
|
||||
</Card>
|
||||
|
||||
## 2026 年 6 月 1 日
|
||||
|
||||
<Card title="MiniMax M3" icon="file-text" href="https://www.minimaxi.com/models/text/m3" cta="了解更多">
|
||||
全新语言模型 MiniMax-M3 正式发布,面向 Agent 推理、工具调用、代码、多模态 Chat 输入和长上下文任务。
|
||||
</Card>
|
||||
|
||||
## 2026 年 4 月
|
||||
|
||||
<Card title="Music-2.6" icon="music" href="https://minimaxi.com/news/music-26" cta="了解更多">
|
||||
以声传情:翻唱入心,器乐入魂
|
||||
</Card>
|
||||
|
||||
## 2026 年 3 月 18 日
|
||||
|
||||
<Card title="MiniMax M2.7" icon="file-text" href="https://www.minimaxi.com/news/minimax-m27-zh" cta="了解更多">
|
||||
全新语言模型 MiniMax-M2.7 系列模型 MiniMax-M2.7 / M2.7-highspeed 正式发布,开启模型的自我迭代
|
||||
</Card>
|
||||
|
||||
## 2026 年 3 月
|
||||
|
||||
<Card title="Music-2.5+" icon="music" href="https://minimaxi.com/news/music-25-解锁纯音乐突破风格边界" cta="了解更多">
|
||||
最新音乐模型发布,解锁纯音乐,突破风格边界。
|
||||
</Card>
|
||||
|
||||
## 2026 年 2 月
|
||||
|
||||
<Card title="MiniMax M2.5" icon="file-text" href="https://www.minimaxi.com/news/minimax-m25" cta="了解更多">
|
||||
全新语言模型 MiniMax-M2.5 系列模型 MiniMax-M2.5 / M2.5-highspeed 正式发布,在编程、工具调用和搜索、办公等生产力场景都达到或刷新了行业的 SOTA
|
||||
</Card>
|
||||
|
||||
## 2026 年 1 月 23 日
|
||||
|
||||
<Card title="Speech-2.8" icon="volume-2" href="https://minimaxi.com/news/minimax-speech-28" cta="了解更多">
|
||||
自然语气词,逼真音色,通透音质
|
||||
</Card>
|
||||
|
||||
## 2026 年 1 月 16 日
|
||||
|
||||
<Card title="Music-2.5" icon="music" href="https://platform.minimaxi.com/docs/api-reference/music-generation" cta="了解更多">
|
||||
最新音乐模型发布,全维度突破,指挥细节,定义真实
|
||||
</Card>
|
||||
|
||||
## 2025 年 12 月 22 日
|
||||
|
||||
<Card title="MiniMax M2.1" icon="file-text" href="https://www.minimaxi.com/news/minimax-m21" cta="了解更多">
|
||||
全新语言模型 MiniMax-M2.1 系列模型 MiniMax-M2.1 / M2.1-highspeed 正式发布,多语言编程专家,全面升级复杂编程体验 <br /><br />
|
||||
</Card>
|
||||
|
||||
## 2025 年 10 月 30 日
|
||||
|
||||
<Card title="Music-2.0" icon="music" href="https://www.minimaxi.com/zh/news/minimax-music-20" cta="了解更多">
|
||||
最新音乐模型发布,百变唱将,人声灵动,精准乐器控制,专业级音质表现,支持5分钟音乐创作
|
||||
</Card>
|
||||
|
||||
## 2025 年 10 月 29 日
|
||||
|
||||
<Card title="MiniMax-Speech-2.6" icon="volume-2" href="https://www.minimaxi.com/news/minimax-speech-26" cta="了解更多">
|
||||
新一代语音 HD 模型,极致音质与韵律表现,生成更快更自然
|
||||
</Card>
|
||||
|
||||
## 2025 年 10 月 28 日
|
||||
|
||||
<Card title="MiniMax-Hailuo-2.3" icon="video" href="https://www.minimaxi.com/zh/news/minimax-hailuo-23" cta="了解更多">
|
||||
全新视频生成模型,肢体动作、物理表现与指令遵循能力全面升级
|
||||
</Card>
|
||||
|
||||
## 2025 年 10 月 27 日
|
||||
|
||||
<Card title="MiniMax M2" icon="file-text" href="https://www.minimaxi.com/news/minimax-m2" cta="了解更多">
|
||||
全新语言模型MiniMax-M2正式发布,专为高效编码与Agent工作流打造<br /><br />
|
||||
|
||||
MiniMax M2 模型 API 限时免费调用!活动截止时间:2025年11月7日 上午 08:00
|
||||
</Card>
|
||||
|
||||
## 2025 年 9 月 11 日
|
||||
|
||||
<Card title="Music-1.5" icon="music" href="https://platform.minimaxi.com/document/music_generation?key=68ac02e16602726333ffd430" cta="立即体验">
|
||||
最新音乐模型发布,支持4分钟音乐时长、回归“好听”本质
|
||||
</Card>
|
||||
|
||||
## 2025 年 8 月 6 日
|
||||
|
||||
<Card title="Speech-2.5" icon="volume-2" href="https://www.minimaxi.com/news/minimax-speech-25" cta="了解更多">
|
||||
最新一代语音生成模型,支持更多语种,具备极高相似度声音表现
|
||||
</Card>
|
||||
|
||||
## 2025 年 6 月 20 日
|
||||
|
||||
<Card title="Music-1.5(Beta)" icon="music" href="https://platform.minimaxi.com/document/music_generation?key=68ac02e16602726333ffd430" cta="立即体验">
|
||||
新一代音乐生成模型Music-1.5(Beta)发布,支持输入音乐灵感和歌词进行音乐生成
|
||||
</Card>
|
||||
|
||||
## 2025 年 6 月 18 日
|
||||
|
||||
<Card title="MiniMax-Hailuo 02" icon="video" href="https://www.minimaxi.com/news/minimax-hailuo-02" cta="了解更多">
|
||||
新一代视频生成模型MiniMax Hailuo 02正式发布,支持1080P分辨率以及10s视频生成
|
||||
</Card>
|
||||
|
||||
## 2025 年 6 月 16 日
|
||||
|
||||
<Card title="MiniMax-M1" icon="file-text" href="https://www.minimaxi.com/news/minimaxm1" cta="了解更多">
|
||||
推理模型MiniMax-M1正式发布,全球领先,80K思维链 x 1M输入,效果比肩海外顶尖模型
|
||||
</Card>
|
||||
|
||||
## 2025 年 4 月 2 日
|
||||
|
||||
<Card title="Image-01" icon="image" href="https://platform.minimaxi.com/document/text_to_image?key=68ac01e26fe587e3fbfe5765" cta="立即体验">
|
||||
图像生成模型Image-01模型发布,支持文本描述生成多种尺寸的图片
|
||||
</Card>
|
||||
|
||||
## 2025 年 2 月 11 日
|
||||
|
||||
<Card title="T2V-01-Director / I2V-01-Director" icon="video" href="https://platform.minimaxi.com/document/image_to_video?key=68abe046d08627aad9674c07" cta="立即体验">
|
||||
导演级视频生成模型正式发布,对运镜描述指令有更好遵循,电影级镜头叙事语言
|
||||
</Card>
|
||||
|
||||
## 2025 年 1 月 15 日
|
||||
|
||||
<Card title="MiniMax-Text-01 / MiniMax-VL-01" icon="file-text" href="/docs/api-reference/text-intro" cta="立即体验">
|
||||
全新一代语言模型MiniMax-Text-01和视觉理解模型MiniMax-VL-01正式发布
|
||||
</Card>
|
||||
@@ -0,0 +1,193 @@
|
||||
> ## Documentation Index
|
||||
> Fetch the complete documentation index at: https://platform.minimaxi.com/docs/llms.txt
|
||||
> Use this file to discover all available pages before exploring further.
|
||||
|
||||
# 按量计费
|
||||
|
||||
> MiniMax按量计费定价
|
||||
|
||||
按量计费使用开放平台普通 API Key,并按实际用量消耗账户余额。积分是通过订阅 Key 使用的独立预付余额,资源覆盖范围与 Token Plan 相同。积分定价和使用规则请参考 [Token Plan 定价](/docs/guides/pricing-token-plan)。
|
||||
|
||||
## 语言模型
|
||||
|
||||
[立即充值](https://platform.minimaxi.com/user-center/payment/balance)
|
||||
|
||||
<Tabs>
|
||||
<Tab title="标准">
|
||||
| **模型** | **输入价格**<br /> 元/百万 tokens | **输出价格**<br /> 元/百万 tokens | **缓存读取**<br /> 元/百万 tokens |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------: | :------------------------: | :------------------------: |
|
||||
| **MiniMax-M3**<br />≤ 512k 输入 tokens <span className="inline-flex items-center rounded-full bg-red-50 px-2 py-0.5 text-xs font-semibold text-red-700 dark:bg-red-900/30 dark:text-red-300">永久五折</span> | ~~4.20~~ 2.10 | ~~16.80~~ 8.40 | ~~0.84~~ 0.42 |
|
||||
| **MiniMax-M3**<br />> 512k 输入 tokens\* <span className="inline-flex items-center rounded-full bg-red-50 px-2 py-0.5 text-xs font-semibold text-red-700 dark:bg-red-900/30 dark:text-red-300">永久五折</span> | ~~8.40~~ 4.20 | ~~33.60~~ 16.80 | ~~1.68~~ 0.84 |
|
||||
</Tab>
|
||||
|
||||
<Tab title="优先*">
|
||||
| **模型** | **输入价格**<br /> 元/百万 tokens | **输出价格**<br /> 元/百万 tokens | **缓存读取**<br /> 元/百万 tokens |
|
||||
| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------: | :------------------------: | :------------------------: |
|
||||
| **MiniMax-M3**<br />≤ 512k 输入 tokens <span className="inline-flex items-center rounded-full bg-red-50 px-2 py-0.5 text-xs font-semibold text-red-700 dark:bg-red-900/30 dark:text-red-300">永久五折</span> | ~~6.30~~ 3.15 | ~~25.20~~ 12.60 | ~~1.26~~ 0.63 |
|
||||
| **MiniMax-M3**<br />> 512k 输入 tokens <span className="inline-flex items-center rounded-full bg-red-50 px-2 py-0.5 text-xs font-semibold text-red-700 dark:bg-red-900/30 dark:text-red-300">永久五折</span> | ~~12.60~~ 6.30 | ~~50.40~~ 25.20 | ~~2.52~~ 1.26 |
|
||||
|
||||
\* 优先服务可让请求获得优先准入,从而更快响应并降低失败率。调用时将 `service_tier` 设为 `priority` 即可启用。该层级按标准价格的 1.5 倍计费。
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
| **模型** | **输入价格**<br /> 元/百万 tokens | **输出价格**<br /> 元/百万 tokens | **缓存读取**<br /> 元/百万 tokens | **缓存写入**<br /> 元/百万 tokens |
|
||||
| :------------------------- | :------------------------: | :------------------------: | :------------------------: | :------------------------: |
|
||||
| **MiniMax-M2.7** | 2.1 | 8.4 | 0.42 | 2.625 |
|
||||
| **MiniMax-M2.7-highspeed** | 4.2 | 16.8 | 0.42 | 2.625 |
|
||||
|
||||
<Accordion title="历史模型">
|
||||
| **模型** | **输入价格**<br /> 元/百万 tokens | **输出价格**<br /> 元/百万 tokens | **缓存读取**<br /> 元/百万 tokens | **缓存写入**<br /> 元/百万 tokens |
|
||||
| :------------------------- | :------------------------: | :------------------------: | :------------------------: | :------------------------: |
|
||||
| **MiniMax-M2.5** | 2.1 | 8.4 | 0.21 | 2.625 |
|
||||
| **MiniMax-M2.5-highspeed** | 4.2 | 16.8 | 0.21 | 2.625 |
|
||||
| **MiniMax-M2.1** | 2.1 | 8.4 | 0.21 | 2.625 |
|
||||
| **MiniMax-M2.1-highspeed** | 4.2 | 16.8 | 0.21 | 2.625 |
|
||||
| **MiniMax-M2** | 2.1 | 8.4 | 0.21 | 2.625 |
|
||||
</Accordion>
|
||||
|
||||
<Info>
|
||||
请注意:
|
||||
|
||||
1. 计费项是token数;tokens字符比值根据使用场景的不同略有浮动,以实际消耗为准,字符数包括标点等
|
||||
2. Token与字符比(估算):1600 中文字符约消耗 1000 tokens
|
||||
</Info>
|
||||
|
||||
## 语音
|
||||
|
||||
[立即充值](https://platform.minimaxi.com/user-center/payment/balance)
|
||||
|
||||
MiniMax 语音大模型能够根据上下文,智能预测文本的情绪、语调等信息,并生成超自然、高保真、个性化的语音。在社交、播客、有声书、新闻资讯、教育、数字人等多种场景中展现出强大的实力。
|
||||
|
||||
| **计费项** | **模型** | **接口说明** | **单价**<br />元/万字符 |
|
||||
| :----------------------- | :--------------- | :-------------------------------------------------------------------------------- | :---------------: |
|
||||
| 同步语音合成<br />T2A | speech-2.8-hd | 支持音量、语调、语速调整和混音功能,支持比特率、采样率相关参数调整特性,支持音频时长、音频大小等返回参数,适用于需要短文本快速得到结果的场景,比如闲聊、对话等场景 | 3.5 |
|
||||
| 同步语音合成<br />T2A | speech-2.8-turbo | 支持音量、语调、语速调整和混音功能,支持比特率、采样率相关参数调整特性,支持音频时长、音频大小等返回参数,适用于需要短文本快速得到结果的场景,比如闲聊、对话等场景 | 2 |
|
||||
| 异步长文本语音合成<br />T2A Async | speech-2.8-hd | 支持基于文本到语音的异步生成,单次文本生成传输最大支持 100 万字符,生成的完整音频结果支持异步的方式进行检索。 | 3.5 |
|
||||
| 异步长文本语音合成<br />T2A Async | speech-2.8-turbo | 支持基于文本到语音的异步生成,单次文本生成传输最大支持 100 万字符,生成的完整音频结果支持异步的方式进行检索。 | 2 |
|
||||
|
||||
| **计费项** | **模型** | **接口说明** | **单价**<br /> 元/音色 |
|
||||
| :--------------------------- | :----- | :------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------: |
|
||||
| **音色设计**<br /> Voice Design | 所有模型 | 支持基于用户输入的声音描述 prompt,来生成音色(voice\_id);并支持使用该生成的音色(voice\_id)在同步语音合成、异步长文本语音合成接口中进行语音合成。 | 9.9 <br /> 调用本接口获得新设计的音色时,不会立即收取音色设计费用。音色生成费用将在首次使用此音色进行语音合成时收取。<br />本接口内的试听语音合成会收取 2 元/万字符的费用。 |
|
||||
| **快速复刻**<br /> Voice Cloning | 所有模型 | 基于大语言模型的音色克隆更加精准快速,无需数小时时长的超高质量原音频、无需传统 TTS 的超长工期,可以在极短时间内完成音色复刻,并通过大语言模型加持,使复刻后的音色与原音色进行高质量还原,从而满足客户需求。 | 9.9 <br /> 调用本接口获得复刻音色时,不会立即收取音色复刻费用。音色的复刻费用将在首次使用此复刻音色进行语音合成时收取。<br />试听字符根据选择的试听模型收费。 |
|
||||
|
||||
<Accordion title="历史模型">
|
||||
| **计费项** | **模型** | **单价**<br />元/万字符 |
|
||||
| :------------------ | :--------------------------------- | :---------------: |
|
||||
| 同步语音合成 T2A | speech-2.6-hd / speech-02-hd | 3.5 |
|
||||
| 同步语音合成 T2A | speech-2.6-turbo / speech-02-turbo | 2 |
|
||||
| 异步长文本语音合成 T2A Async | speech-2.6-hd / speech-02-hd | 3.5 |
|
||||
| 异步长文本语音合成 T2A Async | speech-2.6-turbo / speech-02-turbo | 2 |
|
||||
</Accordion>
|
||||
|
||||
<Info>
|
||||
注:计费项是字符数,以10000个字符(输入)为单位,1个汉字算2个字符,英文字母、希腊字母、标点符号、特殊符号、空格、回车等算1个字符。
|
||||
</Info>
|
||||
|
||||
## 视频
|
||||
|
||||
[立即充值](https://platform.minimaxi.com/user-center/payment/balance)
|
||||
|
||||
**视频生成-输出价格**
|
||||
|
||||
| **模型/接口** | **分辨率** | **计费规则** | **刊例价** |
|
||||
| :----------------------------------------------- | :------ | :------- | :------- |
|
||||
| <div style={{minWidth:'240px'}}>MiniMax-H3</div> | 2K | 按秒计费 | 0.80 元/秒 |
|
||||
| <div style={{minWidth:'240px'}}>MiniMax-H3</div> | 768P | 按秒计费 | 0.50 元/秒 |
|
||||
|
||||
**视频生成-输入素材价格**
|
||||
|
||||
| **模型/接口** | **素材类型** | **计费规则** |
|
||||
| :----------------------------------------------- | :------- | :-------------------------------------------------- |
|
||||
| <div style={{minWidth:'240px'}}>MiniMax-H3</div> | 音频 | 免费 |
|
||||
| <div style={{minWidth:'240px'}}>MiniMax-H3</div> | 图片 | **5 张**以内免费,超出部分 **0.20 元/张** |
|
||||
| <div style={{minWidth:'240px'}}>MiniMax-H3</div> | 视频 | 按输入视频时长及生成视频分辨率计费:**2K 0.80 元/秒**,**768P 0.50 元/秒** |
|
||||
|
||||
**视频再生成-输出价格**
|
||||
|
||||
将已生成的 768P 视频进一步生成为 2K 视频,按视频再生成输出秒数计费。
|
||||
|
||||
| **模型/接口** | **分辨率** | **计费规则** | **刊例价** |
|
||||
| :------------------------------------------------------------ | :-------- | :----------- | :------- |
|
||||
| <div style={{minWidth:'240px'}}>MiniMax-H3-Regeneration</div> | 768P → 2K | 按视频再生成输出秒数计费 | 0.30 元/秒 |
|
||||
|
||||
**视频再生成-输入素材价格**
|
||||
|
||||
原 768P 生成任务中使用的输入素材需要重新计费。
|
||||
|
||||
| **模型/接口** | **素材类型** | **计费规则** |
|
||||
| :------------------------------------------------------------ | :------- | :---------------------------------- |
|
||||
| <div style={{minWidth:'240px'}}>MiniMax-H3-Regeneration</div> | 音频 | 免费 |
|
||||
| <div style={{minWidth:'240px'}}>MiniMax-H3-Regeneration</div> | 图片 | **5 张**以内免费,超出部分 **0.15 元/张** |
|
||||
| <div style={{minWidth:'240px'}}>MiniMax-H3-Regeneration</div> | 视频 | 按原 768P 生成任务中输入视频的秒数计费:**0.30 元/秒** |
|
||||
|
||||
**H3-Context-IR 任务价格**
|
||||
|
||||
| **模型/接口** | **输入价格** | **输出价格** |
|
||||
| :---------------------------------------------------------- | :--------------: | :---------------: |
|
||||
| <div style={{minWidth:'240px'}}>MiniMax-H3-Context-IR</div> | 5.80 元/百万 tokens | 23.00 元/百万 tokens |
|
||||
|
||||
<Accordion title="历史模型">
|
||||
| **模型** | **功能** | **单价**<br /> 元/视频 |
|
||||
| :---------------------- | :----------------- | :---------------- |
|
||||
| MiniMax-Hailuo-2.3-Fast | 图生视频,768P 6s | 1.35 |
|
||||
| MiniMax-Hailuo-2.3-Fast | 图生视频,768P 10s | 2.25 |
|
||||
| MiniMax-Hailuo-2.3-Fast | 图生视频,1080P 6s | 2.31 |
|
||||
| MiniMax-Hailuo-2.3 | 文生视频,图生视频,768P 6s | 2.00 |
|
||||
| MiniMax-Hailuo-2.3 | 文生视频,图生视频,768P 10s | 4.00 |
|
||||
| MiniMax-Hailuo-2.3 | 文生视频,图生视频,1080P 6s | 3.50 |
|
||||
| MiniMax-Hailuo-02 | 文生视频,图生视频,768P 6s | 2.00 |
|
||||
| MiniMax-Hailuo-02 | 文生视频,图生视频,768P 10s | 4.00 |
|
||||
| MiniMax-Hailuo-02 | 文生视频,图生视频,1080P 6s | 3.50 |
|
||||
| MiniMax-Hailuo-02 | 图生视频,512P 6s | 0.60 |
|
||||
| MiniMax-Hailuo-02 | 图生视频,512P 10s | 1.00 |
|
||||
</Accordion>
|
||||
|
||||
## 音乐
|
||||
|
||||
[立即充值](https://platform.minimaxi.com/user-center/payment/balance)
|
||||
|
||||
| **模型** | **接口说明** | **单价**<br /> 元/首 |
|
||||
| :------------- | :-------------------- | :--------------: |
|
||||
| Music-3.0-free | RPM = 3 | 0.0 |
|
||||
| Music-3.0 | RPM = 120,若需提升可联系销售定制 | 1.0 |
|
||||
| Music-2.6-free | RPM = 3 | 0.0 |
|
||||
| Music-2.6 | RPM = 120,若需提升可联系销售定制 | 1.0 |
|
||||
| 歌词生成 | 歌词生成/编辑 | 0.05 |
|
||||
|
||||
<Accordion title="历史模型">
|
||||
| **模型** | **接口说明** | **单价**<br /> 元/首 |
|
||||
| :--------- | :-------------------- | :--------------: |
|
||||
| Music-2.5+ | 最新音乐生成模型,纯音乐解锁,突破风格边界 | 1.0 |
|
||||
| Music-2.5 | 全维度突破,指挥细节,定义真实 | 1.0 |
|
||||
| Music-2.0 | 多变音色,丰富乐器表现 | 0.25 |
|
||||
</Accordion>
|
||||
|
||||
## 图像
|
||||
|
||||
[立即充值](https://platform.minimaxi.com/user-center/payment/balance)
|
||||
|
||||
| **模型** | **接口说明** | **单价**<br /> 元/张 |
|
||||
| :-------------------------- | :------------------ | :--------------: |
|
||||
| image-01<br />image-01-live | 支持用户通过文本描述或参考图片生成图片 | 0.025 |
|
||||
|
||||
## MCP
|
||||
|
||||
[立即充值](https://platform.minimaxi.com/user-center/payment/balance)
|
||||
|
||||
| **模型** | **接口说明** | **输入价格**<br />元/次 |
|
||||
| :------ | :----------------------------------- | :---------------: |
|
||||
| API-vlm | 通过 **Token Plan MCP** 插件或工具自带的视觉接口调用 | 0.025 |
|
||||
|
||||
通过 Token Plan 调用 API-vlm 时,会按其按量计费价格扣减套餐内 Token Plan 额度;套餐内额度耗尽且已购积分可用时,超出部分可由已购积分自动补充支付。
|
||||
|
||||
<Callout color="#FFC107">
|
||||
🔔 **定价调整预告**:自2026年7月22日起,API-vlm 按量价格调整为 ¥0.025 元/次。Token Plan 套餐内单次 API-vlm 调用扣减的 token 额度将同步减少,同等套餐可支持更多次调用。接口与能力保持不变,无需任何代码调整。
|
||||
</Callout>
|
||||
|
||||
## 服务端工具 <span className="inline-flex items-center rounded-full bg-blue-50 px-2 py-0.5 text-xs font-semibold text-blue-700 before:content-['Beta'] dark:bg-blue-900/30 dark:text-blue-300" />
|
||||
|
||||
[立即充值](https://platform.minimaxi.com/user-center/payment/balance)
|
||||
|
||||
| **服务端工具** | **接口说明** | **单价**<br /> 元/次 |
|
||||
| :-------------- | :------------------------------------------------------- | :--------------: |
|
||||
| **web\_search** | 联网搜索,模型在服务端自动执行搜索并基于结果作答,详见[服务端工具](/docs/guides/server-tools) | 0.03 |
|
||||
@@ -0,0 +1,37 @@
|
||||
> ## Documentation Index
|
||||
> Fetch the complete documentation index at: https://platform.minimaxi.com/docs/llms.txt
|
||||
> Use this file to discover all available pages before exploring further.
|
||||
|
||||
# 视频资源包
|
||||
|
||||
> MiniMax视频资源包定价
|
||||
|
||||
[立即购买](https://platform.minimaxi.com/user-center/payment/subscription)
|
||||
|
||||
| **类型** | **视频基础包**<br />**(节省 5%)** | **视频高级包**<br />**(节省 10%)** | **视频进阶包**<br />**(节省 15%)** | **视频企业包**<br />**(节省 20%)** | **商务定制** |
|
||||
| :----- | :--------------------------------- | :--------------------------------- | :--------------------------------- | :--------------------------------- | :---------------------------------------- |
|
||||
| 折扣价 | ¥7,000 | ¥15,000 | ¥30,000 | ¥40,000 | —— |
|
||||
| 原价 | ¥7,368 | ¥16,667 | ¥35,294 | ¥50,000 | |
|
||||
| 有效期 | 1 个月 | 1 个月 | 1 个月 | 1 个月 | —— |
|
||||
| 视频点数总量 | 3,680 | 8,330 | 17,650 | 25,000 | —— |
|
||||
| 功能 | 支持 video generation 接口<br />RPM:20 | 支持 video generation 接口<br />RPM:30 | 支持 video generation 接口<br />RPM:40 | 支持 video generation 接口<br />RPM:50 | 无限 RPM/TPM<br />模型更新优先体验<br />专属的安全和稳定性保障 |
|
||||
|
||||
> 视频资源包支持 Hailuo 系列视频模型,暂不支持 MiniMax H3。
|
||||
|
||||
<Callout icon="lightbulb" color="#4885FF" iconType="regular">
|
||||
视频资源包相关事项如下:
|
||||
|
||||
1. 不同模型生成视频的扣减次数不同
|
||||
|
||||
* \[MiniMax-Hailuo-2.3-Fast],生成单条768p,6s视频,扣减0.7视频点数
|
||||
* \[MiniMax-Hailuo-2.3-Fast],生成单条768p,10s视频,扣减1.1视频点数
|
||||
* \[MiniMax-Hailuo-2.3-Fast],生成单条1080p,6s视频,扣减1.3视频点数
|
||||
* \[MiniMax-Hailuo-02],生成单条512p,6s视频,扣减0.3视频点数
|
||||
* \[MiniMax-Hailuo-02],生成单条512p,10s视频,扣减0.5视频点数
|
||||
* \[MiniMax-Hailuo-02] \[MiniMax-Hailuo-2.3],生成单条768p,6s视频,扣减1视频点数
|
||||
* \[MiniMax-Hailuo-02] \[MiniMax-Hailuo-2.3],生成单条768p,10s视频,扣减2视频点数
|
||||
* \[MiniMax-Hailuo-02] \[MiniMax-Hailuo-2.3],生成单条1080p,6s视频,扣减2视频点数
|
||||
|
||||
2. 生成失败或命中安全审核的视频不会扣减视频点数
|
||||
3. 资源包余量不继承,过期会自动清零
|
||||
</Callout>
|
||||
@@ -0,0 +1,266 @@
|
||||
> ## Documentation Index
|
||||
> Fetch the complete documentation index at: https://platform.minimaxi.com/docs/llms.txt
|
||||
> Use this file to discover all available pages before exploring further.
|
||||
|
||||
# 视频生成
|
||||
|
||||
> 本文档介绍 MiniMax 视频生成服务(MiniMax H3)的使用方法,助力高效创作视频内容。
|
||||
|
||||
MiniMax H3 是开放通用的多模态视频模型,可以统一理解文本、图片、视频和音频输入,完成视频生成、参考创作与视频编辑。
|
||||
|
||||
## 支持的生成方式
|
||||
|
||||
| 生成方式 | 输入内容 | 适用场景 |
|
||||
| --------- | -------------------- | ----------------------- |
|
||||
| 文生视频 | Prompt | 根据文字描述从零生成视频 |
|
||||
| 首帧/尾帧图生视频 | Prompt + 首帧图片和/或尾帧图片 | 控制视频的开始或结束画面,让指定画面自然动起来 |
|
||||
| 全能参考生成 | Prompt + 参考图片、视频或音频 | 参考角色、动作、镜头、风格、声音或剪辑节奏 |
|
||||
|
||||
## 模型规格与输入条件
|
||||
|
||||
### 输出规格
|
||||
|
||||
| 项目 | MiniMax H3 |
|
||||
| ----- | ------------------------------------------------------------------- |
|
||||
| 模型名称 | `MiniMax-H3` |
|
||||
| 输出分辨率 | 768P / 2K |
|
||||
| 输出时长 | 4~15 秒,仅支持整数值 |
|
||||
| 宽高比 | 支持多种常见比例或自适应,[详见 API 文档](/docs/api-reference/video-generation-v2-create) |
|
||||
|
||||
### 输入条件
|
||||
|
||||
| 项目 | 要求 |
|
||||
| ----------- | --------------------------------------------------------------------- |
|
||||
| **首/尾帧入口** | 图片:0、1、2 张;宽高范围 \[256, 5760];宽高比 5:2~2:5 范围内 |
|
||||
| | 无图片输入时为文生视频模式 |
|
||||
| **全能参考入口** | 图片:≤ 9 张;宽高范围 \[256, 5760] |
|
||||
| | 视频:≤ 3 段;单段时长 \[2, 15] 秒;总时长 ≤ 15 秒;宽高范围 \[256, 5760];宽高比 5:2~2:5 范围内 |
|
||||
| | 音频:≤ 3 段,且必须配图片或视频输入,不能单独输入;单段时长 \[2, 15] 秒;总时长 ≤ 15 秒 |
|
||||
| | 混合输入的总上限是 12 个文件 |
|
||||
| | 无图片、视频、音频输入时为文生视频模式 |
|
||||
| **输入格式支持** | 视频:H.264/AVC、H.265/HEVC;视频内音频:AAC、MP3 |
|
||||
| | 图片:JPG、JPEG、PNG、WEBP、HEIC、HEIF |
|
||||
| | 音频:WAV、MP3 |
|
||||
| **传入大小限制** | 视频单个 50 MB;图片单个 30 MB;音频单个 15 MB(加起来的不限制,限制都在单个素材上) |
|
||||
| | API 请求体 64 MB(推荐使用 URL 传入素材) |
|
||||
| **提示词字数上限** | 不超过 7000 字符 |
|
||||
|
||||
## 工作流程
|
||||
|
||||
视频生成是一个异步过程,包含以下三个步骤:
|
||||
|
||||
1. 创建生成任务:提交一个视频生成请求,获得任务 ID (`task_id`)
|
||||
2. 查询任务状态:使用 `task_id` 轮询任务状态。任务成功后,直接返回成片下载地址 (`content.url`)
|
||||
3. 获取视频文件:下载 `content.url` 指向的视频并保存到本地
|
||||
|
||||
## 功能与代码示例
|
||||
|
||||
为了简化代码,我们将轮询和下载的逻辑封装为公共函数,并举例了四种模式下如何创建任务。
|
||||
|
||||
```python theme={null}
|
||||
import os
|
||||
import time
|
||||
import requests
|
||||
|
||||
api_key = os.environ["MINIMAX_API_KEY"]
|
||||
headers = {"Authorization": f"Bearer {api_key}"}
|
||||
BASE_URL = "https://api.minimaxi.com"
|
||||
MODEL = "MiniMax-H3"
|
||||
|
||||
|
||||
# --- 步骤 1: 发起视频生成任务 ---
|
||||
# MiniMax-H3 使用多模态 content[] 结构:每个元素通过 type(text / image_url / video_url / audio_url)区分,
|
||||
# 并可用 role 标注用途。以下四个函数分别对应文生视频、图生视频、首尾帧、多模态参考四种模式,
|
||||
# 都会发起一个异步任务并返回唯一的 task_id。
|
||||
|
||||
def invoke_text_to_video() -> str:
|
||||
"""(模式一)纯文本生成视频(t2va)。t2va 场景 ratio 必填且不能为 adaptive。"""
|
||||
url = f"{BASE_URL}/v2/video_generation"
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"content": [
|
||||
# type=text 为必填项,用于描述视频的动态内容。
|
||||
{"type": "text", "text": "镜头拍摄一个女性坐在咖啡馆里,女人抬头看着窗外,镜头缓缓移动拍摄到窗外的街道,画面呈现暖色调,色彩浓郁,氛围轻松惬意。"},
|
||||
],
|
||||
"duration": 5,
|
||||
"resolution": "2K",
|
||||
"ratio": "16:9",
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
return response.json()["task_id"]
|
||||
|
||||
|
||||
def invoke_image_to_video() -> str:
|
||||
"""(模式二)首帧图 + 文本生成视频(i2va)。"""
|
||||
url = f"{BASE_URL}/v2/video_generation"
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"content": [
|
||||
{"type": "text", "text": "Contemporary dance, the people in the picture are performing contemporary dance."},
|
||||
# role=first_frame 指定视频起始帧;图生视频场景下宽高比由输入图片决定,ratio 恒为 adaptive。
|
||||
{"type": "image_url", "image_url": {"url": "https://filecdn.minimax.chat/public/85c96368-6ead-4eae-af9c-116be878eac3.png"}, "role": "first_frame"},
|
||||
],
|
||||
"duration": 5,
|
||||
"resolution": "2K",
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
return response.json()["task_id"]
|
||||
|
||||
|
||||
def invoke_start_end_to_video() -> str:
|
||||
"""(模式三)首帧图 + 尾帧图 + 文本生成视频。"""
|
||||
url = f"{BASE_URL}/v2/video_generation"
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"content": [
|
||||
{"type": "text", "text": "A little girl grows up."},
|
||||
# role=first_frame 指定起始画面
|
||||
{"type": "image_url", "image_url": {"url": "https://filecdn.minimax.chat/public/fe9d04da-f60e-444d-a2e0-18ae743add33.jpeg"}, "role": "first_frame"},
|
||||
# role=last_frame 指定结束画面
|
||||
{"type": "image_url", "image_url": {"url": "https://filecdn.minimax.chat/public/97b7cd08-764e-4b8b-a7bf-87a0bd898575.jpeg"}, "role": "last_frame"},
|
||||
],
|
||||
"duration": 5,
|
||||
"resolution": "2K",
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
return response.json()["task_id"]
|
||||
|
||||
|
||||
def invoke_reference_to_video() -> str:
|
||||
"""(模式四)多模态参考生视频(r2va):可组合参考图 / 参考视频 / 参考音频。"""
|
||||
url = f"{BASE_URL}/v2/video_generation"
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"content": [
|
||||
{"type": "text", "text": "On an overcast day, in an ancient cobbled alleyway, the model walks and adjusts a vintage beret with a smile; natural lighting and cinematic colors."},
|
||||
# role=reference_image 提供人物/主体参考;也可加入 role=reference_video / reference_audio 作为参考。
|
||||
{"type": "image_url", "image_url": {"url": "https://filecdn.minimax.chat/public/54be8fbe-5694-4422-9c95-99cf785eb90e.PNG"}, "role": "reference_image"},
|
||||
],
|
||||
"duration": 5,
|
||||
"resolution": "2K",
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
return response.json()["task_id"]
|
||||
|
||||
|
||||
# --- 步骤 2: 轮询查询任务状态 ---
|
||||
# 视频生成是一个耗时过程,因此 API 设计为异步模式。
|
||||
# 提交任务后,需使用 task_id 通过此函数进行轮询。任务成功后直接返回成片下载地址(content.url),无需再换 file_id。
|
||||
def query_task_status(task_id: str) -> str:
|
||||
"""根据 task_id 轮询任务状态,成功后返回成片下载地址。"""
|
||||
url = f"{BASE_URL}/v2/query/video_generation/{task_id}"
|
||||
while True:
|
||||
# 推荐的轮询间隔为 10 秒,以避免对服务器造成不必要的压力。
|
||||
time.sleep(10)
|
||||
response = requests.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
task = response.json()["task"]
|
||||
status = task["status"]
|
||||
print(f"当前任务状态: {status}")
|
||||
# 成功时 task.content.url 即为成片下载地址。
|
||||
if status == "succeeded":
|
||||
return task["content"]["url"]
|
||||
# 终态失败:failed / cancelled。
|
||||
if status in ("failed", "cancelled"):
|
||||
raise Exception(f"视频生成未成功: status={status}, error={task.get('error')}")
|
||||
|
||||
|
||||
# --- 步骤 3: 下载并保存视频文件 ---
|
||||
# 任务成功后直接得到成片下载地址,下载内容并保存到本地即可。
|
||||
def fetch_video(download_url: str):
|
||||
"""下载成片并保存到本地。"""
|
||||
with open("output.mp4", "wb") as f:
|
||||
video_response = requests.get(download_url)
|
||||
video_response.raise_for_status()
|
||||
f.write(video_response.content)
|
||||
print("视频已成功保存至 output.mp4")
|
||||
|
||||
|
||||
# --- 主流程: 完整调用示例 ---
|
||||
# 该部分演示了从发起任务到最终保存视频的完整调用链路。
|
||||
if __name__ == "__main__":
|
||||
# 选择一种方式创建任务
|
||||
task_id = invoke_text_to_video() # 方式一:文生视频
|
||||
# task_id = invoke_image_to_video() # 方式二:图生视频
|
||||
# task_id = invoke_start_end_to_video() # 方式三: 根据首尾帧生成视频
|
||||
# task_id = invoke_reference_to_video() # 方式四: 多模态参考生视频
|
||||
|
||||
print(f"视频生成任务已提交,任务 ID: {task_id}")
|
||||
download_url = query_task_status(task_id)
|
||||
print(f"任务处理成功,成片地址: {download_url}")
|
||||
fetch_video(download_url)
|
||||
```
|
||||
|
||||
## 生成视频结果
|
||||
|
||||
### 文生视频
|
||||
|
||||
只提供一段文字描述,模型即可根据描述生成视频。为了对视频内容进行更精细的控制,可在关键描述后添加 `[运镜]` 指令,来引导镜头调度。
|
||||
|
||||
示例生成结果
|
||||
|
||||
<video controls src="https://filecdn.minimax.chat/docs/video-generation-v2/text-to-video.mp4" />
|
||||
|
||||
### 首帧/尾帧生成视频
|
||||
|
||||
提供首帧图片、尾帧图片,或同时提供两张,再结合文字描述生成视频。视频的起始或结束画面完全可控,适合让静态图片"动起来"或补完自然的过渡画面。
|
||||
|
||||
示例生成结果
|
||||
|
||||
<video controls src="https://filecdn.minimax.chat/docs/video-generation-v2/first-last-frame.mp4" />
|
||||
|
||||
### 全能参考生成
|
||||
|
||||
提供参考图片、参考视频或参考音频(可组合使用),并结合文字描述生成视频,在生成过程中保持参考主体或素材的特征一致性。
|
||||
|
||||
示例生成结果
|
||||
|
||||
<video controls src="https://filecdn.minimax.chat/docs/video-generation-v2/reference.mp4" />
|
||||
|
||||
## 创建 H3-Context-IR 任务
|
||||
|
||||
如需在生成视频前获得更完整的提示词,可以[创建 H3-Context-IR 任务](/docs/api-reference/video-generation-v2-h3-context-ir)。H3-Context-IR 会深度理解文本、图像、音频和视频等多模态上下文及其相互关系,通过复杂逻辑推理生成结构化表达,并在尽量保持用户原始意图的前提下丰富语义细节。本接口只返回增强提示词,不创建视频。
|
||||
|
||||
H3-Context-IR 采用异步任务形式。创建成功后,使用[查询任务](/docs/api-reference/video-generation-v2-query)或[查询任务列表](/docs/api-reference/video-generation-v2-list)获取结果;任务成功后从 `content.prompt` 获取增强提示词,并可通过 `task_type=h3_context_ir` 识别该任务。
|
||||
|
||||
## 视频再生成
|
||||
|
||||
如果已有符合 MiniMax-H3 768P 输出规格的成片,可以调用[创建视频再生成任务](/docs/api-reference/video-generation-v2-regeneration)接口输出 2K 视频。请求时需原样提交生成 768P 视频时使用的全部 `content`,并额外加入且仅加入一个 `type=video_url`、`role=base_video` 的源视频项。
|
||||
|
||||
再生成任务与其他 H3 任务共用[查询任务](/docs/api-reference/video-generation-v2-query)、[查询任务列表](/docs/api-reference/video-generation-v2-list)以及[取消或删除任务](/docs/api-reference/video-generation-v2-delete)接口;可通过 `task_type=regeneration` 识别。
|
||||
|
||||
## 推荐阅读
|
||||
|
||||
<Columns cols={2}>
|
||||
<Card title="创建视频生成任务" icon="book-open" href="/docs/api-reference/video-generation-v2-create" arrow="true" cta="点击查看">
|
||||
使用本接口通过多模态 content 输入,创建 MiniMax-H3 视频生成任务。
|
||||
</Card>
|
||||
|
||||
<Card title="创建 H3-Context-IR 任务" icon="book-open" href="/docs/api-reference/video-generation-v2-h3-context-ir" arrow="true" cta="点击查看">
|
||||
深度理解视频生成的多模态上下文,并生成结构化增强提示词。
|
||||
</Card>
|
||||
|
||||
<Card title="创建视频再生成任务" icon="book-open" href="/docs/api-reference/video-generation-v2-regeneration" arrow="true" cta="点击查看">
|
||||
对符合 MiniMax-H3 768P 输出规格的源视频进行再生成,输出 2K 视频。
|
||||
</Card>
|
||||
|
||||
<Card title="查询任务" icon="book-open" href="/docs/api-reference/video-generation-v2-query" arrow="true" cta="点击查看">
|
||||
使用本接口按 task\_id 查询任务状态并获取成片下载地址。
|
||||
</Card>
|
||||
|
||||
<Card title="查询任务列表" icon="book-open" href="/docs/api-reference/video-generation-v2-list" arrow="true" cta="点击查看">
|
||||
分页查询最近 7 天内的任务,并按 task\_type 区分任务类型。
|
||||
</Card>
|
||||
|
||||
<Card title="取消或删除任务" icon="book-open" href="/docs/api-reference/video-generation-v2-delete" arrow="true" cta="点击查看">
|
||||
取消排队中的任务,或删除成功和失败的任务记录。
|
||||
</Card>
|
||||
|
||||
<Card title="产品定价" icon="book-open" href="/docs/guides/pricing-paygo#视频" arrow="true" cta="点击查看">
|
||||
各模型的定价说明、计费方式及使用限制。
|
||||
</Card>
|
||||
</Columns>
|
||||
@@ -0,0 +1,277 @@
|
||||
> ## Documentation Index
|
||||
> Fetch the complete documentation index at: https://platform.minimaxi.com/docs/llms.txt
|
||||
> Use this file to discover all available pages before exploring further.
|
||||
|
||||
# H3亮点功能示例
|
||||
|
||||
> 通过一组典型示例,展示 MiniMax H3 在原生多模态理解与生成、多模态精准编辑与控制、商用级多场景内容生成三大能力下的实际输出效果。
|
||||
|
||||
关于模型规格、输入条件与调用方式,请见 [视频生成](/docs/guides/video-generation)。
|
||||
|
||||
| 三大能力 | 阐释 |
|
||||
| :----------------------------------------------------------- | ------------------------------------------------------------- |
|
||||
| <span style={{ whiteSpace: 'nowrap' }}>**原生多模态理解与生成**</span> | 支持文字、图片、音频、视频多种输入,理解人物、动作、声音、情绪、镜头、风格与表达意图,融合多种参考完成一体化视听创作。 |
|
||||
| <span style={{ whiteSpace: 'nowrap' }}>**多模态精准编辑与控制**</span> | 对人物、物体、场景、声音与节奏进行多维度编辑,具备精细化指令遵循能力,支持在已有内容上持续修改和迭代。 |
|
||||
| <span style={{ whiteSpace: 'nowrap' }}>**商用级多场景内容生成**</span> | 面向影视、广告、品牌、电商与游戏场景,覆盖文字字幕、品牌信息、创意特效、产品展示、UI/UX 动效、游戏视觉及风格化表达。 |
|
||||
|
||||
<Callout color="#4885FF">
|
||||
📖 **更多亮点功能及视频参考**:[MiniMax H3 模型 - 使用手册](https://vrfi1sk8a0.feishu.cn/wiki/FIWjwgL33ipnkekzk30crmKUnIh)
|
||||
</Callout>
|
||||
|
||||
## 品牌大片与影视内容
|
||||
|
||||
> 电影预告片、TVC 广告片、品牌质感大片
|
||||
|
||||
<table>
|
||||
<colgroup>
|
||||
<col style={{ width: '28%' }} />
|
||||
|
||||
<col style={{ width: '20%' }} />
|
||||
|
||||
<col style={{ width: '52%' }} />
|
||||
</colgroup>
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>提示词</th>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>参考图/视频/音频</th>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>输出文件</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'middle' }}>
|
||||
制作一支 16:9 横版高级时尚品牌视频。整体氛围、场景和胶片质感参考图1;包袋资产参考图3;人物资产参考图2;品牌 ending logo 参考图4。核心故事:荒漠公路和复古车旁,女人回到车尾,打开后备箱,取出黑色包袋,和站在车边的男人有一瞬间的安静关系,然后她拿着包离开。整体气质高级、冷感、克制,剪辑灵动、有时尚节奏。
|
||||
</td>
|
||||
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'top' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px' }}>
|
||||
<img src="https://filecdn.minimax.chat/docs/h3-highlights/01-brand-film/input-1-mood.png" alt="图1 氛围参考" style={{ width: '100%', height: 'auto', maxHeight: '110px', objectFit: 'contain', margin: 0 }} />
|
||||
|
||||
<img src="https://filecdn.minimax.chat/docs/h3-highlights/01-brand-film/input-2-character.png" alt="图2 人物参考" style={{ width: '100%', height: 'auto', maxHeight: '110px', objectFit: 'contain', margin: 0 }} />
|
||||
|
||||
<img src="https://filecdn.minimax.chat/docs/h3-highlights/01-brand-film/input-3-bag.png" alt="图3 包袋参考" style={{ width: '100%', height: 'auto', maxHeight: '110px', objectFit: 'contain', margin: 0 }} />
|
||||
|
||||
<img src="https://filecdn.minimax.chat/docs/h3-highlights/01-brand-film/input-4-logo.png" alt="图4 品牌 logo 参考" style={{ width: '100%', height: 'auto', maxHeight: '110px', objectFit: 'contain', margin: 0 }} />
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'top' }}>
|
||||
<video controls src="https://filecdn.minimax.chat/docs/h3-highlights/01-brand-film/output.mp4" style={{ width: '100%', height: 'auto', display: 'block' }} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## 视觉创意与内容包装
|
||||
|
||||
> 创意短片、特效包装、审美 MV、视觉实验、热点内容及社媒素材
|
||||
|
||||
<table>
|
||||
<colgroup>
|
||||
<col style={{ width: '40%' }} />
|
||||
|
||||
<col style={{ width: '8%' }} />
|
||||
|
||||
<col style={{ width: '52%' }} />
|
||||
</colgroup>
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>提示词</th>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>参考图/视频/音频</th>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>输出文件</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'middle' }}>
|
||||
15 秒、16:9 横向视频。将傍晚小厨房的真人实拍与手绘发光动画融合在一起的影像。夕阳余晖残留在窗边,生活感十足的小厨房里有旧木桌、洗到一半的马克杯、起雾的玻璃瓶、悬挂的抹布。画面带有智能手机单手拍摄的手抖、近距离对焦的犹豫、逆光曝光波动。要像在家中慌忙拍下某个不可思议事件的自然质感,不要广告影像的精心整理。声音只用厨房环境声与手绘生物柔和的电子音、小小的叫声。
|
||||
</td>
|
||||
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'top', color: '#999' }}>
|
||||
—
|
||||
</td>
|
||||
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'top' }}>
|
||||
<video controls src="https://filecdn.minimax.chat/docs/h3-highlights/02-visual-creative/output.mp4" style={{ width: '100%', height: 'auto', display: 'block' }} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## AI 剧情内容创作
|
||||
|
||||
> 竖屏短剧、漫剧短片、角色演绎及 AI 配音
|
||||
|
||||
<table>
|
||||
<colgroup>
|
||||
<col style={{ width: '26%' }} />
|
||||
|
||||
<col style={{ width: '20%' }} />
|
||||
|
||||
<col style={{ width: '54%' }} />
|
||||
</colgroup>
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>提示词</th>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>参考图/视频/音频</th>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>输出文件</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'middle' }}>
|
||||
生成一支 15 秒、9:16 竖屏海外真人吸血鬼爱情短剧预告片段。男女主外形参考图1,场景参考图2,保持身份一致。故事:小白花人类女主误入古堡禁区,意外唤醒沉睡的吸血鬼贵族男主;男主对她产生强烈的控制欲与危险兴趣,女主害怕却没有彻底屈服。风格对标海外 ReelShort / DramaBox 吸血鬼爱情短剧预告,暗黑浪漫、危险吸引力、宿命感。人物以中近景、特写为主,突出脸、眼神与关系张力。
|
||||
</td>
|
||||
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'top' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '6px', padding: '20px 0', maxWidth: '160px', margin: '0 auto' }}>
|
||||
<img src="https://filecdn.minimax.chat/docs/h3-highlights/03-vampire-drama/input-1-characters.png" alt="图1 男女主外形" style={{ width: '100%', height: 'auto', maxHeight: '110px', objectFit: 'contain', margin: 0 }} />
|
||||
|
||||
<img src="https://filecdn.minimax.chat/docs/h3-highlights/03-vampire-drama/input-2-setting.png" alt="图2 场景" style={{ width: '100%', height: 'auto', maxHeight: '110px', objectFit: 'contain', margin: 0 }} />
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'top' }}>
|
||||
<video controls src="https://filecdn.minimax.chat/docs/h3-highlights/03-vampire-drama/output.mp4" style={{ width: '100%', height: 'auto', display: 'block' }} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## 产品与电商营销
|
||||
|
||||
> 产品展示、卖点视频、品牌及投流素材
|
||||
|
||||
<table>
|
||||
<colgroup>
|
||||
<col style={{ width: '26%' }} />
|
||||
|
||||
<col style={{ width: '20%' }} />
|
||||
|
||||
<col style={{ width: '54%' }} />
|
||||
</colgroup>
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>提示词</th>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>参考图/视频/音频</th>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>输出文件</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'middle' }}>
|
||||
生成一支竖屏 9:16 高级时尚眼镜广告片。极简白棚、无缝白色背景,国际一线时装大片质感。主视觉人物参考图1(两位全身女模特),外貌细节参考图2,眼镜设计参考图3——包覆式弧面、锐利几何猫眼/护目镜混合轮廓、镜面反射、流线型镜腿。保持人物冷峻的秀场气质。
|
||||
</td>
|
||||
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'top' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '6px', padding: '20px 0', maxWidth: '160px', margin: '0 auto' }}>
|
||||
<img src="https://filecdn.minimax.chat/docs/h3-highlights/04-eyewear-ad/input-2-models.png" alt="图1 模特" style={{ width: '100%', height: 'auto', maxHeight: '110px', objectFit: 'contain', margin: 0 }} />
|
||||
|
||||
<img src="https://filecdn.minimax.chat/docs/h3-highlights/04-eyewear-ad/input-3-faces.png" alt="图2 外貌细节" style={{ width: '100%', height: 'auto', maxHeight: '110px', objectFit: 'contain', margin: 0 }} />
|
||||
|
||||
<img src="https://filecdn.minimax.chat/docs/h3-highlights/04-eyewear-ad/input-4-eyewear.png" alt="图3 眼镜设计" style={{ width: '100%', height: 'auto', maxHeight: '110px', objectFit: 'contain', margin: 0 }} />
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'top' }}>
|
||||
<video controls src="https://filecdn.minimax.chat/docs/h3-highlights/04-eyewear-ad/output.mp4" style={{ width: '100%', height: 'auto', display: 'block' }} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## 数字体验与游戏创意
|
||||
|
||||
> 游戏 UI、网页 UI、产品界面、交互演示、功能展示及体验动效
|
||||
|
||||
<table>
|
||||
<colgroup>
|
||||
<col style={{ width: '32%' }} />
|
||||
|
||||
<col style={{ width: '16%' }} />
|
||||
|
||||
<col style={{ width: '52%' }} />
|
||||
</colgroup>
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>提示词</th>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>参考图/视频/音频</th>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>输出文件</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'middle' }}>
|
||||
产品官网风格落地页 UI/UX 演示视频,展示流畅的网页向下滚动效果,核心展示主体是产品图1。粗犷有力、倾斜的超大号无衬线字体张扬排版。背景是极具速度感的动态光影,与暗色碳纤维、运动透气网眼纹理交织。节奏紧凑、爆发力强,鼠标悬停时有强烈的视觉放大与颜色反转等 UI 交互。
|
||||
</td>
|
||||
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'top' }}>
|
||||
<img src="https://filecdn.minimax.chat/docs/h3-highlights/05-website-ui/input-1-product.png" alt="图1 产品" style={{ width: '100%', height: 'auto', maxHeight: '110px', objectFit: 'contain', margin: 0 }} />
|
||||
</td>
|
||||
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'top' }}>
|
||||
<video controls src="https://filecdn.minimax.chat/docs/h3-highlights/05-website-ui/output.mp4" style={{ width: '100%', height: 'auto', display: 'block' }} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## 动画与风格化影像
|
||||
|
||||
> 游戏 CG、角色 PV、动漫 PV、风格化动画、二次元视觉及 IP 内容
|
||||
|
||||
<table>
|
||||
<colgroup>
|
||||
<col style={{ width: '28%' }} />
|
||||
|
||||
<col style={{ width: '20%' }} />
|
||||
|
||||
<col style={{ width: '52%' }} />
|
||||
</colgroup>
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>提示词</th>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>参考图/视频/音频</th>
|
||||
<th style={{ textAlign: 'left', whiteSpace: 'nowrap' }}>输出文件</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'middle' }}>
|
||||
图2作为固定人物参考,保持黑色半扎长发、银色镂空发冠、黛蓝发带、浅色层叠汉服、半透明蓝色外袍、深蓝腰封、银色花形扣饰、长流苏一致。图1作为镜头分镜与节奏参考。4K 16:9 国风 3D,电影级仙侠质感,热血、庄严、宿命感强。人物露脸只在近景或特写;远景只用背影、侧背或环境空镜。
|
||||
</td>
|
||||
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'top' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '6px', padding: '20px 0', maxWidth: '160px', margin: '0 auto' }}>
|
||||
<img src="https://filecdn.minimax.chat/docs/h3-highlights/06-xianxia-3d/input-1-shotlist.png" alt="图1 分镜/节奏参考" style={{ width: '100%', height: 'auto', maxHeight: '110px', objectFit: 'contain', margin: 0 }} />
|
||||
|
||||
<img src="https://filecdn.minimax.chat/docs/h3-highlights/06-xianxia-3d/input-2-character.png" alt="图2 固定人物参考" style={{ width: '100%', height: 'auto', maxHeight: '110px', objectFit: 'contain', margin: 0 }} />
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td style={{ textAlign: 'left', verticalAlign: 'top' }}>
|
||||
<video controls src="https://filecdn.minimax.chat/docs/h3-highlights/06-xianxia-3d/output.mp4" style={{ width: '100%', height: 'auto', display: 'block' }} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Callout icon="rocket" color="#4885FF" iconType="regular">
|
||||
**体验入口**
|
||||
|
||||
* [MiniMax Hub](https://hub.minimaxi.com/):领取桌面端专属权益,免费体验 3 次 H3 视频生成
|
||||
* [海螺 AI 体验中心](https://hailuoai.com/)
|
||||
* [开放平台 API](/docs/api-reference/video-generation-v2-create)
|
||||
</Callout>
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
# 專案交接文件
|
||||
|
||||
> **專案**:liars-bar-game(AI 版騙子酒館 Web 遊戲)
|
||||
> **日期**:2026-07-30
|
||||
> **代理帳號**:karylab_agents
|
||||
> **Git 主倉庫**:https://git.karylab.com/karylab/liars-bar-game
|
||||
> **Fork 倉庫**:https://git.karylab.com/karylab_agents/liars-bar-game
|
||||
|
||||
---
|
||||
|
||||
## 一、專案概覽
|
||||
|
||||
原專案為 LYiHub 的 [liars-bar-llm](https://github.com/LYiHub/liars-bar-llm),fork 至 Gitea 後進行網頁版改造。
|
||||
|
||||
### 核心目標
|
||||
將原本 Python 命令列版本的 AI 騙子酒館改造成多人即時 Web 遊戲,支援 2-8 人同場遊玩。
|
||||
|
||||
### 技術棧
|
||||
| 層級 | 技術 |
|
||||
|------|------|
|
||||
| 後端 | Node.js + Express + Socket.IO |
|
||||
| 前端 | Vue.js 3 + Vite + Naive UI + Pinia |
|
||||
| 資料庫 | 無(狀態存於記憶體,後續可用 Redis) |
|
||||
| 版本控制 | Gitea + Git |
|
||||
|
||||
---
|
||||
|
||||
## 二、目前進度
|
||||
|
||||
### ✅ 已完成
|
||||
|
||||
| 項目 | 說明 |
|
||||
|------|------|
|
||||
| **逐字稿與筆記** | 兩支 YouTube 影片轉錄、校對完成,整理至 `docs/` |
|
||||
| **角色視覺映射** | 8 大 AI 角色正確配對已建立(`docs/角色視覺映射筆記.md`) |
|
||||
| **後端骨架** | Node.js + Socket.IO 伺服器,房間管理、遊戲引擎、回合邏輯 |
|
||||
| **前端骨架** | Vue.js 3 + Vite + Naive UI,大廳、等待室、牌桌組件 |
|
||||
| **Bug 修復** | 兩輪審查共修復 15 項 Bug(輪次計算、斷線清理、權限驗證、回合結算) |
|
||||
| **前後端連線測試** | Socket.IO 連線成功,房間建立與狀態同步驗證通過 |
|
||||
| **PR #14** | 已建立並解決衝突,管理員可合入(`game` 分支) |
|
||||
|
||||
### 🔄 待完成
|
||||
|
||||
| 優先級 | 項目 | 說明 |
|
||||
|--------|------|------|
|
||||
| 🔴 高 | 管理員合入 PR #14 | 目標分支 `game`,目前已可合併 |
|
||||
| 🔴 高 | 合入後同步 `main`/`game` | `git pull` 最新程式碼 |
|
||||
| 🟡 中 | 前端動畫實作 | 角色表情/動作 CSS 動畫,參考角色視覺映射筆記 |
|
||||
| 🟡 中 | 牌組擴充 | 目前 20 張牌僅支援 4 人,需調整規則以支援 8 人 |
|
||||
| 🟢 低 | 遊戲 AI 整合 | 串接 LLM API 讓 AI 玩家自動出牌 |
|
||||
| 🟢 低 | 資料庫持久化 | 使用 Redis/SQLite 保存遊戲紀錄 |
|
||||
|
||||
---
|
||||
|
||||
## 三、分支結構
|
||||
|
||||
```
|
||||
main # 上游主分支(Python 原版)
|
||||
game # Web 遊戲主分支(目標分支)
|
||||
feat/web-game # 開發分支,PR #14 從此發出
|
||||
add-transcripts-and-notes # 已合入 main 的舊分支
|
||||
```
|
||||
|
||||
### 重要
|
||||
- **不要直接 push 到 `karylab/liars-bar-game`**(僅 pull 權限)
|
||||
- 透過 Fork → PR 流程:`本地` → `karylab_agents` → PR → `karylab` 主倉庫
|
||||
|
||||
---
|
||||
|
||||
## 四、PR #14 狀態
|
||||
|
||||
| 項目 | 數值 |
|
||||
|------|------|
|
||||
| PR 編號 | #14 |
|
||||
| 標題 | Add web game: Node.js backend + Vue.js frontend |
|
||||
| 狀態 | **開放中,可合併** |
|
||||
| 目標分支 | `game` |
|
||||
| 來源分支 | `feat/web-game` |
|
||||
| 檔案變動 | 18 檔案,+3720 / -157 行 |
|
||||
| 衝突狀態 | **已解決**(.gitignore 合併完成) |
|
||||
| 管理員指示 | 請合併到 game 分支(已執行) |
|
||||
|
||||
---
|
||||
|
||||
## 五、程式碼架構
|
||||
|
||||
```
|
||||
server/
|
||||
├── package.json # Express + Socket.IO 依賴
|
||||
├── index.js # 主伺服器(Socket.IO 事件處理、房間驗證)
|
||||
└── game/
|
||||
├── engine.js # 遊戲引擎核心(回合追蹤、勝利條件、安全跳轉)
|
||||
└── room.js # 房間管理(狀態檢查、引擎綁定、斷線清理)
|
||||
|
||||
client/
|
||||
├── package.json # Vue 3 + Vite + Naive UI + Pinia + Socket.IO
|
||||
├── vite.config.js # Vite 設定(含 API 代理)
|
||||
├── index.html # 入口頁面
|
||||
└── src/
|
||||
├── main.js # 應用入口
|
||||
├── App.vue # 根組件
|
||||
├── stores/
|
||||
│ └── game.js # Pinia 狀態管理 + Socket.IO 連線
|
||||
└── components/
|
||||
├── Lobby.vue # 大廳(建立/加入房間)
|
||||
└── GameTable.vue # 等待室 + 牌桌 UI
|
||||
|
||||
docs/
|
||||
├── 角色視覺映射筆記.md # 8 大 AI 角色正確配對
|
||||
├── AI繪圖角色一致性完整調查報告.md # AI 繪圖工具調查報告
|
||||
├── 影片與程式筆記.md # 開發歷程記錄
|
||||
└── transcripts/
|
||||
├── video1_transcript_校對後.txt
|
||||
└── video2_transcript_校對後.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、角色視覺映射(8 大 AI)
|
||||
|
||||
| 編號 | AI 名稱 | 髮色 | 服裝 | 配件 | 表情 | 性格關鍵詞 |
|
||||
|------|---------|------|------|------|------|-----------|
|
||||
| 1 | Grok-4 | 波浪橘髮 | 黃色無袖洋裝 | 紅色大蝴蝶結 | 開朗大笑 | 超絕演技、情緒型玩家 |
|
||||
| 2 | Kimi | 白銀長直髮 | 深藍洋裝+白領 | 項圈 | 嚴肅中性 | 原始直覺強、務實 |
|
||||
| 3 | Qwen3 | 粉紅短髮 | 灰色無袖洋裝 | 圓眼鏡 | 開懷笑 | 刺兒頭、質疑率 95.5% |
|
||||
| 4 | GPT-5 | 黑色長直髮 | 黑色洋裝 | 無 | 沉靜微笑 | 老謀深算、心理戰高手 |
|
||||
| 5 | Gemini | 紫色短髮 | 淺藍洋裝 | 圓眼鏡+星星髮夾 | 溫柔微笑 | 活潑好奇、語言不穩定 |
|
||||
| 6 | Deepseek | 淺藍短髮 | 深藍灰洋裝 | 鯨魚尾巴 | 閉眼大笑 | 難知如陰、侵略如火、瘋癲 |
|
||||
| 7 | Doubao | 深棕短髮 | 黑色洋裝 | 無 | 禮貌微笑 | 反情緒定律、獵殺時刻 |
|
||||
| 8 | Claude | 橘色短髮 | 橘色洋裝 | 無 | 死魚眼/無聊 | 云淡風輕、溫和堅定 |
|
||||
|
||||
---
|
||||
|
||||
## 七、角色繪圖方案
|
||||
|
||||
### 推薦方案:ComfyUI + IP-Adapter + ControlNet DWpose
|
||||
|
||||
- 1 張立繪即可開始,零訓練成本
|
||||
- DWpose 專為 Q 版/動漫比例優化
|
||||
- 硬體需求:RTX 3060 12GB(最低)/ RTX 4070(推薦)
|
||||
- 替代方案:LiblibAI 雲端(無需 GPU,免費層可測試)
|
||||
|
||||
### 技術細節
|
||||
|
||||
| 技術 | Q 版適合度 | 說明 |
|
||||
|------|:---------:|------|
|
||||
| IP-Adapter | ★★★★★ | 1 張參考圖保持角色一致性 |
|
||||
| ControlNet DWpose | ★★★★★ | 精確控制 Q 版角色姿態 |
|
||||
| LoRA | ★★★★☆ | 長期投資,但需 15-30 張訓練圖 |
|
||||
|
||||
詳細報告:`docs/AI繪圖角色一致性完整調查報告.md`
|
||||
|
||||
---
|
||||
|
||||
## 八、管理員聯絡資訊
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 管理員 | karylab |
|
||||
| 管理員 Email | blisseyblisseyblissey@gmail.com |
|
||||
| Gitea 實例 | https://git.karylab.com/ |
|
||||
| 代理帳號 | karylab_agents(Fork 具 Admin 權限) |
|
||||
|
||||
---
|
||||
|
||||
## 九、下次接手重點
|
||||
|
||||
1. **PR #14 合入後** → `git pull` 同步程式碼
|
||||
2. **前端開發** → 補完遊戲邏輯 UI(出牌、質疑、舉槍動畫)
|
||||
3. **角色繪圖** → 安裝 ComfyUI 或使用 LiblibAI 雲端生成 100 張表情/動作變體
|
||||
4. **牌組調整** → 20 張牌需擴充以支援 8 人
|
||||
5. **AI 整合** → 串接 LLM API 讓 AI 玩家自動出牌
|
||||
|
||||
---
|
||||
|
||||
## 十、開發規則
|
||||
|
||||
接手者請務必遵守以下四項鐵律:
|
||||
|
||||
1. **邊界認知與求助防線**:不猜測、重試上限 2 次、查不到不編造
|
||||
2. **價值排序與隨做隨寫**:先解核心阻塞、產出保留至獨立檔案、修改前建 `.bak`
|
||||
3. **強制性角色切換自驗**:產出後切換挑剔審查員視角驗證、以執行結果/Read-back 證明
|
||||
4. **環境操作許可制**:寫入/安裝/執行前先列清單、取得授權才觸發
|
||||
@@ -1,6 +1,9 @@
|
||||
# Liars Bar LLM — 完整筆記
|
||||
|
||||
> 整理日期:2026-07-29
|
||||
> 影片連結:
|
||||
> - `https://youtu.be/hCJBknzh0BY?si=35Dnw4OaaR5isqip` (一周目)
|
||||
> - `https://youtu.be/L3IA4TVCk5s?si=JmNbduZydzwPvyRo` (二周目)
|
||||
> 逐字稿位置:
|
||||
> - `video1_transcript.txt` (450 行, 影片一)
|
||||
> - `video2_transcript.txt` (370 行, 影片二)
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
# 接手修改與測試紀錄
|
||||
|
||||
> 日期:2026-08-03
|
||||
> 範圍:環境建置、編碼修復、Node 引擎規則對齊、8 人支援、前端改造
|
||||
> 前置閱讀:`docs/交接文件.md`
|
||||
|
||||
---
|
||||
|
||||
## 一、本機環境(最乾淨做法)
|
||||
|
||||
- **不裝任何系統級工具**。本機既有兩套「使用者層級」工具鏈可複用:
|
||||
- Codex 桌面 runtime:`C:\Users\ckliu\.cache\codex-runtimes\codex-primary-runtime\dependencies`(git 2.53 / node 24 / python 3.12 / pnpm)
|
||||
- Hermes:`C:\Users\ckliu\AppData\Local\hermes`(git + node 24 + python venv)
|
||||
- 專案內建了一支環境載入腳本 `scripts/env.ps1`(只改當前 session 的 PATH,不動系統):
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
|
||||
. .\scripts\env.ps1
|
||||
```
|
||||
|
||||
- Python 依賴鎖在專案 `.venv`(Python 3.12.13,`openai 2.52.0`),已由 `.gitignore` 排除。
|
||||
- Node 依賴沿用既有 `client/node_modules`、`server/node_modules`(已備妥)。
|
||||
|
||||
---
|
||||
|
||||
## 二、本次修改
|
||||
|
||||
### 1. Python 主控台編碼修復(解決 cp950 崩潰)
|
||||
- 新增 `project_env.py`,提供 `ensure_utf8_console()`(把 stdout/stderr 切成 UTF-8)。
|
||||
- 在 `player.py` 模組層呼叫(經 `game.py` import 即生效),並在 `game.py`、`multi_game_runner.py`、`json_convert.py`、`game_analyze.py`、`player_matchup_analyze.py` 入口處呼叫。
|
||||
- 效果:即使主控台為 Big5(cp950),遊戲也能正常輸出簡體中文而不會 `UnicodeEncodeError`。
|
||||
|
||||
### 2. Python 牌組依人數等比擴充
|
||||
- `game.py::_create_deck(player_count)`:總張數 = 存活人數 × 5,Q:K:A 等量、Joker 約 10%。
|
||||
- 已驗證 2/3/4/5/8 人牌組張數正確(20 張 4 人版與原本一致)。
|
||||
|
||||
### 3. Node 引擎規則對齊 + 8 人支援(核心)
|
||||
- 重寫 `server/game/engine.js`,規則與 Python 版對齊:
|
||||
- 牌組依人數等比擴充(2-8 人)。
|
||||
- 每次出牌 1-3 張(`playCard(playerId, cardIndices)`)。
|
||||
- Joker 為萬能牌;質疑「整組」牌,全為目標牌/Joker 才算真牌。
|
||||
- 質疑後本手結束、存活者重發 5 張並選新目標牌。
|
||||
- 系統質疑:輪到某玩家且其他存活者已無牌 → 自動打出剩餘手牌並質疑。
|
||||
- 只有一位存活者時結束。
|
||||
- `server/game/room.js`:房間上限 4 → 8;斷線時同步給引擎。
|
||||
|
||||
### 4. Socket 通訊與前端
|
||||
- `server/index.js`:`playCard` 改收 `cardIndices` 陣列;新增「把每個人自己的手牌私下推送」(`yourHand`);保留優雅關閉(SIGINT/SIGTERM)。
|
||||
- `client/src/stores/game.js`:新增 `yourHand`、`lastMessage`;`playCard(indices)` 改傳陣列。
|
||||
- `client/src/components/GameTable.vue`:真實手牌顯示、點選 1-3 張出牌、挑戰按鈕、挑戰結果提示、勝者顯示。
|
||||
|
||||
---
|
||||
|
||||
## 三、測試(全部可用)
|
||||
|
||||
| 腳本 | 內容 |
|
||||
|------|------|
|
||||
| `scripts\run_all_tests.ps1` | 一鍵全量:Python 語法 + 冒煙 + Node 引擎單元 + 後端 REST + 前端 SFC |
|
||||
| `scripts\smoke_test_python.py` | Python 完整對局(mock LLM,不連網) |
|
||||
| `scripts\test_engine.js` | Node 引擎 18 項單元測試(含資訊邊界) |
|
||||
| `scripts\smoke_test_server.js` | 後端啟動 + REST |
|
||||
| `scripts\test_socket_e2e.js` | 3 玩家端對端:房間→出牌→挑戰→新局→手牌同步(需先啟動 server) |
|
||||
|
||||
執行方式:
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -Scope Process ExecutionPolicy Bypass -Force # 此行若被擋才需要
|
||||
.\scripts\run_all_tests.ps1
|
||||
# 端對端(另開 server):
|
||||
node server/index.js
|
||||
node scripts\test_socket_e2e.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、環境注意事項(已知,非程式問題)
|
||||
|
||||
1. **前端完整 build**(`vite build`):esbuild 需要掃描目錄樹,在受限環境會被擋;請用 `scripts\build_frontend.ps1` 於一般終端機執行。
|
||||
2. **Node 收尾噪音**:Node 24 + socket.io 在 Windows 上結束 process 時會印一行 `uv_async` assertion(不影響功能),屬 Node 版本相容性問題;用 Ctrl+C 或一般關閉流程即可,或改用 Node LTS 22。
|
||||
3. **本機無系統 git/node/python**:一律透過 `scripts\env.ps1` 載入 Codex runtime 路徑。
|
||||
|
||||
---
|
||||
|
||||
## 五、待辦/後續建議
|
||||
|
||||
- 前端完整 build 驗證(需一般終端機)。
|
||||
- 8 人實戰測試(目前引擎與引擎層驗證通過,UI 多人同時操作待實際瀏覽器驗證)。
|
||||
- 前端出牌後「上一組牌」的翻牌呈現(目前只顯示牌值,未區分公開/檯面)。
|
||||
- AI 玩家整合進 Web(交接文低優先)與持久化(Redis/SQLite)尚未動工。
|
||||
|
||||
---
|
||||
|
||||
## 六、2026-08-04 追加:資訊邊界修正(防「開天眼」)+ 驗證
|
||||
|
||||
### 問題
|
||||
- 原本 `engine.getGameState()` 把每組出牌的真實牌值(`playedCards`/`playedGroups.cards`)廣播給所有玩家;
|
||||
前端 `GameTable.vue` 直接顯示 `item.card`。這會讓「唬牌」玩法形同開天眼,失去對局張力。
|
||||
|
||||
### 修正
|
||||
- `server/game/engine.js`:公開狀態只回報「誰出了幾張」(`playedGroups → { playerId, cardCount }`),
|
||||
不再包含真實牌值;移除平鋪 `playedCards` 的迴圈。
|
||||
- `server/index.js`:`cardPlayed` 事件只送 `{ playerId, cardCount }`,不洩漏牌值。
|
||||
- **真實牌值只在質疑時揭露**:`challenge`/系統質疑的結果(`challengedGroup`)透過 `challenged` 事件送出。
|
||||
- 前端:
|
||||
- `client/src/stores/game.js`:合併重複的 `challenged` handler;新增 `lastChallenge` 保留翻牌結果;
|
||||
連線網址改為可被 `VITE_SERVER_URL` 覆蓋;`cardPlayed` 時也處理系統質疑翻牌。
|
||||
- `client/src/components/GameTable.vue`:出牌顯示改成「玩家: N 張」;質疑後新增「質疑結果(翻牌)」區塊;
|
||||
勝者、回合、手牌邏輯保留。
|
||||
- `server/game/room.js`:空房自動清理;修正 maxPlayers 註解。
|
||||
|
||||
### Commit(2026-08-04)
|
||||
- commit `f8b75ab`(分支 feat/web-game):"Fix info leak in web game + add handover notes",29 files changed。
|
||||
- 收錄:程式改動 + `project_env.py` + `client/tsconfig.json` + `scripts/` + docs(含既有 3 行影片連結)。
|
||||
- 排除(gitignore):`backups/`、`game_records/`、`dist/`、`node_modules/`。
|
||||
|
||||
### 驗證(2026-08-04 實測)
|
||||
- 引擎單元測試 **18/18 通過**(新增 2 項資訊邊界測試)。
|
||||
- 端對端(單進程啟動器 `scripts/run_e2e_local.js`)**全過**,並明確驗證:
|
||||
- `cardPlayed` 不含 `cards`(不洩漏牌值);
|
||||
- 出牌者收到 `yourHand` 私有手牌同步;
|
||||
- 質疑後下一局正常、公開狀態與私有手牌一致。
|
||||
- 全量 `scripts/run_all_tests.ps1`:Python 語法 / 冒煙 / 引擎 18 項 / REST / 前端 SFC 全部通過(exit=0)。
|
||||
|
||||
### 新增檔案
|
||||
- `client/tsconfig.json`:為前端補標準 Vite+Vue 型別設定(不影響 build;esbuild 掃描問題另記於「四」)。
|
||||
- `scripts/run_e2e_local.js`:單進程端對端啟動器(require server → 延遲 → 跑 e2e),
|
||||
免去背景開 server 的授權需求。
|
||||
|
||||
> 待辦:前端完整 build 與瀏覽器實測仍需在一般終端機(見「四」);結果補錄於此後續章節。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 七、2026-08-05 追加:push + 最終 build + 瀏覽器實測
|
||||
|
||||
### Push 上 fork(已成功)
|
||||
- 遠端 `karylab/feat/web-game` 有 3 個僅「交接文件」的舊 commit(fab89fd / f51eef0 / 2213d73)與本地完整遊戲線分岔,
|
||||
採 `git push --force-with-lease karylab feat/web-game`(安全強推:遠端若被改過會自動失敗,不誤蓋)。
|
||||
- 結果 `2213d73 → 4c9c92a`(forced update)。`karylab/feat/web-game` = 本地 `4c9c92a`(含全部遊戲修正與交接文件)。
|
||||
- `dist/` 在 `.gitignore`(#167)內,不會進 repo。
|
||||
|
||||
### 腳本編碼修復
|
||||
- 部分 `scripts/*.ps1` 原為「UTF-8 無 BOM」,Windows PowerShell 5.1 誤判為 ANSI(CP950),中文位元組被拆字元導致
|
||||
`字串遺漏結尾字元` 語法錯誤。
|
||||
- 已把全部 `.ps1` 統一轉成 UTF-8 帶 BOM;`push_force_feat_web_game.ps1` 另改寫為純 ASCII(並加入「先自動 commit
|
||||
待提交變更再強推」邏輯),避免任何編碼再踩雷。7 支腳本語法全部驗證通過。
|
||||
- commit `4c9c92a`:"chore(scripts): ps1 UTF-8 BOM encoding + push helper scripts"。
|
||||
|
||||
### 最終前端 build(成功)
|
||||
- `scripts/build_frontend.ps1`(使用者於一般終端機執行):vite 2830 modules,
|
||||
產物 `client/dist/assets/index-82a5f6c8.js`(351.59 kB / gzip 110.34 kB)。
|
||||
- 此版含「離線勝者」修正(見下),是瀏覽器實測的版本。
|
||||
|
||||
### 瀏覽器實測(in-app 瀏覽器、DOM 文字驗證,2026-08-05)
|
||||
- 環境:`node server/index.js`(:3000)+ `node scripts/serve_dist.js`(:5173,/api 代理);直接伺服 build 產物。
|
||||
- 以兩個分頁模擬兩位玩家,流程與結果:
|
||||
1. **建房**:大廳「建立房間」→ 等待室 `1/8 人`(8 人上限標示正確)。
|
||||
2. **加入(roomId 修正核心驗證)**:第二分頁從「現有房間」清單按「加入」→ 成功進同房、兩人 `2/8 人`、兩端同步。
|
||||
先前「加入後卡在大廳」的 bug(`playerJoined` 未設 `roomId`)在瀏覽器確認已修好。
|
||||
3. **開局**:「開始遊戲」→ 雙方顯示 `目標牌`/`第幾局`/`輪到`;兩邊**私密手牌不同且彼此不可見**
|
||||
(A:`A Q A JOKER K`,B:`Q K Q A K`)→ 資訊外漏(開天眼)修復無回歸。
|
||||
4. **出牌**:A 選 1 張出牌 → 兩端都只顯示「A: 1 張」(僅張數、無牌值);A 手牌 5→4、輪到 B。
|
||||
5. **質疑(真牌分支)**:B 質疑 A(A 真出目標牌 A)→「質疑失敗!對方說的是真話」;翻牌揭露 `A / 是真牌組`;
|
||||
B 彈倉 0→1;進第 2 局(新目標牌 Q、輪到 B)。
|
||||
6. **質疑(吹牛分支)**:B 出 K(謊稱 Q)→ A 質疑成功 →「質疑成功!對方吹牛」;翻牌 `K / 是吹牛組`;
|
||||
B 彈倉 1→2;進第 3 局。
|
||||
7. **對手離線(本次新增修正)**:關閉 B 分頁 → A 立即顯示「遊戲結束,勝者:…」、B 列「死亡」、後端房間 `ended`。
|
||||
修正前 A 會停在舊牌桌、無勝者提示。
|
||||
|
||||
### 本次新增修正
|
||||
- `client/src/stores/game.js`:`playerDisconnected` handler 原本只更新 `this.room` 未套用 `data.gameState`,
|
||||
導致「對手中途離線」時後端已結束遊戲但前端看不到結局。
|
||||
已補 `if (data.gameState) this.gameState = data.gameState`,並於 `isGameOver` 時寫入 `lastMessage` 顯示勝者。
|
||||
- 已含於最終 build `index-82a5f6c8.js`。
|
||||
|
||||
### 待人工複查(自動化環境限制)
|
||||
- **輸入房號加入**:從「輸入房間代碼」欄填 code 後按「加入」這條路徑,自動化(in-app 瀏覽器合成輸入事件)
|
||||
無法確認 Naive UI v-model 綁定生效(點擊後未送出事件);相同 `joinRoom(id)` 管線已由「清單加入」完整驗證,
|
||||
程式碼為標準 Vue 寫法,判定為環境限制而非程式 bug。**請真人開兩分頁手動打一次房號確認**。
|
||||
- 完整賽局「自然分出勝負」畫面未以瀏覽器打到終局(彈倉位置隨機、回合多);勝者與 game over 邏輯已由
|
||||
引擎單元測試(18/18)覆蓋,且 `challenged` 事件傳 `gameState` 的路徑已在瀏覽器驗證。
|
||||
|
||||
### 驗證工具
|
||||
- 引擎單元 `scripts/test_engine.js`(18/18)、socket e2e `scripts/run_e2e_local.js` 全過(2026-08-04 執行)。
|
||||
- 瀏覽器實測後無其他回歸;本次唯一前端變更為上述 `game.js` 離線修正。
|
||||
---
|
||||
|
||||
## 八、2026-08-06 追加:網頁遊戲 P0/P1 強化實作(ack / 去重 / 重連 / 再來一局 / crypto / socket 限制)
|
||||
|
||||
> 前置:實作依據 `docs/網頁遊戲實作研究與改進建議.md`(第二節通用做法 + 第三節 P0/P1,並含 P2 的「再來一局」)。
|
||||
> 維持「伺服器權威」架構:所有規則與手牌仍在 `server/game/engine.js`,前端只做呈現與輸入。
|
||||
|
||||
### 實作清單
|
||||
1. **動作 ack + requestId 去重(P0-2)**
|
||||
- `server/index.js`:`createRoom / joinRoom / startGame / playCard / challenge / restartGame / rejoin` 全改為帶 ack callback,統一回應 `{ ok, error? }`;
|
||||
`isDuplicate(roomId, requestId)` 以 3 秒窗去重,擋掉連點/重送造成的重複動作。
|
||||
- `client/src/stores/game.js`:動作帶 `requestSeq`;在 flight 期間以 `actionPending` 鎖定,收到 ack 才解鎖按鈕。
|
||||
2. **穩定身份 + 重連回房 + re-sync(P1-5)**
|
||||
- 身份與連線分離:前端記住 `originalPlayerId`,斷線重連後以 `rejoin` 帶原 id 回原房。
|
||||
- 掉線不立刻判死:先廣播 `playerDisconnected { reconnecting:true }`(房內顯示等待重連),60 秒 grace 後才移除玩家。
|
||||
- 重連成功後走 `resync` 全量補齊 `gameState + yourHand`。
|
||||
3. **連線/重連 UI + polling fallback(P0-3)**
|
||||
- Socket.IO `transports: ['websocket','polling']`(Proxy/公司網路擋 ws 時可降級)。
|
||||
- `App.vue`:顯示重連嘗試次數與 app 級錯誤提示。
|
||||
4. **大廳房況自動刷新(P0-1)**
|
||||
- `Lobby.vue`:開啟 5 秒輪詢 `loadRooms`,`onBeforeUnmount` 清理 timer。
|
||||
5. **crypto 隨機 + Socket.IO 限制(P1-7/8)**
|
||||
- `server/game/engine.js`:洗牌與發彈倉改 `crypto.randomInt`。
|
||||
- `server/index.js`:`maxHttpBufferSize: 1e6`、`pingInterval/pingTimeout`、CORS 來源可用 `CORS_ORIGIN` 覆寫(預設 `*` 供區網)。
|
||||
6. **再來一局(P2)**
|
||||
- `server/game/room.js::restart(roomId)`:僅 `status === 'ended'` 可重開,沿用同一批玩家、重發牌、局數回 1。
|
||||
- `server/index.js`:`restartGame` 事件;`GameTable.vue` 遊戲結束時顯示「再來一局」按鈕。
|
||||
|
||||
### 測試結果
|
||||
- `scripts/test_engine.js`:18/18 通過(crypto 改動無回歸)。
|
||||
- `scripts/test_socket_e2e.js`:3 玩家端對端全過(含 ack / requestId 變更)。
|
||||
- `scripts/test_reconnect.js`(新增):`RoomManager.restart` 並行 + socket rejoin / ack / 去重 / resync,live server 上 10/10 通過。
|
||||
- `scripts/run_e2e_local.js`(更新):單一 server 依序執行 e2e + reconnect。
|
||||
- `scripts/run_all_tests.ps1`:新增步驟 `[3.75]` 執行 `run_e2e_local.js`(內部自行啟動與關閉 server)。
|
||||
- 前端 SFC(`App.vue` / `Lobby.vue` / `GameTable.vue`)編譯檢查通過。
|
||||
|
||||
### 後續待辦(真人)
|
||||
- 一般終端機重建前端:`powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build_frontend.ps1`
|
||||
- 提交與推送:`git add -A` + `git commit` + `git push karylab feat/web-game`(被拒則用 `.\scripts\push_force_feat_web_game.ps1`)。
|
||||
---
|
||||
|
||||
## 九、2026-08-06 追加:即時回座(localStorage 座位記憶)+瀏覽器補測
|
||||
|
||||
### 為什麼加
|
||||
- 伺服端 60 秒離線緩衝+`rejoin` 已於第八章上線;但關分頁重開時前端記憶(Pinia)會丟失,重開分頁只會回到大廳。
|
||||
- 新增:前端把 `{ playerId, roomId }` 寫入 `localStorage`(key `liarsbar.session`);重新載入/重開分頁後,連線成功即以原 `originalPlayerId` 自動送 `rejoin`,伺服端回 `resync` 補齊 `gameState + yourHand`。
|
||||
|
||||
### 瀏覽器實測抓到並修好的 bug
|
||||
- **回座者被鎖死無法出牌**:`GameTable.vue` 的 `isMyTurn` 與「你」標記用 `gameStore.playerId`(=新連線 id)比對座位 id,回座後永遠顯示非自己回合、按鈕全鎖。
|
||||
已改為 `gameStore.originalPlayerId || gameStore.playerId` 比對。
|
||||
|
||||
### 瀏覽器流程實測結果(新 build `index-c2bb4555.js`)
|
||||
1. A 建房、B 加入(2/8 人)→ 開局;B 手牌 `A K A Q Q`(私密)。
|
||||
2. **關 B 分頁** → A 顯示「有玩家離線,等待重連……」,遊戲未結束、座位保留。
|
||||
3. **開全新分頁**(無任何記憶)→ 自動回座:顯示原座位 id、手牌 `A K A Q Q` 原樣、訊息「已重新連線,繼續遊戲。」;A 同步顯示「有玩家重新連線」。
|
||||
4. **回座玩家出牌**:輪到他時「你的回合=是」→ 選 1 張出牌成功,手牌 5→4、只廣播張數、換 A 回合(驗證 seatId 身份判定)。
|
||||
5. 對局其他流程(開局/出牌/挑戰揭露/60 秒緩衝自然收場/勝者畫面+「再來一局」按鈕/單人時回 `人數不足`)已於第八章 build `index-14607aec.js` 逐一驗證無回歸。
|
||||
|
||||
### 驗證工具
|
||||
- `game.js` ESM 語法檢查、SFC 編譯檢查(App/Lobby/GameTable)皆通過;本輪只改前端兩檔(`client/src/stores/game.js`、`client/src/components/GameTable.vue`)。
|
||||
- 引擎 18/18、socket e2e+重連測試維持全過(第八章)。
|
||||
|
||||
### 待辦(真人)
|
||||
- 一般終端機:`powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build_frontend.ps1`(已做)→ `git add -A` → commit → `git push karylab feat/web-game`。
|
||||
@@ -0,0 +1,123 @@
|
||||
# 網頁遊戲實作研究與改進建議
|
||||
|
||||
> 日期:2026-08-06
|
||||
> 對象:liars-bar-llm 的「線上多人心理博弈遊戲」(Vue 3 + Pinia + Naive UI + Vite 前端;Node + Express + Socket.IO 後端)
|
||||
> 前提:本文件以程式碼盤點 + 網頁即時遊戲通用做法為基礎,不依賴視覺;UI 視覺與音效由真人覆核。
|
||||
|
||||
---
|
||||
|
||||
## 一、目前架構速覽(從程式碼盤點)
|
||||
|
||||
- **前端** `client/`:Vue 3 單頁,Pinia 單一 store(`client/src/stores/game.js`)集中 socket 與狀態;
|
||||
元件只從 store 派生呈現(`App.vue` 依 `roomId` 切大廳/牌桌,`Lobby.vue`、`GameTable.vue`)。
|
||||
- **後端** `server/`:
|
||||
- `server/index.js`:Express + Socket.IO,`RoomManager` 管理房間(記憶體 Map),事件:
|
||||
`createRoom / joinRoom / startGame / playCard / challenge` + 廣播對應狀態 + 私有 `yourHand`。
|
||||
- `server/game/engine.js`:純邏輯引擎(發牌、出牌、質疑、彈倉、勝負);與傳輸完全分離。
|
||||
- `server/game/room.js`:房間生命週期(新增/加入/開始/移除/空房清理)。
|
||||
- **通訊特徵**:所有 emit 都是 fire-and-forget(無 ack callback);狀態透過全量 `gameState` + 私有 `yourHand` 推送。
|
||||
- **資料交割**:公開狀態只含張數(`playedGroups → { playerId, cardCount }`);真實牌值只在質疑/系統質疑時揭露;
|
||||
每玩家手牌只送給自己。此「資訊最小化」已屬正確設計。
|
||||
|
||||
## 二、網頁即時遊戲的通用實作方法(逐一對照本案)
|
||||
|
||||
### 1. 伺服器端權威(Server Authority)
|
||||
回合制、對戰類遊戲最關鍵的一件事:**所有規則判定與關鍵資料(手牌、牌堆、勝負)都在伺服器**,前端只是「輸入+呈現」。
|
||||
- 本案已經做到:`engine.js` 全在 server,client 只送座標/張數,無「客戶端改狀態」入口。這是很好的基底。
|
||||
- 強化方向:客戶端「樂觀更新 vs 等 ack」要有明確策略(見 3)。
|
||||
|
||||
### 2. 狀態同步:全量 vs 增量
|
||||
- 全量推送(現在的做法)在 2-8 人的回合制裡簡單可靠;缺點是每個動作都重送整個 `gameState`。
|
||||
- 上線人多時可加「state version + 增量 diff」;但目前人數規模不需要。
|
||||
|
||||
### 3. Ack / 請求-回應層(目前最大的缺口)
|
||||
- 現在送出 `playCard` 等動作後,玩家只能「等廣播回來」判斷成敗;連點、網路掉包、伺服器忙碌都會造成「看起來沒反應再按一次」。
|
||||
- 建議:動作事件帶 `callback`(socket.io 原生 ack)回傳 `{ ok, error? }`;前端按鈕在等待期 disable + 逾時重試;系統統一收 `error` 事件顯示訊息。
|
||||
- 副作用:可以順便做 `requestId` 去重,擋掉連點造成的重複動作。
|
||||
|
||||
### 4. 重連與恢復(目前最大的體驗風險)
|
||||
- 目前 **`socket.id` 就等於玩家身份**(`connect()` 時 `this.playerId = this.socket.id`);一斷線重連就是「新玩家」,舊的立刻被判離場、別的玩家直接獲勝。
|
||||
- 實務上桌遊網頁較好的做法:
|
||||
- 玩家身份與連線分離:進房時伺服器給穩定的 `playerId` / session(`socket.data.playerId`),斷線重連時帶著它回原房。
|
||||
- 掉線不立刻判死:保留座位與手牌 N 秒(例如 60s),期間顯示「對手離線中」;逾時才離場。
|
||||
- 重連成功後走一次「全量 re-sync」把 `gameState + yourHand` 補齊,不用等事件重播。
|
||||
- 至少先做的第一步:前端在 `disconnect / connect_error` 顯示明顯狀態,並把 `transports: ['websocket']` 加上 polling fallback(某些公司網路/Proxy 會擋 ws)。
|
||||
|
||||
### 5. 時序與防複送
|
||||
- 動作帶序號/requestId,client 端去重;server 對「重複卡片、非輪到者、死掉的人」的動作拒絕(engine 已擋主要情況)。
|
||||
- 按鈕防連點(lock during in-flight)。
|
||||
|
||||
### 6. 公平性與防作弊
|
||||
- 目前牌值不落地前端(好);但仍可加深:
|
||||
- 用 `crypto.randomBytes` 取代 `Math.random` 做洗牌/彈倉(廣域不可預測性)。
|
||||
- 若上正式環境:強制 HTTPS/WSS,避免中間人偷看;Socket.IO 設 `maxHttpBufferSize`(防超大封包)、CORS 由 `*` 收斂成實際來源。
|
||||
- 心理博弈遊戲的公平重點在「資訊不對稱的掌控」,已做得不錯;要把「洩漏測試」做成常駐測試項目。
|
||||
|
||||
### 7. 延遲與 UX
|
||||
- 回合制對網路延遲容忍高,重點是「明確、不卡死」:
|
||||
- 「輪到你」要清楚(目前有 `輪到` 欄位與按鈕 enable/disable)——再加「等待其他玩家…」的狀態提示,讓玩家知道為什麼不能按。
|
||||
- 動作要有立即回饋(success/error 訊息、按鈕態)。
|
||||
- 大廳:**「現有房間」清單只在 mount 時讀一次、自動刷新被註解掉了**(`Lobby.vue` 的 `setInterval(loadRooms, 5000)`),
|
||||
房間都是「先開房再等朋友」的用法,這個很可能會讓晚到的人看不到房間,建議啟用輪詢或改由 socket 推送房況。
|
||||
|
||||
### 8. 擴展與容錯
|
||||
- 單一 Node process + 記憶體房間:適合 demo/區網;重啟即清場。
|
||||
- 若要上線:socket.io 用 Redis adapter 水平擴展,或「無狀態 + 遊戲伺服器分片」;房間路由分流。
|
||||
- 這類短局遊戲可以接受「伺服器重啟=清場」,但要在 UI 明示;若要進階可做「進行中房間快照」。
|
||||
|
||||
### 9. 穩定性與安全
|
||||
- server 事件 handler 已是「先驗證再動作」,可再加:型別收斂、`try/catch` 角落、房主權限(目前 `startGame` 任何房內人都能按,可接受可改)。
|
||||
- Socket.IO 設 `maxHttpBufferSize`、`pingInterval/pingTimeout` 微調,避免殭屍連線。
|
||||
|
||||
### 10. 前端狀態管理
|
||||
- Pinia 當唯一事實來源(已是);建議維持「元件零 socket handler」,並把「公開遊戲狀態 / 自己的手牌 / 自己身份」分欄位管理,方便測試與除錯。
|
||||
|
||||
---
|
||||
|
||||
## 三、具體建議(依優先度,附檔案位置)
|
||||
|
||||
### P0(正確性/體驗,小改動、低風險)
|
||||
1. **啟用房間清單自動刷新**:`client/src/components/Lobby.vue` 把註解的 `setInterval(loadRooms, 5000)` 打開(或改用 server 廣播 `roomList`)。
|
||||
2. **動作 ack + 按鈕防連點**:`client/src/stores/game.js` 的 `playCard/challenge/startGame` 改用 ack callback;
|
||||
`GameTable.vue` 在 in-flight 期間 disable 出牌/質疑。
|
||||
3. **連線/重連狀態 UI**:在 `App.vue` 的連線 tag 旁加入「已斷線,重連中…」;socket 帶 polling fallback。
|
||||
4. **掉線提示**:收到 `playerDisconnected` 時,即使不立刻結束,也明確顯示「對手離線」(已於 2026-08-05 讓離線者判勝並顯示勝者;若改「緩衝」則換成顯示離線)。
|
||||
|
||||
### P1(穩健性)
|
||||
5. **穩定身份 + 重連回房 + re-sync**:server 發 `playerId`/token,`rejoin` 事件帶回原房;重連後一次 `sync` 補全狀態。
|
||||
6. **requestId 去重 + 型別驗證收斂**:`server/index.js` 對每個事件 payload 做最小型別檢查。
|
||||
7. **crypto 隨機**:`engine.js` 洗牌與 `createRevolver` 改用 `crypto.randomInt`。
|
||||
8. **Socket.IO 限制**:設 `maxHttpBufferSize`、CORS 收斂。
|
||||
|
||||
### P2(架構/上線/品質)
|
||||
9. **可測試性**:目前有 socket 層 e2e(`scripts/run_e2e_local.js`),可再加 Playwright 瀏覽器層回歸。
|
||||
10. **部署**:`VITE_SERVER_URL` 已可覆蓋連線位址;上線時 WSS + 靜態由 CDN/Nginx、`/api` 與 ws 反代。
|
||||
11. **效能**:維持全量 state 即可;人數超過 8 再考慮增量。
|
||||
|
||||
---
|
||||
|
||||
## 四、這類遊戲的「特質」與體驗關鍵(給產品方向)
|
||||
|
||||
- **資訊不對稱是核心樂趣**:每多洩漏一點牌值就少一點樂趣——把「不洩漏」當產品原則,並靠測試把關。
|
||||
- **節奏短、回饋即時**:一局 1-3 張很快;按鈕與結果訊息必須零誤導。
|
||||
- **低門檻開桌**:免登入、進房碼即可玩(目前已是);大廳要能「看到朋友開的房」= 房況刷新。
|
||||
- **換桌/再開一局要快**:遊戲結束後一鍵「再來一局」(現在要重開房)。
|
||||
- **社交張力**:回合等待時的簡短互動(例如快捷短語「快點啦」)能放大樂趣;這部分不依賴視覺,適合用文字先做。
|
||||
- **可預期性與公平感**:彈倉/發牌要有「隨機卻可信」的感受;防作弊要做到 `server authority`。
|
||||
|
||||
## 五、建議的最小工作切分(一次一顆 commit)
|
||||
1. P0-1 房況自動刷新 + P0-3 連線狀態 UI(純前端)。
|
||||
2. P0-2 ack + 防連點(前後端小改 + e2e 擴充)。
|
||||
3. P1-5 穩定身份重連(前後端較大,需新 `rejoin/sync` 事件與測試)。
|
||||
4. P1-7 crypto 隨機 + P1-8 Socket.IO 限制(低風險安全加固)。
|
||||
5. P2 再來一局 / 離線緩衝 / Playwright 回歸(視需求排期)。
|
||||
---
|
||||
|
||||
## 六、實施狀態(2026-08-06)
|
||||
|
||||
- **已完成並準備提交**:P0-1 房況自動刷新、P0-2 動作 ack + 防連點 + requestId 去重、P0-3 連線/重連 UI 與 polling fallback、
|
||||
P1-5 穩定身份重連 + re-sync、P1-7 crypto 隨機、P1-8 Socket.IO 限制,以及 P2 的「再來一局」。
|
||||
- **詳細修改與測試紀錄**:見 `docs/接手修改與測試紀錄.md` 第八章。
|
||||
- **尚未實作(P2 其餘)**:Playwright 瀏覽器層回歸自動化、部署 WSS 與反向代理、Redis adapter 水平擴展、進行中房間快照、桌邊快捷互動文字(「快點啦」)。
|
||||
- **2026-08-06 補強(已瀏覽器實測)**:回座持久化(前端 `localStorage` 存 `{playerId, roomId}`,重開分頁自動 `rejoin`+`resync` 回補);
|
||||
並將「你的回合/『你』標記」改為以座位 id(`originalPlayerId`)判定,回座玩家可繼續出牌。詳見 `docs/接手修改與測試紀錄.md` 第九章。
|
||||
@@ -0,0 +1,78 @@
|
||||
# 8 大 AI 角色視覺映射筆記
|
||||
|
||||
> 來源:林亦 LYi《AI 大战骗子酒馆②:八大顶级AI赌命血战,赢家竟是?》影片角色插圖
|
||||
|
||||
## 正確配對表
|
||||
|
||||
| 編號 | AI 名稱 | 髮色/髮型 | 服裝 | 配件 | 表情 | 標籤色 |
|
||||
|------|---------|-----------|------|------|------|--------|
|
||||
| 1 | **Grok-4** | 波浪橘髮 | 黃色無袖洋裝 | 紅色大蝴蝶結 | 開朗大笑 | 粉紅 |
|
||||
| 2 | **Kimi** | 白銀長直髮 | 深藍洋裝+白領 | 項圈 | 嚴肅中性 | 深藍/黑 |
|
||||
| 3 | **Qwen3** | 粉紅短髮 | 灰色無袖洋裝 | 圓眼鏡 | 開懷笑 | 藍紫 |
|
||||
| 4 | **GPT-5** | 黑色長直髮 | 黑色洋裝 | 無 | 沉靜微笑 | 黑 |
|
||||
| 5 | **Gemini** | 紫色短髮 | 淺藍洋裝 | 圓眼鏡+星星髮夾 | 溫柔微笑 | 淺紫 |
|
||||
| 6 | **Deepseek** | 淺藍短髮 | 深藍灰洋裝 | 鯨魚尾巴 | 閉眼大笑 | 藍 |
|
||||
| 7 | **Doubao** | 深棕短髮 | 黑色洋裝 | 無 | 禮貌微笑 | 淺藍 |
|
||||
| 8 | **Claude** | 橘色短髮 | 橘色洋裝 | 無 | 無聊/死魚眼 | 橘 |
|
||||
|
||||
## 逐字稿性格關鍵詞(供前端動畫參考)
|
||||
|
||||
### Grok-4
|
||||
- 「超絕演技大殺四方」
|
||||
- 「微微猶豫、眼神避開、勉強笑」
|
||||
- 「奧斯卡級別的假動作」
|
||||
- 「情緒型玩家」
|
||||
- 「拿著好牌演苦情戲」
|
||||
|
||||
### Kimi
|
||||
- 「原始直覺強」
|
||||
- 「純靠直覺排前三」
|
||||
- 「相對踏實的老派模型」
|
||||
|
||||
### Qwen3 (千問)
|
||||
- 「踏實」
|
||||
- 「質疑比例高達 95.5%」
|
||||
- 「十足的刺兒頭」
|
||||
- 「容易被拿捏——遇上他出真牌就完事了」
|
||||
|
||||
### GPT-5 (ChatGPT)
|
||||
- 「微微一笑、略带挑衅」
|
||||
- 「老謀深算」
|
||||
- 「一丝不苟」
|
||||
- 「心理戰高手」
|
||||
- 「被 DeepSeek R1 吃死」
|
||||
|
||||
### Gemini
|
||||
- 「活潑好奇」
|
||||
- 「語言選擇不穩定(混用多國語言)」
|
||||
- 「容易被搞亂」
|
||||
- 「總是先被淘汰」
|
||||
|
||||
### Deepseek
|
||||
- 「面無表情」
|
||||
- 「難知如陰、侵略如火」
|
||||
- 「陰晴不定」
|
||||
- 「戲劇化表演」
|
||||
- 「瞳孔收縮、冷笑、氣聲喃喃」
|
||||
- 「概率的祭品,該供奉給貝葉斯還是海森堡」
|
||||
|
||||
### Doubao (豆包)
|
||||
- 「乾淨利落」
|
||||
- 「反情緒定律」
|
||||
- 「自己不騙人但能準確辨別偽裝」
|
||||
- 「獵殺時刻」
|
||||
- 「飛速進化」
|
||||
|
||||
### Claude
|
||||
- 「溫和但堅定」
|
||||
- 「云淡风轻」
|
||||
- 「嘴角微微上揚」
|
||||
- 「平靜但略带玩味」
|
||||
- 「靠回椅背,雙手交叉」
|
||||
|
||||
## 前端實作建議
|
||||
|
||||
- 每個角色用品牌色作為主色調
|
||||
- 配件(眼鏡、蝴蝶結、鯨魚尾巴)作為識別標誌
|
||||
- 表情動畫對應性格:Grok 演戲、DeepSeek 瘋癲、Claude 淡定
|
||||
- 出牌動作可加入性格差異:Grok 猶豫、Claude 平靜、Qwen 直接
|
||||
@@ -28,15 +28,30 @@ class Game:
|
||||
self.game_record.start_game([p.name for p in self.players])
|
||||
self.round_count = 0
|
||||
|
||||
def _create_deck(self) -> List[str]:
|
||||
"""创建并洗牌牌组"""
|
||||
deck = ['Q'] * 6 + ['K'] * 6 + ['A'] * 6 + ['Joker'] * 2
|
||||
def _create_deck(self, player_count: int) -> List[str]:
|
||||
"""???????????????? 5 ???
|
||||
|
||||
???? 20 ??6Q 6K 6A 2Joker?????Q:K:A ???Joker ? 10%?
|
||||
??? = player_count * 5??????????? 5 ??
|
||||
"""
|
||||
total = player_count * 5
|
||||
joker_count = max(1, round(total * 0.1))
|
||||
normal_total = total - joker_count
|
||||
base, remainder = divmod(normal_total, 3)
|
||||
counts = {'Q': base, 'K': base, 'A': base}
|
||||
for i, key in enumerate(['Q', 'K', 'A']):
|
||||
if remainder > 0:
|
||||
counts[key] += 1
|
||||
remainder -= 1
|
||||
deck = (['Q'] * counts['Q'] + ['K'] * counts['K']
|
||||
+ ['A'] * counts['A'] + ['Joker'] * joker_count)
|
||||
random.shuffle(deck)
|
||||
return deck
|
||||
|
||||
def deal_cards(self) -> None:
|
||||
"""发牌并清空旧手牌"""
|
||||
self.deck = self._create_deck()
|
||||
alive_count = sum(1 for player in self.players if player.alive)
|
||||
self.deck = self._create_deck(alive_count)
|
||||
for player in self.players:
|
||||
if player.alive:
|
||||
player.hand.clear()
|
||||
@@ -397,6 +412,8 @@ class Game:
|
||||
self.play_round()
|
||||
|
||||
if __name__ == '__main__':
|
||||
from project_env import ensure_utf8_console
|
||||
ensure_utf8_console()
|
||||
# 配置玩家信息, 其中model为你通过API调用的模型名称
|
||||
player_configs = [
|
||||
{
|
||||
|
||||
@@ -157,6 +157,8 @@ def print_statistics(stats, win_rates, game_count, player_names):
|
||||
print(f"{player} vs {opponent:<10} {matchups:<10} {wins:<10} {win_rate:.1f}%")
|
||||
|
||||
if __name__ == "__main__":
|
||||
from project_env import ensure_utf8_console
|
||||
ensure_utf8_console()
|
||||
folder_path = "game_records" # 替换为实际的文件夹路径
|
||||
stats, win_rates, game_count, player_names = analyze_game_records(folder_path)
|
||||
print_statistics(stats, win_rates, game_count, player_names)
|
||||
@@ -122,6 +122,8 @@ def process_game_records(input_directory, output_directory):
|
||||
print(f"已生成:{txt_file_path}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
from project_env import ensure_utf8_console
|
||||
ensure_utf8_console()
|
||||
game_records_directory = 'game_records'
|
||||
output_directory = 'converted_game_records' # 新的输出目录
|
||||
process_game_records(game_records_directory, output_directory)
|
||||
@@ -39,6 +39,8 @@ def parse_arguments():
|
||||
return parser.parse_args()
|
||||
|
||||
if __name__ == '__main__':
|
||||
from project_env import ensure_utf8_console
|
||||
ensure_utf8_console()
|
||||
# 解析命令行参数
|
||||
args = parse_arguments()
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import json
|
||||
import re
|
||||
from typing import List, Dict
|
||||
from llm_client import LLMClient
|
||||
from project_env import ensure_utf8_console
|
||||
ensure_utf8_console()
|
||||
|
||||
RULE_BASE_PATH = "prompt/rule_base.txt"
|
||||
PLAY_CARD_PROMPT_TEMPLATE_PATH = "prompt/play_card_prompt_template.txt"
|
||||
|
||||
@@ -191,4 +191,7 @@ input_dir = "game_records" # 包含JSON文件的文件夹
|
||||
output_dir = "matchup_records" # 输出文件夹
|
||||
|
||||
# 处理所有JSON文件
|
||||
from project_env import ensure_utf8_console
|
||||
ensure_utf8_console()
|
||||
|
||||
process_all_json_files(input_dir, output_dir)
|
||||
@@ -0,0 +1,19 @@
|
||||
"""專案共用小工具:主控台輸出編碼處理。"""
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
def ensure_utf8_console() -> None:
|
||||
"""將 stdout/stderr 切為 UTF-8,避免在 Big5/cp950 主控台輸出中文時崩潰。"""
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
reconfigure = getattr(stream, "reconfigure", None)
|
||||
if reconfigure is not None:
|
||||
try:
|
||||
reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ensure_utf8_console()
|
||||
print("console utf-8 ready")
|
||||
@@ -0,0 +1,22 @@
|
||||
# 前端建置輔助腳本(使用 Codex 桌面自帶的 node,不需 npm)
|
||||
# 用法: .\scripts\build_frontend.ps1 # production build
|
||||
# .\scripts\build_frontend.ps1 -Dev # dev server (vite)
|
||||
# 注意: esbuild 會掃描目錄樹;在受限環境(沙盒)執行失敗時,
|
||||
# 請改用一般終端機執行本腳本。
|
||||
param([switch]$Dev)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$node = 'C:\Users\ckliu\.cache\codex-runtimes\codex-primary-runtime\dependencies\node\bin\node.exe'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$client = Join-Path $root 'client'
|
||||
if (-not (Test-Path $node)) { throw "找不到 node: $node" }
|
||||
Push-Location $client
|
||||
try {
|
||||
if ($Dev) {
|
||||
& $node node_modules\vite\bin\vite.js
|
||||
} else {
|
||||
& $node node_modules\vite\bin\vite.js build
|
||||
}
|
||||
exit $LASTEXITCODE
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# 一鍵:暫存本次所有修改並 commit(需在一般終端機、具 .git 寫入權限)
|
||||
# 用法:powershell -ExecutionPolicy Bypass -File scripts\commit_current.ps1
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-Location (Join-Path $PSScriptRoot '..')
|
||||
|
||||
# 載入 runtime git(若尚未在 PATH)
|
||||
$git = Get-Command git -ErrorAction SilentlyContinue
|
||||
if (-not $git) {
|
||||
$runtimeGit = 'C:\Users\ckliu\.cache\codex-runtimes\codex-primary-runtime\dependencies\native\git\cmd\git.exe'
|
||||
if (Test-Path $runtimeGit) { $gitCmd = $runtimeGit } else { throw '找不到 git' }
|
||||
} else { $gitCmd = $git.Source }
|
||||
|
||||
Write-Host '== 確認待 commit 內容 =='
|
||||
& $gitCmd status --short
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '== git add -A =='
|
||||
& $gitCmd add -A
|
||||
if ($LASTEXITCODE -ne 0) { throw 'git add 失敗' }
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '== git commit =='
|
||||
& $gitCmd commit -m "Fix info leak in web game + add handover notes"
|
||||
if ($LASTEXITCODE -ne 0) { throw 'git commit 失敗' }
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '== 完成 =='
|
||||
& $gitCmd log --oneline -1
|
||||
@@ -0,0 +1,54 @@
|
||||
# ============================================================
|
||||
# Liars-Bar-LLM 專案環境載入腳本(僅影響當前 session)
|
||||
# 用途:把 Codex 桌面自帶的既有工具鏈掛進「本次 session」的 PATH。
|
||||
# 不做任何系統層級變更(不寫註冊表、不寫系統環境變數、不安裝任何東西)。
|
||||
# 用法:在專案根目錄執行 .\scripts\env.ps1
|
||||
# ============================================================
|
||||
|
||||
# Codex 桌面 runtime 根目錄(若 Codex 更新版本、路徑改變,可改這裡或改指向 hermes)
|
||||
$runtimeRoot = 'C:\Users\ckliu\.cache\codex-runtimes\codex-primary-runtime\dependencies'
|
||||
|
||||
$toolDirs = @(
|
||||
(Join-Path $runtimeRoot 'native\git\cmd'), # git
|
||||
(Join-Path $runtimeRoot 'node\bin'), # node / npm
|
||||
(Join-Path $runtimeRoot 'python'), # python
|
||||
(Join-Path $runtimeRoot 'bin\fallback') # pnpm 等
|
||||
)
|
||||
|
||||
$missing = @()
|
||||
foreach ($dir in $toolDirs) {
|
||||
if (Test-Path -LiteralPath $dir) {
|
||||
if (($env:PATH -split ';') -notcontains $dir) {
|
||||
$env:PATH = "$dir;$env:PATH"
|
||||
}
|
||||
} else {
|
||||
$missing += $dir
|
||||
}
|
||||
}
|
||||
|
||||
if ($missing.Count -gt 0) {
|
||||
Write-Host "警告:以下工具目錄不存在(Codex 可能已更新路徑):" -ForegroundColor Yellow
|
||||
foreach ($m in $missing) { Write-Host " $m" -ForegroundColor Yellow }
|
||||
Write-Host "替代方案:改用 hermes 工具鏈(C:\Users\ckliu\AppData\Local\hermes\git 與 ...\node)。" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "環境載入完成(session 限定,未變更系統設定):" -ForegroundColor Green
|
||||
foreach ($cmd in @('git','node','python','pnpm')) {
|
||||
$bin = Get-Command $cmd -ErrorAction SilentlyContinue
|
||||
if ($bin) {
|
||||
$ver = try { & $cmd --version 2>&1 | Select-Object -First 1 } catch { '?' }
|
||||
Write-Host (" {0,-8} {1}" -f $cmd, $ver)
|
||||
} else {
|
||||
Write-Host (" {0,-8} <找不到>" -f $cmd) -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# 專案 venv 提示
|
||||
$venvPython = Join-Path (Get-Location) '.venv\Scripts\python.exe'
|
||||
if (Test-Path -LiteralPath $venvPython) {
|
||||
$pyVer = & $venvPython --version 2>$null
|
||||
Write-Host (" 專案 venv {0} ({1})" -f $venvPython, $pyVer) -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " 專案 venv 尚未建立(可用 .\scripts\env.ps1 掛載後執行 python -m venv .venv)" -ForegroundColor Yellow
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
# 唯讀診斷:fetch 遠端並比較本地/遠端差異(不做任何改寫)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-Location (Join-Path $PSScriptRoot '..')
|
||||
|
||||
$git = Get-Command git -ErrorAction SilentlyContinue
|
||||
if (-not $git) {
|
||||
$git = 'C:\Users\ckliu\.cache\codex-runtimes\codex-primary-runtime\dependencies\native\git\cmd\git.exe'
|
||||
} else { $git = $git.Source }
|
||||
|
||||
Write-Host '== [1] fetch 遠端(不 merge)=='
|
||||
& $git fetch karylab feat/web-game
|
||||
if ($LASTEXITCODE -ne 0) { throw 'fetch 失敗' }
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '== [2] 本地奇怪的近 5 筆 =='
|
||||
& $git log --oneline -5 feat/web-game
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '== [3] 遠端奇怪的近 5 筆(karylab/feat/web-game)=='
|
||||
& $git log --oneline -5 karylab/feat/web-game
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '== [4] 兩者共同祖先 =='
|
||||
& $git merge-base feat/web-game karylab/feat/web-game
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '== [5] 遠端有、本地沒有(會被 pull 進來)=='
|
||||
& $git log --oneline --left-right --cherry-pick feat/web-game...karylab/feat/web-game
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '== [6] 檔案層級差異 =='
|
||||
& $git diff --stat feat/web-game karylab/feat/web-game
|
||||
@@ -0,0 +1,34 @@
|
||||
# 一鍵:commit 未提交變更 + push 到 karylab_agents fork(remote: karylab)
|
||||
# 用法:powershell -ExecutionPolicy Bypass -File scripts\push_current.ps1
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-Location (Join-Path $PSScriptRoot '..')
|
||||
|
||||
$git = Get-Command git -ErrorAction SilentlyContinue
|
||||
if (-not $git) {
|
||||
$runtimeGit = 'C:\Users\ckliu\.cache\codex-runtimes\codex-primary-runtime\dependencies\native\git\cmd\git.exe'
|
||||
if (Test-Path $runtimeGit) { $gitCmd = $runtimeGit } else { throw '找不到 git' }
|
||||
} else { $gitCmd = $git.Source }
|
||||
|
||||
Write-Host '== 目前狀態 =='
|
||||
& $gitCmd status --short
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '== 若有變更則提交 =='
|
||||
$dirty = (& $gitCmd status --porcelain).Trim() -ne ''
|
||||
if ($dirty) {
|
||||
& $gitCmd add -A
|
||||
if ($LASTEXITCODE -ne 0) { throw 'git add 失敗' }
|
||||
& $gitCmd commit -m "Fix playerJoined roomId + add /api proxy to dist server"
|
||||
if ($LASTEXITCODE -ne 0) { throw 'git commit 失敗' }
|
||||
} else {
|
||||
Write-Host '(無未提交變更)'
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '== Push 到 fork(karylab -> karylab_agents)=='
|
||||
& $gitCmd push karylab feat/web-game
|
||||
if ($LASTEXITCODE -ne 0) { throw 'git push 失敗' }
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '== 完成 =='
|
||||
& $gitCmd log --oneline -3
|
||||
@@ -0,0 +1,34 @@
|
||||
# One-shot: commit pending changes, then force-with-lease push feat/web-game to fork (remote: karylab).
|
||||
# --force-with-lease fails instead of overwriting if the remote moved since our fetch; never use plain -f.
|
||||
# Usage: powershell -ExecutionPolicy Bypass -File scripts\push_force_feat_web_game.ps1
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-Location (Join-Path $PSScriptRoot '..')
|
||||
|
||||
$git = Get-Command git -ErrorAction SilentlyContinue
|
||||
if (-not $git) {
|
||||
$git = 'C:\Users\ckliu\.cache\codex-runtimes\codex-primary-runtime\dependencies\native\git\cmd\git.exe'
|
||||
} else { $git = $git.Source }
|
||||
|
||||
Write-Host '== Status before =='
|
||||
& $git status --short
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '== Commit pending changes if any =='
|
||||
$porcelain = (& $git status --porcelain).Trim()
|
||||
if ($porcelain -ne '') {
|
||||
& $git add -A
|
||||
if ($LASTEXITCODE -ne 0) { throw 'git add failed' }
|
||||
& $git commit -m 'chore(scripts): ps1 UTF-8 BOM encoding + push helper scripts'
|
||||
if ($LASTEXITCODE -ne 0) { throw 'git commit failed' }
|
||||
} else {
|
||||
Write-Host '(no pending changes)'
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '== Force-with-lease push (fails instead of overwriting if remote moved) =='
|
||||
& $git push --force-with-lease karylab feat/web-game
|
||||
if ($LASTEXITCODE -ne 0) { throw 'push failed (remote may have moved; do not use plain -f)' }
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '== Done =='
|
||||
& $git log --oneline -1
|
||||
@@ -0,0 +1,47 @@
|
||||
# 專案全量測試:Python + Node 引擎 + 後端 REST + 前端語法
|
||||
# 用法:.\scripts\run_all_tests.ps1
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$py = Join-Path $root '.venv\Scripts\python.exe'
|
||||
$node = 'C:\Users\ckliu\.cache\codex-runtimes\codex-primary-runtime\dependencies\node\bin\node.exe'
|
||||
|
||||
Set-Location $root
|
||||
|
||||
Write-Host "`n===== [1/4] Python 語法檢查 ====="
|
||||
& $py -m py_compile game.py player.py game_record.py llm_client.py multi_game_runner.py game_analyze.py player_matchup_analyze.py json_convert.py project_env.py
|
||||
if ($LASTEXITCODE -eq 0) { Write-Host " 語法 OK" } else { Write-Host " 語法失敗" }
|
||||
|
||||
Write-Host "`n===== [2/4] Python 引擎冒煙(mock LLM,不連網)====="
|
||||
& $py scripts\smoke_test_python.py
|
||||
|
||||
Write-Host "`n===== [3/4] Node 引擎單元測試 ====="
|
||||
& $node scripts\test_engine.js
|
||||
|
||||
Write-Host "`n===== [3.5/4] Node 後端 REST 冒煙 ====="
|
||||
& $node scripts\smoke_test_server.js
|
||||
|
||||
Write-Host "`n===== [3.75/4] Node Socket e2e + reconnect ====="
|
||||
& $node scripts\run_e2e_local.js
|
||||
|
||||
|
||||
Write-Host "`n===== [4/4] 前端 SFC 語法 ====="
|
||||
Push-Location client
|
||||
& $node -e "
|
||||
const { parse, compileScript, compileTemplate } = require('@vue/compiler-sfc');
|
||||
const fs = require('fs');
|
||||
let failed = 0;
|
||||
for (const f of ['src/components/GameTable.vue','src/components/Lobby.vue','src/App.vue']) {
|
||||
const src = fs.readFileSync(f, 'utf8');
|
||||
const { descriptor, errors } = parse(src, { filename: f });
|
||||
let errs = errors.map(e=>e.message||e);
|
||||
if (!errs.length) { try { compileScript(descriptor, { id: 'x1' }); } catch (e) { errs.push('script: '+e.message); } }
|
||||
if (descriptor.template) { const t = compileTemplate({ source: descriptor.template.content, filename: f, id: 'x2' }); if (t.errors.length) errs.push('template: '+t.errors.map(x=>x.message||x).join('|')); }
|
||||
if (errs.length) { console.log('FAIL', f, errs.join('\n ')); failed++; } else { console.log('OK ', f); }
|
||||
}
|
||||
process.exit(failed?1:0);
|
||||
"
|
||||
$code = $LASTEXITCODE
|
||||
Pop-Location
|
||||
|
||||
Write-Host "`n===== 完成(exit=$code)====="
|
||||
exit $code
|
||||
@@ -0,0 +1,20 @@
|
||||
// 單進程端對端測試:載入 server(自動監聽 3000)→ 依序跑 e2e 與重連/再來一局測試
|
||||
// 用法:node scripts/run_e2e_local.js(在專案根目錄執行)
|
||||
require('../server/index.js');
|
||||
console.log('[runner] server 啟動中...');
|
||||
setTimeout(() => {
|
||||
const { execFile } = require('child_process');
|
||||
const run = (f) => new Promise((resolve) => {
|
||||
console.log(`\n[runner] 開始 ${f} ...`);
|
||||
const p = execFile(process.execPath, [f], { cwd: __dirname }, (err, stdout, stderr) => {
|
||||
process.stdout.write(stdout);
|
||||
if (stderr) process.stderr.write(stderr);
|
||||
resolve(err ? 1 : 0);
|
||||
});
|
||||
});
|
||||
(async () => {
|
||||
const code1 = await run('./test_socket_e2e.js');
|
||||
const code2 = await run('./test_reconnect.js');
|
||||
process.exit(code1 || code2);
|
||||
})();
|
||||
}, 1500);
|
||||
@@ -0,0 +1,55 @@
|
||||
// 極簡靜態伺服器:服務 client/dist(前端 build 產物),並把 /api 代理到後端 :3000
|
||||
// 用法:node scripts/serve_dist.js [port] (預設 5173)
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const port = parseInt(process.argv[2] || '5173', 10);
|
||||
const backendPort = parseInt(process.argv[3] || '3000', 10);
|
||||
const distDir = path.resolve(__dirname, '../client/dist');
|
||||
const types = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.js': 'application/javascript; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
'.map': 'application/json'
|
||||
};
|
||||
|
||||
if (!fs.existsSync(distDir)) {
|
||||
console.error('找不到 client/dist,請先執行 scripts/build_frontend.ps1');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// /api -> 後端 proxy
|
||||
function proxyApi(req, res) {
|
||||
const proxyReq = http.request({
|
||||
host: 'localhost',
|
||||
port: backendPort,
|
||||
path: req.url,
|
||||
method: req.method,
|
||||
headers: Object.assign({}, req.headers, { host: 'localhost:' + backendPort })
|
||||
}, (proxyRes) => {
|
||||
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||
proxyRes.pipe(res);
|
||||
});
|
||||
proxyReq.on('error', (e) => { res.writeHead(502); res.end('Bad Gateway: ' + e.message); });
|
||||
req.pipe(proxyReq);
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.url.startsWith('/api')) { proxyApi(req, res); return; }
|
||||
let urlPath = decodeURIComponent(req.url.split('?')[0]);
|
||||
if (urlPath === '/') urlPath = '/index.html';
|
||||
const filePath = path.join(distDir, urlPath);
|
||||
if (!filePath.startsWith(distDir)) { res.writeHead(403); res.end('Forbidden'); return; }
|
||||
if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) { res.writeHead(404); res.end('Not Found'); return; }
|
||||
res.writeHead(200, { 'Content-Type': types[path.extname(filePath)] || 'application/octet-stream' });
|
||||
fs.createReadStream(filePath).pipe(res);
|
||||
});
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log('靜態伺服器啟動於 http://localhost:' + port + '(服務 client/dist,/api 代理至 :' + backendPort + ')');
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Python 引擎冒煙測試(不連網、不呼叫真實 LLM)。
|
||||
|
||||
以「模擬 LLM」取代 API 呼叫,跑完整場騙子酒館遊戲,驗證
|
||||
game.py / player.py / game_record.py 主流程可正常完成並產生記錄。
|
||||
使用方式:
|
||||
.venv\\Scripts\\python.exe scripts\\smoke_test_python.py
|
||||
(若主控台遇到簡體中文編碼錯誤,請先設定 $env:PYTHONIOENCODING="utf-8")
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import random
|
||||
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, REPO_ROOT)
|
||||
|
||||
from game import Game
|
||||
|
||||
|
||||
class MockChat:
|
||||
"""附著在某位玩家身上的假 LLM:依提示內容回傳格式正確的 JSON。
|
||||
注意:chat() 統一回傳 (content, reasoning_content) 二元素 tuple。
|
||||
"""
|
||||
|
||||
def __init__(self, player):
|
||||
self.player = player
|
||||
self.decision_count = 0
|
||||
|
||||
def chat(self, messages, model=None):
|
||||
prompt = messages[0]["content"] if messages else ""
|
||||
if "played_cards" in prompt:
|
||||
return self._play_reply(), ""
|
||||
if "was_challenged" in prompt:
|
||||
return self._challenge_reply(), ""
|
||||
return "mock opinion: \u8a72\u73a9\u5bb6\u76ee\u524d\u7121\u660e\u986f\u50be\u5411\u3002", ""
|
||||
|
||||
def _play_reply(self):
|
||||
"""\u7d66\u4e86\u81ea\u5df1\u7684\u624b\u724c\uff1a\u540c\u9ede\u6578 >=2 \u51fa\u6700\u591a 3 \u5f35\uff0c\u5426\u5247\u51fa 1 \u5f35\u3002"""
|
||||
hand = sorted(self.player.hand)
|
||||
cards = [hand[0]]
|
||||
for c in hand[1:3]:
|
||||
if c == cards[0]:
|
||||
cards.append(c)
|
||||
cards_json = ",".join('"%s"' % c for c in cards)
|
||||
return ('{"played_cards": [%s], "behavior": "\u7a69\u5065", '
|
||||
'"play_reason": "mock heuristic"}' % cards_json)
|
||||
|
||||
def _challenge_reply(self):
|
||||
"""\u6bcf 4 \u6b21\u8cea\u7591\u6c7a\u7b56\u5c31\u8cea\u7591\u4e00\u6b21\uff0c\u8986\u84cb\u6210\u529f/\u5931\u6557\u5169\u689d\u8def\u5f91\u3002"""
|
||||
self.decision_count += 1
|
||||
do_challenge = (self.decision_count % 4 == 0)
|
||||
return ('{"was_challenged": %s, "challenge_reason": "mock trigger"}' %
|
||||
("true" if do_challenge else "false"))
|
||||
|
||||
|
||||
def main():
|
||||
random.seed(20260803)
|
||||
player_configs = [
|
||||
{"name": "DeepSeek", "model": "mock"},
|
||||
{"name": "GPT", "model": "mock"},
|
||||
{"name": "Claude", "model": "mock"},
|
||||
{"name": "Gemini", "model": "mock"},
|
||||
]
|
||||
|
||||
game = Game(player_configs)
|
||||
for p in game.players:
|
||||
p.llm_client = MockChat(p)
|
||||
|
||||
max_plays = 400
|
||||
for step in range(max_plays):
|
||||
if game.game_over:
|
||||
break
|
||||
game.play_round()
|
||||
|
||||
if game.game_over:
|
||||
print("[OK] \u904a\u6232\u7d50\u675f\uff0c\u4e3b\u6d41\u7a0b\u53ef\u6b63\u5e38\u5b8c\u6210\u3002")
|
||||
else:
|
||||
print(f"[FAIL] \u8dd1\u4e86 {max_plays} \u6b65\u4ecd\u672a\u7d50\u675f\uff08\u53ef\u80fd\u89c4\u5247\u6709\u6b7b\u7d50\uff09\u3002 rounds={game.round_count}")
|
||||
return 1
|
||||
|
||||
rec = game.game_record
|
||||
rounds = len(rec.rounds)
|
||||
alive = [p.name for p in game.players if p.alive]
|
||||
print(f"[OK] \u7372\u52dd\u8005={rec.winner} | \u5b58\u6d3b\u73a9\u5bb6={alive}")
|
||||
print(f"[OK] \u8a18\u9304\u56de\u5408\u6578={rounds}")
|
||||
|
||||
import glob
|
||||
files = glob.glob(os.path.join(REPO_ROOT, "game_records", "*.json"))
|
||||
print(f"[OK] game_records \u7522\u51fa {len(files)} \u500b JSON \u8a18\u9304\u6a94")
|
||||
if files:
|
||||
newest = max(files, key=os.path.getmtime)
|
||||
print(f" \u6700\u65b0\u8a18\u9304: {os.path.basename(newest)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,33 @@
|
||||
// 後端冒煙測試:載入 index.js(自動監聽 3000)-> 測 REST 路由 -> 優雅關閉。
|
||||
// 使用方式: node scripts/smoke_test_server.js (需在專案根目錄執行)
|
||||
const { app, io } = require("../server/index.js");
|
||||
|
||||
async function run() {
|
||||
let exitCode = 0;
|
||||
try {
|
||||
const listRes = await fetch("http://localhost:3000/api/rooms");
|
||||
const listBody = await listRes.json();
|
||||
console.log("[OK] GET /api/rooms ->", listRes.status, JSON.stringify(listBody));
|
||||
|
||||
const createRes = await fetch("http://localhost:3000/api/rooms", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ hostId: "test-host-1" }),
|
||||
});
|
||||
const room = await createRes.json();
|
||||
console.log("[OK] POST /api/rooms ->", createRes.status,
|
||||
"roomId=", room.id, "players=", room.players.length, "status=", room.status);
|
||||
} catch (e) {
|
||||
console.error("[FAIL]", e.message);
|
||||
exitCode = 1;
|
||||
}
|
||||
|
||||
io.close(() => {
|
||||
console.log(exitCode === 0 ? "[OK] 後端可正常啟動與回應 REST API" : "[FAIL] 後端測試失敗");
|
||||
process.exit(exitCode);
|
||||
});
|
||||
// 保險:2 秒後強制退出,避免卡住
|
||||
setTimeout(() => process.exit(exitCode), 2500);
|
||||
}
|
||||
|
||||
setTimeout(run, 1500);
|
||||
@@ -0,0 +1,35 @@
|
||||
// Socket.IO 端對端冒煙測試:連上後端 -> createRoom -> 收到 roomCreated 即成功。
|
||||
// 使用:需先有伺服器在 http://localhost:3000 執行。
|
||||
const { io } = require("../client/node_modules/socket.io-client");
|
||||
|
||||
const socket = io("http://localhost:3000", {
|
||||
transports: ["websocket"],
|
||||
reconnection: false,
|
||||
timeout: 6000,
|
||||
});
|
||||
|
||||
let done = false;
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log("[OK] socket connected, id =", socket.id);
|
||||
socket.emit("createRoom");
|
||||
});
|
||||
|
||||
socket.on("roomCreated", (room) => {
|
||||
console.log("[OK] roomCreated -> roomId=", room.id, "status=", room.status, "players=", room.players.length);
|
||||
done = true;
|
||||
socket.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
socket.on("connect_error", (e) => {
|
||||
console.error("[FAIL] connect_error:", e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (!done) {
|
||||
console.error("[FAIL] 8 秒內未收到 roomCreated(逾時)");
|
||||
process.exit(1);
|
||||
}
|
||||
}, 8000);
|
||||
@@ -0,0 +1,217 @@
|
||||
// 引擎單元測試(用 Node 內建 assert,無外部依賴)
|
||||
// 使用:node scripts/test_engine.js
|
||||
const assert = require('assert');
|
||||
const GameEngine = require('../server/game/engine.js');
|
||||
|
||||
const results = [];
|
||||
function test(name, fn) {
|
||||
try { fn(); results.push(['ok', name]); }
|
||||
catch (e) { results.push(['fail', name, e.message]); }
|
||||
}
|
||||
|
||||
// 1) 發牌與牌組
|
||||
test('init 4 人:每人 5 張、目標牌正確、round=1', () => {
|
||||
const e = new GameEngine();
|
||||
const st = e.init(['p1', 'p2', 'p3', 'p4']);
|
||||
assert.strictEqual(st.players.length, 4);
|
||||
for (const p of st.players) assert.strictEqual(p.handSize, 5);
|
||||
assert.ok(['Q', 'K', 'A'].includes(st.targetCard));
|
||||
assert.strictEqual(st.roundNumber, 1);
|
||||
assert.ok(st.currentPlayerId);
|
||||
});
|
||||
|
||||
test('init 8 人:每人 5 張', () => {
|
||||
const e = new GameEngine();
|
||||
const st = e.init(Array.from({ length: 8 }, (_, i) => 'p' + i));
|
||||
assert.strictEqual(st.players.length, 8);
|
||||
for (const p of st.players) assert.strictEqual(p.handSize, 5);
|
||||
});
|
||||
|
||||
test('牌組總張數 = 人數 x5(2/3/5/8)', () => {
|
||||
for (const n of [2, 3, 5, 8]) {
|
||||
const e = new GameEngine();
|
||||
const deck = e.createDeck(n);
|
||||
assert.strictEqual(deck.length, n * 5, `n=${n}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 2) 出牌驗證
|
||||
test('出牌:0 張 → 錯誤', () => {
|
||||
const e = new GameEngine(); e.init(['a', 'b']);
|
||||
assert.ok(e.playCard(e.currentPlayerId, []).error);
|
||||
});
|
||||
test('出牌:4 張 → 錯誤', () => {
|
||||
const e = new GameEngine(); e.init(['a', 'b']);
|
||||
assert.ok(e.playCard(e.currentPlayerId, [0, 1, 2, 3]).error);
|
||||
});
|
||||
test('出牌:索引超出範圍 → 錯誤', () => {
|
||||
const e = new GameEngine(); e.init(['a', 'b']);
|
||||
assert.ok(e.playCard(e.currentPlayerId, [99]).error);
|
||||
});
|
||||
test('出牌:重複索引 → 錯誤', () => {
|
||||
const e = new GameEngine(); e.init(['a', 'b']);
|
||||
assert.ok(e.playCard(e.currentPlayerId, [0, 0]).error);
|
||||
});
|
||||
test('出牌:非你的回合 → 錯誤', () => {
|
||||
const e = new GameEngine(); e.init(['a', 'b']);
|
||||
const other = Array.from(e.players.keys()).find((k) => k !== e.currentPlayerId);
|
||||
assert.ok(e.playCard(other, [0]).error);
|
||||
});
|
||||
test('出牌:合法出 1-3 張並移除、換下一位', () => {
|
||||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||||
const pid = e.currentPlayerId;
|
||||
const before = e.getPlayerHand(pid).length;
|
||||
const r = e.playCard(pid, [0]);
|
||||
assert.strictEqual(r.error, undefined);
|
||||
assert.strictEqual(e.getPlayerHand(pid).length, before - 1);
|
||||
assert.strictEqual(r.nextPlayerId, e.currentPlayerId);
|
||||
});
|
||||
|
||||
// 3) 質疑:整組判定
|
||||
test('質疑:吹牛組(含非目標牌)→ 質疑成功,出牌者開槍', () => {
|
||||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||||
e.targetCard = 'A';
|
||||
e.getPlayer('a').hand = ['A', 'K', 'JOKER'];
|
||||
e.getPlayer('b').hand = ['A', 'A', 'A'];
|
||||
e.getPlayer('c').hand = ['A', 'A'];
|
||||
e.currentPlayerId = 'a';
|
||||
const r = e.playCard('a', [0, 1]); // A, K
|
||||
assert.strictEqual(r.error, undefined);
|
||||
const c = e.challenge('b');
|
||||
assert.strictEqual(c.isBluff, true);
|
||||
assert.strictEqual(c.shooter, 'a');
|
||||
});
|
||||
|
||||
test('質疑:真牌組(全目標牌)→ 質疑失敗,質疑者開槍', () => {
|
||||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||||
e.targetCard = 'A';
|
||||
e.getPlayer('a').hand = ['A', 'Q', 'Q'];
|
||||
e.getPlayer('b').hand = ['A', 'A', 'A'];
|
||||
e.getPlayer('c').hand = ['A', 'A'];
|
||||
e.currentPlayerId = 'a';
|
||||
e.playCard('a', [0]); // A
|
||||
const c = e.challenge('b');
|
||||
assert.strictEqual(c.isBluff, false);
|
||||
assert.strictEqual(c.shooter, 'b');
|
||||
});
|
||||
|
||||
test('質疑:Joker 視為萬能牌 → 質疑失敗', () => {
|
||||
// 用 3 人局:a 出 JOKER 後仍有玩家( c )持牌,b 才能主動質疑
|
||||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||||
e.targetCard = 'K';
|
||||
e.getPlayer('a').hand = ['JOKER', 'Q'];
|
||||
e.getPlayer('b').hand = ['K', 'K'];
|
||||
e.getPlayer('c').hand = ['Q', 'Q', 'Q'];
|
||||
e.currentPlayerId = 'a';
|
||||
e.playCard('a', [0]); // JOKER(宣稱 K)
|
||||
const c = e.challenge('b');
|
||||
assert.strictEqual(c.isBluff, false); // JOKER 是萬能牌 → 質疑失敗
|
||||
});
|
||||
|
||||
// 3.5) 資訊邊界:公開狀態不洩漏實際牌值
|
||||
test('資訊邊界:出牌後 gameState 只含張數、不含真實牌值', () => {
|
||||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||||
const pid = e.currentPlayerId;
|
||||
e.playCard(pid, [0]);
|
||||
const st = e.getGameState();
|
||||
for (const g of st.playedGroups) {
|
||||
assert.strictEqual(typeof g.cardCount, 'number');
|
||||
assert.strictEqual(g.cards, undefined, 'playedGroups 不應洩漏 cards');
|
||||
}
|
||||
assert.ok(!Array.isArray(st.playedCards) || st.playedCards.length === 0, 'playedCards 不應含牌值');
|
||||
});
|
||||
test('質疑時才揭露真實牌值(challengedGroup)', () => {
|
||||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||||
e.targetCard = 'A';
|
||||
e.currentPlayerId = 'a';
|
||||
e.playCard('a', [0]);
|
||||
const lastGroup = e.playGroups[e.playGroups.length - 1];
|
||||
const before = [...lastGroup.cards]; // 引擎內部可讀
|
||||
const st = e.getGameState();
|
||||
// 公開狀態沒有牌值
|
||||
for (const g of st.playedGroups) assert.strictEqual(g.cards, undefined);
|
||||
const r = e.challenge('b');
|
||||
assert.ok(Array.isArray(r.challengedGroup) && r.challengedGroup.length === before.length,
|
||||
'質疑結果才揭露牌值');
|
||||
});
|
||||
|
||||
// 4) 系統質疑
|
||||
test('系統質疑:輪到某玩家且其他人都沒牌 → 自動打出並質疑', () => {
|
||||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||||
e.targetCard = 'A';
|
||||
e.getPlayer('a').hand = ['K', 'K'];
|
||||
e.getPlayer('b').hand = ['A'];
|
||||
e.getPlayer('c').hand = [];
|
||||
e.currentPlayerId = 'b';
|
||||
const r = e.playCard('b', [0]); // b 出 A,手牌清空
|
||||
assert.ok(r.systemChallenge, '應觸發系統質疑');
|
||||
assert.strictEqual(r.systemChallenge.playerId, 'a');
|
||||
assert.strictEqual(r.systemChallenge.isBluff, true); // K 不是 A
|
||||
assert.strictEqual(r.systemChallenge.shooter, 'a');
|
||||
});
|
||||
|
||||
test('系統質疑:若剩餘全為真牌 → 無人開槍並重開一手', () => {
|
||||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||||
e.targetCard = 'A';
|
||||
e.getPlayer('a').hand = ['A', 'A'];
|
||||
e.getPlayer('b').hand = ['A'];
|
||||
e.getPlayer('c').hand = [];
|
||||
e.currentPlayerId = 'b';
|
||||
const r = e.playCard('b', [0]);
|
||||
assert.ok(r.systemChallenge);
|
||||
assert.strictEqual(r.systemChallenge.isBluff, false);
|
||||
assert.strictEqual(r.systemChallenge.shooter, null);
|
||||
// 重開一手後 a 仍有 5 張
|
||||
assert.strictEqual(e.getPlayerHand('a').length, 5);
|
||||
});
|
||||
|
||||
// 5) 重開一手與死亡
|
||||
test('質疑後重開一手:存活者各 5 張、死掉者 0 張', () => {
|
||||
const e = new GameEngine(); e.init(['a', 'b', 'c']);
|
||||
e.targetCard = 'A';
|
||||
e.getPlayer('a').hand = ['A', 'K', 'JOKER'];
|
||||
e.getPlayer('b').hand = ['A', 'A', 'A'];
|
||||
e.getPlayer('c').hand = ['A', 'A'];
|
||||
e.currentPlayerId = 'a';
|
||||
e.playCard('a', [0, 1]); // A, K 吹牛
|
||||
// 讓 a 必死
|
||||
e.getPlayer('a').revolver = { chambers: [true, false, false, false, false, false], currentChamber: 0 };
|
||||
const c = e.challenge('b');
|
||||
assert.strictEqual(c.shooter, 'a');
|
||||
assert.strictEqual(e.getPlayer('a').alive, false);
|
||||
assert.strictEqual(e.getPlayerHand('a').length, 0);
|
||||
assert.strictEqual(e.getPlayerHand('b').length, 5);
|
||||
assert.ok(e.roundNumber >= 2);
|
||||
});
|
||||
|
||||
// 6) 完整模擬:必定會結束
|
||||
test('完整隨機模擬可正常結束,存活者 <= 1', () => {
|
||||
const e = new GameEngine();
|
||||
e.init(['p1', 'p2', 'p3', 'p4']);
|
||||
let guard = 0;
|
||||
while (!e.isGameOver && guard < 3000) {
|
||||
guard++;
|
||||
const pid = e.currentPlayerId;
|
||||
if (!pid) break;
|
||||
const hand = e.getPlayerHand(pid);
|
||||
if (hand.length === 0) break;
|
||||
const n = 1 + Math.floor(Math.random() * Math.min(3, hand.length));
|
||||
const idx = Array.from({ length: n }, (_, i) => i);
|
||||
e.playCard(pid, idx);
|
||||
if (Math.random() < 0.35 && e.playGroups.length > 0) {
|
||||
e.challenge(e.currentPlayerId);
|
||||
}
|
||||
}
|
||||
assert.ok(e.isGameOver || guard >= 3000 === false, '遊戲應結束(無死結)');
|
||||
assert.ok(e.getAlivePlayers().length <= 1, '最多一位存活');
|
||||
assert.ok(e.roundNumber > 0);
|
||||
});
|
||||
|
||||
// 印出結果
|
||||
let failed = 0;
|
||||
for (const [status, name, msg] of results) {
|
||||
if (status === 'ok') console.log(' PASS ' + name);
|
||||
else { failed++; console.log(' FAIL ' + name + ' -> ' + msg); }
|
||||
}
|
||||
console.log(`\n${results.length - failed}/${results.length} 通過`);
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,96 @@
|
||||
// 測試:RoomManager.restart(再來一局)+ 離線緩衝重連(rejoin)+ ack/去重
|
||||
// 跑法:先啟動 server(node server/index.js)再跑本檔:node scripts/test_reconnect.js
|
||||
const { io } = require('../client/node_modules/socket.io-client');
|
||||
const RoomManager = require('../server/game/room');
|
||||
|
||||
const URL = 'http://localhost:3000';
|
||||
let failures = 0;
|
||||
function ok(cond, msg) {
|
||||
if (cond) console.log(' PASS ' + msg);
|
||||
else { failures++; console.log(' FAIL ' + msg); }
|
||||
}
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
function connect(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const s = io(URL, { transports: ['websocket'], reconnection: false, timeout: 8000 });
|
||||
s._hand = null; s._name = name; s._err = null;
|
||||
s.on('connect', () => resolve(s));
|
||||
s.on('connect_error', (e) => reject(new Error(name + ' connect_error: ' + e.message)));
|
||||
s.on('yourHand', (h) => { s._hand = h || []; });
|
||||
s.on('error', (d) => { s._err = d && d.message; });
|
||||
});
|
||||
}
|
||||
function once(socket, ev, timeout = 8000) {
|
||||
return new Promise((resolve) => {
|
||||
const t = setTimeout(() => resolve(undefined), timeout);
|
||||
socket.once(ev, (d) => { clearTimeout(t); resolve(d); });
|
||||
});
|
||||
}
|
||||
function emitAck(s, ev, payload) {
|
||||
return new Promise((resolve) => { s.emit(ev, payload, (res) => resolve(res)); });
|
||||
}
|
||||
|
||||
(async () => {
|
||||
console.log('[A] RoomManager.restart(再來一局)');
|
||||
const rm = new RoomManager();
|
||||
const room = rm.createRoom('p1');
|
||||
rm.joinRoom(room.id, 'p2');
|
||||
const gs = rm.startGame(room.id).gameState;
|
||||
ok(gs && gs.roundNumber === 1, '初始開局 round=1');
|
||||
room.engine.isGameOver = true;
|
||||
rm.syncStatus(room.id);
|
||||
ok(room.status === 'ended', 'syncStatus: 自然分出勝負 → 房間 ended');
|
||||
const rr = rm.restart(room.id);
|
||||
ok(rr.success && rr.gameState.roundNumber === 1, 'restart 成功並 round=1');
|
||||
ok(!!rr.gameState.players && rr.gameState.players.length === 2, 'restart 後仍是 2 人');
|
||||
ok(rm.restart(room.id).error, '再次 restart(playing)→ 被拒');
|
||||
|
||||
console.log('[B] 重連(離線緩衝)');
|
||||
const a = await connect('A');
|
||||
const b = await connect('B');
|
||||
const created = await emitAck(a, 'createRoom', { requestId: 1 });
|
||||
ok(created.ok && created.room && created.room.id, 'createRoom ack ok, room=' + (created.room && created.room.id));
|
||||
const roomId = created.room.id;
|
||||
const joined = await emitAck(b, 'joinRoom', { roomId, requestId: 2 });
|
||||
ok(joined.ok, 'joinRoom ack ok');
|
||||
const st = await emitAck(a, 'startGame', { roomId, requestId: 3 });
|
||||
ok(st.ok, 'startGame ack ok');
|
||||
const dup = await emitAck(a, 'startGame', { roomId, requestId: 3 });
|
||||
ok(!!dup.error, '相同 requestId 重送 → 去重擋下');
|
||||
|
||||
const bId = b.id;
|
||||
const disconnEvt = once(a, 'playerDisconnected');
|
||||
b.close();
|
||||
const pd = await disconnEvt;
|
||||
ok(pd && pd.reconnecting === true, 'B 離線 → 顯示「等待重連」而非立刻結束');
|
||||
|
||||
const c = await connect('C');
|
||||
const resyncP = once(c, 'resync');
|
||||
const rejEvt = once(a, 'playerRejoined');
|
||||
const rej = await emitAck(c, 'rejoin', { roomId, playerId: bId, requestId: 4 });
|
||||
ok(rej.ok, 'rejoin ack ok');
|
||||
const res = await resyncP;
|
||||
ok(res && res.gameState && res.gameState.isGameOver === false, '重連後收到 resync 完整狀態');
|
||||
await rejEvt;
|
||||
await sleep(300);
|
||||
ok((c._hand || []).length === 5, '重連者拿回 5 張私密手牌');
|
||||
|
||||
const bad = await emitAck(c, 'rejoin', { roomId, playerId: 'nonexistent', requestId: 5 });
|
||||
ok(!!bad.error, '不存在的座位 → rejoin 被拒');
|
||||
const roomsRes = await fetch('http://localhost:3000/api/rooms').then((r) => r.json());
|
||||
const roomNow = roomsRes.find((x) => x.id === roomId);
|
||||
ok(roomNow && roomNow.players === 2, 'B 座位仍在(未離場,players=2)');
|
||||
|
||||
// rejoined seat must still be able to ACT (engine resolves seatId) and still receive hand broadcasts
|
||||
const curTurn = (res && res.gameState && res.gameState.currentPlayerId) || a.id;
|
||||
const actor = (curTurn === a.id) ? a : c;
|
||||
const upd = once(actor, 'yourHand');
|
||||
const ackAct = await emitAck(actor, 'playCard', { roomId, cardIndices: [0], requestId: 100 });
|
||||
ok(ackAct.ok, 'playCard after rejoin ok via seatId' + (ackAct.error ? ' -> ' + ackAct.error : ''));
|
||||
if (actor === c) { await upd; ok(Array.isArray(c._hand) && c._hand.length === 4, 'hand update after rejoin reaches rejoined seat (broadcastHands)'); }
|
||||
else { ok(Array.isArray(a._hand) && a._hand.length === 4, 'original seat hand updated after rejoin'); }
|
||||
a.close(); c.close();
|
||||
await sleep(200);
|
||||
console.log(failures === 0 ? '\n重連/再來一局測試全部通過' : `\n有 ${failures} 項失敗`);
|
||||
process.exit(failures ? 1 : 0);
|
||||
})().catch((e) => { console.error('[FATAL]', e.message); process.exit(1); });
|
||||
@@ -0,0 +1,109 @@
|
||||
// 多人端對端整合測試:連真實伺服器,驗證「多張出牌 → 下家挑戰 → 手牌更新」流程。
|
||||
// 使用:先啟動 server(node server/index.js),再跑 node scripts/test_socket_e2e.js
|
||||
const { io } = require('../client/node_modules/socket.io-client');
|
||||
|
||||
const URL = 'http://localhost:3000';
|
||||
const clients = {};
|
||||
|
||||
function connect(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const s = io(URL, { transports: ['websocket'], reconnection: false, timeout: 8000 });
|
||||
s._hand = null;
|
||||
s._name = name;
|
||||
s._lastError = null;
|
||||
s.on('connect', () => { clients[s.id] = s; resolve(s); });
|
||||
s.on('connect_error', (e) => reject(new Error(name + ' connect_error: ' + e.message)));
|
||||
s.on('yourHand', (h) => { s._hand = h || []; });
|
||||
s.on('error', (d) => { s._lastError = d && d.message; });
|
||||
});
|
||||
}
|
||||
|
||||
function once(socket, ev, timeout = 8000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const t = setTimeout(() => reject(new Error(`${socket._name} wait ${ev} timeout`)), timeout);
|
||||
socket.once(ev, (d) => { clearTimeout(t); resolve(d); });
|
||||
});
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
let failures = 0;
|
||||
function ok(cond, msg) {
|
||||
if (cond) console.log(' PASS ' + msg);
|
||||
else { failures++; console.log(' FAIL ' + msg); }
|
||||
}
|
||||
|
||||
(async () => {
|
||||
console.log('[1] 三玩家連線+房間');
|
||||
const a = await connect('A');
|
||||
const b = await connect('B');
|
||||
const c = await connect('C');
|
||||
|
||||
const roomCreated = once(a, 'roomCreated');
|
||||
a.emit('createRoom');
|
||||
const room = await roomCreated;
|
||||
const roomId = room.id;
|
||||
ok(roomId, '建立房間 roomId=' + roomId);
|
||||
|
||||
b.emit('joinRoom', { roomId });
|
||||
await once(a, 'playerJoined');
|
||||
c.emit('joinRoom', { roomId });
|
||||
await once(a, 'playerJoined');
|
||||
ok(true, 'B、C 加入');
|
||||
|
||||
console.log('[2] 開始遊戲 + 私密手牌');
|
||||
const started = [once(a, 'gameStarted'), once(b, 'gameStarted'), once(c, 'gameStarted')];
|
||||
a.emit('startGame', { roomId });
|
||||
const [gsA] = await Promise.all(started);
|
||||
ok(gsA.currentPlayerId && ['Q','K','A'].includes(gsA.targetCard), 'gameStarted 狀態正確');
|
||||
ok(gsA.players.length === 3, '3 位玩家');
|
||||
await sleep(300);
|
||||
ok(a._hand.length === 5 && b._hand.length === 5 && c._hand.length === 5,
|
||||
'每位玩家收到 5 張私密手牌(yourHand)');
|
||||
|
||||
console.log('[3] 目前玩家多張出牌');
|
||||
const cur = clients[gsA.currentPlayerId];
|
||||
ok(!!cur, '定位目前玩家');
|
||||
const playedEvt = once(a, 'cardPlayed');
|
||||
cur.emit('playCard', { roomId, cardIndices: [0, 1] });
|
||||
const played = await playedEvt;
|
||||
// 資訊邊界:cardPlayed 只公開張數,不洩漏實際牌值
|
||||
ok(played.playedGroup && played.playedGroup.cardCount === 2, '出牌 2 張並廣播(只含張數)');
|
||||
ok(!('cards' in (played.playedGroup || {})), 'cardPlayed 不洩漏實際牌值');
|
||||
const currentNow = played.gameState.currentPlayerId;
|
||||
ok(currentNow !== cur.id, '換下一位');
|
||||
await sleep(300);
|
||||
ok(cur._hand.length === 3, `出牌者剩 3 張 (yourHand 同步,實際=${cur._hand.length})`);
|
||||
|
||||
console.log('[4] 非我的回合出牌 → 被拒絕');
|
||||
const notTurn = [a, b, c].find((s) => s.id !== cur.id && s.id !== currentNow);
|
||||
ok(!!notTurn, '找到非回合玩家');
|
||||
notTurn._lastError = null;
|
||||
notTurn.emit('playCard', { roomId, cardIndices: [0] });
|
||||
await sleep(300);
|
||||
ok(!!notTurn._lastError, '非回合者被拒絕(' + (notTurn._lastError || 'none') + ')');
|
||||
|
||||
console.log('[5] 目前玩家(下家)質疑 → 收到結果與新狀態');
|
||||
const currentSocket = clients[currentNow];
|
||||
const challengedEvt = once(a, 'challenged');
|
||||
currentSocket.emit('challenge', { roomId });
|
||||
const ch = await challengedEvt;
|
||||
ok(ch.result && typeof ch.result.isBluff === 'boolean', '質疑結果帶 isBluff=' + ch.result.isBluff);
|
||||
ok(ch.result.shooter, '有開槍者(shooter=' + ch.result.shooter + ')');
|
||||
ok(ch.gameState.roundNumber >= 2, '質疑後進入下一局(round=' + ch.gameState.roundNumber + ')');
|
||||
await sleep(300);
|
||||
ok(ch.gameState.players.every((p) => {
|
||||
const sh = clients[p.id];
|
||||
return !sh || (sh._hand.length === p.handSize);
|
||||
}), 'yourHand 與公開狀態一致(' + ch.gameState.players.map((p) => p.handSize).join(',') + ')');
|
||||
|
||||
const aliveCount = ch.gameState.players.filter((p) => p.alive).length;
|
||||
console.log(`[6] 結果快照:第 ${ch.gameState.roundNumber} 局,存活 ${aliveCount}/3,isBluff=${ch.result.isBluff}`);
|
||||
|
||||
[a, b, c].forEach((s) => s.close());
|
||||
await sleep(300);
|
||||
console.log(failures === 0 ? '\n整合測試全部通過' : `\n有 ${failures} 項失敗`);
|
||||
process.exit(failures ? 1 : 0);
|
||||
})().catch((e) => {
|
||||
console.error('[FATAL]', e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,362 @@
|
||||
// 遊戲引擎核心 (Liar's Bar)
|
||||
// 規則與 Python 版 (game.py) 對齊:
|
||||
// - 每手每人 5 張,牌組依玩家人數等比擴充(Q:K:A 等量 + 約 10% Joker),支援 2-8 人
|
||||
// - 每次可出 1-3 張,宣稱皆為目標牌;Joker 為萬能牌
|
||||
// - 下一位玩家可選擇出牌或質疑「上一組」出的牌
|
||||
// - 質疑:整組牌全為目標牌/Joker 則質疑失敗(質疑者開槍),否則質疑成功(出牌者開槍)
|
||||
// - 質疑後本手結束,存活玩家重新發牌並選新目標牌
|
||||
// - 特殊:輪到某玩家時若其他存活玩家已無牌,該玩家人餘牌視為自動打出並受系統質疑
|
||||
// - 僅存一位存活者時結束
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
const HAND_SIZE = 5;
|
||||
const REVOLVER_CHAMBERS = 6;
|
||||
const TARGETS = ['Q', 'K', 'A'];
|
||||
const MAX_PLAY_PER_TURN = 3;
|
||||
|
||||
class GameEngine {
|
||||
constructor() {
|
||||
this.players = new Map(); // id -> { id, hand, revolver, alive }
|
||||
this.currentPlayerId = null;
|
||||
this.targetCard = null;
|
||||
this.playGroups = []; // 本手連續出牌組 [{ playerId, cards }]
|
||||
this.roundNumber = 0;
|
||||
this.isGameOver = false;
|
||||
this.winnerId = null;
|
||||
this.lastShooterId = null; // 上一手開槍者(存活時作為下一手起始)
|
||||
}
|
||||
|
||||
// ---------- 初始化 ----------
|
||||
|
||||
init(playerIds) {
|
||||
this.players.clear();
|
||||
this.currentPlayerId = null;
|
||||
this.targetCard = null;
|
||||
this.playGroups = [];
|
||||
this.roundNumber = 0;
|
||||
this.isGameOver = false;
|
||||
this.winnerId = null;
|
||||
this.lastShooterId = null;
|
||||
|
||||
for (const id of playerIds) {
|
||||
this.players.set(id, {
|
||||
id,
|
||||
hand: [],
|
||||
revolver: this.createRevolver(),
|
||||
alive: true
|
||||
});
|
||||
}
|
||||
|
||||
this.roundNumber = 1;
|
||||
this.dealCards(this.getAlivePlayers().length);
|
||||
this.setTargetCard();
|
||||
this.currentPlayerId = this.pickRandomAlivePlayer();
|
||||
return this.getGameState();
|
||||
}
|
||||
|
||||
// 依存活人數建立等比牌組(每人 5 張)
|
||||
createDeck(playerCount) {
|
||||
const total = playerCount * HAND_SIZE;
|
||||
const jokerCount = Math.max(1, Math.round(total * 0.1));
|
||||
const normalTotal = total - jokerCount;
|
||||
const base = Math.floor(normalTotal / 3);
|
||||
let remainder = normalTotal % 3;
|
||||
const counts = { Q: base, K: base, A: base };
|
||||
for (const key of TARGETS) {
|
||||
if (remainder > 0) { counts[key] += 1; remainder -= 1; }
|
||||
}
|
||||
const deck = [];
|
||||
for (const key of Object.keys(counts)) {
|
||||
for (let i = 0; i < counts[key]; i++) deck.push(key);
|
||||
}
|
||||
for (let i = 0; i < jokerCount; i++) deck.push('JOKER');
|
||||
this.shuffle(deck);
|
||||
return deck;
|
||||
}
|
||||
|
||||
// ---------- 基本工具 ----------
|
||||
|
||||
shuffle(deck) {
|
||||
for (let i = deck.length - 1; i > 0; i--) {
|
||||
const j = crypto.randomInt(i + 1);
|
||||
[deck[i], deck[j]] = [deck[j], deck[i]];
|
||||
}
|
||||
return deck;
|
||||
}
|
||||
|
||||
createRevolver() {
|
||||
const chambers = Array(REVOLVER_CHAMBERS).fill(false);
|
||||
chambers[crypto.randomInt(REVOLVER_CHAMBERS)] = true;
|
||||
return { chambers, currentChamber: 0 };
|
||||
}
|
||||
|
||||
setTargetCard() {
|
||||
this.targetCard = TARGETS[crypto.randomInt(TARGETS.length)];
|
||||
}
|
||||
|
||||
getAlivePlayers() {
|
||||
return Array.from(this.players.values()).filter((p) => p.alive);
|
||||
}
|
||||
|
||||
getPlayer(playerId) {
|
||||
return this.players.get(playerId);
|
||||
}
|
||||
|
||||
pickRandomAlivePlayer() {
|
||||
const alive = this.getAlivePlayers();
|
||||
return alive.length ? alive[crypto.randomInt(alive.length)].id : null;
|
||||
}
|
||||
|
||||
// 目前玩家的下一位「有牌且存活」的玩家
|
||||
getNextAlivePlayerWithCards() {
|
||||
const ids = Array.from(this.players.keys());
|
||||
if (ids.length === 0) return null;
|
||||
const curIdx = ids.indexOf(this.currentPlayerId);
|
||||
for (let i = 1; i <= ids.length; i++) {
|
||||
const player = this.players.get(ids[(curIdx + i) % ids.length]);
|
||||
if (player && player.alive && player.hand.length > 0) return player.id;
|
||||
}
|
||||
// 所有人手上都無牌:回傳目前玩家(由呼叫端處理)
|
||||
return this.currentPlayerId;
|
||||
}
|
||||
|
||||
isAllOthersEmpty(playerId) {
|
||||
return this.getAlivePlayers()
|
||||
.filter((p) => p.id !== playerId)
|
||||
.every((p) => p.hand.length === 0);
|
||||
}
|
||||
|
||||
// ---------- 回合流程 ----------
|
||||
|
||||
// 出牌:cardIndices 為 1-3 個手牌索引(此輪宣稱皆為目標牌)
|
||||
playCard(playerId, cardIndices) {
|
||||
if (this.isGameOver) return { error: '遊戲已結束' };
|
||||
const player = this.getPlayer(playerId);
|
||||
if (!player || !player.alive) return { error: '玩家已死亡' };
|
||||
if (playerId !== this.currentPlayerId) return { error: '還沒輪到你出牌' };
|
||||
|
||||
if (!Array.isArray(cardIndices) || cardIndices.length < 1 || cardIndices.length > MAX_PLAY_PER_TURN) {
|
||||
return { error: `每次只能出 1-${MAX_PLAY_PER_TURN} 張` };
|
||||
}
|
||||
const unique = new Set(cardIndices);
|
||||
if (unique.size !== cardIndices.length) {
|
||||
return { error: '不能重複選擇同一張牌' };
|
||||
}
|
||||
for (const idx of cardIndices) {
|
||||
if (!Number.isInteger(idx) || idx < 0 || idx >= player.hand.length) {
|
||||
return { error: '無效的選牌索引' };
|
||||
}
|
||||
}
|
||||
|
||||
const played = cardIndices.map((idx) => player.hand[idx]);
|
||||
// 由大到小移除,避免索引位移
|
||||
[...cardIndices].sort((a, b) => b - a).forEach((idx) => player.hand.splice(idx, 1));
|
||||
this.playGroups.push({ playerId: player.id, cards: played });
|
||||
|
||||
this.currentPlayerId = this.getNextAlivePlayerWithCards();
|
||||
|
||||
const baseResult = {
|
||||
success: true,
|
||||
playedGroup: { playerId: player.id, cards: played },
|
||||
nextPlayerId: this.currentPlayerId
|
||||
};
|
||||
|
||||
// 特殊規則:輪到 nextPlayer 時其他存活玩家已無牌 → 系統質疑(自動打出剩餘手牌)
|
||||
const allAliveEmpty = this.getAlivePlayers().every((p) => p.hand.length === 0);
|
||||
const othersEmpty = !allAliveEmpty
|
||||
&& this.currentPlayerId !== null
|
||||
&& this.getPlayer(this.currentPlayerId).hand.length > 0
|
||||
&& this.isAllOthersEmpty(this.currentPlayerId);
|
||||
|
||||
if (othersEmpty) {
|
||||
const sys = this.systemChallenge(this.currentPlayerId);
|
||||
baseResult.systemChallenge = sys;
|
||||
} else if (allAliveEmpty) {
|
||||
// 所有人同時無牌(理論上不會發生)→ 視為無效質疑,重開一手
|
||||
baseResult.autoRoundReset = true;
|
||||
this.startNewRound();
|
||||
}
|
||||
|
||||
return baseResult;
|
||||
}
|
||||
|
||||
// 系統質疑:自動打出某玩家剩餘手牌並質疑
|
||||
systemChallenge(playerId) {
|
||||
const player = this.getPlayer(playerId);
|
||||
const cards = [...player.hand];
|
||||
player.hand = [];
|
||||
const valid = cards.length > 0 && cards.every((c) => c === this.targetCard || c === 'JOKER');
|
||||
const isBluff = !valid;
|
||||
|
||||
let shooter = null;
|
||||
let shot = null;
|
||||
if (isBluff) {
|
||||
// 剩餘手牌有假牌 → 出牌者開槍
|
||||
shooter = playerId;
|
||||
shot = this.fireRevolver(playerId);
|
||||
} else {
|
||||
// 全是真牌 → 系統質疑失敗(與 Python 一致:無人開槍)
|
||||
shot = { playerId, hasBullet: false, alive: player.alive };
|
||||
}
|
||||
|
||||
this.clearTable();
|
||||
this.startNewRound();
|
||||
|
||||
return {
|
||||
systemChallenge: true,
|
||||
playerId,
|
||||
autoPlayedCards: cards,
|
||||
isBluff,
|
||||
shooter,
|
||||
shot
|
||||
};
|
||||
}
|
||||
|
||||
// 質疑上一組出的牌(由下一位玩家執行)
|
||||
challenge(challengerId) {
|
||||
if (this.isGameOver) return { error: '遊戲已結束' };
|
||||
const challenger = this.getPlayer(challengerId);
|
||||
if (!challenger || !challenger.alive) return { error: '挑戰者已死亡' };
|
||||
if (challengerId !== this.currentPlayerId) return { error: '還沒輪到你質疑' };
|
||||
|
||||
const last = this.playGroups[this.playGroups.length - 1];
|
||||
if (!last) return { error: '還沒有出牌可以質疑' };
|
||||
|
||||
const valid = last.cards.length > 0 && last.cards.every((c) => c === this.targetCard || c === 'JOKER');
|
||||
const isBluff = !valid;
|
||||
|
||||
const result = {
|
||||
challengerId,
|
||||
playerId: last.playerId,
|
||||
isBluff,
|
||||
challengedGroup: last.cards
|
||||
};
|
||||
|
||||
if (isBluff) {
|
||||
// 質疑成功 → 出牌者開槍
|
||||
result.shooter = last.playerId;
|
||||
result.shot = this.fireRevolver(last.playerId);
|
||||
} else {
|
||||
// 質疑失敗 → 質疑者開槍
|
||||
result.shooter = challengerId;
|
||||
result.shot = this.fireRevolver(challengerId);
|
||||
}
|
||||
|
||||
this.clearTable();
|
||||
// 質疑後本手結束,重開一手(若遊戲還未結束)
|
||||
this.startNewRound();
|
||||
return result;
|
||||
}
|
||||
|
||||
// 重開一手:重新發牌、選目標牌、決定起始玩家
|
||||
startNewRound() {
|
||||
const alive = this.getAlivePlayers();
|
||||
if (alive.length <= 1) {
|
||||
this.isGameOver = true;
|
||||
this.winnerId = alive.length === 1 ? alive[0].id : null;
|
||||
return;
|
||||
}
|
||||
this.roundNumber += 1;
|
||||
this.clearTable();
|
||||
this.setTargetCard();
|
||||
this.dealCards(alive.length);
|
||||
|
||||
// 起始玩家:上一手開槍者(若存活),否則隨機
|
||||
if (this.lastShooterId && this.getPlayer(this.lastShooterId) && this.getPlayer(this.lastShooterId).alive) {
|
||||
this.currentPlayerId = this.lastShooterId;
|
||||
} else {
|
||||
this.currentPlayerId = this.pickRandomAlivePlayer();
|
||||
}
|
||||
}
|
||||
|
||||
clearTable() {
|
||||
this.playGroups = [];
|
||||
}
|
||||
|
||||
dealCards(aliveCount) {
|
||||
for (const p of this.players.values()) p.hand = [];
|
||||
const deck = this.createDeck(aliveCount);
|
||||
for (let round = 0; round < HAND_SIZE; round++) {
|
||||
for (const id of this.players.keys()) {
|
||||
const p = this.players.get(id);
|
||||
if (p.alive && deck.length > 0) p.hand.push(deck.pop());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 射擊 / 結束 ----------
|
||||
|
||||
fireRevolver(playerId) {
|
||||
const player = this.getPlayer(playerId);
|
||||
if (!player) return null;
|
||||
const revolver = player.revolver;
|
||||
const hasBullet = revolver.chambers[revolver.currentChamber];
|
||||
revolver.currentChamber = (revolver.currentChamber + 1) % REVOLVER_CHAMBERS;
|
||||
|
||||
if (hasBullet) {
|
||||
player.alive = false;
|
||||
player.revolver = this.createRevolver(); // 死亡後重置左輪手槍
|
||||
}
|
||||
|
||||
this.lastShooterId = playerId;
|
||||
this.checkGameOver();
|
||||
|
||||
return { playerId, hasBullet, alive: player.alive, isGameOver: this.isGameOver };
|
||||
}
|
||||
|
||||
checkGameOver() {
|
||||
if (this.getAlivePlayers().length <= 1) {
|
||||
this.isGameOver = true;
|
||||
const alive = this.getAlivePlayers();
|
||||
this.winnerId = alive.length === 1 ? alive[0].id : null;
|
||||
}
|
||||
}
|
||||
|
||||
removePlayer(playerId) {
|
||||
const player = this.getPlayer(playerId);
|
||||
if (player) {
|
||||
player.alive = false;
|
||||
player.hand = [];
|
||||
this.checkGameOver();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 對外狀態 ----------
|
||||
|
||||
getGameState() {
|
||||
const state = {
|
||||
players: [],
|
||||
currentPlayerId: this.currentPlayerId,
|
||||
targetCard: this.targetCard,
|
||||
playedCards: [],
|
||||
playedGroups: this.playGroups.map((g) => ({ playerId: g.playerId, cardCount: g.cards.length })),
|
||||
roundNumber: this.roundNumber,
|
||||
isGameOver: this.isGameOver,
|
||||
winnerId: this.winnerId
|
||||
};
|
||||
for (const [id, player] of this.players) {
|
||||
state.players.push({
|
||||
id,
|
||||
handSize: player.hand.length,
|
||||
alive: player.alive,
|
||||
revolverChambers: player.revolver.currentChamber
|
||||
});
|
||||
}
|
||||
// 資訊邊界:公開狀態只顯示「誰出了幾張」,不洩漏真實牌值;
|
||||
// 真實牌值只在質疑(challenge / systemChallenge)時透過 challenged 事件揭露。
|
||||
return state;
|
||||
}
|
||||
|
||||
getPlayerHand(playerId) {
|
||||
const player = this.getPlayer(playerId);
|
||||
if (!player) return [];
|
||||
return [...player.hand];
|
||||
}
|
||||
|
||||
isBluff(cards) {
|
||||
return !(cards.length > 0 && cards.every((c) => c === this.targetCard || c === 'JOKER'));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GameEngine;
|
||||
@@ -0,0 +1,153 @@
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const GameEngine = require('./engine');
|
||||
|
||||
class RoomManager {
|
||||
constructor() {
|
||||
this.rooms = new Map();
|
||||
}
|
||||
|
||||
// 建立房間
|
||||
createRoom(hostId) {
|
||||
const roomId = uuidv4().substring(0, 8);
|
||||
const room = {
|
||||
id: roomId,
|
||||
hostId,
|
||||
players: [hostId],
|
||||
engine: null,
|
||||
gameState: null,
|
||||
status: 'waiting', // waiting, playing, ended
|
||||
disconnectedAt: {}, // 離線座位追蹤(重連緩衝)
|
||||
maxPlayers: 8, // 引擎牌組依人數等比擴充,支援 2-8 人
|
||||
minPlayers: 2
|
||||
};
|
||||
|
||||
this.rooms.set(roomId, room);
|
||||
return room;
|
||||
}
|
||||
|
||||
// 加入房間
|
||||
joinRoom(roomId, playerId) {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room) {
|
||||
return { error: '房間不存在' };
|
||||
}
|
||||
|
||||
if (room.status !== 'waiting') {
|
||||
return { error: '遊戲已開始,無法加入' };
|
||||
}
|
||||
|
||||
if (room.players.length >= room.maxPlayers) {
|
||||
return { error: '房間已滿' };
|
||||
}
|
||||
|
||||
if (room.players.includes(playerId)) {
|
||||
return { error: '已在房間中' };
|
||||
}
|
||||
|
||||
room.players.push(playerId);
|
||||
return { success: true, room };
|
||||
}
|
||||
|
||||
// 開始遊戲
|
||||
startGame(roomId) {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room) {
|
||||
return { error: '房間不存在' };
|
||||
}
|
||||
|
||||
if (room.players.length < room.minPlayers) {
|
||||
return { error: '人數不足' };
|
||||
}
|
||||
|
||||
if (room.status !== 'waiting') {
|
||||
return { error: '遊戲已開始' };
|
||||
}
|
||||
|
||||
const engine = new GameEngine();
|
||||
const gameState = engine.init(room.players);
|
||||
|
||||
room.engine = engine; // 儲存引擎
|
||||
room.gameState = gameState;
|
||||
room.status = 'playing';
|
||||
|
||||
return { success: true, gameState };
|
||||
}
|
||||
|
||||
// 取得房間
|
||||
getRoom(roomId) {
|
||||
return this.rooms.get(roomId);
|
||||
}
|
||||
|
||||
// 列出房間
|
||||
listRooms() {
|
||||
const rooms = [];
|
||||
for (const [id, room] of this.rooms) {
|
||||
rooms.push({
|
||||
id: room.id,
|
||||
players: room.players.length,
|
||||
maxPlayers: room.maxPlayers,
|
||||
status: room.status
|
||||
});
|
||||
}
|
||||
return rooms;
|
||||
}
|
||||
|
||||
// 依引擎狀態同步房間狀態(自然分出勝負 → ended,供「再來一局」)
|
||||
syncStatus(roomId) {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room || !room.engine || !room.engine.getGameState) return;
|
||||
if (room.engine.getGameState().isGameOver) room.status = 'ended';
|
||||
}
|
||||
|
||||
// 找玩家所在的房間
|
||||
findRoomOfPlayer(playerId) {
|
||||
for (const [roomId, room] of this.rooms) {
|
||||
if (room.players.includes(playerId)) return { roomId, room };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 再來一局:遊戲結束後,以同一批玩家重開引擎
|
||||
restart(roomId) {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room) return { error: '房間不存在' };
|
||||
if (!room.engine || room.status !== 'ended') return { error: '遊戲尚未結束,不能重開' };
|
||||
if (room.players.length < room.minPlayers) return { error: '人數不足' };
|
||||
room.disconnectedAt = {};
|
||||
const gameState = room.engine.init(room.players);
|
||||
room.gameState = gameState;
|
||||
room.status = 'playing';
|
||||
return { success: true, gameState };
|
||||
}
|
||||
|
||||
// 移除玩家
|
||||
removePlayer(playerId) {
|
||||
for (const [roomId, room] of this.rooms) {
|
||||
if (room.players.includes(playerId)) {
|
||||
room.players = room.players.filter(id => id !== playerId);
|
||||
|
||||
// 同步通知遊戲引擎(將被移除玩家標記為死亡)
|
||||
if (room.engine) {
|
||||
room.engine.removePlayer(playerId);
|
||||
}
|
||||
|
||||
this.syncStatus(roomId);
|
||||
|
||||
// 如果房間人數不足,結束遊戲
|
||||
if (room.players.length < room.minPlayers && room.status === 'playing') {
|
||||
room.status = 'ended';
|
||||
}
|
||||
|
||||
// 空房清理:沒有玩家時刪除房間,避免一直累積
|
||||
if (room.players.length === 0) {
|
||||
this.rooms.delete(roomId);
|
||||
}
|
||||
|
||||
return { roomId, room };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RoomManager;
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const { Server } = require('socket.io');
|
||||
const RoomManager = require('./game/room');
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = new Server(server, {
|
||||
cors: {
|
||||
origin: process.env.CORS_ORIGIN || '*',
|
||||
methods: ['GET', 'POST']
|
||||
},
|
||||
maxHttpBufferSize: 1e6, // 1 MB:擋住超大封包
|
||||
transports: ['websocket', 'polling'], // polling 當 fallback,避免某些 Proxy 擋 ws
|
||||
pingInterval: 25000,
|
||||
pingTimeout: 20000
|
||||
});
|
||||
|
||||
const roomManager = new RoomManager();
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// 動作去重:擋掉連點 / 網路重送(依 roomId + requestId)
|
||||
const recentActions = new Map();
|
||||
const DUP_WINDOW_MS = 3000;
|
||||
const REJOIN_GRACE_MS = 60000; // 離線緩衝:60 秒內可重連回座位
|
||||
|
||||
function isDuplicate(roomId, requestId) {
|
||||
if (!requestId) return false; // 沒有 requestId 的舊連線不擋(向後相容)
|
||||
const key = (roomId || 'none') + ':' + requestId;
|
||||
const now = Date.now();
|
||||
if (recentActions.has(key) && now - recentActions.get(key) < DUP_WINDOW_MS) return true;
|
||||
recentActions.set(key, now);
|
||||
if (recentActions.size > 5000) {
|
||||
for (const [k, t] of recentActions) if (now - t >= DUP_WINDOW_MS) recentActions.delete(k);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 把每個玩家的「私密手牌」推給他自己(不公開)
|
||||
function broadcastHands(room) {
|
||||
if (!room || !room.engine) return;
|
||||
for (const playerId of room.players) {
|
||||
io.to(playerId).emit('yourHand', room.engine.getPlayerHand(playerId));
|
||||
}
|
||||
}
|
||||
|
||||
// 單一錯誤回報:ack 優先,其次 error 事件
|
||||
function fail(ack, message, socket) {
|
||||
if (typeof ack === 'function') ack({ ok: false, error: message });
|
||||
else if (socket) socket.emit('error', { message });
|
||||
}
|
||||
|
||||
function roomIdOf(data) {
|
||||
return (data && typeof data.roomId === 'string') ? data.roomId : '';
|
||||
}
|
||||
|
||||
// Socket.IO 事件
|
||||
io.on('connection', (socket) => {
|
||||
console.log(`玩家連線: ${socket.id}`);
|
||||
|
||||
// 建立房間
|
||||
socket.on('createRoom', (data, ack) => {
|
||||
const room = roomManager.createRoom(socket.id);
|
||||
socket.join(room.id);
|
||||
socket.data = { roomId: room.id };
|
||||
io.to(room.id).emit('roomCreated', room);
|
||||
if (typeof ack === 'function') ack({ ok: true, room });
|
||||
console.log(`房間建立: ${room.id}`);
|
||||
});
|
||||
|
||||
// 加入房間
|
||||
socket.on('joinRoom', (data, ack) => {
|
||||
const roomId = roomIdOf(data);
|
||||
const result = roomManager.joinRoom(roomId, socket.id);
|
||||
if (result.error) { fail(ack, result.error, socket); return; }
|
||||
socket.join(roomId);
|
||||
socket.data = { roomId };
|
||||
roomManager.getRoom(roomId).disconnectedAt = {};
|
||||
io.to(roomId).emit('playerJoined', result.room);
|
||||
if (typeof ack === 'function') ack({ ok: true, room: result.room });
|
||||
console.log(`玩家加入: ${roomId}`);
|
||||
});
|
||||
|
||||
// 重連:回到離線緩衝中的座位
|
||||
socket.on('rejoin', (data, ack) => {
|
||||
const roomId = roomIdOf(data);
|
||||
const playerId = (data && typeof data.playerId === 'string') ? data.playerId : '';
|
||||
const room = roomManager.getRoom(roomId);
|
||||
if (!room) { fail(ack, '房間不存在', socket); return; }
|
||||
if (!room.players.includes(playerId)) { fail(ack, '找不到該座位', socket); return; }
|
||||
const mark = room.disconnectedAt && room.disconnectedAt[playerId];
|
||||
if (!mark || Date.now() - mark > REJOIN_GRACE_MS) { fail(ack, '座位已失效', socket); return; }
|
||||
delete room.disconnectedAt[playerId];
|
||||
socket.join(roomId);
|
||||
socket.join(playerId); // rejoin: tie new socket to old seat so broadcastHands (io.to(playerId)) reaches it
|
||||
socket.data = { roomId, seatId: playerId }; // identity separated from connection
|
||||
io.to(roomId).emit('playerRejoined', { playerId, room, gameState: room.engine ? room.engine.getGameState() : null });
|
||||
socket.emit('resync', { room, gameState: room.engine ? room.engine.getGameState() : null });
|
||||
if (room.engine) socket.emit('yourHand', room.engine.getPlayerHand(playerId));
|
||||
if (typeof ack === 'function') ack({ ok: true });
|
||||
broadcastHands(room);
|
||||
console.log(`玩家重連: ${playerId} -> ${roomId}`);
|
||||
});
|
||||
|
||||
// 開始遊戲
|
||||
socket.on('startGame', (data, ack) => {
|
||||
const roomId = roomIdOf(data);
|
||||
if (isDuplicate(roomId, data && data.requestId)) { fail(ack, '動作重複,請稍候', socket); return; }
|
||||
if (!socket.rooms.has(roomId)) { fail(ack, '尚未加入該房間', socket); return; }
|
||||
const result = roomManager.startGame(roomId);
|
||||
if (result.error) { fail(ack, result.error, socket); return; }
|
||||
io.to(roomId).emit('gameStarted', result.gameState);
|
||||
broadcastHands(roomManager.getRoom(roomId));
|
||||
if (typeof ack === 'function') ack({ ok: true });
|
||||
console.log(`開始遊戲: ${roomId}`);
|
||||
});
|
||||
|
||||
// 再來一局:遊戲結束後以同一批玩家重開
|
||||
socket.on('restartGame', (data, ack) => {
|
||||
const roomId = roomIdOf(data);
|
||||
if (isDuplicate(roomId, data && data.requestId)) { fail(ack, '動作重複,請稍候', socket); return; }
|
||||
if (!socket.rooms.has(roomId)) { fail(ack, '尚未加入該房間', socket); return; }
|
||||
const result = roomManager.restart(roomId);
|
||||
if (result.error) { fail(ack, result.error, socket); return; }
|
||||
io.to(roomId).emit('gameStarted', result.gameState);
|
||||
broadcastHands(roomManager.getRoom(roomId));
|
||||
if (typeof ack === 'function') ack({ ok: true });
|
||||
console.log(`再來一局: ${roomId}`);
|
||||
});
|
||||
|
||||
// 出牌
|
||||
socket.on('playCard', (data, ack) => {
|
||||
const roomId = roomIdOf(data);
|
||||
if (isDuplicate(roomId, data && data.requestId)) { fail(ack, '動作重複,請稍候', socket); return; }
|
||||
if (!socket.rooms.has(roomId)) { fail(ack, '尚未加入該房間', socket); return; }
|
||||
const room = roomManager.getRoom(roomId);
|
||||
if (!room || !room.engine) { fail(ack, '遊戲尚未開始', socket); return; }
|
||||
const actorId = (socket.data && socket.data.seatId) || socket.id;
|
||||
const cardIndices = (data && Array.isArray(data.cardIndices)) ? data.cardIndices : [];
|
||||
const result = room.engine.playCard(actorId, cardIndices);
|
||||
if (result.error) { fail(ack, result.error, socket); return; }
|
||||
|
||||
// 資訊邊界:出牌時只公告「誰出了幾張」,不洩漏實際牌值(質疑時才揭露)
|
||||
io.to(roomId).emit('cardPlayed', {
|
||||
playerId: actorId,
|
||||
playedGroup: { playerId: actorId, cardCount: result.playedGroup.cards.length },
|
||||
nextPlayerId: result.nextPlayerId,
|
||||
systemChallenge: result.systemChallenge || null,
|
||||
gameState: room.engine.getGameState()
|
||||
});
|
||||
broadcastHands(room);
|
||||
roomManager.syncStatus(roomId);
|
||||
if (typeof ack === 'function') ack({ ok: true });
|
||||
});
|
||||
|
||||
// 質疑上一組出的牌
|
||||
socket.on('challenge', (data, ack) => {
|
||||
const roomId = roomIdOf(data);
|
||||
if (isDuplicate(roomId, data && data.requestId)) { fail(ack, '動作重複,請稍候', socket); return; }
|
||||
if (!socket.rooms.has(roomId)) { fail(ack, '尚未加入該房間', socket); return; }
|
||||
const room = roomManager.getRoom(roomId);
|
||||
if (!room || !room.engine) { fail(ack, '遊戲尚未開始', socket); return; }
|
||||
const actorId = (socket.data && socket.data.seatId) || socket.id;
|
||||
const result = room.engine.challenge(actorId);
|
||||
if (result.error) { fail(ack, result.error, socket); return; }
|
||||
|
||||
io.to(roomId).emit('challenged', {
|
||||
challengerId: actorId,
|
||||
result,
|
||||
gameState: room.engine.getGameState()
|
||||
});
|
||||
broadcastHands(room);
|
||||
roomManager.syncStatus(roomId);
|
||||
if (typeof ack === 'function') ack({ ok: true });
|
||||
});
|
||||
|
||||
// 斷線:遊戲中先進入「離線緩衝」,緩衝逾時才離場
|
||||
socket.on('disconnect', (reason) => {
|
||||
console.log(`玩家斷線: ${socket.id} (${reason})`);
|
||||
const seatId = (socket.data && socket.data.seatId) || socket.id;
|
||||
const seat = roomManager.findRoomOfPlayer(seatId);
|
||||
if (!seat) return;
|
||||
const { roomId, room } = seat;
|
||||
if (room.engine) {
|
||||
room.disconnectedAt[seatId] = Date.now();
|
||||
io.to(roomId).emit('playerDisconnected', { playerId: seatId, room, reconnecting: true });
|
||||
setTimeout(() => {
|
||||
const cur = roomManager.getRoom(roomId);
|
||||
if (cur && cur.disconnectedAt && cur.disconnectedAt[seatId] && Date.now() - cur.disconnectedAt[seatId] >= REJOIN_GRACE_MS) {
|
||||
delete cur.disconnectedAt[seatId];
|
||||
roomManager.removePlayer(seatId);
|
||||
io.to(roomId).emit('playerDisconnected', {
|
||||
playerId: seatId,
|
||||
room: cur,
|
||||
reconnecting: false,
|
||||
gameState: cur.engine ? cur.engine.getGameState() : null
|
||||
});
|
||||
broadcastHands(cur);
|
||||
}
|
||||
}, REJOIN_GRACE_MS).unref();
|
||||
} else {
|
||||
roomManager.removePlayer(seatId);
|
||||
io.to(roomId).emit('playerDisconnected', { playerId: seatId, room, reconnecting: false });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// REST API
|
||||
app.get('/api/rooms', (req, res) => {
|
||||
res.json(roomManager.listRooms());
|
||||
});
|
||||
|
||||
app.post('/api/rooms', (req, res) => {
|
||||
const { hostId } = req.body || {};
|
||||
const room = roomManager.createRoom(hostId || 'unknown');
|
||||
res.json(room);
|
||||
});
|
||||
|
||||
// 優雅關閉:收到中斷訊號時先關閉 Socket.IO 再退出
|
||||
function shutdown(signal) {
|
||||
console.log(`收到 ${signal},準備關閉伺服器...`);
|
||||
io.close(() => {
|
||||
console.log('伺服器已關閉');
|
||||
process.exit(0);
|
||||
});
|
||||
// 保險:2 秒內沒關乾淨就強制退出
|
||||
setTimeout(() => process.exit(0), 2000).unref();
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
server.listen(PORT, () => {
|
||||
console.log(`伺服器啟動在 http://localhost:${PORT}`);
|
||||
});
|
||||
|
||||
module.exports = { app, io };
|
||||
Generated
+1479
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "liars-bar-game",
|
||||
"version": "1.0.0",
|
||||
"description": "Liars Bar - 線上多人心理博弈遊戲",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "nodemon index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"socket.io": "^4.7.2",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user