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

टॉकिंग अवतार के लिए लिप-सिंक बनाम Amplitude

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

AI assistant अवतार गाइड का कवर इलस्ट्रेशन: Talking अवतार के लिए Lip-Sync बनाम Amplitude, Charmotion डेमो कैरेक्टर के साथ

ज़्यादातर stylized avatar के लिए amplitude काफ़ी है: Web Audio AnalyserNode से आवाज़ का लेवल मापें, RMS निकालें, और hysteresis के साथ talking लूप को चालू और बंद करें। असली lip-sync के लिए आपके speech provider से viseme timing चाहिए, जैसे Azure Speech के viseme events या Amazon Polly के speech marks, और साथ में ऐसा कैरेक्टर जिसमें हर viseme के लिए मुँह का अलग आकार हो।

मुख्य बातें

  • Amplitude gating इस सवाल का जवाब देता है: "क्या अभी आवाज़ चल रही है?" Visemes इस सवाल का जवाब देते हैं: "इस आवाज़ के लिए मुँह का कौन-सा आकार सही है?"
  • getByteTimeDomainData इस्तेमाल करें, उसे RMS में बदलें, और दो thresholds के साथ एक hold time रखें ताकि स्टेट बार-बार न झपके।
  • Azure Speech 22 viseme IDs भेजता है, जिनके offsets 100-नैनोसेकंड ticks में होते हैं; Polly मिलीसेकंड वाले समय के साथ speech marks लौटाता है।
  • हर viseme के लिए बने मुँह के आकार के बिना viseme डेटा किसी काम का नहीं, इसलिए तरीका चुनने से पहले अपना कैरेक्टर जाँच लें।

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

क्या मुझे असली lip-sync चाहिए या amplitude काफ़ी है?

यह चेहरे पर निर्भर करता है। Lip-sync का मतलब है बोली जा रही आवाज़ों के हिसाब से मुँह को मिलाना। इसके लिए दो चीज़ें चाहिए: timing डेटा जो बताए कि कौन-सी आवाज़ कब आती है, और ऐसा कैरेक्टर जिसमें आवाज़ों के हर समूह (viseme) के लिए मुँह का अलग पोज़ बना हो। Amplitude पर चलने वाली talking के लिए इनमें से कुछ नहीं चाहिए। वह सिर्फ़ यह देखती है कि आवाज़ अभी सुनाई दे रही है या नहीं, और जब तक सुनाई दे, talking लूप चलाती है।

Amplitude gatingAzure visemesPolly speech marks
आपको क्या मिलता हैलाइव loudness वैल्यू22 viseme IDs, वैकल्पिक blend shapesसमय के साथ viseme नाम
Timing यूनिटहर एनिमेशन फ़्रेम पर100-ns ticks (ms के लिए 10,000 से भाग दें)ऑडियो शुरू होने से गिने गए मिलीसेकंड
किसी भी voice source के साथ काम करता हैहाँ, realtime agents के साथ भीसिर्फ़ Azure TTSसिर्फ़ Polly TTS
कैरेक्टर में क्या चाहिएएक talking लूपहर viseme के लिए मुँह का पोज़हर viseme के लिए मुँह का पोज़
मेहनतकमज़्यादाज़्यादा

स्रोत: Microsoft Learn, Amazon Polly docs।

छोटे, stylized कैरेक्टर, जैसे chat के पास 64 px का badge या सादे मुँह वाला मैस्कॉट, आमतौर पर सिर्फ़ लय से ही "बोलते हुए" लगते हैं। Visemes का फ़ायदा बड़े और ज़्यादा इंसानी चेहरों पर दिखता है, जहाँ मुँह का मेल न खाना साफ़ नज़र आता है।

आपका कैरेक्टर क्या सपोर्ट करता है, इसके बारे में ईमानदार रहें। उदाहरण के लिए, Charmotion के सेट में talking लूप (mood 7) है, लेकिन मुँह के आकार या viseme के इनपुट नहीं हैं, इसलिए वे amplitude gating के साथ ठीक बैठते हैं, phoneme lip-sync के साथ नहीं। यह chat और voice agents के लिए सोच-समझकर चुना गया मेल है, जहाँ शब्द टेक्स्ट के रूप में भी दिखते हैं।

