Upload 23 files
Browse files
.gitattributes
CHANGED
|
@@ -36,3 +36,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 36 |
data/Anime2.5DRig/eye_close.psd filter=lfs diff=lfs merge=lfs -text
|
| 37 |
data/Anime2.5DRig/mouth_close.psd filter=lfs diff=lfs merge=lfs -text
|
| 38 |
data/Anime2.5DRig/sample.psd filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 36 |
data/Anime2.5DRig/eye_close.psd filter=lfs diff=lfs merge=lfs -text
|
| 37 |
data/Anime2.5DRig/mouth_close.psd filter=lfs diff=lfs merge=lfs -text
|
| 38 |
data/Anime2.5DRig/sample.psd filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
data/Anime2.5DRig/avatar_data/avatar_20260712_014405.psd filter=lfs diff=lfs merge=lfs -text
|
app/main.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
from pathlib import Path
|
| 2 |
-
import mimetypes
|
| 3 |
import os
|
| 4 |
from urllib.parse import quote
|
| 5 |
|
|
@@ -18,8 +17,6 @@ AVATAR_DATA_DIR = Path(
|
|
| 18 |
os.getenv("AVATAR_DATA_DIR", "/buckets/Anime2.5DRig/avatar_data")
|
| 19 |
).resolve()
|
| 20 |
|
| 21 |
-
mimetypes.add_type("image/vnd.adobe.photoshop", ".psd")
|
| 22 |
-
|
| 23 |
app = FastAPI(title="ChatGPT Live Avatar Test")
|
| 24 |
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
| 25 |
|
|
|
|
| 1 |
from pathlib import Path
|
|
|
|
| 2 |
import os
|
| 3 |
from urllib.parse import quote
|
| 4 |
|
|
|
|
| 17 |
os.getenv("AVATAR_DATA_DIR", "/buckets/Anime2.5DRig/avatar_data")
|
| 18 |
).resolve()
|
| 19 |
|
|
|
|
|
|
|
| 20 |
app = FastAPI(title="ChatGPT Live Avatar Test")
|
| 21 |
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
| 22 |
|
data/Anime2.5DRig/avatar_data/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 旧アバターPSD配置先
|
| 2 |
+
|
| 3 |
+
v0.1.5以降、このリポジトリ内フォルダはアバター選択に使用しません。切替対象PSDはStorage Bucketの次の場所へ配置します。
|
| 4 |
+
|
| 5 |
+
```text
|
| 6 |
+
/buckets/Anime2.5DRig/avatar_data/
|
| 7 |
+
```
|
| 8 |
+
|
| 9 |
+
Spaceの `/api/avatars` が拡張子 `.psd` のファイルを列挙し、Tampermonkey版の🎭メニューへ表示します。
|
| 10 |
+
|
| 11 |
+
サブフォルダは列挙しません。ファイル名は表示名として使用され、選択状態はTampermonkeyへ保存されます。
|
data/Anime2.5DRig/avatar_data/avatar_20260712_014405.psd
ADDED
|
|
Git LFS Details
|
data/Anime2.5DRig/index.html
CHANGED
|
@@ -990,27 +990,6 @@ requestAnimationFrame(tick);
|
|
| 990 |
}
|
| 991 |
}
|
| 992 |
|
| 993 |
-
async function loadAvatarBuffer(name, buffer, replyTarget, replyOrigin) {
|
| 994 |
-
try {
|
| 995 |
-
const safeName = String(name || 'avatar.psd').split(/[\\/]/).pop();
|
| 996 |
-
if (!/\.psd$/i.test(safeName)) throw new Error('PSDファイル名ではありません');
|
| 997 |
-
if (!(buffer instanceof ArrayBuffer)) throw new Error('PSDデータがArrayBufferではありません');
|
| 998 |
-
if (buffer.byteLength < 4 || buffer.byteLength > 150 * 1024 * 1024) throw new Error('PSDサイズが許容範囲外です');
|
| 999 |
-
const signature = new Uint8Array(buffer, 0, 4);
|
| 1000 |
-
if (String.fromCharCode(...signature) !== '8BPS') throw new Error('PSD署名が不正です');
|
| 1001 |
-
if (typeof window.__loadPsdBuffer !== 'function') throw new Error('PSD loaderが利用できません');
|
| 1002 |
-
await window.__loadPsdBuffer(buffer);
|
| 1003 |
-
if (!document.getElementById('drop').classList.contains('hidden')) {
|
| 1004 |
-
const detail = document.getElementById('dropStatus').textContent || 'PSD解析に失敗しました';
|
| 1005 |
-
throw new Error(detail.replace(/^エラー:\s*/, ''));
|
| 1006 |
-
}
|
| 1007 |
-
disableInternalLipSync();
|
| 1008 |
-
replyTarget?.postMessage({ source: 'anime2.5drig', type: 'avatar-loaded', name: safeName }, replyOrigin);
|
| 1009 |
-
} catch (error) {
|
| 1010 |
-
replyTarget?.postMessage({ source: 'anime2.5drig', type: 'avatar-error', message: error.message }, replyOrigin);
|
| 1011 |
-
}
|
| 1012 |
-
}
|
| 1013 |
-
|
| 1014 |
window.addEventListener('message', (event) => {
|
| 1015 |
if (!ALLOWED_PARENTS.has(event.origin)) return;
|
| 1016 |
const data = event.data;
|
|
@@ -1018,7 +997,6 @@ requestAnimationFrame(tick);
|
|
| 1018 |
if (data.type === 'mouth-open') setMouth(data.value);
|
| 1019 |
if (data.type === 'reset-mouth') setMouth(0);
|
| 1020 |
if (data.type === 'load-avatar') loadAvatarUrl(data.url, event.source, event.origin);
|
| 1021 |
-
if (data.type === 'load-avatar-buffer') loadAvatarBuffer(data.name, data.buffer, event.source, event.origin);
|
| 1022 |
if (data.type === 'ping') {
|
| 1023 |
event.source?.postMessage({ source: 'anime2.5drig', type: 'ready' }, event.origin);
|
| 1024 |
}
|
|
|
|
| 990 |
}
|
| 991 |
}
|
| 992 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 993 |
window.addEventListener('message', (event) => {
|
| 994 |
if (!ALLOWED_PARENTS.has(event.origin)) return;
|
| 995 |
const data = event.data;
|
|
|
|
| 997 |
if (data.type === 'mouth-open') setMouth(data.value);
|
| 998 |
if (data.type === 'reset-mouth') setMouth(0);
|
| 999 |
if (data.type === 'load-avatar') loadAvatarUrl(data.url, event.source, event.origin);
|
|
|
|
| 1000 |
if (data.type === 'ping') {
|
| 1001 |
event.source?.postMessage({ source: 'anime2.5drig', type: 'ready' }, event.origin);
|
| 1002 |
}
|
userscripts/README.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
| 24 |
6. 診断版 `chatgpt-live-audio-diagnostic.user.js` は無効化または削除する。
|
| 25 |
7. Firefoxで `https://chatgpt.com/` を再読み込みする。
|
| 26 |
|
| 27 |
-
既に本実装を登録済みの場合は、Tampermonkeyのエディターで内容を最新版へ全置換してください。最新版の `@version` は `0.1.
|
| 28 |
|
| 29 |
本実装では、診断版ファイルを更新・上書きするのではなく、別ファイルの `chatgpt-live-avatar.user.js` をTampermonkeyへ登録します。
|
| 30 |
|
|
@@ -49,8 +49,6 @@ Storage Bucketの次のフォルダへPSDを配置します。
|
|
| 49 |
|
| 50 |
Space起動時にこのフォルダがマウントされている必要があります。`/api/avatars` がPSD一覧を返し、🎭メニューへ反映します。PSDは実行中Spaceの `/AvatarData/<ファイル名>` から配信されます。サブフォルダ内のPSDは対象外です。
|
| 51 |
|
| 52 |
-
v0.1.6以降は、選択したPSDをTampermonkeyの `GM_xmlhttpRequest` でArrayBufferとして取得し、Anime2.5DRig iframeへ直接渡します。読込失敗時は🎭メニュー内に具体的なエラーを表示します。
|
| 53 |
-
|
| 54 |
状態ランプは待機中が黄色、音声stream接続中が緑です。ChatGPT Liveの再生中audio要素が後から生成・交換された場合も1秒ごとに再検出します。
|
| 55 |
|
| 56 |
操作バーの短い状態表示:
|
|
|
|
| 24 |
6. 診断版 `chatgpt-live-audio-diagnostic.user.js` は無効化または削除する。
|
| 25 |
7. Firefoxで `https://chatgpt.com/` を再読み込みする。
|
| 26 |
|
| 27 |
+
既に本実装を登録済みの場合は、Tampermonkeyのエディターで内容を最新版へ全置換してください。最新版の `@version` は `0.1.5` です。
|
| 28 |
|
| 29 |
本実装では、診断版ファイルを更新・上書きするのではなく、別ファイルの `chatgpt-live-avatar.user.js` をTampermonkeyへ登録します。
|
| 30 |
|
|
|
|
| 49 |
|
| 50 |
Space起動時にこのフォルダがマウントされている必要があります。`/api/avatars` がPSD一覧を返し、🎭メニューへ反映します。PSDは実行中Spaceの `/AvatarData/<ファイル名>` から配信されます。サブフォルダ内のPSDは対象外です。
|
| 51 |
|
|
|
|
|
|
|
| 52 |
状態ランプは待機中が黄色、音声stream接続中が緑です。ChatGPT Liveの再生中audio要素が後から生成・交換された場合も1秒ごとに再検出します。
|
| 53 |
|
| 54 |
操作バーの短い状態表示:
|
userscripts/chatgpt-live-avatar.user.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
// ==UserScript==
|
| 2 |
// @name ChatGPT Live Anime2.5DRig Avatar
|
| 3 |
// @namespace https://mcuo-gpt-voice.hf.space/
|
| 4 |
-
// @version 0.1.
|
| 5 |
// @description ChatGPT LiveのAI応答音声でAnime2.5DRigアバターをリップシンクします(Firefox Android向け)。
|
| 6 |
// @author mcuo
|
| 7 |
// @match https://chatgpt.com/*
|
|
@@ -49,7 +49,6 @@
|
|
| 49 |
lastSent: -1,
|
| 50 |
animationFrame: null,
|
| 51 |
scanTimer: null,
|
| 52 |
-
loadingAvatar: null,
|
| 53 |
};
|
| 54 |
|
| 55 |
const root = document.createElement("section");
|
|
@@ -177,12 +176,6 @@
|
|
| 177 |
}
|
| 178 |
#${ROOT_ID} .avatar-picker[hidden] { display: none; }
|
| 179 |
#${ROOT_ID} .avatar-picker select { width: 100%; min-height: 36px; }
|
| 180 |
-
#${ROOT_ID} .avatar-picker .avatar-error {
|
| 181 |
-
display: block;
|
| 182 |
-
margin-top: 6px;
|
| 183 |
-
color: #ffaaa5;
|
| 184 |
-
overflow-wrap: anywhere;
|
| 185 |
-
}
|
| 186 |
#${ROOT_ID}.menu-hidden .avatar-tools,
|
| 187 |
#${ROOT_ID}.menu-hidden .sensitivity-panel,
|
| 188 |
#${ROOT_ID}.menu-hidden .avatar-picker {
|
|
@@ -211,7 +204,6 @@
|
|
| 211 |
<div class="avatar-picker" hidden>
|
| 212 |
<label>アバター</label>
|
| 213 |
<select aria-label="アバター選択"><option>読込中…</option></select>
|
| 214 |
-
<small class="avatar-error" hidden></small>
|
| 215 |
</div>
|
| 216 |
`;
|
| 217 |
document.documentElement.appendChild(root);
|
|
@@ -252,7 +244,6 @@
|
|
| 252 |
const sensitivityOutput = sensitivityPanel.querySelector("output");
|
| 253 |
const avatarPicker = root.querySelector(".avatar-picker");
|
| 254 |
const avatarSelect = avatarPicker.querySelector("select");
|
| 255 |
-
const avatarError = avatarPicker.querySelector(".avatar-error");
|
| 256 |
let avatarItems = [];
|
| 257 |
|
| 258 |
function postToAvatar(type, value) {
|
|
@@ -287,30 +278,6 @@
|
|
| 287 |
});
|
| 288 |
}
|
| 289 |
|
| 290 |
-
function requestArrayBuffer(url) {
|
| 291 |
-
return new Promise((resolve, reject) => {
|
| 292 |
-
GM_xmlhttpRequest({
|
| 293 |
-
method: "GET",
|
| 294 |
-
url,
|
| 295 |
-
responseType: "arraybuffer",
|
| 296 |
-
timeout: 60000,
|
| 297 |
-
onload: (response) => {
|
| 298 |
-
if (response.status < 200 || response.status >= 300) {
|
| 299 |
-
reject(new Error(`PSD取得 HTTP ${response.status}`));
|
| 300 |
-
return;
|
| 301 |
-
}
|
| 302 |
-
if (!(response.response instanceof ArrayBuffer)) {
|
| 303 |
-
reject(new Error("PSDレスポンスがArrayBufferではありません"));
|
| 304 |
-
return;
|
| 305 |
-
}
|
| 306 |
-
resolve(response.response);
|
| 307 |
-
},
|
| 308 |
-
onerror: () => reject(new Error("PSDの取得に失敗しました")),
|
| 309 |
-
ontimeout: () => reject(new Error("PSDの取得がタイムアウトしました")),
|
| 310 |
-
});
|
| 311 |
-
});
|
| 312 |
-
}
|
| 313 |
-
|
| 314 |
async function loadAvatarList() {
|
| 315 |
try {
|
| 316 |
const response = await requestJson(`${SPACE_ORIGIN}/api/avatars`);
|
|
@@ -333,7 +300,6 @@
|
|
| 333 |
const selected = avatarItems.find((item) => item.name === settings.avatar) || avatarItems[0];
|
| 334 |
settings.avatar = selected.name;
|
| 335 |
avatarSelect.value = selected.name;
|
| 336 |
-
if (state.avatarReady) loadSelectedAvatar();
|
| 337 |
}
|
| 338 |
} catch (error) {
|
| 339 |
avatarSelect.innerHTML = `<option>一覧取得失敗</option>`;
|
|
@@ -342,35 +308,11 @@
|
|
| 342 |
}
|
| 343 |
}
|
| 344 |
|
| 345 |
-
|
| 346 |
const selected = avatarItems.find((item) => item.name === settings.avatar);
|
| 347 |
if (!selected || !state.avatarReady) return;
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
avatarError.hidden = true;
|
| 351 |
-
avatarError.textContent = "";
|
| 352 |
-
stateLabel.textContent = "PSD取得中";
|
| 353 |
-
try {
|
| 354 |
-
const buffer = await requestArrayBuffer(new URL(selected.url, SPACE_ORIGIN).href);
|
| 355 |
-
stateLabel.textContent = "PSD解析中";
|
| 356 |
-
const message = {
|
| 357 |
-
source: "gpt-voice-avatar",
|
| 358 |
-
type: "load-avatar-buffer",
|
| 359 |
-
name: selected.name,
|
| 360 |
-
buffer,
|
| 361 |
-
};
|
| 362 |
-
try {
|
| 363 |
-
frame.contentWindow?.postMessage(message, SPACE_ORIGIN, [buffer]);
|
| 364 |
-
} catch (_) {
|
| 365 |
-
frame.contentWindow?.postMessage(message, SPACE_ORIGIN);
|
| 366 |
-
}
|
| 367 |
-
} catch (error) {
|
| 368 |
-
state.loadingAvatar = null;
|
| 369 |
-
stateLabel.textContent = "PSD失敗";
|
| 370 |
-
avatarError.textContent = error.message;
|
| 371 |
-
avatarError.hidden = false;
|
| 372 |
-
avatarPicker.hidden = false;
|
| 373 |
-
}
|
| 374 |
}
|
| 375 |
|
| 376 |
function setConnected(connected) {
|
|
@@ -505,17 +447,10 @@
|
|
| 505 |
loadSelectedAvatar();
|
| 506 |
}
|
| 507 |
if (event.data?.source === "anime2.5drig" && event.data.type === "avatar-loaded") {
|
| 508 |
-
state.loadingAvatar = null;
|
| 509 |
-
avatarError.hidden = true;
|
| 510 |
-
avatarError.textContent = "";
|
| 511 |
stateLabel.textContent = state.running ? (state.analyser ? "動作中" : "音声待ち") : "停止中";
|
| 512 |
}
|
| 513 |
if (event.data?.source === "anime2.5drig" && event.data.type === "avatar-error") {
|
| 514 |
-
state.loadingAvatar = null;
|
| 515 |
stateLabel.textContent = "PSD失敗";
|
| 516 |
-
avatarError.textContent = event.data.message || "PSD読み込みに失敗しました";
|
| 517 |
-
avatarError.hidden = false;
|
| 518 |
-
avatarPicker.hidden = false;
|
| 519 |
}
|
| 520 |
});
|
| 521 |
|
|
|
|
| 1 |
// ==UserScript==
|
| 2 |
// @name ChatGPT Live Anime2.5DRig Avatar
|
| 3 |
// @namespace https://mcuo-gpt-voice.hf.space/
|
| 4 |
+
// @version 0.1.5
|
| 5 |
// @description ChatGPT LiveのAI応答音声でAnime2.5DRigアバターをリップシンクします(Firefox Android向け)。
|
| 6 |
// @author mcuo
|
| 7 |
// @match https://chatgpt.com/*
|
|
|
|
| 49 |
lastSent: -1,
|
| 50 |
animationFrame: null,
|
| 51 |
scanTimer: null,
|
|
|
|
| 52 |
};
|
| 53 |
|
| 54 |
const root = document.createElement("section");
|
|
|
|
| 176 |
}
|
| 177 |
#${ROOT_ID} .avatar-picker[hidden] { display: none; }
|
| 178 |
#${ROOT_ID} .avatar-picker select { width: 100%; min-height: 36px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
#${ROOT_ID}.menu-hidden .avatar-tools,
|
| 180 |
#${ROOT_ID}.menu-hidden .sensitivity-panel,
|
| 181 |
#${ROOT_ID}.menu-hidden .avatar-picker {
|
|
|
|
| 204 |
<div class="avatar-picker" hidden>
|
| 205 |
<label>アバター</label>
|
| 206 |
<select aria-label="アバター選択"><option>読込中…</option></select>
|
|
|
|
| 207 |
</div>
|
| 208 |
`;
|
| 209 |
document.documentElement.appendChild(root);
|
|
|
|
| 244 |
const sensitivityOutput = sensitivityPanel.querySelector("output");
|
| 245 |
const avatarPicker = root.querySelector(".avatar-picker");
|
| 246 |
const avatarSelect = avatarPicker.querySelector("select");
|
|
|
|
| 247 |
let avatarItems = [];
|
| 248 |
|
| 249 |
function postToAvatar(type, value) {
|
|
|
|
| 278 |
});
|
| 279 |
}
|
| 280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
async function loadAvatarList() {
|
| 282 |
try {
|
| 283 |
const response = await requestJson(`${SPACE_ORIGIN}/api/avatars`);
|
|
|
|
| 300 |
const selected = avatarItems.find((item) => item.name === settings.avatar) || avatarItems[0];
|
| 301 |
settings.avatar = selected.name;
|
| 302 |
avatarSelect.value = selected.name;
|
|
|
|
| 303 |
}
|
| 304 |
} catch (error) {
|
| 305 |
avatarSelect.innerHTML = `<option>一覧取得失敗</option>`;
|
|
|
|
| 308 |
}
|
| 309 |
}
|
| 310 |
|
| 311 |
+
function loadSelectedAvatar() {
|
| 312 |
const selected = avatarItems.find((item) => item.name === settings.avatar);
|
| 313 |
if (!selected || !state.avatarReady) return;
|
| 314 |
+
postToAvatar("load-avatar", selected.url);
|
| 315 |
+
stateLabel.textContent = "PSD読込中";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
}
|
| 317 |
|
| 318 |
function setConnected(connected) {
|
|
|
|
| 447 |
loadSelectedAvatar();
|
| 448 |
}
|
| 449 |
if (event.data?.source === "anime2.5drig" && event.data.type === "avatar-loaded") {
|
|
|
|
|
|
|
|
|
|
| 450 |
stateLabel.textContent = state.running ? (state.analyser ? "動作中" : "音声待ち") : "停止中";
|
| 451 |
}
|
| 452 |
if (event.data?.source === "anime2.5drig" && event.data.type === "avatar-error") {
|
|
|
|
| 453 |
stateLabel.textContent = "PSD失敗";
|
|
|
|
|
|
|
|
|
|
| 454 |
}
|
| 455 |
});
|
| 456 |
|