कंटेंट पर जाएँ
charmotion

स्ट्रीमिंग इवेंट से चैट अवतार एनिमेट करें

Charmotion टीम द्वारा · अपडेट:

AI assistant avatar गाइड का कवर इलस्ट्रेशन: Streaming Events से Chat Avatar को एनिमेट करें, Charmotion के demo कैरेक्टर के साथ

स्ट्रीमिंग LLM से चैट अवतार को एनिमेट करने के लिए, Vercel AI SDK के useChat status को अवतार स्टेट से मैप करें: submitted को thinking, streaming को talking, ready को idle (साथ में एक बार success रिएक्शन), और error को error रिएक्शन से। एक ही effect से Rive view model का नंबर और ट्रिगर चलाएँ, और canvas को मैसेज लिस्ट के बाहर mounted रखें।

मुख्य बातें

  • useChat status की चार वैल्यू होती हैं: submitted, streaming, ready और error। अवतार की हर स्टेट चलाने के लिए इतना काफ़ी है।
  • लूपिंग स्टेट के लिए नंबर और वन-शॉट के लिए ट्रिगर इस्तेमाल करें, और success सिर्फ़ streaming से ready वाले ट्रांज़िशन पर फ़ायर करें।
  • यूज़र के stop करने को अलग केस मानें, ताकि उस पर success रिएक्शन न चले।
  • thinking को कम से कम थोड़ी देर बनाए रखें और अवतार को मैसेज लिस्ट के बाहर सिर्फ़ एक बार render करें, ताकि फ़्लिकर और रीस्टार्ट न हों।

पहले से कोई कैरेक्टर या लोगो है? अपने कैरेक्टर को असली ऐप स्क्रीन में देखें: स्प्लैश, ऑनबोर्डिंग, लोडिंग और एम्प्टी स्टेट स्क्रीन। यह फ़्री है, आपके ब्राउज़र में चलता है और कुछ भी अपलोड नहीं होता।

कौन-से स्ट्रीमिंग इवेंट अवतार की कौन-सी स्टेट चलाएँ?

Vercel AI SDK का useChat hook पहले से ही पूरे रिक्वेस्ट लाइफ़ साइकिल को एक फ़ील्ड में ट्रैक करता है। useChat reference के अनुसार, इसका status 'ready' (idle), 'submitted' (रिक्वेस्ट भेजी गई), 'streaming' (रिस्पॉन्स आ रहा है) या 'error' (रिक्वेस्ट फ़ेल हुई) होता है। ये चार वैल्यू लूपिंग मूड और वन-शॉट रिएक्शन वाले कैरेक्टर पर आसानी से मैप हो जाती हैं।

useChat statusक्या हो रहा हैअवतार
submittedरिक्वेस्ट भेजी गई, अभी कोई token नहींthinking लूप (mood 3)
streamingToken आ रहे हैंtalking लूप (mood 7)
streaming के बाद readyजवाब पूरा हुआsuccess एक बार फ़ायर करें, फिर idle (mood 0)
यूज़र के stop करने के बाद readyजवाब बीच में रोका गयाidle, कोई रिएक्शन नहीं
errorरिक्वेस्ट फ़ेल हुईerror फ़ायर करें, फिर sad (mood 4)
कुछ मिनट तक कोई गतिविधि नहीं (आपका टाइमर)सेशन idlesleeping (mood 8)

यहाँ के मूड नंबर दस मूड वाली कैरेक्टर फ़ाइल के अनुसार हैं (idle 0, thinking 3, sad 4, talking 7, sleeping 8)। अगर आपकी फ़ाइल में अलग वैल्यू या नाम हैं, तो नीचे दिए कोड में सिर्फ़ constant map बदलें।

listening ही वह स्टेट है जिसके बारे में useChat नहीं बता सकता, क्योंकि टाइपिंग किसी भी रिक्वेस्ट से पहले होती है। अगर आपको यह चाहिए, तो इसे अपने इनपुट के focus और change इवेंट से सेट करें। टाइप की जाने वाली चैट में कई टीमें इसे छोड़ देती हैं और यूज़र के लिखते समय अवतार को idle रहने देती हैं।

Chat assistant स्क्रीन, जिनमें एनिमेटेड avatar सुन रहा है, सोच रहा है, बोल रहा है और पुष्टि कर रहा है
बातचीत से मैप की गई assistant avatar स्टेट।

useChat status को Rive नंबर इनपुट से कैसे मैप करें?