AnalyserNode से speech का लेवल कैसे मापें?

Web Audio API का AnalyserNode अपने से गुज़रने वाले ऑडियो को बदले बिना real-time time-domain और frequency डेटा देता है (MDN)। talking gate के लिए time-domain waveform सबसे आसान इनपुट है। getByteTimeDomainData() एक Uint8Array को 0 से 255 तक के samples से भरता है, जिसमें 128 का मतलब खामोशी है (MDN)। इसे −1…1 में बदलें और root mean square निकालें:

const ctx = new AudioContext();
const analyser = ctx.createAnalyser();
analyser.fftSize = 1024;

// Pick the source that carries the assistant's voice:
const source = ctx.createMediaElementSource(audioElement);        // <audio> playback
// const source = ctx.createMediaStreamSource(remoteStream);      // WebRTC track
source.connect(analyser);
analyser.connect(ctx.destination); // only for media element sources, so it stays audible

const samples = new Uint8Array(analyser.fftSize);

function rms(): number {
  analyser.getByteTimeDomainData(samples);
  let sum = 0;
  for (let i = 0; i < samples.length; i++) {
    const v = (samples[i] - 128) / 128;
    sum += v * v;
  }
  return Math.sqrt(sum / samples.length);
}

Media element source, element के ऑडियो का रास्ता बदलकर उसे graph से होकर भेजता है, इसलिए analyser को आगे destination से जोड़ें, वरना आवाज़ बंद हो जाएगी। WebRTC track से बना stream source सिर्फ़ ऑडियो को टैप करता है; आउटपुट उसी <audio> element को संभालने दें जो track प्ले करता है।

AudioContext को किसी user gesture से बनाएँ या resume करें, जैसे बातचीत शुरू करने वाला बटन, वरना browsers उसे suspended ही रखेंगे (MDN autoplay गाइड)।

अगर आप speech frequencies को ज़्यादा महत्व देना चाहते हैं तो getByteFrequencyData() भी काम करता है: यह 0 से 255 तक की frequencyBinCount वैल्यू देता है, जो 0 Hz से sample rate के आधे तक बराबर फैली होती हैं (MDN)। चालू/बंद gate के लिए RMS आसान है और काफ़ी है।

लेवल को बिना flicker के talking और idle में कैसे बदलें?

Speech में syllables और शब्दों के बीच छोटे-छोटे gaps भरे होते हैं। एक ही threshold रखने पर avatar एक सेकंड में कई बार talking और idle के बीच बदलता है, जो टूटा हुआ लगता है। Hysteresis इस्तेमाल करें: talking शुरू करने के लिए ऊँचा threshold, रोकने के लिए नीचा threshold, और सच में रोकने से पहले एक hold time।

const ON = 0.04;      // start talking above this RMS
const OFF = 0.02;     // consider silence below this
const HOLD_MS = 300;  // silence must last this long before stopping

let talking = false;
let quietSince = 0;
let level = 0;

function tick(now: number) {
  level = level * 0.8 + rms() * 0.2; // light smoothing

  if (!talking && level > ON) {
    talking = true;
    setMood(MOOD.talking);
  } else if (talking) {
    if (level > OFF) quietSince = 0;
    else if (!quietSince) quietSince = now;
    else if (now - quietSince > HOLD_MS) {
      talking = false;
      quietSince = 0;
      setMood(MOOD.listening); // or MOOD.idle
    }
  }
  requestAnimationFrame(tick);
}
requestAnimationFrame(tick);

Thresholds और hold time शुरुआती वैल्यू हैं जिन्हें अपनी असली आवाज़ और volume के हिसाब से tune करना है, ये कोई standard नहीं हैं। कुछ वाक्यों के लिए level log करें और ON को noise floor से थोड़ा ऊपर सेट करें।

