runonweb 0.0.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/README.md +165 -0
- package/package.json +126 -0
- package/src/caption/index.ts +310 -0
- package/src/clean/index.ts +263 -0
- package/src/core/cache.ts +248 -0
- package/src/core/device.ts +34 -0
- package/src/core/index.ts +18 -0
- package/src/core/pipeline.ts +108 -0
- package/src/core/progress.ts +20 -0
- package/src/depth/index.ts +127 -0
- package/src/detect/index.ts +274 -0
- package/src/embed/index.ts +172 -0
- package/src/emoji/index.ts +132 -0
- package/src/image/engine.d.ts +57 -0
- package/src/image/engine.js +14090 -0
- package/src/image/index.ts +230 -0
- package/src/image/sizes.ts +28 -0
- package/src/ocr/index.ts +416 -0
- package/src/ocr/sizes.ts +12 -0
- package/src/remove-bg/index.ts +170 -0
- package/src/stt/index.ts +320 -0
- package/src/translate/bergamot.ts +280 -0
- package/src/translate/index.ts +240 -0
- package/src/translate/registry.ts +133 -0
- package/src/translate/worker.ts +181 -0
- package/src/tts/index.ts +220 -0
- package/src/tts/kitten.ts +280 -0
- package/src/tts/kokoro.ts +140 -0
- package/src/tts/phonemes.ts +150 -0
- package/src/tts/sizes.ts +30 -0
- package/src/tts/split.ts +31 -0
- package/src/tts/supertonic.ts +126 -0
- package/src/tts/types.ts +11 -0
- package/src/tts/voices.ts +131 -0
- package/src/tts/wav.ts +52 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Device, ProgressCallback, ResolvedDevice } from '../core/index.ts'
|
|
2
|
+
import { resolveDevice, toProgressInfo } from '../core/index.ts'
|
|
3
|
+
import { splitUtterances } from './split.ts'
|
|
4
|
+
import { toFloat32 } from './wav.ts'
|
|
5
|
+
import type { SpeakChunk } from './types.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Supertonic 2 (Supertone). One 44.1 kHz voice model for English, Korean, Spanish,
|
|
9
|
+
* Portuguese and French; 10 preset voices shared across languages. OpenRAIL-M license.
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_MODEL = 'onnx-community/Supertonic-TTS-2-ONNX'
|
|
12
|
+
const SAMPLE_RATE = 44_100
|
|
13
|
+
const VOICE_CACHE = 'runonweb-supertonic-voices'
|
|
14
|
+
|
|
15
|
+
export const SUPERTONIC_LANGUAGES = ['en', 'ko', 'es', 'pt', 'fr'] as const
|
|
16
|
+
export type SupertonicLanguage = (typeof SUPERTONIC_LANGUAGES)[number]
|
|
17
|
+
|
|
18
|
+
type RawAudio = { audio?: Float32Array | number[]; data?: Float32Array | number[]; sampling_rate?: number }
|
|
19
|
+
|
|
20
|
+
type SupertonicPipe = {
|
|
21
|
+
(
|
|
22
|
+
text: string,
|
|
23
|
+
options: { speaker_embeddings: Float32Array; num_inference_steps?: number; speed?: number }
|
|
24
|
+
): Promise<RawAudio>
|
|
25
|
+
dispose?: () => Promise<void>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type LoadedSupertonic = {
|
|
29
|
+
pipe: SupertonicPipe
|
|
30
|
+
modelId: string
|
|
31
|
+
device: ResolvedDevice
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function loadSupertonic(options: {
|
|
35
|
+
model?: string
|
|
36
|
+
device?: Device
|
|
37
|
+
onProgress?: ProgressCallback
|
|
38
|
+
}): Promise<LoadedSupertonic> {
|
|
39
|
+
const device = await resolveDevice(options.device ?? 'auto')
|
|
40
|
+
options.onProgress?.({ status: 'loading', progress: 0 })
|
|
41
|
+
|
|
42
|
+
const { pipeline, env } = await import('@huggingface/transformers')
|
|
43
|
+
env.allowLocalModels = false
|
|
44
|
+
const modelId = options.model ?? DEFAULT_MODEL
|
|
45
|
+
|
|
46
|
+
const pipe = await pipeline('text-to-speech', modelId, {
|
|
47
|
+
device,
|
|
48
|
+
dtype: 'fp32',
|
|
49
|
+
progress_callback: (data: Record<string, unknown>) => {
|
|
50
|
+
options.onProgress?.(toProgressInfo(data))
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
options.onProgress?.({ status: 'ready', progress: 100 })
|
|
55
|
+
return { pipe: pipe as unknown as SupertonicPipe, modelId, device }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function* streamSupertonic(
|
|
59
|
+
st: LoadedSupertonic,
|
|
60
|
+
text: string,
|
|
61
|
+
voice: string,
|
|
62
|
+
speed: number,
|
|
63
|
+
language: string,
|
|
64
|
+
steps = 5
|
|
65
|
+
): AsyncGenerator<SpeakChunk> {
|
|
66
|
+
const lang = (SUPERTONIC_LANGUAGES as readonly string[]).includes(language) ? language : 'en'
|
|
67
|
+
const style = await loadVoice(st.modelId, voice)
|
|
68
|
+
for (const piece of splitUtterances(text)) {
|
|
69
|
+
const raw = await st.pipe(`<${lang}>${piece}</${lang}>`, {
|
|
70
|
+
speaker_embeddings: style,
|
|
71
|
+
num_inference_steps: steps,
|
|
72
|
+
speed,
|
|
73
|
+
})
|
|
74
|
+
yield { audio: toFloat32(raw.audio ?? raw.data ?? []), samplingRate: raw.sampling_rate || SAMPLE_RATE, text: piece }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const voiceCache = new Map<string, Promise<Float32Array>>()
|
|
79
|
+
|
|
80
|
+
/** Supertonic voice ids are `F1`…`F5` and `M1`…`M5`; we expose them as `st_f1`… */
|
|
81
|
+
export function supertonicVoiceFile(voice: string): string {
|
|
82
|
+
const m = /^(?:st_)?([fm])([1-5])$/i.exec(voice)
|
|
83
|
+
if (!m) throw new Error(`Unknown Supertonic voice "${voice}"`)
|
|
84
|
+
return `${m[1]!.toUpperCase()}${m[2]}`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function loadVoice(modelId: string, voice: string): Promise<Float32Array> {
|
|
88
|
+
const file = supertonicVoiceFile(voice)
|
|
89
|
+
const key = `${modelId}/${file}`
|
|
90
|
+
let pending = voiceCache.get(key)
|
|
91
|
+
if (!pending) {
|
|
92
|
+
pending = fetchVoice(modelId, file).catch((err) => {
|
|
93
|
+
voiceCache.delete(key)
|
|
94
|
+
throw err
|
|
95
|
+
})
|
|
96
|
+
voiceCache.set(key, pending)
|
|
97
|
+
}
|
|
98
|
+
return pending
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function fetchVoice(modelId: string, file: string): Promise<Float32Array> {
|
|
102
|
+
const url = `https://huggingface.co/${modelId}/resolve/main/voices/${file}.bin`
|
|
103
|
+
let cache: Cache | null = null
|
|
104
|
+
try {
|
|
105
|
+
cache = await caches.open(VOICE_CACHE)
|
|
106
|
+
const hit = await cache.match(url)
|
|
107
|
+
if (hit) return new Float32Array(await hit.arrayBuffer())
|
|
108
|
+
} catch {
|
|
109
|
+
cache = null
|
|
110
|
+
}
|
|
111
|
+
const res = await fetch(url)
|
|
112
|
+
if (!res.ok) throw new Error(`Voice "${file}" not found (${res.status})`)
|
|
113
|
+
const buffer = await res.arrayBuffer()
|
|
114
|
+
try {
|
|
115
|
+
await cache?.put(url, new Response(buffer.slice(0), { headers: { 'content-type': 'application/octet-stream' } }))
|
|
116
|
+
} catch {
|
|
117
|
+
// Cache is best-effort.
|
|
118
|
+
}
|
|
119
|
+
return new Float32Array(buffer)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function disposeSupertonic(st: LoadedSupertonic | null) {
|
|
123
|
+
void st?.pipe.dispose?.()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export { DEFAULT_MODEL as SUPERTONIC_MODEL, SAMPLE_RATE as SUPERTONIC_SAMPLE_RATE }
|
package/src/tts/types.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { DEFAULT_TTS_SIZE, type TTSSize } from './sizes.ts'
|
|
2
|
+
|
|
3
|
+
export type TTSLocale = 'en-US' | 'en-GB' | 'es' | 'fr' | 'multi'
|
|
4
|
+
export type TTSGender = 'female' | 'male'
|
|
5
|
+
|
|
6
|
+
export type TTSVoice = {
|
|
7
|
+
id: string
|
|
8
|
+
name: string
|
|
9
|
+
locale: TTSLocale
|
|
10
|
+
gender: TTSGender
|
|
11
|
+
/** Subjective grade. A is best. Kitten voices are ungraded. */
|
|
12
|
+
grade: string
|
|
13
|
+
size: TTSSize
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_VOICE = 'af_heart'
|
|
17
|
+
export const DEFAULT_TINY_VOICE = 'bella'
|
|
18
|
+
export const DEFAULT_MULTI_VOICE = 'st_f1'
|
|
19
|
+
|
|
20
|
+
/** Kokoro v1.0 voices (small) + KittenTTS voices (tiny) + Supertonic 2 presets (multi, any of its 5 languages). */
|
|
21
|
+
export const TTS_VOICES: readonly TTSVoice[] = [
|
|
22
|
+
{ id: 'af_heart', name: 'Heart', locale: 'en-US', gender: 'female', grade: 'A', size: 'small' },
|
|
23
|
+
{ id: 'af_bella', name: 'Bella', locale: 'en-US', gender: 'female', grade: 'A-', size: 'small' },
|
|
24
|
+
{ id: 'af_nicole', name: 'Nicole', locale: 'en-US', gender: 'female', grade: 'B-', size: 'small' },
|
|
25
|
+
{ id: 'af_aoede', name: 'Aoede', locale: 'en-US', gender: 'female', grade: 'C+', size: 'small' },
|
|
26
|
+
{ id: 'af_kore', name: 'Kore', locale: 'en-US', gender: 'female', grade: 'C+', size: 'small' },
|
|
27
|
+
{ id: 'af_sarah', name: 'Sarah', locale: 'en-US', gender: 'female', grade: 'C+', size: 'small' },
|
|
28
|
+
{ id: 'am_fenrir', name: 'Fenrir', locale: 'en-US', gender: 'male', grade: 'C+', size: 'small' },
|
|
29
|
+
{ id: 'am_michael', name: 'Michael', locale: 'en-US', gender: 'male', grade: 'C+', size: 'small' },
|
|
30
|
+
{ id: 'am_puck', name: 'Puck', locale: 'en-US', gender: 'male', grade: 'C+', size: 'small' },
|
|
31
|
+
{ id: 'af_alloy', name: 'Alloy', locale: 'en-US', gender: 'female', grade: 'C', size: 'small' },
|
|
32
|
+
{ id: 'af_nova', name: 'Nova', locale: 'en-US', gender: 'female', grade: 'C', size: 'small' },
|
|
33
|
+
{ id: 'af_sky', name: 'Sky', locale: 'en-US', gender: 'female', grade: 'C-', size: 'small' },
|
|
34
|
+
{ id: 'af_jessica', name: 'Jessica', locale: 'en-US', gender: 'female', grade: 'D', size: 'small' },
|
|
35
|
+
{ id: 'af_river', name: 'River', locale: 'en-US', gender: 'female', grade: 'D', size: 'small' },
|
|
36
|
+
{ id: 'am_echo', name: 'Echo', locale: 'en-US', gender: 'male', grade: 'D', size: 'small' },
|
|
37
|
+
{ id: 'am_eric', name: 'Eric', locale: 'en-US', gender: 'male', grade: 'D', size: 'small' },
|
|
38
|
+
{ id: 'am_liam', name: 'Liam', locale: 'en-US', gender: 'male', grade: 'D', size: 'small' },
|
|
39
|
+
{ id: 'am_onyx', name: 'Onyx', locale: 'en-US', gender: 'male', grade: 'D', size: 'small' },
|
|
40
|
+
{ id: 'am_santa', name: 'Santa', locale: 'en-US', gender: 'male', grade: 'D-', size: 'small' },
|
|
41
|
+
{ id: 'am_adam', name: 'Adam', locale: 'en-US', gender: 'male', grade: 'F+', size: 'small' },
|
|
42
|
+
{ id: 'bf_emma', name: 'Emma', locale: 'en-GB', gender: 'female', grade: 'B-', size: 'small' },
|
|
43
|
+
{ id: 'bf_isabella', name: 'Isabella', locale: 'en-GB', gender: 'female', grade: 'C', size: 'small' },
|
|
44
|
+
{ id: 'bm_george', name: 'George', locale: 'en-GB', gender: 'male', grade: 'C', size: 'small' },
|
|
45
|
+
{ id: 'bm_fable', name: 'Fable', locale: 'en-GB', gender: 'male', grade: 'C', size: 'small' },
|
|
46
|
+
{ id: 'bm_lewis', name: 'Lewis', locale: 'en-GB', gender: 'male', grade: 'D+', size: 'small' },
|
|
47
|
+
{ id: 'bf_alice', name: 'Alice', locale: 'en-GB', gender: 'female', grade: 'D', size: 'small' },
|
|
48
|
+
{ id: 'bf_lily', name: 'Lily', locale: 'en-GB', gender: 'female', grade: 'D', size: 'small' },
|
|
49
|
+
{ id: 'bm_daniel', name: 'Daniel', locale: 'en-GB', gender: 'male', grade: 'D', size: 'small' },
|
|
50
|
+
{ id: 'ef_dora', name: 'Dora', locale: 'es', gender: 'female', grade: 'D', size: 'small' },
|
|
51
|
+
{ id: 'em_alex', name: 'Alex', locale: 'es', gender: 'male', grade: 'D', size: 'small' },
|
|
52
|
+
{ id: 'em_santa', name: 'Santa', locale: 'es', gender: 'male', grade: 'D', size: 'small' },
|
|
53
|
+
{ id: 'ff_siwis', name: 'Siwis', locale: 'fr', gender: 'female', grade: 'B-', size: 'small' },
|
|
54
|
+
{ id: 'bella', name: 'Bella', locale: 'en-US', gender: 'female', grade: '', size: 'tiny' },
|
|
55
|
+
{ id: 'luna', name: 'Luna', locale: 'en-US', gender: 'female', grade: '', size: 'tiny' },
|
|
56
|
+
{ id: 'rosie', name: 'Rosie', locale: 'en-US', gender: 'female', grade: '', size: 'tiny' },
|
|
57
|
+
{ id: 'kiki', name: 'Kiki', locale: 'en-US', gender: 'female', grade: '', size: 'tiny' },
|
|
58
|
+
{ id: 'jasper', name: 'Jasper', locale: 'en-US', gender: 'male', grade: '', size: 'tiny' },
|
|
59
|
+
{ id: 'bruno', name: 'Bruno', locale: 'en-US', gender: 'male', grade: '', size: 'tiny' },
|
|
60
|
+
{ id: 'hugo', name: 'Hugo', locale: 'en-US', gender: 'male', grade: '', size: 'tiny' },
|
|
61
|
+
{ id: 'leo', name: 'Leo', locale: 'en-US', gender: 'male', grade: '', size: 'tiny' },
|
|
62
|
+
{ id: 'st_f1', name: 'F1', locale: 'multi', gender: 'female', grade: '', size: 'multi' },
|
|
63
|
+
{ id: 'st_f2', name: 'F2', locale: 'multi', gender: 'female', grade: '', size: 'multi' },
|
|
64
|
+
{ id: 'st_f3', name: 'F3', locale: 'multi', gender: 'female', grade: '', size: 'multi' },
|
|
65
|
+
{ id: 'st_f4', name: 'F4', locale: 'multi', gender: 'female', grade: '', size: 'multi' },
|
|
66
|
+
{ id: 'st_f5', name: 'F5', locale: 'multi', gender: 'female', grade: '', size: 'multi' },
|
|
67
|
+
{ id: 'st_m1', name: 'M1', locale: 'multi', gender: 'male', grade: '', size: 'multi' },
|
|
68
|
+
{ id: 'st_m2', name: 'M2', locale: 'multi', gender: 'male', grade: '', size: 'multi' },
|
|
69
|
+
{ id: 'st_m3', name: 'M3', locale: 'multi', gender: 'male', grade: '', size: 'multi' },
|
|
70
|
+
{ id: 'st_m4', name: 'M4', locale: 'multi', gender: 'male', grade: '', size: 'multi' },
|
|
71
|
+
{ id: 'st_m5', name: 'M5', locale: 'multi', gender: 'male', grade: '', size: 'multi' },
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
/** Languages Supertonic 2 (`multi`) can speak with any of its voices. */
|
|
75
|
+
export const TTS_LANGUAGES = {
|
|
76
|
+
en: 'English',
|
|
77
|
+
ko: 'Korean',
|
|
78
|
+
es: 'Spanish',
|
|
79
|
+
pt: 'Portuguese',
|
|
80
|
+
fr: 'French',
|
|
81
|
+
} as const
|
|
82
|
+
|
|
83
|
+
export type TTSLanguage = keyof typeof TTS_LANGUAGES
|
|
84
|
+
|
|
85
|
+
const LOCALE_LABEL: Record<TTSLocale, string> = {
|
|
86
|
+
'en-US': 'American English',
|
|
87
|
+
'en-GB': 'British English',
|
|
88
|
+
es: 'Spanish',
|
|
89
|
+
fr: 'French',
|
|
90
|
+
multi: 'Supertonic · English, Korean, Spanish, Portuguese, French',
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function voicesFor(size: TTSSize = DEFAULT_TTS_SIZE): TTSVoice[] {
|
|
94
|
+
return TTS_VOICES.filter((v) => v.size === size)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function defaultVoiceFor(size: TTSSize = DEFAULT_TTS_SIZE): string {
|
|
98
|
+
return size === 'tiny' ? DEFAULT_TINY_VOICE : size === 'multi' ? DEFAULT_MULTI_VOICE : DEFAULT_VOICE
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getVoice(id: string): TTSVoice | undefined {
|
|
102
|
+
return TTS_VOICES.find((v) => v.id === id)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const TTS_VOICE_GROUPS = voiceGroupsFor(DEFAULT_TTS_SIZE)
|
|
106
|
+
|
|
107
|
+
export function voiceGroupsFor(size: TTSSize = DEFAULT_TTS_SIZE) {
|
|
108
|
+
const voices = voicesFor(size)
|
|
109
|
+
const locales = [...new Set(voices.map((v) => v.locale))]
|
|
110
|
+
return locales.map((locale) => ({
|
|
111
|
+
locale,
|
|
112
|
+
label: LOCALE_LABEL[locale],
|
|
113
|
+
voices: voices.filter((v) => v.locale === locale),
|
|
114
|
+
}))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function voiceLabel(voice: TTSVoice): string {
|
|
118
|
+
if (voice.locale === 'multi') return `${voice.name} · ${voice.gender}`
|
|
119
|
+
const region =
|
|
120
|
+
voice.locale === 'en-GB' ? 'British' : voice.locale === 'es' ? 'Spanish' : voice.locale === 'fr' ? 'French' : 'American'
|
|
121
|
+
const grade = voice.grade ? `${voice.gender} · ${voice.grade}` : voice.gender
|
|
122
|
+
return `${voice.name} · ${region} ${grade}`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** eSpeak language for Kokoro voices that `phonemizer` cannot handle (Spanish, French). */
|
|
126
|
+
export function kokoroPhonemeLang(voiceId: string): 'es' | 'fr-fr' | null {
|
|
127
|
+
const prefix = voiceId.slice(0, 2)
|
|
128
|
+
if (prefix === 'ef' || prefix === 'em') return 'es'
|
|
129
|
+
if (prefix === 'ff') return 'fr-fr'
|
|
130
|
+
return null
|
|
131
|
+
}
|
package/src/tts/wav.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Encode mono Float32 PCM as a 16-bit WAV Blob. */
|
|
2
|
+
export function float32ToWavBlob(samples: Float32Array, sampleRate: number): Blob {
|
|
3
|
+
const numChannels = 1
|
|
4
|
+
const bytesPerSample = 2
|
|
5
|
+
const blockAlign = numChannels * bytesPerSample
|
|
6
|
+
const buffer = new ArrayBuffer(44 + samples.length * bytesPerSample)
|
|
7
|
+
const view = new DataView(buffer)
|
|
8
|
+
|
|
9
|
+
writeString(view, 0, 'RIFF')
|
|
10
|
+
view.setUint32(4, 36 + samples.length * bytesPerSample, true)
|
|
11
|
+
writeString(view, 8, 'WAVE')
|
|
12
|
+
writeString(view, 12, 'fmt ')
|
|
13
|
+
view.setUint32(16, 16, true)
|
|
14
|
+
view.setUint16(20, 1, true)
|
|
15
|
+
view.setUint16(22, numChannels, true)
|
|
16
|
+
view.setUint32(24, sampleRate, true)
|
|
17
|
+
view.setUint32(28, sampleRate * blockAlign, true)
|
|
18
|
+
view.setUint16(32, blockAlign, true)
|
|
19
|
+
view.setUint16(34, 8 * bytesPerSample, true)
|
|
20
|
+
writeString(view, 36, 'data')
|
|
21
|
+
view.setUint32(40, samples.length * bytesPerSample, true)
|
|
22
|
+
|
|
23
|
+
let offset = 44
|
|
24
|
+
for (let i = 0; i < samples.length; i++) {
|
|
25
|
+
const s = Math.max(-1, Math.min(1, samples[i]!))
|
|
26
|
+
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true)
|
|
27
|
+
offset += 2
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return new Blob([buffer], { type: 'audio/wav' })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function concatFloat32(chunks: Float32Array[]): Float32Array {
|
|
34
|
+
const total = chunks.reduce((n, c) => n + c.length, 0)
|
|
35
|
+
const out = new Float32Array(total)
|
|
36
|
+
let offset = 0
|
|
37
|
+
for (const chunk of chunks) {
|
|
38
|
+
out.set(chunk, offset)
|
|
39
|
+
offset += chunk.length
|
|
40
|
+
}
|
|
41
|
+
return out
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function toFloat32(data: Float32Array | number[] | ArrayLike<number>): Float32Array {
|
|
45
|
+
return data instanceof Float32Array ? data : new Float32Array(data)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeString(view: DataView, offset: number, str: string) {
|
|
49
|
+
for (let i = 0; i < str.length; i++) {
|
|
50
|
+
view.setUint8(offset + i, str.charCodeAt(i))
|
|
51
|
+
}
|
|
52
|
+
}
|