Rive data binding के साथ, useRive में autoBind: true सेट करें, तो डिफ़ॉल्ट view model instance rive.viewModelInstance के रूप में उपलब्ध हो जाता है। फिर useViewModelInstanceNumber और useViewModelInstanceTrigger hooks setValue और trigger फ़ंक्शन लौटाते हैं (Rive React data binding)। Next.js App Router में यह client component होना चाहिए, इसलिए फ़ाइल "use client" से शुरू होती है।

"use client";

import { useEffect, useRef } from "react";
import {
  useRive,
  useViewModelInstanceNumber,
  useViewModelInstanceTrigger,
} from "@rive-app/react-webgl2";

type ChatStatus = "submitted" | "streaming" | "ready" | "error";
const MOOD = { idle: 0, thinking: 3, sad: 4, talking: 7 } as const;

export function ChatAvatar({ status, stopped }: { status: ChatStatus; stopped: boolean }) {
  const { rive, RiveComponent } = useRive({
    src: "/character.riv",
    stateMachine: "CharacterViewModelMachine",
    autoplay: true,
    autoBind: true,
  });
  const vmi = rive?.viewModelInstance;
  const { setValue: setMood } = useViewModelInstanceNumber("mood", vmi);
  const { trigger: success } = useViewModelInstanceTrigger("success", vmi);
  const { trigger: error } = useViewModelInstanceTrigger("error", vmi);
  const last = useRef<ChatStatus | null>(null);

  useEffect(() => {
    if (!vmi || last.current === status) return;
    const was = last.current;
    last.current = status;

    if (status === "submitted") setMood(MOOD.thinking);
    else if (status === "streaming") setMood(MOOD.talking);
    else if (status === "error") {
      error();
      setMood(MOOD.sad);
    } else {
      if ((was === "streaming" || was === "submitted") && !stopped) success();
      setMood(MOOD.idle);
    }
  }, [vmi, status, stopped, setMood, success, error]);

  return (
    <div aria-hidden="true" style={{ width: 96, height: 96 }}>
      <RiveComponent />
    </div>
  );
}

last ref ही इसे सुरक्षित बनाता है। hook फ़ंक्शन की identity बदलने पर effect दोबारा चल सकता है, लेकिन वह तभी कुछ करता है जब status सच में बदला हो, इसलिए एक ट्रांज़िशन पर ट्रिगर कभी दो बार फ़ायर नहीं होता।

अगर आप React hooks इस्तेमाल नहीं कर रहे, तो सामान्य web runtime यही काम imperative तरीके से करता है: rive.viewModelInstance.number("mood").value = 3 और rive.viewModelInstance.trigger("success").trigger() (Rive web data binding)।

जवाब पूरा होने पर success एनिमेशन सिर्फ़ एक बार कैसे चलाएँ?

मूड नहीं, ट्रिगर इस्तेमाल करें। ट्रिगर वन-शॉट होता है: वह अपना एनिमेशन चलाता है और फिर स्टेट मशीन उस समय चल रहे लूप पर लौट आती है। मूड एक रेस्टिंग लूप है जो तब तक चलता रहता है जब तक आप उसे बदल न दें। अगर आप success को मूड की तरह बनाते, तो उससे हटने के लिए टाइमर चाहिए होता, और वह टाइमर अगले मैसेज से टकरा सकता था।

इसे किसी वैल्यू पर नहीं, ट्रांज़िशन पर फ़ायर करें। status === "ready" पहले मैसेज से पहले और error से रिकवरी के बाद भी true होता है, इसलिए सिर्फ़ वैल्यू जाँचने पर पेज लोड होते ही जश्न शुरू हो जाएगा। ऊपर के component में previous-status ref ट्रिगर को सिर्फ़ उन जवाबों तक सीमित रखता है जो सच में स्ट्रीम हुए।

क्रम उतना मायने नहीं रखता जितना आप सोच सकते हैं। कोड में success() चलता है और फिर उसी टिक में मूड वापस idle पर सेट हो जाता है। वन-शॉट बचता है या नहीं, यह इस पर निर्भर करता है कि फ़ाइल कैसे जुड़ी है। Charmotion फ़ाइल में मूड बदलने से कभी वन-शॉट नहीं कटता: रिएक्शन पूरा होता है, फिर कैरेक्टर नए मूड में आ जाता है। Charmotion यह फ़ाइल आपके अपने कैरेक्टर या लोगो से बनाता है, Character व्यू मॉडल पर दस मूड और छह ट्रिगर (success, error, yes, no, enter, exit) के साथ, 17 एनिमेशन वाले सेट के हिस्से के रूप में; .riv हर पेड प्लान में शामिल है। अगर आप अपनी फ़ाइल खुद बनाते हैं, तो इस पर भरोसा करने से पहले Rive एडिटर में यह व्यवहार जाँच लें।