setMood सिर्फ़ तब कॉल करें जब स्टेट बदले, जैसा ऊपर दिखाया गया है। हर फ़्रेम पर वही वैल्यू लिखना बेकार का काम है। Rive view model के साथ setMood बस vmi.number("mood").value = m जितना छोटा हो सकता है।

अगर आपका कैरेक्टर मुँह खुलने का कोई continuous नंबर देता है, तो आप उसकी जगह हर फ़्रेम पर level को उसकी range के हिसाब से scale करके दे सकते हैं। यह उस खास फ़ाइल की खूबी है; कई मैस्कॉट सेट इसकी जगह talking लूप इस्तेमाल करते हैं।

Visemes क्या हैं और कौन-से TTS providers इन्हें भेजते हैं?

Viseme किसी phoneme का visual रूप है: एक जैसी दिखने वाली आवाज़ों के समूह के लिए मुँह की स्थिति। उदाहरण के लिए "P", "b" और "m" तीनों में होंठ बंद होते हैं, इसलिए इनका viseme एक ही है। जो text-to-speech engines जानते हैं कि वे कौन-से phonemes बना रहे हैं, वे timestamps के साथ visemes बता सकते हैं।

Azure AI Speech 22 visemes देता है, जिनकी numbering 0 (खामोशी) से 21 (p, b, m) तक है। Speech SDK हर viseme के लिए VisemeReceived event भेजता है, जिसमें 100 नैनोसेकंड के ticks में audio offset होता है। Azure 3D चेहरों के लिए 60 fps पर blend-shape frames भी दे सकता है, हर frame में 55 facial positions के साथ, और SVG मुँह की इमेज भी, जो सिर्फ़ en-US locale के लिए सपोर्टेड हैं (Microsoft Learn)।

Amazon Polly speech marks लौटाता है: speech के बारे में metadata, जिसे ऑडियो की जगह request किया जाता है। ये line-delimited JSON stream के रूप में आते हैं, जिसमें हर object में ऑडियो की शुरुआत से मिलीसेकंड में time, एक type (sentence, word, viseme या ssml) और एक value होता है, जो visemes के लिए viseme का नाम है (Amazon Polly)। Polly अपने neural, long-form और standard engines के लिए speech marks डॉक्यूमेंट करता है (Amazon Polly)।

Realtime speech-to-speech models, जैसे OpenAI Realtime API, हमारी Realtime गाइड में बताए गए events में per-phoneme timing के बिना ऑडियो stream करते हैं, और यही एक वजह है कि voice agents के लिए आमतौर पर amplitude gating चुना जाता है।

Azure viseme IDs और Polly speech marks में क्या फ़र्क है?

मुख्य फ़र्क delivery और units का है। Azure synthesis के दौरान viseme events भेजता है; Polly एक अलग metadata response देता है जिसे आप ऑडियो के साथ fetch करते हैं।

JavaScript के लिए Azure Speech SDK में, synthesize करने से पहले visemeReceived को subscribe करें और offset को मिलीसेकंड में बदलें:

import * as sdk from "microsoft-cognitiveservices-speech-sdk";

const synthesizer = new sdk.SpeechSynthesizer(speechConfig, audioConfig);
const visemes: { id: number; ms: number }[] = [];

synthesizer.visemeReceived = (_sender, e) => {
  visemes.push({ id: e.visemeId, ms: e.audioOffset / 10000 }); // 100-ns ticks -> ms
};

synthesizer.speakTextAsync(text, () => synthesizer.close(), (err) => {
  console.error(err);
  synthesizer.close();
});

Polly के साथ, OutputFormat: "json" और SpeechMarkTypes: ["viseme"] के साथ एक request करें, और उसी text और voice के साथ ऑडियो के लिए दूसरी request करें। Marks को newlines पर split करके और हर line पर JSON.parse कॉल करके parse करें।

ID सेट भी अलग हैं। Azure 0 से 21 तक के नंबर इस्तेमाल करता है, जबकि Polly छोटे viseme नाम। दोनों के लिए आपके कैरेक्टर के mouth pose से मिलाने वाली अलग lookup table चाहिए, और दोनों यह मानकर चलते हैं कि वे pose मौजूद हैं। जिस कैरेक्टर के पास सिर्फ़ talking लूप है, उसे इनमें से किसी से भी कोई फ़ायदा नहीं होता।

