@pixl-app/sdk 0.0.0-stage → 0.1.1
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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +71 -2
- package/dist/api-types/api-scopes.d.ts +4 -0
- package/dist/api-types/chat.d.ts +60 -0
- package/dist/api-types/device-apps.d.ts +2 -0
- package/dist/api-types/events.d.ts +17 -0
- package/dist/api-types/index.d.ts +8 -0
- package/dist/api-types/modules.d.ts +4 -0
- package/dist/api-types/spotify.d.ts +108 -0
- package/dist/api-types/streamelements.d.ts +3 -0
- package/dist/api-types/tts.d.ts +27 -0
- package/dist/chat.d.ts +53 -0
- package/dist/client.d.ts +194 -0
- package/dist/feed.d.ts +57 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +875 -0
- package/dist/pixl.js +6 -0
- package/dist/tts.d.ts +128 -0
- package/package.json +55 -5
package/dist/pixl.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
(()=>{function S(t,e=1){let r=encodeURIComponent(t.id);switch(t.provider){case"twitch":return`https://static-cdn.jtvnw.net/emoticons/v2/${r}/${t.animated?"default":"static"}/dark/${e===4?3:e}.0`;case"7tv":return`https://cdn.7tv.app/emote/${r}/${e}x.webp`;case"bttv":return`https://cdn.betterttv.net/emote/${r}/${e===4?3:e}x.webp`;case"ffz":return`https://cdn.frankerfacez.com/emote/${r}/${e}`}}var M={"&":"&","<":"<",">":">",'"':""","'":"'"};function d(t){return t.replace(/[&<>"']/g,(e)=>M[e])}function b(t,e){let r=d(t.name);return`<img class="pixl-emote" src="${d(S(t,e))}" alt="${r}" data-provider="${t.provider}" data-emote="${d(t.id)}">`}function f(t,e={},r={}){let n=r.size??2,s=t.split(/(\s+)/),i=[],o=-1;for(let a of s){if(!a)continue;if(/^\s+$/.test(a)){i.push(d(a));continue}let l=Object.prototype.hasOwnProperty.call(e,a)?e[a]:void 0;if(!l){i.push(d(a)),o=-1;continue}if(l.zeroWidth&&o>=0){i.length=o+1;let u=i[o],c=u.startsWith('<span class="pixl-emote-stack">')?u.slice(31,-7):u;i[o]=`<span class="pixl-emote-stack">${c}${b(l,n)}</span>`;continue}i.push(b(l,n)),o=i.length-1}return i.join("")}function j(t=[]){return t.map((e)=>`<img class="pixl-badge" src="${d(e.url)}" alt="${d(e.title)}" data-provider="${e.provider}">`).join("")}var _=`.pixl-emote{height:1.75em;vertical-align:middle;margin:-.25em 0}
|
|
2
|
+
.pixl-emote-stack{display:inline-grid;vertical-align:middle}
|
|
3
|
+
.pixl-emote-stack>.pixl-emote{grid-area:1/1;justify-self:center;align-self:center;margin:0}
|
|
4
|
+
.pixl-badge{height:1.1em;vertical-align:middle;margin-right:.25em}`;function L(t){switch(t.type){case"linear-gradient":case"radial-gradient":{let e=t.stops.map((n)=>`${n.color} ${Math.round(n.at*1000)/10}%`).join(", "),r=`${t.repeating?"repeating-":""}${t.type}`;return t.type==="linear-gradient"?`${r}(${t.angle}deg, ${e})`:`${r}(${t.shape}, ${e})`}case"color":return`linear-gradient(${t.color}, ${t.color})`;case"image":return`url("${t.url.replace(/"/g,"%22")}")`}}function w(t,e="inherit"){let r=t.layers.filter((s)=>s.opacity>0);if(!r.length)return`color:${e}`;let n=t.shadows.map((s)=>`drop-shadow(${s.x}px ${s.y}px ${s.blur}px ${s.color})`).join(" ");return[`color:${e}`,`background-image:${r.map(L).join(", ")}`,"background-size:cover","-webkit-background-clip:text","background-clip:text","-webkit-text-fill-color:transparent",n?`filter:${n}`:""].filter(Boolean).join(";")}class g{pixl;paints=new Map;queue=new Map;timer=null;constructor(t){this.pixl=t}css=_;emoteUrl(t,e=1){return S(t,e)}badgesHtml(t){return j(t)}paintCss(t,e){return w(t,e)}html(t,e,r){return f(t,e,r)}async emotes(){return(await this.pixl.request("/emotes")).emotes}paint(t){let e=this.paints.get(t);if(!e)e=new Promise((r)=>{this.queue.set(t,r),this.timer??=setTimeout(()=>this.flush(),20)}),this.paints.set(t,e);return e}async paintStyle(t,e="inherit"){let r=t?await this.paint(t):null;return r?w(r,e):`color:${e}`}async flush(){this.timer=null;let t=this.queue;this.queue=new Map;let e=[...t.keys()],r=new Map;try{for(let n=0;n<e.length;n+=100){let s=await this.pixl.request("/7tv/paints",{ids:e.slice(n,n+100).join(",")});for(let i of s.paints)r.set(i.id,i)}}catch(n){console.warn("[pixl] 7TV-Paints nicht geladen:",n);for(let s of e)this.paints.delete(s)}for(let[n,s]of t)s(r.get(n)??null)}}var T=[2000,5000,1e4,30000];class v{ready=!1;error=null;pixl;since;types;handler;delivered=new Set;errorListeners=new Set;buffer=null;loadSeq=0;stopped=!1;latestAt=null;retryTimer=null;retryAttempt=0;unsubscribeStatus;onEvent=(t,e)=>this.handleLive(e);constructor(t,e,r){if(e?.since===void 0)throw Error("pixl.feed: `since` fehlt");this.pixl=t,this.since=e.since instanceof Date?e.since.toISOString():String(e.since),this.types=e.types?.length?new Set(e.types):null,this.handler=r,t.on("*",this.onEvent),this.unsubscribeStatus=t.onStatus((n)=>{if(n==="connected")this.retryAttempt=0,this.load()})}onError(t){if(this.errorListeners.add(t),this.error)t(this.error);return()=>this.errorListeners.delete(t)}stop(){this.stopped=!0,this.clearRetry(),this.pixl.off("*",this.onEvent),this.unsubscribeStatus(),this.errorListeners.clear(),this.buffer=null}async load(){let t=++this.loadSeq;this.clearRetry(),this.buffer=[];let e=null;try{let n=await this.fetchHistory();if(t!==this.loadSeq||this.stopped)return;this.error=null,this.retryAttempt=0;for(let s of n)this.deliver({event:s.event,data:s.data,replay:!!(s.data?.replay||s.data?.test),id:s.id,at:s.at,history:!0})}catch(n){if(t!==this.loadSeq||this.stopped)return;e=n instanceof Error?n:Error(String(n)),this.error=e;for(let s of this.errorListeners)s(e)}let r=this.buffer??[];this.buffer=null,this.ready=!0;for(let n of r)this.deliver(n);if(e)this.scheduleRetry(e)}async fetchHistory(){let t=this.latestAt===null?null:this.latestAt-300000,e=[],r;do{let n=await this.pixl.events({since:this.since,types:this.types?[...this.types]:void 0,limit:200,cursor:r});e.push(...n.events),r=n.nextCursor??void 0;let s=n.events[n.events.length-1];if(t!==null&&s&&Date.parse(s.at)<t)break}while(r);return e.reverse()}scheduleRetry(t){let e=t.status;if(e!==void 0&&e>=400&&e<500&&e!==429&&e!==408)return;if(this.pixl.status!=="connected")return;let r=T[Math.min(this.retryAttempt,T.length-1)];this.retryAttempt+=1,this.retryTimer=setTimeout(()=>{if(this.retryTimer=null,!this.stopped&&this.pixl.status==="connected")this.load()},r+Math.random()*r*0.3)}clearRetry(){if(this.retryTimer)clearTimeout(this.retryTimer);this.retryTimer=null}handleLive(t){if(this.stopped)return;if(this.types&&!this.types.has(t.event))return;if(this.buffer){this.buffer.push(t);return}this.deliver(t)}deliver(t){if(t.id){if(this.delivered.has(t.id))return;this.delivered.add(t.id)}if(t.at){let e=Date.parse(t.at);if(!Number.isNaN(e)&&(this.latestAt===null||e>this.latestAt))this.latestAt=e}try{this.handler(t.data,t)}catch(e){console.error("[pixl] Fehler im feed()-Handler:",e)}}}class x{pixl;constructor(t){this.pixl=t}async speak(t,e={}){return await(await this.pixl._fetch("/tts",{method:"POST",json:{text:t,voice:e.voice,filters:e.filters}})).json()}async voices(){return await(await this.pixl._fetch("/tts/voices")).json()}async audio(t){return(await this.pixl._fetch(`/tts/${encodeURIComponent(t)}/audio`)).blob()}async marks(t){return(await(await this.pixl._fetch(`/tts/${encodeURIComponent(t)}/marks`)).json()).marks}player(t={}){return new k(this.pixl,this,t)}}class k{pixl;tts;options;queue=[];current=null;wordFrame=null;volume;destroyed=!1;warnedAutoplay=!1;onReady;onSkip;onClear;constructor(t,e,r){this.pixl=t;this.tts=e;this.options=r;this.volume=P(r.volume??1),this.onReady=(n,s)=>{let i=F(n,s);if(!i||s.replay&&this.options.ignoreReplays)return;if(this.current?.item.id===i.id||this.queue.some((a)=>a.id===i.id))return;this.queue.push(i);let o=this.options.maxQueue??20;if(this.queue.length>o)this.queue.splice(0,this.queue.length-o);this.next()},this.onSkip=(n)=>{let s=typeof n.id==="string"?n.id:void 0;if(!s||this.current?.item.id===s)this.skip();else this.queue=this.queue.filter((i)=>i.id!==s)},this.onClear=()=>this.clear(),t.on("tts.ready",this.onReady),t.on("tts.skip",this.onSkip),t.on("tts.clear",this.onClear)}skip(){if(!this.current)return;this.current.skipped=!0,this.current.finish()}clear(){this.queue=[],this.skip()}setVolume(t){if(this.volume=P(t),this.current?.audio)this.current.audio.volume=this.volume}get pending(){return this.queue}destroy(){this.destroyed=!0,this.pixl.off("tts.ready",this.onReady),this.pixl.off("tts.skip",this.onSkip),this.pixl.off("tts.clear",this.onClear),this.clear()}followWords(t,e,r){this.stopWords();let n=0,s=(o)=>r[o].end??r[o+1]?.start??(Number.isFinite(t.duration)?Math.round(t.duration*1000):r[o].start),i=()=>{let o=t.currentTime*1000;while(n<r.length&&r[n].start<=o){let a=n++,l={word:r[a].word,index:a,start:r[a].start,end:s(a)};y(()=>this.options.onWord?.(l,e))}this.wordFrame=n<r.length&&!t.ended?E(i):null};this.wordFrame=E(i)}stopWords(){if(this.wordFrame!==null)I(this.wordFrame);this.wordFrame=null}async next(){if(this.current||this.destroyed)return;let t=this.queue.shift();if(!t)return;let e=!1,r,n=new Promise((i)=>r=i),s={item:t,audio:null,url:null,skipped:!1,finish:()=>{if(e)return;if(e=!0,this.stopWords(),s.audio?.pause(),s.url)URL.revokeObjectURL(s.url);r()}};this.current=s;try{let[i,o]=await Promise.all([this.tts.audio(t.id),this.options.onWord?this.tts.marks(t.id).catch((a)=>{console.warn("[pixl] Zeitmarken nicht verfügbar, onWord bleibt für diesen Eintrag stumm:",a);return}):Promise.resolve(void 0)]);if(o)t.marks=o;if(!e){s.url=URL.createObjectURL(i);let a=new Audio(s.url);if(a.volume=this.volume,a.addEventListener("ended",s.finish),a.addEventListener("error",s.finish),s.audio=a,y(()=>this.options.onStart?.(t)),o?.length)this.followWords(a,t,o);await a.play().catch((l)=>{if(!this.warnedAutoplay&&l instanceof DOMException&&l.name==="NotAllowedError")this.warnedAutoplay=!0,console.warn("[pixl] Der Browser blockiert die Wiedergabe ohne Klick. In OBS und StreamElements passiert das nicht, im normalen Browser einmal auf die Seite klicken.");s.finish()})}}catch(i){console.error("[pixl] TTS-Audio konnte nicht geladen werden:",i),s.finish()}if(await n,y(()=>this.options.onEnd?.(t,{skipped:s.skipped})),this.current=null,this.queue.length&&!this.destroyed)setTimeout(()=>void this.next(),this.options.gapMs??500)}}function E(t){return typeof requestAnimationFrame==="function"?requestAnimationFrame(t):setTimeout(t,30)}function I(t){if(typeof cancelAnimationFrame==="function")cancelAnimationFrame(t);else clearTimeout(t)}function F(t,e){if(typeof t.id!=="string")return null;return{id:t.id,text:typeof t.text==="string"?t.text:"",voice:typeof t.voice==="string"?t.voice:"",source:typeof t.source==="string"?t.source:"",...typeof t.eventId==="string"?{eventId:t.eventId}:{},...t.textTruncated===!0?{textTruncated:!0}:{},replay:e.replay}}function P(t){return Number.isFinite(t)?Math.min(1,Math.max(0,t)):1}function y(t){try{t()}catch(e){console.error("[pixl] Fehler in einem TTS-Handler:",e)}}class h extends Error{status;problem;constructor(t,e,r){super(e);this.name="PixlApiError",this.status=t,this.problem=r}}var q="https://pixl.lennox.zip",C=[500,1000,2000,3000,5000,1e4],U=45000,W=15000;function R(t){if(t instanceof Date)return t.toISOString();return String(t)}class p{static defaultBaseUrl;baseUrl;key;tts;chat;handlers=new Map;stateHandlers=new Map;statusHandlers=new Set;states=new Map;_status="closed";abort=null;reconnectTimer=null;watchdog=null;attempt=0;stopped=!1;logs;everConnected=!1;lostAt=null;constructor(t){if(!t?.key)throw Error("pixl: `key` fehlt (API-Schlüssel aus den Einstellungen)");if(this.key=t.key,this.logs=t.logs!==!1,this.baseUrl=(t.baseUrl??p.defaultBaseUrl??q).replace(/\/+$/,""),this.tts=new x(this),this.chat=new g(this),t.autoConnect!==!1)queueMicrotask(()=>this.connect())}on(t,e){let r=this.handlers.get(t);if(!r)this.handlers.set(t,r=new Set);return r.add(e),this}off(t,e){return this.handlers.get(t)?.delete(e),this}state(t,e){let r=this.stateHandlers.get(t);if(!r)this.stateHandlers.set(t,r=new Set);if(r.add(e),this.states.has(t))e(this.states.get(t),t);return this}offState(t,e){return this.stateHandlers.get(t)?.delete(e),this}getState(t){return this.states.get(t)}get status(){return this._status}onStatus(t){return this.statusHandlers.add(t),t(this._status),()=>this.statusHandlers.delete(t)}setStatus(t){if(t===this._status)return;this._status=t;for(let e of this.statusHandlers)m(()=>e(t))}async request(t,e={}){let r=new URL(`${this.baseUrl}/api/v1${t}`);for(let[s,i]of Object.entries(e))if(i!==void 0&&i!=="")r.searchParams.set(s,i);let n=await fetch(r,{headers:{Authorization:`Bearer ${this.key}`}});if(!n.ok){let s=await n.json().catch(()=>null);throw new h(n.status,s?.error??`HTTP ${n.status}`)}return await n.json()}async _fetch(t,e={}){let r={Authorization:`Bearer ${this.key}`};if(e.json!==void 0)r["Content-Type"]="application/json";let n=await fetch(`${this.baseUrl}/api/v1${t}`,{method:e.method??"GET",headers:r,body:e.json!==void 0?JSON.stringify(e.json):void 0});if(!n.ok){let s=await n.json().catch(()=>null);throw new h(n.status,s?.error??`HTTP ${n.status}`,s?.problem)}return n}events(t={}){return this.request("/events",{since:t.since!==void 0?R(t.since):void 0,until:t.until!==void 0?R(t.until):void 0,types:t.types?.join(","),limit:t.limit!==void 0?String(t.limit):void 0,cursor:t.cursor})}org(){return this.request("/org")}subs(){return this.request("/subs")}nowPlaying(){return this.request("/spotify/now-playing")}static progress(t,e=Date.now()){if(!t.track)return 0;let r=t.playing?t.progressMs+(e-Date.parse(t.at)):t.progressMs;return Math.max(0,Math.min(r,t.track.durationMs))}streamElements(t,e={}){let r={};for(let[n,s]of Object.entries(e))r[n]=s===void 0?void 0:String(s);return this.request(`/streamelements/${t}`,r)}async fetchStates(){return(await this.request("/states")).states}async fetchAllEvents(t={}){let e=[],r;do{let n=await this.events({...t,limit:200,cursor:r});e.push(...n.events),r=n.nextCursor??void 0}while(r);return e.reverse()}feed(t,e){return new v(this,t,e)}connect(){if(this.abort)return;this.stopped=!1,this.run()}disconnect(){if(this.stopped=!0,this.reconnectTimer)clearTimeout(this.reconnectTimer);this.reconnectTimer=null,this.abort?.abort(),this.abort=null,this.setStatus("closed")}async run(){let t=new AbortController;this.abort=t,this.setStatus("connecting");let e=!1;try{this.watchdog=setTimeout(()=>t.abort(),W);let r=await fetch(`${this.baseUrl}/api/v1/stream`,{headers:{Authorization:`Bearer ${this.key}`,Accept:"text/event-stream"},signal:t.signal,cache:"no-store"});if(this.clearWatchdog(),r.status===401||r.status===403){e=!0;return}if(!r.ok||!r.body)throw Error(`HTTP ${r.status}`);if(this.attempt=0,this.lostAt!==null)this.log(`Live-Verbindung wieder da nach ${((Date.now()-this.lostAt)/1000).toFixed(1)} s`);this.lostAt=null,this.everConnected=!0,this.setStatus("connected"),await this.readStream(r.body,t,()=>{e=!0})}catch{}finally{if(this.clearWatchdog(),this.abort===t)this.abort=null;if(e)this.log("Live-Verbindung abgelehnt (Schlüssel ungültig oder widerrufen), kein neuer Versuch"),this.stopped=!0,this.setStatus("unauthorized");else if(!this.stopped){if(this.lostAt===null)this.lostAt=Date.now(),this.log(this.everConnected?"Live-Verbindung getrennt, verbinde neu":"Live-Verbindung nicht erreichbar, versuche es weiter");this.setStatus("disconnected"),this.scheduleReconnect()}}}log(t){if(this.logs)console.info(`[pixl] ${t}`)}scheduleReconnect(){let t=C[Math.min(this.attempt,C.length-1)];this.attempt+=1;let e=t+Math.random()*t*0.3;this.reconnectTimer=setTimeout(()=>{if(this.reconnectTimer=null,!this.stopped)this.connect()},e)}armWatchdog(t){this.clearWatchdog(),this.watchdog=setTimeout(()=>t.abort(),U)}clearWatchdog(){if(this.watchdog)clearTimeout(this.watchdog);this.watchdog=null}async readStream(t,e,r){let n=t.getReader(),s=new TextDecoder,i="",o=[];this.armWatchdog(e);for(;;){let{value:a,done:l}=await n.read();if(l)return;this.armWatchdog(e),i+=s.decode(a,{stream:!0});let u;while((u=i.indexOf(`
|
|
5
|
+
`))>=0){let c=i.slice(0,u).replace(/\r$/,"");if(i=i.slice(u+1),c===""){if(o.length){let O=o.join(`
|
|
6
|
+
`);if(o=[],this.dispatch(O)==="revoked"){r(),await n.cancel().catch(()=>{});return}}}else if(c.startsWith("data:"))o.push(c.slice(5).replace(/^ /,""))}}}dispatch(t){let e;try{e=JSON.parse(t)}catch{return}if(e.event==="api_key.revoked")return"revoked";if(e.event==="state.snapshot"){let n=e.data?.states??[];for(let{key:s,value:i}of n)this.setState(s,i);return}if(e.event==="state.updated"){let{key:n,value:s}=e.data;if(typeof n==="string")this.setState(n,s);return}let r={event:e.event,data:e.data??{},replay:!!e.replay,id:e.id,at:e.at};this.emit(r)}emit(t){if(t.event==="twitch.chat_message"&&typeof t.data.text==="string"&&t.data.html===void 0)t.data.html=f(t.data.text,t.data.emotes);for(let e of this.handlers.get(t.event)??[])m(()=>e(t.data,t));for(let e of this.handlers.get("*")??[])m(()=>e(t.data,t))}setState(t,e){this.states.set(t,e);for(let r of this.stateHandlers.get(t)??[])m(()=>r(e,t))}}function m(t){try{t()}catch(e){console.error("[pixl] Fehler in einem Handler:",e)}}function A(t){if(!t)return;try{let e=new URL(t);return e.pathname.replace(/\/+$/,"").endsWith("/sdk/pixl.js")?e.origin:void 0}catch{return}}var D=typeof document<"u"?document.currentScript:null,H=A(D?.src);if(H)p.defaultBaseUrl=H;Object.assign(globalThis,{Pixl:p,PixlApiError:h});})();
|
package/dist/tts.d.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { TtsFilters, TtsMark } from "./api-types/index.js";
|
|
2
|
+
import type { Pixl } from "./client.js";
|
|
3
|
+
export type { TtsFilters, TtsMark };
|
|
4
|
+
export type TtsVoiceInfo = {
|
|
5
|
+
/** Stimmen-ID für speak(), z.B. `polly:hans` */
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
language: string;
|
|
9
|
+
gender: "female" | "male";
|
|
10
|
+
description: string;
|
|
11
|
+
/** Credits pro Zeichen */
|
|
12
|
+
creditsPerChar?: number;
|
|
13
|
+
/** standard, ai (KI) oder custom (eigene Stimme des Kanals) */
|
|
14
|
+
tier?: string;
|
|
15
|
+
};
|
|
16
|
+
export type TtsSpoken = {
|
|
17
|
+
id: string;
|
|
18
|
+
/** Der bereinigte Text, so wie er vorgelesen wird */
|
|
19
|
+
text: string;
|
|
20
|
+
voice: string;
|
|
21
|
+
};
|
|
22
|
+
/** Ein Wort, wie es onWord bekommt: mit Position im Satz und festem Ende */
|
|
23
|
+
export type TtsWord = {
|
|
24
|
+
word: string;
|
|
25
|
+
index: number;
|
|
26
|
+
start: number;
|
|
27
|
+
end: number;
|
|
28
|
+
};
|
|
29
|
+
/** Ein Eintrag aus `tts.ready` */
|
|
30
|
+
export type TtsItem = {
|
|
31
|
+
id: string;
|
|
32
|
+
text: string;
|
|
33
|
+
voice: string;
|
|
34
|
+
/** `api` (speak()) oder `dashboard` (aus pixl gesendet) */
|
|
35
|
+
source: string;
|
|
36
|
+
/** ID eines zugehörigen Events im Eventlog, falls es eins gibt */
|
|
37
|
+
eventId?: string;
|
|
38
|
+
/** true bei erneut gesendeten Einträgen ("Nochmal senden" im Verlauf) */
|
|
39
|
+
replay: boolean;
|
|
40
|
+
/** Zeitmarken pro Wort, nur im Player mit `onWord` (sonst undefined) */
|
|
41
|
+
marks?: TtsMark[];
|
|
42
|
+
/**
|
|
43
|
+
* true, wenn `text` für die Live-Verbindung gekürzt wurde (sehr lange Texte, die
|
|
44
|
+
* nicht in eine Nachricht passen). Das Audio ist trotzdem vollständig, die Wörter
|
|
45
|
+
* stehen vollständig in `marks`.
|
|
46
|
+
*/
|
|
47
|
+
textTruncated?: boolean;
|
|
48
|
+
};
|
|
49
|
+
export type TtsPlayerOptions = {
|
|
50
|
+
/** Lautstärke von 0 bis 1 (Standard 1) */
|
|
51
|
+
volume?: number;
|
|
52
|
+
/** Erneut gesendete Einträge nicht abspielen (Standard: false) */
|
|
53
|
+
ignoreReplays?: boolean;
|
|
54
|
+
/** Pause zwischen zwei Einträgen in ms (Standard 500) */
|
|
55
|
+
gapMs?: number;
|
|
56
|
+
/** Höchstens so viele warten lassen, ältere fallen raus (Standard 20) */
|
|
57
|
+
maxQueue?: number;
|
|
58
|
+
/** Läuft, bevor ein Eintrag startet, z.B. um Untertitel einzublenden */
|
|
59
|
+
onStart?: (item: TtsItem) => void;
|
|
60
|
+
/** Läuft, wenn ein Eintrag fertig ist, auch wenn er übersprungen wurde */
|
|
61
|
+
onEnd?: (item: TtsItem, info: {
|
|
62
|
+
skipped: boolean;
|
|
63
|
+
}) => void;
|
|
64
|
+
/**
|
|
65
|
+
* Läuft bei jedem Wort, sobald es gesprochen wird, z.B. für Karaoke-Untertitel.
|
|
66
|
+
* Mit gesetztem onWord lädt der Player die Zeitmarken mit (`item.marks` schon in
|
|
67
|
+
* onStart). Kostet beim Anbieter extra, deshalb nur, wenn du es wirklich brauchst.
|
|
68
|
+
*/
|
|
69
|
+
onWord?: (word: TtsWord, item: TtsItem) => void;
|
|
70
|
+
};
|
|
71
|
+
export declare class PixlTts {
|
|
72
|
+
private readonly pixl;
|
|
73
|
+
constructor(pixl: Pixl);
|
|
74
|
+
/**
|
|
75
|
+
* Text vorlesen lassen. Braucht einen Schlüssel mit `tts:write` und verbraucht
|
|
76
|
+
* Credits des Kanals. Ohne `voice` gilt die Standardstimme des Kanals.
|
|
77
|
+
* Abgespielt wird nicht hier, sondern in jedem Overlay mit `player()`.
|
|
78
|
+
* `filters: false` schaltet Links, Wiederholungen und gesperrte Wörter ab, oder
|
|
79
|
+
* einzeln, z.B. `{ links: false }`.
|
|
80
|
+
*/
|
|
81
|
+
speak(text: string, options?: {
|
|
82
|
+
voice?: string;
|
|
83
|
+
filters?: TtsFilters | false;
|
|
84
|
+
}): Promise<TtsSpoken>;
|
|
85
|
+
/** Verfügbare Stimmen und die Standardstimme des Kanals */
|
|
86
|
+
voices(): Promise<{
|
|
87
|
+
voices: TtsVoiceInfo[];
|
|
88
|
+
default: string;
|
|
89
|
+
}>;
|
|
90
|
+
/** Audio eines Eintrags als MP3 (Schlüssel mit `tts:read`) */
|
|
91
|
+
audio(id: string): Promise<Blob>;
|
|
92
|
+
/**
|
|
93
|
+
* Zeitmarken pro Wort zu einem Eintrag (Schlüssel mit `tts:read`). Entstehen beim
|
|
94
|
+
* ersten Abruf und kosten dann beim Anbieter extra, danach kommen sie aus dem
|
|
95
|
+
* Zwischenspeicher. Wirft einen PixlApiError (404), wenn es keine gibt.
|
|
96
|
+
*/
|
|
97
|
+
marks(id: string): Promise<TtsMark[]>;
|
|
98
|
+
/** Spielt jedes `tts.ready` der Reihe nach ab, hört auf Überspringen und Stoppen aus pixl. Braucht `tts:read`. */
|
|
99
|
+
player(options?: TtsPlayerOptions): TtsPlayer;
|
|
100
|
+
}
|
|
101
|
+
export declare class TtsPlayer {
|
|
102
|
+
private readonly pixl;
|
|
103
|
+
private readonly tts;
|
|
104
|
+
private readonly options;
|
|
105
|
+
private queue;
|
|
106
|
+
private current;
|
|
107
|
+
private wordFrame;
|
|
108
|
+
private volume;
|
|
109
|
+
private destroyed;
|
|
110
|
+
private warnedAutoplay;
|
|
111
|
+
private readonly onReady;
|
|
112
|
+
private readonly onSkip;
|
|
113
|
+
private readonly onClear;
|
|
114
|
+
constructor(pixl: Pixl, tts: PixlTts, options: TtsPlayerOptions);
|
|
115
|
+
/** Was gerade läuft abbrechen, danach geht es mit dem Nächsten weiter */
|
|
116
|
+
skip(): void;
|
|
117
|
+
/** Warteschlange leeren und das Laufende abbrechen */
|
|
118
|
+
clear(): void;
|
|
119
|
+
/** Lautstärke von 0 bis 1, gilt auch für das Laufende */
|
|
120
|
+
setVolume(volume: number): void;
|
|
121
|
+
/** Wartende Einträge (ohne den laufenden) */
|
|
122
|
+
get pending(): readonly TtsItem[];
|
|
123
|
+
/** Player abbauen: hört auf zuzuhören und bricht alles ab */
|
|
124
|
+
destroy(): void;
|
|
125
|
+
private followWords;
|
|
126
|
+
private stopWords;
|
|
127
|
+
private next;
|
|
128
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@pixl-app/sdk",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "pixl SDK: Events, Chat, Spotify und TTS eines Kanals in eigenen Widgets, etwa StreamElements Custom Widgets oder OBS-Browserquellen",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://docs.pixl.lennox.zip/sdk",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/pixl-app/sdk.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/pixl-app/sdk/issues"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"pixl",
|
|
16
|
+
"twitch",
|
|
17
|
+
"streamelements",
|
|
18
|
+
"obs",
|
|
19
|
+
"overlay",
|
|
20
|
+
"widget"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./pixl.js": "./dist/pixl.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"CHANGELOG.md"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"dev": "bun scripts/dev.ts",
|
|
41
|
+
"build": "bun run build:script && bun run build:esm && bun run build:types",
|
|
42
|
+
"build:script": "bun build ./src/global.ts --outfile ./dist/pixl.js --target browser --format iife --minify",
|
|
43
|
+
"build:esm": "bun build ./src/index.ts --outfile ./dist/index.js --target browser --format esm",
|
|
44
|
+
"build:types": "tsc -p tsconfig.build.json",
|
|
45
|
+
"check": "tsc -p tsconfig.json",
|
|
46
|
+
"format": "prettier --write .",
|
|
47
|
+
"lint": "prettier --check .",
|
|
48
|
+
"check:version": "bun scripts/check-version.ts",
|
|
49
|
+
"test": "bun test"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/obs-studio": "*",
|
|
53
|
+
"prettier": "^3.9.10",
|
|
54
|
+
"typescript": "^6.0.3"
|
|
55
|
+
}
|
|
56
|
+
}
|