अगर पहचान सकें, तो success को उन्हीं जवाबों के लिए रखें जिन्होंने सच में कुछ किया हो। लंबी बातचीत में हर एक जवाब के बाद रिएक्शन शोर जैसा लग सकता है। एक आम बीच का रास्ता यह है कि इसे सिर्फ़ तब फ़ायर करें जब कोई tool call पूरा हुआ हो, जैसे नोट सेव होना या बुकिंग, और सामान्य जवाबों को बस idle पर लौटने दें।

talking: mood 7, जब assistant जवाब देता है।

error आने पर या यूज़र के stop दबाने पर avatar को क्या करना चाहिए?

useChat मौजूदा streaming response को रोकने के लिए stop() देता है, साथ में onError और onFinish callbacks भी (useChat reference)। रोकी गई stream भी पूरी हुई stream की तरह ही ready पर खत्म होती है, इसलिए avatar सिर्फ़ status से दोनों में फ़र्क नहीं कर सकता। Stop को खुद रिकॉर्ड करें:

"use client";

import { useState } from "react";
import { useChat } from "@ai-sdk/react";
import { ChatAvatar } from "./ChatAvatar";

export function Chat() {
  const { messages, sendMessage, status, stop } = useChat();
  const [stopped, setStopped] = useState(false);

  const send = (text: string) => {
    setStopped(false);
    sendMessage({ text });
  };
  const handleStop = () => {
    setStopped(true);
    stop();
  };

  return (
    <aside>
      <ChatAvatar status={status} stopped={stopped} />
      {/* message list, input and a stop button that calls handleStop */}
    </aside>
  );
}

Error के लिए error ट्रिगर फ़ायर करें और यूज़र के दोबारा कोशिश करने तक sad या neutral मूड पर रहें। रिएक्शन छोटा रखें और कभी flash न करें; error message खुद input के पास text में होना चाहिए। जब अगला request जाता है, तो submitted avatar को अपने आप thinking पर वापस ले आता है।

Stop कोई विफलता नहीं है। बिना रिएक्शन के idle पर लौटें, या अगर आपके कैरेक्टर में no या yes जैसा ट्रिगर है और वह आपके टोन से मेल खाता है, तो छोटी सी स्वीकृति के लिए उसका इस्तेमाल करें।

तेज़ जवाबों पर thinking और talking के बीच flicker से कैसे बचें?

तेज़ models एक सेकंड से भी काफ़ी कम समय में submitted से streaming पर पहुँच सकते हैं। अगर thinking लूप को सिर्फ़ कुछ ही फ़्रेम मिलें, तो avatar ऐसा लगता है जैसे उसमें glitch आ गया हो। इसका हल है minimum dwell time: thinking शुरू होने के बाद, अगली स्टेट आने देने से पहले उसे थोड़ी देर बनाए रखें।

import { useEffect, useRef, useState } from "react";

type ChatStatus = "submitted" | "streaming" | "ready" | "error";

/** Delays leaving "submitted" until it has been shown for at least `ms`. */
export function useMinDwell(status: ChatStatus, ms = 400): ChatStatus {
  const [shown, setShown] = useState(status);
  const since = useRef(Date.now());

  useEffect(() => {
    if (status === shown) return;
    const elapsed = Date.now() - since.current;
    const wait = shown === "submitted" ? Math.max(0, ms - elapsed) : 0;
    const t = setTimeout(() => {
      since.current = Date.now();
      setShown(status);
    }, wait);
    return () => clearTimeout(t);
  }, [status, shown, ms]);

  return shown;
}

// <ChatAvatar status={useMinDwell(status)} stopped={stopped} />

400 ms एक शुरुआती मान है, कोई मानक नहीं; इसे अपने model के असली time to first token के हिसाब से ट्यून करें। Delay सिर्फ़ avatar पर लगाएँ। Message list को tokens आते ही render करते रहना चाहिए।

चूँकि बहुत तेज़ जवाब dwell के बाद सीधे submitted से ready पर जा सकता है, इसलिए ऊपर वाला avatar component दोनों में से किसी भी पिछली स्टेट पर success फ़ायर करता है। इसके बिना तेज़ जवाबों को कभी उनका रिएक्शन नहीं मिलता।

