talking-head-studio 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +459 -0
  3. package/dist/TalkingHead.d.ts +35 -0
  4. package/dist/TalkingHead.d.ts.map +1 -0
  5. package/dist/TalkingHead.js +107 -0
  6. package/dist/TalkingHead.web.d.ts +35 -0
  7. package/dist/TalkingHead.web.d.ts.map +1 -0
  8. package/dist/TalkingHead.web.js +117 -0
  9. package/dist/__tests__/TalkingHead.test.d.ts +2 -0
  10. package/dist/__tests__/TalkingHead.test.d.ts.map +1 -0
  11. package/dist/__tests__/TalkingHead.test.js +23 -0
  12. package/dist/__tests__/sketchfab.test.d.ts +2 -0
  13. package/dist/__tests__/sketchfab.test.d.ts.map +1 -0
  14. package/dist/__tests__/sketchfab.test.js +21 -0
  15. package/dist/appearance/apply.d.ts +7 -0
  16. package/dist/appearance/apply.d.ts.map +1 -0
  17. package/dist/appearance/apply.js +56 -0
  18. package/dist/appearance/index.d.ts +5 -0
  19. package/dist/appearance/index.d.ts.map +1 -0
  20. package/dist/appearance/index.js +3 -0
  21. package/dist/appearance/matchers.d.ts +3 -0
  22. package/dist/appearance/matchers.d.ts.map +1 -0
  23. package/dist/appearance/matchers.js +32 -0
  24. package/dist/appearance/schema.d.ts +9 -0
  25. package/dist/appearance/schema.d.ts.map +1 -0
  26. package/dist/appearance/schema.js +20 -0
  27. package/dist/editor/AvatarCanvas.d.ts +16 -0
  28. package/dist/editor/AvatarCanvas.d.ts.map +1 -0
  29. package/dist/editor/AvatarCanvas.js +85 -0
  30. package/dist/editor/AvatarCanvasErrorBoundary.d.ts +17 -0
  31. package/dist/editor/AvatarCanvasErrorBoundary.d.ts.map +1 -0
  32. package/dist/editor/AvatarCanvasErrorBoundary.js +41 -0
  33. package/dist/editor/AvatarModel.d.ts +12 -0
  34. package/dist/editor/AvatarModel.d.ts.map +1 -0
  35. package/dist/editor/AvatarModel.js +31 -0
  36. package/dist/editor/RigidAccessory.d.ts +15 -0
  37. package/dist/editor/RigidAccessory.d.ts.map +1 -0
  38. package/dist/editor/RigidAccessory.js +76 -0
  39. package/dist/editor/SkinnedClothing.d.ts +7 -0
  40. package/dist/editor/SkinnedClothing.d.ts.map +1 -0
  41. package/dist/editor/SkinnedClothing.js +88 -0
  42. package/dist/editor/index.d.ts +6 -0
  43. package/dist/editor/index.d.ts.map +1 -0
  44. package/dist/editor/index.js +4 -0
  45. package/dist/editor/types.d.ts +28 -0
  46. package/dist/editor/types.d.ts.map +1 -0
  47. package/dist/editor/types.js +1 -0
  48. package/dist/html.d.ts +13 -0
  49. package/dist/html.d.ts.map +1 -0
  50. package/dist/html.js +560 -0
  51. package/dist/index.d.ts +4 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +2 -0
  54. package/dist/index.web.d.ts +4 -0
  55. package/dist/index.web.d.ts.map +1 -0
  56. package/dist/index.web.js +2 -0
  57. package/dist/sketchfab/api.d.ts +12 -0
  58. package/dist/sketchfab/api.d.ts.map +1 -0
  59. package/dist/sketchfab/api.js +52 -0
  60. package/dist/sketchfab/categories.d.ts +5 -0
  61. package/dist/sketchfab/categories.d.ts.map +1 -0
  62. package/dist/sketchfab/categories.js +124 -0
  63. package/dist/sketchfab/index.d.ts +7 -0
  64. package/dist/sketchfab/index.d.ts.map +1 -0
  65. package/dist/sketchfab/index.js +3 -0
  66. package/dist/sketchfab/types.d.ts +51 -0
  67. package/dist/sketchfab/types.d.ts.map +1 -0
  68. package/dist/sketchfab/types.js +1 -0
  69. package/dist/sketchfab/useSketchfabSearch.d.ts +19 -0
  70. package/dist/sketchfab/useSketchfabSearch.d.ts.map +1 -0
  71. package/dist/sketchfab/useSketchfabSearch.js +78 -0
  72. package/dist/voice/convertToWav.d.ts +6 -0
  73. package/dist/voice/convertToWav.d.ts.map +1 -0
  74. package/dist/voice/convertToWav.js +74 -0
  75. package/dist/voice/index.d.ts +6 -0
  76. package/dist/voice/index.d.ts.map +1 -0
  77. package/dist/voice/index.js +3 -0
  78. package/dist/voice/useAudioPlayer.d.ts +11 -0
  79. package/dist/voice/useAudioPlayer.d.ts.map +1 -0
  80. package/dist/voice/useAudioPlayer.js +61 -0
  81. package/dist/voice/useAudioRecording.d.ts +14 -0
  82. package/dist/voice/useAudioRecording.d.ts.map +1 -0
  83. package/dist/voice/useAudioRecording.js +162 -0
  84. package/package.json +120 -0
  85. package/src/TalkingHead.tsx +207 -0
  86. package/src/TalkingHead.web.tsx +210 -0
  87. package/src/__tests__/TalkingHead.test.tsx +32 -0
  88. package/src/__tests__/sketchfab.test.ts +24 -0
  89. package/src/appearance/apply.ts +94 -0
  90. package/src/appearance/index.ts +4 -0
  91. package/src/appearance/matchers.ts +43 -0
  92. package/src/appearance/schema.ts +35 -0
  93. package/src/editor/AvatarCanvas.tsx +167 -0
  94. package/src/editor/AvatarCanvasErrorBoundary.tsx +64 -0
  95. package/src/editor/AvatarModel.tsx +49 -0
  96. package/src/editor/RigidAccessory.tsx +130 -0
  97. package/src/editor/SkinnedClothing.tsx +114 -0
  98. package/src/editor/index.ts +5 -0
  99. package/src/editor/r3f-shim.d.ts +34 -0
  100. package/src/editor/types.ts +30 -0
  101. package/src/html.ts +572 -0
  102. package/src/index.ts +8 -0
  103. package/src/index.web.ts +8 -0
  104. package/src/sketchfab/api.ts +82 -0
  105. package/src/sketchfab/categories.ts +127 -0
  106. package/src/sketchfab/index.ts +6 -0
  107. package/src/sketchfab/types.ts +40 -0
  108. package/src/sketchfab/useSketchfabSearch.ts +110 -0
  109. package/src/voice/convertToWav.ts +87 -0
  110. package/src/voice/index.ts +7 -0
  111. package/src/voice/useAudioPlayer.ts +78 -0
  112. package/src/voice/useAudioRecording.ts +207 -0