viseme timestamps को audio playback के साथ sync कैसे करें?

Viseme का समय audio की शुरुआत के हिसाब से होता है, इसलिए उन्हें playback clock के अनुसार शेड्यूल करें, न कि डेटा आने के समय के अनुसार। हर एनिमेशन फ़्रेम पर audio.currentTime पढ़कर हर उस mark को लागू करना जिसका समय हो चुका है, मुँह को सही ताल में रखता है, भले ही playback अटक जाए या tab फ़्रेम छोड़ दे:

type Mark = { time: number; value: string }; // Polly: ms from audio start

function playWithVisemes(audio: HTMLAudioElement, marks: Mark[], show: (v: string) => void) {
  let i = 0;
  const tick = () => {
    const now = audio.currentTime * 1000;
    while (i < marks.length && marks[i].time <= now) show(marks[i++].value);
    if (!audio.paused && !audio.ended) requestAnimationFrame(tick);
    else show("sil");
  };
  audio.addEventListener("play", () => requestAnimationFrame(tick), { once: true });
  audio.play();
}

यही लूप Azure के लिए भी काम करता है, बस time की जगह ms स्टोर करें। अगर यूज़र seek करे या आप playback फिर से शुरू करें, तो नई पोज़िशन के बाद का पहला mark खोजकर i को reset करें।

नतीजे को smooth करें। हर mark पर एक mouth pose से सीधे दूसरे पर कूदना मशीनी लगता है; एनिमेशन फ़ाइल में pose के बीच छोटा blend, या एक फ़्रेम से छोटे marks को छोड़ देना, कहीं बेहतर दिखता है। यही बात amplitude पर भी लागू होती है: ऊपर के gate में smoothing factor और hold time इसीलिए हैं कि मोशन शांत रहे।

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

क्या talking अवतार को एनिमेट करने के लिए amplitude काफ़ी है?

स्टाइलाइज़्ड 2D मैस्कॉट के लिए आमतौर पर हाँ। AnalyserNode से मिले audio level के आधार पर talking लूप को gate करना phoneme डेटा के बिना भी बोलने जैसा दिखता है और realtime agents समेत किसी भी voice source के साथ काम करता है। असली lip-sync के लिए TTS provider से viseme timing और हर viseme के लिए एक mouth shape चाहिए।

getByteTimeDomainData इस्तेमाल करें या getByteFrequencyData?

on/off talking gate के लिए getByteTimeDomainData सबसे आसान है: 128 के आसपास के samples को −1 से 1 में बदलें और RMS निकालें। getByteFrequencyData तब काम आता है जब आप speech frequencies को ज़्यादा वज़न देना चाहें या visualizer बनाना चाहें। दोनों ही स्थितियों में value को smooth करें और hysteresis इस्तेमाल करें, ताकि अवतार टिमटिमाए नहीं।

Azure Speech कितने viseme देता है?

Azure Speech 22 viseme ID देता है, silence के लिए 0 से लेकर p, b और m के लिए 21 तक। JavaScript SDK इन्हें visemeReceived के ज़रिए audioOffset के साथ देता है, जो 100-नैनोसेकंड ticks में होता है, इसलिए milliseconds के लिए 10,000 से भाग दें। Azure 60 fps पर 55 values वाले blend-shape frames भी लौटा सकता है।

क्या सिर्फ़ talking लूप वाले कैरेक्टर को lip-sync किया जा सकता है?

असल में नहीं। Viseme डेटा बताता है कि कौन सा mouth shape दिखाना है, इसलिए उसके लिए ऐसा कैरेक्टर चाहिए जो उन shapes के साथ बना हो। सिर्फ़ talking लूप होने पर सबसे अच्छा नतीजा amplitude gating से मिलता है: आवाज़ सुनाई देते समय लूप चलाएँ और रुकावट के दौरान listening या idle पर लौट जाएँ।

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

स्रोत

संबंधित गाइड