mcuo commited on
Commit
33dbd26
·
verified ·
1 Parent(s): a6be688

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

  • SHA256: fae36f2204f4a90edd6c181588f9d4d65cbd250f1b29c7ffad4f8f765b4a21fa
  • Pointer size: 133 Bytes
  • Size of remote file: 13.2 MB
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.6` です。
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.6
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
- async function loadSelectedAvatar() {
346
  const selected = avatarItems.find((item) => item.name === settings.avatar);
347
  if (!selected || !state.avatarReady) return;
348
- if (state.loadingAvatar === selected.name) return;
349
- state.loadingAvatar = selected.name;
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