@@ -0,0 +1,76 @@
1
+ // @ts-nocheck
2
+ import { useGLTF, PivotControls } from '@react-three/drei';
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ import { Box3, Vector3 } from 'three';
5
+ export function RigidAccessory({ assetId, url, avatarScene, attachBone, offsetPosition, offsetRotation, scale = 1, isEditing = false, onPlacementChange, }) {
6
+ const { scene } = useGLTF(url);
7
+ // Clone uniquely for this instance
8
+ const clone = useMemo(() => scene.clone(true), [scene]);
9
+ const [basePos, setBasePos] = useState(null);
10
+ useEffect(() => {
11
+ if (!avatarScene)
12
+ return;
13
+ let targetBone = null;
14
+ avatarScene.traverse((child) => {
15
+ if (child.isBone && child.name === attachBone) {
16
+ targetBone = child;
17
+ }
18
+ });
19
+ if (!targetBone)
20
+ return;
21
+ const box = new Box3().setFromObject(clone);
22
+ const meshMin = box.min;
23
+ const meshCenter = new Vector3();
24
+ box.getCenter(meshCenter);
25
+ const boneWorldPos = new Vector3();
26
+ targetBone.getWorldPosition(boneWorldPos);
27
+ setBasePos(new Vector3(boneWorldPos.x - meshCenter.x, boneWorldPos.y - meshMin.y, boneWorldPos.z - meshCenter.z));
28
+ }, [avatarScene, attachBone, clone]);
29
+ // Apply transforms live
30
+ useEffect(() => {
31
+ if (!basePos)
32
+ return;
33
+ clone.position.copy(basePos);
34
+ if (offsetPosition) {
35
+ clone.position.x += offsetPosition[0];
36
+ clone.position.y += offsetPosition[1];
37
+ clone.position.z += offsetPosition[2];
38
+ }
39
+ if (offsetRotation) {
40
+ clone.rotation.set(...offsetRotation);
41
+ }
42
+ if (scale !== undefined) {
43
+ clone.scale.set(scale, scale, scale);
44
+ }
45
+ }, [basePos, offsetPosition, offsetRotation, scale, clone]);
46
+ const handleDragEnd = () => {
47
+ if (!basePos)
48
+ return;
49
+ // The PivotControls autoTransform updates the object directly
50
+ const newOffsetPos = [
51
+ clone.position.x - basePos.x,
52
+ clone.position.y - basePos.y,
53
+ clone.position.z - basePos.z,
54
+ ];
55
+ const newOffsetRot = [
56
+ clone.rotation.x,
57
+ clone.rotation.y,
58
+ clone.rotation.z,
59
+ ];
60
+ const currentScale = clone.scale.x;
61
+ onPlacementChange?.(assetId, {
62
+ bone: attachBone,
63
+ position: newOffsetPos,
64
+ rotation: newOffsetRot,
65
+ scale: currentScale,
66
+ });
67
+ };
68
+ if (!basePos)
69
+ return null;
70
+ if (isEditing) {
71
+ return (<PivotControls anchor={[0, 0, 0]} scale={100} fixed={true} depthTest={false} activeAxes={[true, true, true]} onDragEnd={handleDragEnd} autoTransform={true}>
72
+ <primitive object={clone}/>
73
+ </PivotControls>);
74
+ }
75
+ return <primitive object={clone}/>;
76
+ }
@@ -0,0 +1,7 @@
1
+ interface SkinnedClothingProps {
2
+ url: string;
3
+ avatarSkeleton: any;
4
+ }
5
+ export declare function SkinnedClothing({ url, avatarSkeleton }: SkinnedClothingProps): import("react").JSX.Element;
6
+ export {};
7
+ //# sourceMappingURL=SkinnedClothing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SkinnedClothing.d.ts","sourceRoot":"","sources":["../../src/editor/SkinnedClothing.tsx"],"names":[],"mappings":"AAMA,UAAU,oBAAoB;IAC5B,GAAG,EAAE,MAAM,CAAC;IAEZ,cAAc,EAAE,GAAG,CAAC;CACrB;AAwED,wBAAgB,eAAe,CAAC,EAAE,GAAG,EAAE,cAAc,EAAE,EAAE,oBAAoB,+BA+B5E"}
@@ -0,0 +1,88 @@
1
+ // @ts-nocheck
2
+ import { useGLTF } from '@react-three/drei';
3
+ import { useEffect, useRef } from 'react';
4
+ import * as THREE from 'three';
5
+ import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils.js';
6
+ /**
7
+ * Rebind a skinned mesh's bones to the avatar skeleton by matching bone names.
8
+ * This handles hair packs / clothing that share the same rig convention but
9
+ * were exported as separate files with their own skeleton.
10
+ */
11
+ function rebindToAvatarSkeleton(mesh, avatarSkeleton) {
12
+ const avatarBonesByName = new Map();
13
+ avatarSkeleton.bones.forEach((bone) => avatarBonesByName.set(bone.name, bone));
14
+ // Build a new bone array for this mesh by matching names
15
+ const newBones = [];
16
+ const oldBones = mesh.skeleton.bones;
17
+ const boneIndexMap = new Map(); // old index → new index
18
+ oldBones.forEach((oldBone, oldIdx) => {
19
+ const matched = avatarBonesByName.get(oldBone.name);
20
+ if (matched) {
21
+ boneIndexMap.set(oldIdx, newBones.length);
22
+ newBones.push(matched);
23
+ }
24
+ });
25
+ if (newBones.length === 0) {
26
+ // No bone names matched at all — just bind directly and hope for the best
27
+ mesh.bind(avatarSkeleton);
28
+ return;
29
+ }
30
+ // Remap skinIndex buffer to point at the new bone indices
31
+ const skinIndex = mesh.geometry.attributes.skinIndex;
32
+ const remappedIndex = skinIndex.array.slice();
33
+ for (let i = 0; i < remappedIndex.length; i++) {
34
+ const oldIdx = remappedIndex[i];
35
+ const newIdx = boneIndexMap.get(oldIdx);
36
+ remappedIndex[i] = newIdx !== undefined ? newIdx : 0;
37
+ }
38
+ mesh.geometry.setAttribute('skinIndex', new THREE.Uint16BufferAttribute(remappedIndex, skinIndex.itemSize));
39
+ // Build inverse bind matrices for the matched bones
40
+ const inverseBindMatrices = [];
41
+ newBones.forEach((bone) => {
42
+ const m = new THREE.Matrix4();
43
+ // Use the avatar skeleton's inverse bind matrix if bone index aligns,
44
+ // otherwise compute from world matrix
45
+ const avatarIdx = avatarSkeleton.bones.indexOf(bone);
46
+ if (avatarIdx >= 0 && avatarSkeleton.boneInverses[avatarIdx]) {
47
+ m.copy(avatarSkeleton.boneInverses[avatarIdx]);
48
+ }
49
+ else {
50
+ m.copy(bone.matrixWorld).invert();
51
+ }
52
+ inverseBindMatrices.push(...m.elements);
53
+ });
54
+ const newSkeleton = new THREE.Skeleton(newBones, newBones.map((_, i) => {
55
+ const m = new THREE.Matrix4();
56
+ m.fromArray(inverseBindMatrices, i * 16);
57
+ return m;
58
+ }));
59
+ mesh.bind(newSkeleton);
60
+ }
61
+ export function SkinnedClothing({ url, avatarSkeleton }) {
62
+ const { scene } = useGLTF(url);
63
+ const groupRef = useRef(null);
64
+ useEffect(() => {
65
+ const group = groupRef.current;
66
+ if (!group || !scene || !avatarSkeleton)
67
+ return;
68
+ const clone = SkeletonUtils.clone(scene);
69
+ clone.traverse((child) => {
70
+ if (child.isSkinnedMesh) {
71
+ rebindToAvatarSkeleton(child, avatarSkeleton);
72
+ }
73
+ });
74
+ group.add(clone);
75
+ return () => {
76
+ group.remove(clone);
77
+ clone.traverse((child) => {
78
+ if (child.geometry)
79
+ child.geometry.dispose();
80
+ if (child.material) {
81
+ const materials = Array.isArray(child.material) ? child.material : [child.material];
82
+ materials.forEach((m) => m.dispose());
83
+ }
84
+ });
85
+ };
86
+ }, [scene, avatarSkeleton]);
87
+ return <group ref={groupRef}/>;
88
+ }
@@ -0,0 +1,6 @@
1
+ export { AvatarCanvas } from './AvatarCanvas';
2
+ export { AvatarModel } from './AvatarModel';
3
+ export { RigidAccessory } from './RigidAccessory';
4
+ export { SkinnedClothing } from './SkinnedClothing';
5
+ export type { AvatarEditorProps, AssetPlacement, EquippedAsset } from './types';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/editor/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { AvatarCanvas } from './AvatarCanvas';
2
+ export { AvatarModel } from './AvatarModel';
3
+ export { RigidAccessory } from './RigidAccessory';
4
+ export { SkinnedClothing } from './SkinnedClothing';
@@ -0,0 +1,28 @@
1
+ import type { AvatarAppearance } from '../appearance';
2
+ import type React from 'react';
3
+ export interface AssetPlacement {
4
+ bone: string;
5
+ position: [number, number, number];
6
+ rotation: [number, number, number];
7
+ scale?: number;
8
+ }
9
+ export interface EquippedAsset {
10
+ id: string;
11
+ url: string;
12
+ type: 'rigid' | 'skinned';
13
+ slot?: string;
14
+ attach_bone?: string;
15
+ offset_position?: [number, number, number];
16
+ offset_rotation?: [number, number, number];
17
+ }
18
+ export interface AvatarEditorProps {
19
+ avatarUrl: string;
20
+ appearance?: AvatarAppearance;
21
+ equipped?: EquippedAsset[];
22
+ placements?: Record<string, AssetPlacement>;
23
+ editingAssetId?: string | null;
24
+ onPlacementChange?: (assetId: string, placement: AssetPlacement) => void;
25
+ className?: string;
26
+ style?: React.CSSProperties;
27
+ }
28
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/editor/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC5C,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,KAAK,IAAI,CAAC;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;CAC7B"}
@@ -0,0 +1 @@
1
+ export {};
package/dist/html.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export type AvatarConfig = {
2
+ avatarUrl: string;
3
+ authToken?: string | null;
4
+ mood: 'neutral' | 'happy' | 'sad' | 'angry' | 'excited' | 'thinking' | 'concerned' | 'surprised';
5
+ cameraView: 'head' | 'upper' | 'full';
6
+ cameraDistance: number;
7
+ /** Initial colors only — live updates go via postMessage (setHairColor etc.) */
8
+ initialHairColor?: string;
9
+ initialSkinColor?: string;
10
+ initialEyeColor?: string;
11
+ };
12
+ export declare function buildAvatarHtml(config: AvatarConfig): string;
13
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,KAAK,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CAAC;IACjG,UAAU,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IACtC,cAAc,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CA+iB5D"}