@ing.jorgeu/wargaming-overlay 2.4.0 → 3.0.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 (44) hide show
  1. package/PUBLISHING.md +13 -20
  2. package/README.md +244 -4
  3. package/THIRD_PARTY_ASSETS.md +8 -0
  4. package/battle_reader.py +50 -7
  5. package/cli.js +27 -7
  6. package/device_connection.py +14 -5
  7. package/game-store.js +26 -3
  8. package/package.json +25 -6
  9. package/public/army-list.css +18 -0
  10. package/public/army-list.html +1 -0
  11. package/public/army-list.js +59 -0
  12. package/public/control.html +3 -3
  13. package/public/control.js +3 -2
  14. package/public/detachments.css +2 -2
  15. package/public/live-score.css +3 -3
  16. package/public/live-score.js +8 -4
  17. package/public/match-view.js +15 -4
  18. package/public/roster-view.js +8 -0
  19. package/public/rosters-control.js +77 -0
  20. package/public/style.css +8 -0
  21. package/reader-status.js +19 -0
  22. package/rosters.js +39 -0
  23. package/scripts/setup-kokoro.js +2 -0
  24. package/scripts/tts-demo.js +10 -0
  25. package/server.js +53 -11
  26. package/tts/kokoro-assets.js +27 -0
  27. package/tts/kokoro-engine.js +31 -0
  28. package/tts/kokoro-worker.js +13 -0
  29. package/tts/licenses/espeak-ng-COPYING +674 -0
  30. package/tts/playback.js +28 -0
  31. package/tts/queue.js +22 -0
  32. package/tts/sanitizer.js +31 -0
  33. package/tts/speech.js +72 -0
  34. package/tts/voice-command.js +14 -0
  35. package/tts/wav.js +6 -0
  36. package/tts/windows-playback.js +14 -0
  37. package/turn-tracker.js +33 -21
  38. package/youtube/auth.js +71 -0
  39. package/youtube/chat.proto +29 -0
  40. package/youtube/index.js +23 -0
  41. package/youtube/live-broadcast.js +13 -0
  42. package/youtube/live-chat.js +24 -0
  43. package/youtube/poll-chat.js +15 -0
  44. package/youtube/service.js +73 -0