हर message पर canvas को remount होने से कैसे रोकें?

Rive के React docs चेतावनी देते हैं कि अगर React बार-बार canvas को unmount या दोबारा create करता है, तो एनिमेशन restart हो सकता है, गायब हो सकता है या दिखना बंद हो सकता है (Rive React best practices)। Chat UI अनजाने में ठीक यही करते हैं, क्योंकि वे हर streamed token पर re-render होते हैं।

  • Message list के बाहर एक ही avatar render करें। इसे header में या input के पास रखें, हर assistant bubble के अंदर नहीं। अगर आप हर message के पास avatar चाहते हैं, तो live Rive avatar सिर्फ़ सबसे नए जवाब के लिए और पुराने जवाबों के लिए static image इस्तेमाल करें।
  • useRive और RiveComponent को एक ही छोटे component में रखें, जैसे ChatAvatar में। RiveComponent को tree में ऊपर या नीचे pass न करें।
  • इसे स्थिर position दें और बदलने वाली key न दें। Message id से बनी key हर turn पर canvas को remount कर देती है।
  • Props के रूप में सिर्फ़ primitives pass करें (status, एक boolean) और अगर parent अक्सर re-render होता है, तो इसे React.memo में wrap करें।

एक instance परफ़ॉर्मेंस में भी मदद करता है। Offscreen होने पर इसे pause करने और मोशन कम करें सेटिंग संभालने के लिए एक्सेसिबल और तेज़ AI avatar एनिमेशन देखें, और package setup के लिए React और Next.js में Rive देखें।

useChat से Rive chat avatar चलाएँ

  1. Data Binding के साथ फ़ाइल लोड करें. "use client" component के अंदर autoBind: true और स्टेट मशीन के नाम के साथ useRive कॉल करें।
  2. Properties पाएँ. success और error के लिए useViewModelInstanceNumber("mood", vmi) और useViewModelInstanceTrigger इस्तेमाल करें।
  3. Status को मूड से मैप करें. एक ही effect में submitted पर thinking, streaming पर talking, ready पर idle और error पर sad सेट करें।
  4. Transitions पर रिएक्शन फ़ायर करें. पिछले status को एक ref में ट्रैक करें और success सिर्फ़ तब फ़ायर करें जब जवाब यूज़र के stop किए बिना पूरा हुआ हो।
  5. तेज़ जवाबों को स्मूद बनाएँ. अगली स्टेट दिखाने से पहले thinking को थोड़ी minimum dwell तक बनाए रखें।
  6. एक बार mount करें. Avatar को message list के बाहर, स्थिर position पर और बिना बदलने वाली key के render करें।

अक्सर पूछे जाने वाले सवाल

useChat की status values क्या हैं?

useChat का status request भेजे जाने पर 'submitted', response आते समय 'streaming', idle होने पर 'ready' और request विफल होने पर 'error' होता है। submitted को thinking लूप से, streaming को talking से, ready को idle से (वैकल्पिक success रिएक्शन के साथ) और error को error रिएक्शन से मैप करें।

क्या पहले token पर ही avatar को talking पर switch करना चाहिए?

हाँ। Response आना शुरू होते ही useChat 'streaming' पर चला जाता है, और thinking से talking पर switch करने का यही सही समय है। पूरे जवाब का इंतज़ार करने से assistant धीमा लगता है। Thinking को थोड़ी minimum dwell तक बनाए रखें, ताकि बहुत तेज़ जवाबों पर flicker न हो।

मेरा success एनिमेशन page load पर क्यों चलता है?

क्योंकि पहले message से पहले भी status 'ready' होता है। जब भी status 'ready' हो तब ट्रिगर फ़ायर करने के बजाय, ref से ट्रैक करके 'streaming' या 'submitted' से 'ready' पर transition होने पर फ़ायर करें। जब यूज़र ने stop दबाया हो तब भी इसे छोड़ दें, क्योंकि रोकी गई stream भी 'ready' पर खत्म होती है।

क्या यह सामान्य Rive web runtime के साथ काम करता है?

हाँ। React hooks के बिना, अपने status listener से rive.viewModelInstance.number("mood").value सेट करें और rive.viewModelInstance.trigger("success").trigger() कॉल करें। Mapping, पिछले status की जाँच और stop handling बिल्कुल वैसे ही रहते हैं; सिर्फ़ व्यू मॉडल तक पहुँचने का तरीका बदलता है, hooks से instance पर सीधे property access तक।

इस गाइड से झटपट जवाब

स्रोत

संबंधित गाइड