@@ -0,0 +1,27 @@
1
+ const fs=require('node:fs/promises'),path=require('node:path'),os=require('node:os');
2
+ const {createHash,randomUUID}=require('node:crypto');
3
+ const MODEL='onnx-community/Kokoro-82M-v1.0-ONNX';
4
+ const REVISION='1939ad2a8e416c0acfeecc08a694d14ef25f2231';
5
+ const ASSETS={
6
+ 'onnx/model_quantized.onnx':'fbae9257e1e05ffc727e951ef9b9c98418e6d79f1c9b6b13bd59f5c9028a1478',
7
+ 'voices/ef_dora.bin':'f66ec66bd295acb18372e37008533a9a3228483ccd294e7538d5d9294ac9a532',
8
+ 'voices/em_alex.bin':'27809e9eafdcbcfff90a3016c697568676531de2a2c39cee29c96c7bd6b83e95',
9
+ 'voices/em_santa.bin':'ad43b774e1ca24d05c6161297d8aeb770ac3d29bb95daf516727af5f7d543683',
10
+ 'tokenizer.json':'77a02c8e164413299b4b4c403b14f8e0e1c1b727db4d46a09d6327b861060a34'
11
+ };
12
+ function cacheDirectory(){return process.env.KOKORO_CACHE_DIR||path.join(os.homedir(),'.cache','wargaming-overlay','kokoro',REVISION);}
13
+ const hash=b=>createHash('sha256').update(b).digest('hex');
14
+ async function asset(name,{cacheDir=cacheDirectory(),fetchImpl=fetch,signal,log=()=>{}}={}){
15
+ if(!Object.hasOwn(ASSETS,name))throw Error('Recurso Kokoro desconocido.');
16
+ const dest=path.join(cacheDir,name);
17
+ try{const b=await fs.readFile(dest);if(hash(b)===ASSETS[name])return b;}catch(e){if(e.code!=='ENOENT')throw e;}
18
+ log(`[Kokoro] Descargando ${name} (solo la primera vez).`);
19
+ const response=await fetchImpl(`https://huggingface.co/${MODEL}/resolve/${REVISION}/${name}`,{signal:signal||AbortSignal.timeout(90000)});
20
+ if(!response.ok)throw Error(`Descarga Kokoro: HTTP ${response.status}.`);
21
+ const data=Buffer.from(await response.arrayBuffer());if(hash(data)!==ASSETS[name])throw Error('La descarga Kokoro no pasó la verificación de integridad.');
22
+ await fs.mkdir(path.dirname(dest),{recursive:true});const temporary=dest+'.'+randomUUID()+'.tmp';
23
+ try{await fs.writeFile(temporary,data);await fs.rename(temporary,dest);}finally{await fs.rm(temporary,{force:true});}
24
+ return data;
25
+ }
26
+ async function ensureAssets(options){const [model,tokenizer,...voices]=await Promise.all(['onnx/model_quantized.onnx','tokenizer.json','voices/ef_dora.bin','voices/em_alex.bin','voices/em_santa.bin'].map(name=>asset(name,options)));return {model,voices:Object.fromEntries(['ef_dora','em_alex','em_santa'].map((v,i)=>[v,voices[i]])),vocab:JSON.parse(tokenizer).model.vocab};}
27
+ module.exports={ensureAssets,asset,ASSETS,MODEL,REVISION,cacheDirectory};
@@ -0,0 +1,31 @@
1
+ const {ensureAssets}=require('./kokoro-assets');
2
+ let engine;
3
+ async function load(options={}){
4
+ if(!engine)engine=(async()=>{
5
+ const wasm=process.platform==='win32'||process.env.KOKORO_RUNTIME==='wasm';
6
+ const ort=require(wasm?'onnxruntime-web':'onnxruntime-node');
7
+ if(wasm)ort.env.wasm.numThreads=1;
8
+ const {model,voices,vocab}=await ensureAssets(options);
9
+ const {default:init}=await import('@echogarden/espeak-ng-emscripten');
10
+ const module=await init();const espeak=new module.eSpeakNGWorker();
11
+ const session=await ort.InferenceSession.create(model,{executionProviders:[wasm?'wasm':'cpu'],intraOpNumThreads:2});
12
+ return {ort,voices,vocab,espeak,session};
13
+ })().catch(e=>{engine=null;throw e;});
14
+ return engine;
15
+ }
16
+ async function generate(text,{voice='ef_dora',speed=1,...options}={}){
17
+ if(!['ef_dora','em_alex','em_santa'].includes(voice))throw Error('Voz española desconocida.');
18
+ if(!Number.isFinite(speed)||speed<0.5||speed>2)throw Error('kokoroSpeed debe estar entre 0.5 y 2.');
19
+ const {ort,voices,vocab,espeak,session}=await load(options);
20
+ espeak.set_voice('es');
21
+ // Preserve clause punctuation and map eSpeak IPA affricates to Kokoro's vocabulary.
22
+ const phones=String(text).normalize('NFC').split(/([:;,.!?])/).map(part=>/^[,:;.!?]$/.test(part)?part:espeak.synthesize_ipa(part.replace(/[<>\[\]]/g,'')).ipa.replaceAll('_','').trim().replace(/\n/g,' ')).join(' ').replaceAll('tʃ','ʧ').replaceAll('dʒ','ʤ').replace(/\u0361/g,'');
23
+ const tokens=Array.from(phones).filter(c=>vocab[c]!==undefined).map(c=>vocab[c]);
24
+ if(!tokens.length)throw Error('No hay texto pronunciable.');
25
+ if(tokens.length>509)throw Error('El texto excede el límite de Kokoro.');
26
+ const ids=[0,...tokens,0];const offset=tokens.length*256*4;
27
+ const bytes=voices[voice].subarray(offset,offset+1024);const style=new Float32Array(bytes.buffer.slice(bytes.byteOffset,bytes.byteOffset+bytes.length));
28
+ const out=await session.run({input_ids:new ort.Tensor('int64',BigInt64Array.from(ids,BigInt),[1,ids.length]),style:new ort.Tensor('float32',style,[1,256]),speed:new ort.Tensor('float32',Float32Array.of(speed),[1])});
29
+ return {samples:out.waveform.data,sampleRate:24000};
30
+ }
31
+ module.exports={generate};
@@ -0,0 +1,13 @@
1
+ // Isolate native synthesis/playback from the overlay. Killing this process cancels both.
2
+ const {generate}=require('./kokoro-engine');
3
+ const {play}=require('./playback');
4
+ let input='';process.stdin.setEncoding('utf8');
5
+ process.stdin.on('data',chunk=>{input+=chunk;if(input.length>20000)process.exit(1);});
6
+ process.stdin.on('end',async()=>{
7
+ try{
8
+ const {text,voice,speed,cacheDir}=JSON.parse(input);const start=performance.now();
9
+ const {samples,sampleRate}=await generate(text,{voice,speed,cacheDir,log:message=>console.error(message)});
10
+ console.error(`[Kokoro] Audio generado en ${((performance.now()-start)/1000).toFixed(2)} s.`);
11
+ if(process.platform==='win32')await require('./windows-playback').playWindows(samples,sampleRate);else await play(samples,sampleRate);process.exit(0);
12
+ }catch(e){console.error('[Kokoro] '+e.message);process.exit(1);}
13
+ });