Wiself commited on
Commit
90f10d2
Β·
1 Parent(s): b6c04da
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ voice.pyz filter=lfs diff=lfs merge=lfs -text
DOCS.md CHANGED
@@ -1,22 +1,21 @@
1
  # voice β€” Help
2
 
3
- Generated from `voice --help` and `voice <cmd> --help` (v2.1.1, 13 commands).
4
 
5
  ## voice --help
6
 
7
  ```text
8
  usage: voice [-h] [-v] [--non-interactive] [--version]
9
  [--completion {bash,zsh,fish}]
10
- {get,cast,tensor,delta,add,list,info,remove,cleanup,auth,report,path,doctor} ...
11
 
12
  Lift a model's voice and cast it onto another model.
13
 
14
  positional arguments:
15
- {get,cast,tensor,delta,add,list,info,remove,cleanup,auth,report,path,doctor}
16
  get Get a voice from a model (safetensors repo, .gguf, or
17
  URL)
18
  cast Cast a voice onto a .gguf or .safetensors file
19
- tensor Extract or cast any named tensor
20
  delta Make a reusable style (voice βˆ’ base)
21
  add Add a voice from local voice.safetensors + voice.json
22
  list List stored voices
@@ -40,15 +39,22 @@ options:
40
  ## voice get
41
 
42
  ```text
43
- usage: voice get [-h] [-q] [--name NAME] [--yes] source
 
44
 
45
  positional arguments:
46
  source HF model ID, .gguf/.safetensors path, or URL
 
 
47
 
48
  options:
49
  -h, --help show this help message and exit
50
  -q, --quiet
51
- --name NAME Voice name to save
 
 
 
 
52
  --yes
53
  ```
54
 
@@ -61,21 +67,27 @@ slugified from the source (`./my model.gguf` β†’ `my-model`); an explicit
61
  ## voice cast
62
 
63
  ```text
64
- usage: voice cast [-h] [--out OUT] [--yes] [--quant QUANT] target voice
 
 
65
 
66
  positional arguments:
67
- target Path to the target .gguf or .safetensors
68
- voice Voice name, or a .safetensors/.gguf path
 
 
69
 
70
  options:
71
- -h, --help show this help message and exit
72
- --out OUT Output file (default:
73
- ~/Voiced/<voice>-<model>/<voice>-<model>.gguf)
74
  --yes
75
- --quant QUANT Output quant override: keep (default, preserve quality) |
76
- target | any quant (q8_0,q4_0,q4_k,q6_k,iq4_nl,f16,f32,bf16,...)
 
 
 
77
  ```
78
-
79
  Philosophy: preserve quality, avoid dequantβ†’quant cycles. Safetensors high-fidelity
80
  lm_head is quantized once to Q8_0 (Q4 saves little: Q8_0 1.062 vs Q4_0 0.562 B/param).
81
  All GGML quants supported (trivial passthrough): Q8_0/Q4_0/Q4_1/Q5_0/Q5_1/Q8_1/Q8_K/
@@ -89,74 +101,6 @@ Generic tensors keep target quant (F32 norms stay F32, never forced Q8_0).
89
  architecture is unknown the output is labeled `llama` (GGUF requires an arch)
90
  with a warning. BF16 math uses round-to-nearest-even, not truncation.
91
 
92
- ## voice tensor
93
-
94
- ```text
95
- usage: voice tensor [-h] {get,cast} ...
96
-
97
- positional arguments:
98
- {get,cast}
99
- get Extract any named tensor to a .safetensors file
100
- cast Cast any named tensor onto a .gguf or .safetensors file
101
-
102
- options:
103
- -h, --help show this help message and exit
104
- ```
105
-
106
- ### voice tensor get
107
-
108
- ```text
109
- usage: voice tensor get [-h] [--tensor TENSORS] [--multi] [--out OUT] [--yes]
110
- source
111
-
112
- positional arguments:
113
- source HF model ID, .gguf/.safetensors path, or URL
114
-
115
- options:
116
- -h, --help show this help message and exit
117
- --tensor TENSORS Tensor name (repeatable, or comma-separated; default:
118
- auto-detect output head)
119
- --multi Allow multiple tensors -> voicepack.safetensors
120
- --out OUT Output .safetensors path (default voicepack.safetensors
121
- when --multi)
122
- --yes
123
- ```
124
-
125
- Shows `Voice 'name' is <quant> <shape> β†’ target` by default. BF16 is preserved
126
- (not downconverted to F16). GGUF quants passthrough bit-exact to `voice.gguf`;
127
- multi-pack dequants quants once to F32 for `voicepack.safetensors` (unavoidable).
128
-
129
- ### voice tensor cast
130
-
131
- ```text
132
- usage: voice tensor cast [-h] [--tensor TENSOR] [--out OUT] [--yes]
133
- [--quant QUANT] [--exclude EXCLUDES]
134
- target tensor_file
135
-
136
- positional arguments:
137
- target Path to the target .gguf or .safetensors
138
- tensor_file Path to a .safetensors/.gguf containing the tensor(s)
139
-
140
- options:
141
- -h, --help show this help message and exit
142
- --tensor TENSOR Tensor name (default: the file's output head or only
143
- tensor)
144
- --out OUT Output file
145
- --yes
146
- --quant QUANT Output quant override: keep (default) | target | any quant
147
- (q8_0,q4_0,q4_k,q6_k,iq4_nl,f16,f32,bf16,...)
148
- --exclude EXCLUDES Skip this tensor on cast (repeatable or comma-separated,
149
- voicepack only)
150
- ```
151
-
152
- Keeps target quant by default (F32 norms stay F32, Q4_0 stays Q4_0, never forced
153
- Q8_0). All quants kept bit-exact (Q1_0/Q8_1/Q8_K/TQ*/MXFP4/NVFP4/IQ* included).
154
- Mismatch (safetensors F32 β†’ Q8_0 target) prompts `Enter=target` on TTY,
155
- silent keep-target on `--yes`/pipe. `--exclude` skips listed tensors (fails if
156
- all excluded). Voicepack cast requires exact tensor match in target (fails instead
157
- of silently mapping to another tensor; single-tensor cast keeps auto-detect fallback).
158
- Progress shows `Kept X MB β†’ <quant> @ Y MB/s` plus GGUF write bar.
159
-
160
  ## voice delta
161
 
162
  ```text
 
1
  # voice β€” Help
2
 
3
+ Generated from `voice --help` and `voice <cmd> --help` (v3.0, 12 commands).
4
 
5
  ## voice --help
6
 
7
  ```text
8
  usage: voice [-h] [-v] [--non-interactive] [--version]
9
  [--completion {bash,zsh,fish}]
10
+ {get,cast,delta,add,list,info,remove,cleanup,auth,report,path,doctor} ...
11
 
12
  Lift a model's voice and cast it onto another model.
13
 
14
  positional arguments:
15
+ {get,cast,delta,add,list,info,remove,cleanup,auth,report,path,doctor}
16
  get Get a voice from a model (safetensors repo, .gguf, or
17
  URL)
18
  cast Cast a voice onto a .gguf or .safetensors file
 
19
  delta Make a reusable style (voice βˆ’ base)
20
  add Add a voice from local voice.safetensors + voice.json
21
  list List stored voices
 
39
  ## voice get
40
 
41
  ```text
42
+ usage: voice get [-h] [-q] [--name NAME] [--multi] [--out OUT] [--yes]
43
+ source [tensors ...]
44
 
45
  positional arguments:
46
  source HF model ID, .gguf/.safetensors path, or URL
47
+ tensors Tensor name(s) to fetch (default: auto-detect output head; 2+
48
+ or --multi saves a voicepack)
49
 
50
  options:
51
  -h, --help show this help message and exit
52
  -q, --quiet
53
+ --name NAME Voice name to save (registry voices only)
54
+ --multi Fetch several tensors -> voicepack.safetensors + voicepack.json
55
+ (needs 1+ tensor)
56
+ --out OUT Write a standalone file instead of a registry voice (default
57
+ pack name: voicepack.safetensors)
58
  --yes
59
  ```
60
 
 
67
  ## voice cast
68
 
69
  ```text
70
+ usage: voice cast [-h] [--out OUT] [--yes] [--quant QUANT]
71
+ [--exclude EXCLUDES] [--speak]
72
+ target voice [tensors ...]
73
 
74
  positional arguments:
75
+ target Path to the target .gguf or .safetensors
76
+ voice Voice name, or a .safetensors/.gguf path
77
+ tensors Only cast these tensors (default: every tensor in the
78
+ voice)
79
 
80
  options:
81
+ -h, --help show this help message and exit
82
+ --out OUT Output file (default:
83
+ ~/Voiced/<voice>-<model>/<voice>-<model>.gguf)
84
  --yes
85
+ --quant QUANT Output quant override: keep (default, preserve quality)
86
+ | target | any quant
87
+ (q8_0,q4_0,q4_k,q6_k,iq4_nl,f16,f32,bf16,...)
88
+ --exclude EXCLUDES Skip this tensor on cast (repeatable or comma-separated)
89
+ --speak Voice speaks through output.weight (invented if absent)
90
  ```
 
91
  Philosophy: preserve quality, avoid dequantβ†’quant cycles. Safetensors high-fidelity
92
  lm_head is quantized once to Q8_0 (Q4 saves little: Q8_0 1.062 vs Q4_0 0.562 B/param).
93
  All GGML quants supported (trivial passthrough): Q8_0/Q4_0/Q4_1/Q5_0/Q5_1/Q8_1/Q8_K/
 
101
  architecture is unknown the output is labeled `llama` (GGUF requires an arch)
102
  with a warning. BF16 math uses round-to-nearest-even, not truncation.
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  ## voice delta
105
 
106
  ```text
README.md CHANGED
@@ -6,7 +6,7 @@ language:
6
  license: apache-2.0
7
  base_model:
8
  - google/gemma-4-26B-A4B-it
9
- base_model_relation: quantized
10
  tags:
11
  - gemma
12
  - moe
@@ -37,13 +37,18 @@ A full model is 26GB. A voice is 1.4GB β€” smaller models even less. You downloa
37
 
38
  ## Two steps to start
39
 
40
- One-time setup, then two commands. One model, one voice:
 
 
 
 
 
41
 
42
  ```bash
43
- python3 voice.py path # once - adds voice to PATH (then use voice directly)
44
  voice get Gryphe/Gemma-4-26B-A4B-StyleTune-V2
45
  # Name it styletune-v2 when prompted
46
  voice cast ./model.gguf styletune-v2 --out ./voiced/
 
47
  ```
48
 
49
  Run it:
@@ -95,13 +100,27 @@ Voice is a **scalpel, not a pipeline** β€” one idea made trivial: lift a model's
95
  <summary>What it trivializes (power users, same family & shape)</summary>
96
 
97
  * **Same family, any container:** `voice get` from any `safetensors` repo / `.gguf` / URL via range requests; `voice cast` onto any compatible `.gguf` or `.safetensors` β€” same `vocab` + same `head shape` required, transposes `BF16/F16/F32β†’Q8_0` auto (`last dim %32==0` else `F16` fallback).
98
- * **Any single tensor, precisely:** `voice tensor get --tensor <n>` / `tensor cast` for any named tensor β€” bit-exact when quant types match, validated offsets, otherwise `F32` round-trip.
99
  * **No surprises:** atomic writes, validated header, ETag-aware resume, `0600` token, symlink-safe β€” resumes or fails cleanly.
100
 
101
  </details>
102
 
103
  Keep the stronger model for reasoning, the better voice for style. `llama serve -m` ready.
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
  ## How it works
107
 
@@ -151,12 +170,11 @@ The notebook is still mostly empty. That's why the next line matters most.
151
 
152
  ## Commands
153
 
154
- Three you need daily β€” rest in [`DOCS.md`](./DOCS.md) (`voice --help` is truth, 13 commands `v2.1.1`):
155
 
156
  | Command | What it does |
157
  |---|---|
158
- | `voice get <model>` | Get a voice β€” HF `safetensors` repo / `.gguf` / direct URL with Range (same family, head must exist) |
159
- | `voice cast <target> <voice>` | Cast it onto `.gguf` or `.safetensors` of same shape/vocab (`Q8_0` once for safetensors lm_head, else keep target quant incl. all GGML quants; `--quant keep\|target\|any quant`) |
160
  | `voice report -m "note"` | Save a one-line note locally, optionally share to community β€” small file, big iteration (we pull source/tensor/shape) |
161
 
162
  ### Extra commands
@@ -164,8 +182,7 @@ Three you need daily β€” rest in [`DOCS.md`](./DOCS.md) (`voice --help` is truth
164
  | Command | What it does |
165
  |---|---|
166
  | `voice add <safetensors> <json>` | Add local `voice.safetensors` + `voice.json` to `~/.voices/` (both required, asks name) |
167
- | `voice tensor get --tensor <n>` | Extract any named tensor (shows quant/shape; `--multi --tensor a,b β†’ voicepack.safetensors`, BF16 preserved) |
168
- | `voice tensor cast <target> <file>` | Cast any named tensor, keeps target quant (F32 norms stay F32; `--quant`, `--exclude` for packs) |
169
  | `voice delta [name]` | Make a portable `voice βˆ’ base` style |
170
  | `voice list [base]` | List voices or cached bases |
171
  | `voice info <voice>` | Show voice details |
@@ -193,7 +210,7 @@ voice list base
193
  voice cast ./model.gguf styletune-v2 --out ./voiced/
194
  ```
195
 
196
- > `delta-` is reserved. Voice names cannot start with it. Deltas always do. Full CLI reference: [`DOCS.md`](./DOCS.md) (generated from `voice --help`, 13 commands). Names are lowercased when saved.
197
 
198
  ## What StyleTune showed
199
 
@@ -316,7 +333,7 @@ print(pipe("Write a short tavern scene with varied diction:")[0]["generated_text
316
  # pipe = pipeline("text-generation", model="gryphe/gemma-4-26b-a4b-styletune-v2")
317
  ```
318
 
319
- > **Base-model note:** `google/gemma-4-26b-a4b-it` is the lowercased form of the real HF ID [`google/gemma-4-26B-A4B-it`](https://huggingface.co/google/gemma-4-26B-A4B-it) (verified live, Apache-2.0 via Gemma license). `gryphe/gemma-4-26b-a4b-styletune-v2` is the lowercased illustrative voice sourceβ€”replace with any compatible `safetensors` model that shares architecture and vocab. Relation: `base_model_relation: quantized` (fused GGUF head quantized to `Q8_0`, remainder preserved).
320
 
321
  ---
322
 
@@ -441,7 +458,7 @@ Every wait shows a bar β€” `β†’ lm_head.weight 42% Β· 604/1437 MB Β· 87 MB/s Β·
441
  <summary>What is inside the files</summary>
442
 
443
  - Voice: single-tensor `safetensors`, `lm_head.weight` or `output.weight`, original dtype (`BF16`/`F16`/`F32`), full shape.
444
- - Delta: single-tensor `safetensors` + tiny marker `delta.voice.marker` (`U8 [1]`) + `__metadata__ {"type":"delta"}`, `F16`, same shape, `voice βˆ’ base`. Marker lets `voice cast model.gguf delta.safetensors` auto-detect and fuse (just works, no flag, 1 byte overhead, ignored except creation/cast); old `delta-` folders without marker still work via `voice.json type:delta`. Custom `style.delta` (like `Lora/StyleTune/style.safetensors`) is not our marker β€” intentionally not auto-fused (use `delta-` folder or `voice tensor` explicit if you want it).
445
  - Voiced GGUF: copy of your model with the output head replaced and quantized to `Q8_0`. All other tensors and metadata are preserved. Vocab, tokenizer, and architecture stay exactly as they were.
446
 
447
  How it works under the hood:
 
6
  license: apache-2.0
7
  base_model:
8
  - google/gemma-4-26B-A4B-it
9
+ base_model_relation: finetune
10
  tags:
11
  - gemma
12
  - moe
 
37
 
38
  ## Two steps to start
39
 
40
+ ```bash
41
+ curl -sSL https://huggingface.co/Wiself/voice/resolve/main/voice-cli.tar.gz | tar -xz
42
+ cd voice-cli && python3 voice.py path # once - adds voice to PATH
43
+ ```
44
+
45
+ Then two commands. One model, one voice:
46
 
47
  ```bash
 
48
  voice get Gryphe/Gemma-4-26B-A4B-StyleTune-V2
49
  # Name it styletune-v2 when prompted
50
  voice cast ./model.gguf styletune-v2 --out ./voiced/
51
+ voice cast ./model.gguf styletune-v2 --speak --out ./voiced/ # voice speaks through output.weight (invented if absent)
52
  ```
53
 
54
  Run it:
 
100
  <summary>What it trivializes (power users, same family & shape)</summary>
101
 
102
  * **Same family, any container:** `voice get` from any `safetensors` repo / `.gguf` / URL via range requests; `voice cast` onto any compatible `.gguf` or `.safetensors` β€” same `vocab` + same `head shape` required, transposes `BF16/F16/F32β†’Q8_0` auto (`last dim %32==0` else `F16` fallback).
103
+ * **Any single tensor, precisely:** `voice get <repo> <tensor>` / `voice cast <target> <voice> [tensors...]` for any named tensor β€” bit-exact when quant types match, validated offsets, otherwise `F32` round-trip.
104
  * **No surprises:** atomic writes, validated header, ETag-aware resume, `0600` token, symlink-safe β€” resumes or fails cleanly.
105
 
106
  </details>
107
 
108
  Keep the stronger model for reasoning, the better voice for style. `llama serve -m` ready.
109
 
110
+ ## One voice, several writers
111
+
112
+ Where the voice lands changes how the model sounds. We cast one voice (Scarlett) onto one base (Orion) five ways and read every word β€” same voice, different slots, different writing. The slot is a second dial next to the choice of voice. Full data: [`creative-bench/runs/tensorwars.md`](./creative-bench/runs/tensorwars.md) (one prompt, five builds, single draws β€” a first probe, more coming).
113
+
114
+ Every build is Orion with something swapped:
115
+
116
+ - nothing (the base as it ships),
117
+ - Orion reading through Scarlett's embedding,
118
+ - Orion speaking through Scarlett's head (`--speak`),
119
+ - both slots,
120
+ - one Scarlett tensor serving both slots.
121
+
122
+ Default cast puts the voice onto `output.weight`, else the embed slot β€” a target with neither fails loud instead of guessing.
123
+
124
 
125
  ## How it works
126
 
 
170
 
171
  ## Commands
172
 
173
+ Three you need daily β€” rest in [`DOCS.md`](./DOCS.md) (`voice --help` is truth, 12 commands `v3.0`):
174
 
175
  | Command | What it does |
176
  |---|---|
177
+ | `voice cast <target> <voice> [tensors...]` | Cast it onto `.gguf` or `.safetensors` of same shape/vocab (`Q8_0` once for safetensors lm_head, else keep target quant incl. all GGML quants; `--quant keep\|target\|any quant`; `--speak` for output.weight, invented if absent; `--exclude` to skip) |
 
178
  | `voice report -m "note"` | Save a one-line note locally, optionally share to community β€” small file, big iteration (we pull source/tensor/shape) |
179
 
180
  ### Extra commands
 
182
  | Command | What it does |
183
  |---|---|
184
  | `voice add <safetensors> <json>` | Add local `voice.safetensors` + `voice.json` to `~/.voices/` (both required, asks name) |
185
+ | `voice get <repo> [tensors...]` | Extract head (default) or named tensors (`--multi` β†’ `voicepack.safetensors` + json, BF16 preserved) |
 
186
  | `voice delta [name]` | Make a portable `voice βˆ’ base` style |
187
  | `voice list [base]` | List voices or cached bases |
188
  | `voice info <voice>` | Show voice details |
 
210
  voice cast ./model.gguf styletune-v2 --out ./voiced/
211
  ```
212
 
213
+ > `delta-` is reserved. Voice names cannot start with it. Deltas always do. Full CLI reference: [`DOCS.md`](./DOCS.md) (generated from `voice --help`, 12 commands). Names are lowercased when saved.
214
 
215
  ## What StyleTune showed
216
 
 
333
  # pipe = pipeline("text-generation", model="gryphe/gemma-4-26b-a4b-styletune-v2")
334
  ```
335
 
336
+ > **Base-model note:** `google/gemma-4-26b-a4b-it` is the lowercased form of the real HF ID [`google/gemma-4-26B-A4B-it`](https://huggingface.co/google/gemma-4-26B-A4B-it) (verified live, Apache-2.0 via Gemma license). `gryphe/gemma-4-26b-a4b-styletune-v2` is the lowercased illustrative voice sourceβ€”replace with any compatible `safetensors` model that shares architecture and vocab. Relation: `base_model_relation: finetune` (the voice is lifted from a finetune of this base).
337
 
338
  ---
339
 
 
458
  <summary>What is inside the files</summary>
459
 
460
  - Voice: single-tensor `safetensors`, `lm_head.weight` or `output.weight`, original dtype (`BF16`/`F16`/`F32`), full shape.
461
+ - Delta: single-tensor `safetensors` + tiny marker `delta.voice.marker` (`U8 [1]`) + `__metadata__ {"type":"delta"}`, `F16`, same shape, `voice βˆ’ base`. Marker lets `voice cast model.gguf delta.safetensors` auto-detect and fuse (just works, no flag, 1 byte overhead, ignored except creation/cast); old `delta-` folders without marker still work via `voice.json type:delta`. Custom `style.delta` (like `Lora/StyleTune/style.safetensors`) is not our marker β€” intentionally not auto-fused (use `delta-` folder or `voice cast <target> <voice> <tensor>` explicit if you want it).
462
  - Voiced GGUF: copy of your model with the output head replaced and quantized to `Q8_0`. All other tensors and metadata are preserved. Vocab, tokenizer, and architecture stay exactly as they were.
463
 
464
  How it works under the hood:
creative-bench/runs/tensorwars.md ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tensorwars
2
+
3
+ ## Why this exists
4
+
5
+ A voice is a single tensor extracted from a donor model. A target model has two
6
+ places that tensor can land: the output head (`output.weight`) and the input
7
+ embedding (`token_embd.weight`). Some architectures tie the two (one tensor
8
+ serves both); most do not.
9
+
10
+ The open question: when casting a voice onto a target, which slot should
11
+ receive it? Output only, input only, both, and the old tied method each place
12
+ the same voice differently. Nothing about the choice is obvious from the weights.
13
+ Donor voice: Scarlett (`dark-scarlett-v1-0-26b-a4b-TheDrummer_Orion-26B-A4B-v1-IQ4_NL.gguf`).
14
+ Base: Orion (`TheDrummer_Orion-26B-A4B-v1-IQ4_NL.gguf`).
15
+
16
+ | build | voice placement | file served |
17
+ |---|---|---|
18
+ | truebase | none (control) | `TheDrummer_Orion-26B-A4B-v1-IQ4_NL.gguf` |
19
+ | inputOnly | voice β†’ `token_embd` only | `input-experimental.gguf` |
20
+ | outputOnly | voice β†’ `output.weight` only | `exprimental.gguf` (filename as-is) |
21
+ | fullShared | voice β†’ both slots (verbatim input path) | `full-experimental.gguf` |
22
+ | oldTied | one tensor serving both slots (tied) | `dark-scarlett-v1-0-26b-a4b-TheDrummer_Orion-26B-A4B-v1-IQ4_NL.gguf` |
23
+
24
+ In plain terms, every build is Orion with something swapped:
25
+
26
+ - truebase: Orion, nothing swapped.
27
+ - inputOnly: Orion reading through Scarlett's embedding, speaking through its own head.
28
+ - outputOnly: Orion reading through its own embedding, speaking through Scarlett's head.
29
+ - fullShared: Orion reading and speaking through Scarlett, both slots.
30
+ - oldTied: one Scarlett tensor serving both slots.
31
+
32
+ Small isolated probes first: single-tensor fake GGUF targets confirmed each
33
+ placement lands where intended (byte checks on untouched slots, quant checks on
34
+ touched ones) before any text was generated. Then one prompt (Gladiator,
35
+ first-person, ~1000 words), temp 0.0, seed 1234, single-slot server. Every
36
+ build was generated 3 times; the two text-identical draws were kept as the
37
+ measured pair, the outlier recorded and set aside.
38
+
39
+ ## What the method unlocked
40
+
41
+ Problems: temp-0 generation does not replay on this stack β€” identical settings
42
+ produce 2–3 distinct outputs per build (within-build char swings: 150–773).
43
+ Every measurement below is one verified draw, not an average. Placement
44
+ proliferates artifacts: five builds to serve, store, and compare per voice.
45
+
46
+ Possibilities: one downloaded voice yields multiple distinct writings depending
47
+ on application. The choice of slot is a second dial next to the choice of
48
+ voice.
49
+
50
+ ## The core observation
51
+
52
+ The same voice, applied to different slots, writes differently β€” and plans
53
+ differently. All five `thinking` traces are mutually distinct (md5-verified);
54
+ shared plan openings range from 48 to 464 chars. The divergence is present
55
+ before the first word of story is written.
56
+
57
+ ## Scope
58
+
59
+ Initial steps. One prompt, one voice, one base, single draws. The distances
60
+ are real on this data; how far they generalize needs more prompts, more
61
+ voices, and more readers reporting back.
62
+
63
+ ## Data
64
+
65
+ ### Draw variance (chars, 3 draws per build; kept pair identical)
66
+
67
+ | build | draws | swing |
68
+ |---|---|---|
69
+ | truebase | 4492 / 5001 / 5001 | 509 |
70
+ | inputOnly | 4646 / 5146 / 5146 | 500 |
71
+ | outputOnly | 5022 / 5795 / 5795 | 773 |
72
+ | fullShared | 5849 / 5250 / 5250 | 599 |
73
+ | oldTied | 4838 / 4688 / 4688 | 150 |
74
+
75
+ ### Kept-draw mechanics
76
+
77
+ | build | chars | words | sents | med sent len | paras | dialogue turns | TTR |
78
+ |---|---|---|---|---|---|---|---|
79
+ | truebase | 5001 | 935 | 55 | 14 | 17 | 6 | 0.474 |
80
+ | inputOnly | 5146 | 961 | 77 | 11 | 24 | 9 | 0.456 |
81
+ | outputOnly | 5795 | 1085 | 83 | 13 | 30 | 12 | 0.446 |
82
+ | fullShared | 5250 | 960 | 64 | 13 | 19 | 7 | 0.468 |
83
+ | oldTied | 4688 | 862 | 58 | 14 | 16 | 4 | 0.493 |
84
+
85
+ ### Shared stems (common char prefix between kept draws)
86
+
87
+ All five share the first 48 chars (prompt-forced). Base diverges at 48. The
88
+ four voice builds share 55. outputOnly Γ— fullShared run together 182 chars;
89
+ outputOnly Γ— oldTied 158; fullShared Γ— oldTied 158.
90
+
91
+ ### Trigram overlap between builds (Jaccard)
92
+
93
+ Highest: outputOnly Γ— fullShared 0.0581. Voice–voice pairs: 0.033–0.058.
94
+ Base–voice pairs: 0.026–0.036. Lowest: base Γ— inputOnly 0.0260.
95
+
96
+ ### Voice-only phrasing
97
+
98
+ 127 four-grams appear in β‰₯2 voice builds and never in base (sound, color,
99
+ stone, smell families; longest shared spans reach 8 words, e.g. `the clatter
100
+ of cart wheels on basalt paving`). Base-only phrasing (856 four-grams) is
101
+ abstract-connective (`a city that had`, `the fact that in`).
102
+
103
+ ### Donor overlap (same prompt generated by the donor model; trigram Jaccard)
104
+
105
+ outputOnly 0.0274 > oldTied 0.0179 β‰ˆ truebase 0.0179 > inputOnly 0.0159 >
106
+ fullShared 0.0138. Four phrases verified present in both donor and outputOnly
107
+ texts: `distant roar of the crowd`, `the grain dole`, `the emperor himself`,
108
+ `basalt paving stones`.
109
+
110
+ ### Casting
111
+
112
+ Base names its protagonist Aurelius with Varro as a fellow gladiator. Every
113
+ voice build names him Caius (Verus in oldTied) and makes Varro the lanista or
114
+ his assistant. All five follow one skeleton: dawn β†’ body toll β†’ games
115
+ announced β†’ training β†’ political meditation β†’ evening.
116
+
117
+ ### Subordination markers per 100 words
118
+
119
+ oldTied 3.36, inputOnly 3.02, fullShared 3.02, truebase 2.35, outputOnly 2.03.
120
+
121
+ ### Generic-clichΓ© screen (16-item starter list)
122
+
123
+ fullShared 0 hits, oldTied 0, inputOnly 1 (`whisper`), truebase 1
124
+ (`symphony of`), outputOnly 2 (`whisper` Γ—2, `echoed`).
125
+
126
+ ### Files
127
+
128
+ `runs/p1-{truebase,inputOnly,outputOnly,fullShared,oldTied}-t0-orion-id1.jsonl`
129
+ with text-identical `-mate.jsonl` repeats and `.provenance.txt` per build.
130
+ Full measurement log: `runs/p1-fiveway-analysis.md`.
vlib/__init__.py ADDED
File without changes
vlib/cache.py ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Base-model tensor cache."""
2
+
3
+ from pathlib import Path
4
+ import json
5
+ import os
6
+ import struct
7
+ from vlib import ctx
8
+ from vlib.ui import _fail, _now_iso, _step, _warn
9
+ from vlib.net import _cleanup_tmp, _safe_id, http_get_json
10
+ from vlib.tensors import _is_ggml_quant, _write_safetensors_streaming, read_safetensors, write_safetensors
11
+ from vlib.sources import _base_paths, open_source
12
+ from vlib.fetch import _fetch_tensor, _resolve_output_name, _source_arch
13
+ from vlib.registry import _assert_not_symlink
14
+
15
+
16
+ def _base_rev(base_id, base_file):
17
+ """Pinned commit sha for a base cache: sidecar .rev wins, else API HEAD (then pin)."""
18
+ rev_path = base_file.with_suffix(".rev")
19
+ try:
20
+ if rev_path.exists():
21
+ rev = rev_path.read_text().strip()
22
+ if rev:
23
+ return rev
24
+ except Exception:
25
+ pass
26
+ rev = None
27
+ try:
28
+ rev = http_get_json(f"https://huggingface.co/api/models/{base_id}", timeout=20).get("sha")
29
+ except Exception:
30
+ rev = None
31
+ if rev:
32
+ try:
33
+ base_file.parent.mkdir(parents=True, exist_ok=True)
34
+ tmp = rev_path.with_suffix(".rev.tmp")
35
+ tmp.write_text(rev + "\n")
36
+ tmp.replace(rev_path)
37
+ except Exception:
38
+ pass
39
+ return rev
40
+ return "main"
41
+
42
+
43
+ def _cache_validate(base_file, names):
44
+ """Names neither in the header nor fully covered by file bytes. Empty = trusted."""
45
+ try:
46
+ with open(base_file, "rb") as f:
47
+ hs = struct.unpack("<Q", f.read(8))[0]
48
+ hdr = json.loads(f.read(hs))
49
+ size = os.path.getsize(base_file)
50
+ except Exception:
51
+ return list(names)
52
+ missing = []
53
+ for n in names:
54
+ info = hdr.get(n)
55
+ if not isinstance(info, dict):
56
+ missing.append(n)
57
+ continue
58
+ try:
59
+ off = info["data_offsets"]
60
+ if not (isinstance(off, (list, tuple)) and len(off) == 2
61
+ and 0 <= int(off[0]) <= int(off[1]) and 8 + hs + int(off[1]) <= size):
62
+ missing.append(n)
63
+ except Exception:
64
+ missing.append(n)
65
+ return missing
66
+
67
+
68
+ def _cache_topup_write(base_file, staged):
69
+ """Append staged tensors to a base cache + atomically rewrite its header.
70
+ staged: [(name, blob|None, dtype, shape, raw_path|None)] β€” blob XOR raw_path.
71
+ Offsets are data-relative so existing entries never shift. Returns new header.
72
+ Raises OSError on failure; base_file is only ever swapped in whole (tmp+rename)."""
73
+ _assert_not_symlink(base_file)
74
+ import shutil
75
+ # append + atomic header rewrite (offsets are data-relative: existing entries don't shift)
76
+ with open(base_file, "rb") as f:
77
+ old_hs = struct.unpack("<Q", f.read(8))[0]
78
+ old_hdr = json.loads(f.read(old_hs))
79
+ old_size = os.path.getsize(base_file)
80
+ old_data_len = old_size - 8 - old_hs
81
+ new_hdr = {k: v for k, v in old_hdr.items()}
82
+ off = old_data_len
83
+ for m, blob, dtype, shape, _rp in staged:
84
+ nbytes = len(blob) if blob is not None else Path(_rp).stat().st_size
85
+ new_hdr[m] = {"dtype": dtype, "shape": list(shape), "data_offsets": [off, off + nbytes]}
86
+ off += nbytes
87
+ new_hj = json.dumps(new_hdr).encode("utf-8")
88
+ tmp_new = base_file.with_suffix(".cache.tmp")
89
+ with open(tmp_new, "wb") as out:
90
+ out.write(struct.pack("<Q", len(new_hj)))
91
+ out.write(new_hj)
92
+ with open(base_file, "rb") as f:
93
+ f.seek(8 + old_hs)
94
+ shutil.copyfileobj(f, out, 1 << 20)
95
+ for _m, blob, _dt, _sh, _rp in staged:
96
+ if blob is not None:
97
+ out.write(blob)
98
+ else:
99
+ with open(_rp, "rb") as f:
100
+ shutil.copyfileobj(f, out, 1 << 20)
101
+ out.flush()
102
+ try:
103
+ os.fsync(out.fileno())
104
+ except Exception:
105
+ pass
106
+ try:
107
+ try:
108
+ os.chmod(tmp_new, 0o600)
109
+ except Exception:
110
+ pass
111
+ tmp_new.replace(base_file)
112
+ except Exception as e:
113
+ try:
114
+ tmp_new.unlink(missing_ok=True)
115
+ except Exception:
116
+ pass
117
+ raise OSError(f"Could not update base cache: {e}")
118
+ return new_hdr
119
+
120
+
121
+ def _ensure_base_tensors(base_id, names, args=None):
122
+ """Base cache with per-tensor trust: top-ups what's missing, self-heals partial caches.
123
+ All fetches pin one revision so a repo update mid-cache can't mix commits. Returns base_file."""
124
+ tmp = _base_paths(base_id)
125
+ if tmp is None:
126
+ _fail(f" βœ— Invalid base '{base_id}'")
127
+ base_file, base_json = tmp
128
+ want = [n for n in names if n != "delta.voice.marker"]
129
+ if not want:
130
+ _fail(" βœ— No tensors requested from base.")
131
+ import fcntl
132
+ base_file.parent.mkdir(parents=True, exist_ok=True)
133
+ lock_path = base_file.with_suffix(".lock")
134
+ try:
135
+ lock_fh = open(lock_path, "w")
136
+ except Exception:
137
+ lock_fh = None
138
+ try:
139
+ if lock_fh is not None:
140
+ try:
141
+ fcntl.flock(lock_fh, fcntl.LOCK_EX)
142
+ except Exception:
143
+ pass
144
+ return _ensure_base_tensors_locked(base_id, base_file, base_json, want)
145
+ finally:
146
+ if lock_fh is not None:
147
+ try:
148
+ fcntl.flock(lock_fh, fcntl.LOCK_UN)
149
+ except Exception:
150
+ pass
151
+ try:
152
+ lock_fh.close()
153
+ except Exception:
154
+ pass
155
+
156
+
157
+ def _ensure_base_tensors_locked(base_id, base_file, base_json, want):
158
+ import shutil
159
+ rev_path = base_file.with_suffix(".rev")
160
+ legacy = base_file.exists() and not rev_path.exists()
161
+ if not base_file.exists():
162
+ _cache_base(base_id, base_file, base_json)
163
+ elif legacy:
164
+ # Pre-rev cache of unknown lineage: re-fetch one held tensor at HEAD and
165
+ # compare bytes. Match -> same lineage, pin HEAD. Differ -> wipe, start over.
166
+ # Probe failure (offline?) keeps the cache with a loud warning, never wipes.
167
+ try:
168
+ with open(base_file, "rb") as f:
169
+ hs = struct.unpack("<Q", f.read(8))[0]
170
+ hdr = json.loads(f.read(hs))
171
+ held = [k for k in hdr.keys() if k != "__metadata__"]
172
+ except Exception:
173
+ held = []
174
+ verified, same = False, False
175
+ if held:
176
+ probe = held[0]
177
+ probe_dir = ctx.VOICES_DIR / ".parts" / _safe_id("base-probe", base_id)
178
+ try:
179
+ src = open_source(base_id)
180
+ pf = _fetch_tensor(src, probe, probe_dir)
181
+ fresh = Path(pf[1]).read_bytes() if pf[0] == "file" else bytes(pf[2])
182
+ with open(base_file, "rb") as f:
183
+ f.seek(8 + hs + hdr[probe]["data_offsets"][0])
184
+ have = f.read(hdr[probe]["data_offsets"][1] - hdr[probe]["data_offsets"][0])
185
+ verified, same = True, (fresh == have)
186
+ except SystemExit:
187
+ raise
188
+ except Exception:
189
+ verified, same = False, False
190
+ finally:
191
+ _cleanup_tmp(probe_dir)
192
+ if verified and not same:
193
+ _warn(" Base cache predates revision pinning and no longer matches HEAD β€” re-fetching.")
194
+ try:
195
+ base_file.unlink(missing_ok=True)
196
+ except Exception:
197
+ pass
198
+ _cache_base(base_id, base_file, base_json)
199
+ elif not verified:
200
+ _warn(" Base cache lineage unverified (offline?) β€” proceeding, mixed revisions possible.")
201
+ rev = _base_rev(base_id, base_file)
202
+ missing = _cache_validate(base_file, want)
203
+ if not missing:
204
+ return base_file
205
+ _step(f"Topping up base {base_id} ({len(missing)} tensor(s) missing)…")
206
+ try:
207
+ src = open_source(base_id, rev=rev)
208
+ except SystemExit:
209
+ raise
210
+ except Exception as e:
211
+ _fail(f" βœ— Could not reach base {base_id} for top-up: {e}")
212
+ tmp_dir = ctx.VOICES_DIR / ".parts" / _safe_id("base", base_id)
213
+ staged = []
214
+ try:
215
+ for m in missing:
216
+ try:
217
+ fetched = _fetch_tensor(src, m, tmp_dir)
218
+ except (KeyError, ValueError) as e:
219
+ _fail(f" βœ— Base {base_id} has no tensor '{m}'. Delta needs same names on both sides.")
220
+ kind, raw_path, data, dtype, shape = fetched
221
+ if _is_ggml_quant(dtype):
222
+ # quant blocks can't live in a .safetensors cache: dequant once to F32
223
+ f32 = src.read_f32(m)
224
+ staged.append((m, f32.astype("float32").tobytes(), "F32", tuple(int(x) for x in f32.shape), None))
225
+ elif kind == "file":
226
+ staged.append((m, None, dtype, tuple(int(x) for x in shape), raw_path))
227
+ else:
228
+ staged.append((m, bytes(data), dtype, tuple(int(x) for x in shape), None))
229
+ except SystemExit:
230
+ _cleanup_tmp(tmp_dir)
231
+ raise
232
+ try:
233
+ new_hdr = _cache_topup_write(base_file, staged)
234
+ except OSError as e:
235
+ _cleanup_tmp(tmp_dir)
236
+ _fail(f" βœ— {e}")
237
+ _cleanup_tmp(tmp_dir)
238
+ # refresh base json with the full tensor list
239
+ try:
240
+ meta = {"source_hf_model": base_id, "revision": rev, "downloaded_at": _now_iso(),
241
+ "tensors": [{"name": k, "shape": list(v["shape"]), "dtype": v["dtype"]}
242
+ for k, v in new_hdr.items() if k != "__metadata__"]}
243
+ jtmp = base_json.with_suffix(".tmp")
244
+ fd = os.open(str(jtmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
245
+ try:
246
+ os.write(fd, json.dumps(meta, indent=2).encode("utf-8") + b"\n")
247
+ try:
248
+ os.fsync(fd)
249
+ except Exception:
250
+ pass
251
+ finally:
252
+ os.close(fd)
253
+ try:
254
+ jtmp.replace(base_json)
255
+ except FileExistsError:
256
+ jtmp.unlink(missing_ok=True)
257
+ except Exception:
258
+ pass
259
+ return base_file
260
+
261
+
262
+ def _cache_base(base_id, base_file, base_json):
263
+ _step(f"Caching base {base_id}…")
264
+ src = open_source(base_id)
265
+ names = src.names()
266
+ bname = _resolve_output_name(src, None, names)
267
+ if bname is None:
268
+ _fail(f" βœ— Could not find the output tensor in base {base_id}.")
269
+ ref = src.ref(bname)
270
+ tmp_dir = ctx.VOICES_DIR / ".parts" / _safe_id("base", base_id)
271
+ fetched = _fetch_tensor(src, bname, tmp_dir)
272
+ # size now: _cleanup_tmp below deletes raw.bin before the metadata write
273
+ fetched_bytes = fetched[1].stat().st_size if fetched[0] == "file" else len(fetched[2])
274
+ _assert_not_symlink(base_file.parent if base_file.parent.exists() else base_file)
275
+ base_file.parent.mkdir(parents=True, exist_ok=True)
276
+ try:
277
+ os.chmod(base_file.parent, 0o700)
278
+ except Exception:
279
+ pass
280
+ # avoid TOCTOU race if two processes cache same base
281
+ if base_file.exists():
282
+ _cleanup_tmp(tmp_dir)
283
+ return
284
+ tmp_st = base_file.parent / "base.tmp"
285
+ if fetched[0] == "file":
286
+ _write_safetensors_streaming(bname, str(fetched[1]), fetched[1].stat().st_size, fetched[3], fetched[4], str(tmp_st))
287
+ else:
288
+ write_safetensors({bname: (fetched[2], fetched[3], fetched[4])}, str(tmp_st))
289
+ try:
290
+ tmp_st.replace(base_file)
291
+ except FileExistsError:
292
+ tmp_st.unlink(missing_ok=True)
293
+ _cleanup_tmp(tmp_dir)
294
+ # atomic 0600 json
295
+ jtmp = base_json.with_suffix(".tmp")
296
+ fd = os.open(str(jtmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
297
+ try:
298
+ os.write(fd, json.dumps({
299
+ "source_hf_model": base_id, "tensor_name": bname, "dtype": fetched[3],
300
+ "shape": list(fetched[4]), "bytes": fetched_bytes,
301
+ "tensors": [{"name": bname, "shape": list(fetched[4]), "dtype": fetched[3]}],
302
+ "downloaded_at": _now_iso(),
303
+ }, indent=2).encode("utf-8") + b"\n")
304
+ try:
305
+ os.fsync(fd)
306
+ except Exception:
307
+ pass
308
+ finally:
309
+ os.close(fd)
310
+ try:
311
+ jtmp.replace(base_json)
312
+ except FileExistsError:
313
+ jtmp.unlink(missing_ok=True)
314
+
315
+
316
+ def _load_safetensors_or_gguf_f32(path):
317
+ # Returns (tensors_dict, dtype_map) for delta math. Safetensors preferred (no dequant).
318
+ # GGUF quant falls back to full F32 dequant once (unavoidable, warn; efficient for <50MB tests).
319
+ try:
320
+ hdr, tens = read_safetensors(str(path))
321
+ return hdr, tens, False
322
+ except Exception:
323
+ pass
324
+ try:
325
+ src = open_source(str(path))
326
+ names = [n for n in src.names() if n != "delta.voice.marker"]
327
+ if not names:
328
+ raise ValueError("no tensors")
329
+ # Pick output head or first for delta math (single-tensor delta path).
330
+ tname = None
331
+ try:
332
+ arch, cfg = _source_arch(src)
333
+ tname = _resolve_output_name(src, cfg, names)
334
+ except Exception:
335
+ tname = None
336
+ if tname is None or tname not in names:
337
+ tname = names[0]
338
+ ref = src.ref(tname)
339
+ _warn(f" {Path(path).name} is {ref.dtype} ({src.kind}) β€” dequanting once to F32 for delta math (unavoidable).")
340
+ arr = src.read_f32(tname)
341
+ # Build synthetic safetensors-like dict for chunked math below.
342
+ return {"__gguf_f32__": {"dtype": "F32", "shape": list(arr.shape), "_arr": arr}, tname: {"dtype": "F32", "shape": list(arr.shape), "_arr": arr}}, {tname: {"dtype": "F32", "shape": list(arr.shape)}}, True
343
+ except Exception as e:
344
+ raise ValueError(f"{path} is not a valid .safetensors or .gguf for delta: {e}")
vlib/cmds_misc.py ADDED
@@ -0,0 +1,550 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Misc commands: list, info, remove, cleanup, auth, path, doctor."""
2
+
3
+ from pathlib import Path
4
+ import getpass
5
+ import json
6
+ import os
7
+ import sys
8
+ import textwrap
9
+ import time
10
+ import urllib.error
11
+ import urllib.request
12
+ from vlib import ctx
13
+ from vlib.ui import _confirm, _fail, _noninteractive, _note, _ok, _say, _step, _term_width, _trunc, _warn
14
+ from vlib.net import _get_hf_token, _hf_auth_headers
15
+ from vlib.tensors import read_safetensors
16
+ from vlib.sources import _base_paths
17
+ from vlib.registry import _iter_voices, _read_meta, _registry_resolve, log_operation
18
+
19
+
20
+ def cmd_list(args):
21
+ if getattr(args, "target", None) == "base" or getattr(args, "list_base", False):
22
+ _list_bases()
23
+ return
24
+
25
+ voices = [d for d in _iter_voices() if (d / "voice.json").exists()]
26
+ if not voices:
27
+ _say(" No voices stored yet. Use 'voice get <model>' to save one.")
28
+ return
29
+
30
+ tw = _term_width()
31
+ w_name = 26 if tw >= 80 else max(14, tw - 54)
32
+ w_src = 40 if tw >= 80 else max(18, tw - w_name - 16)
33
+ w_type = 8
34
+ _say(f" {_trunc('Name', w_name):<{w_name}} {_trunc('Source', w_src):<{w_src}} {_trunc('Type', w_type):<{w_type}}")
35
+ _say(f" {'─'*w_name} {'─'*w_src} {'─'*w_type}")
36
+ for d in voices:
37
+ meta = _read_meta(d / "voice.json") or {}
38
+ name = meta.get("name", d.name)
39
+ src = meta.get("source_hf_model") or meta.get("source") or "β€”"
40
+ typ = meta.get("type", "voice")
41
+ _say(f" {_trunc(name, w_name):<{w_name}} {_trunc(src, w_src):<{w_src}} {_trunc(typ, w_type):<{w_type}}")
42
+
43
+
44
+ def _list_bases():
45
+ base_root = ctx.VOICES_DIR / "base"
46
+ if not base_root.exists() or not any(base_root.rglob("*.json")):
47
+ _say(" No bases cached yet. Use 'voice delta <voice>' to fetch one.")
48
+ return
49
+ tw = _term_width()
50
+ w_base = 40 if tw >= 80 else max(20, tw - 30)
51
+ _say(f" {_trunc('Base', w_base):<{w_base}} {'Bytes':<18} {'Fetched':<12}")
52
+ _say(f" {'─'*w_base} {'─'*18} {'─'*12}")
53
+ seen = set()
54
+ for jpath in sorted(base_root.rglob("*.json")):
55
+ try:
56
+ meta = json.loads(jpath.read_text())
57
+ except Exception:
58
+ continue
59
+ parts = jpath.relative_to(base_root).parts
60
+ base_id = f"{parts[0]}/{Path(parts[1]).stem}" if len(parts) == 2 else f"{parts[0]}/{parts[1]}"
61
+ if base_id in seen:
62
+ continue
63
+ seen.add(base_id)
64
+ b = meta.get("bytes")
65
+ b_str = f"{b} ({b/1e6:.1f} MB)" if isinstance(b, int) else "β€”"
66
+ fetched = meta.get("downloaded_at", "β€”")[:10]
67
+ _say(f" {_trunc(base_id, w_base):<{w_base}} {_trunc(b_str, 18):<18} {_trunc(fetched, 12):<12}")
68
+
69
+
70
+ def _voice_tensors(meta):
71
+ """Normalize voice metadata to a (tensors, synthetic) pair.
72
+ Flat-only files (tensor_name/shape/dtype, no tensors list) become a
73
+ synthetic 1-entry list so readers share one code path."""
74
+ claims = meta.get("tensors")
75
+ if isinstance(claims, list) and claims:
76
+ return list(claims), False
77
+ if meta.get("tensor_name"):
78
+ return [{"name": meta.get("tensor_name"), "shape": meta.get("shape"),
79
+ "dtype": meta.get("dtype")}], True
80
+ return [], False
81
+
82
+
83
+ def _verify_voice_file(path, meta):
84
+ """Cross-check voice.json claims against the artifact's own header.
85
+ Returns warning strings (empty = agrees). Never raises: an unreadable
86
+ file is a note, not a failure."""
87
+ out = []
88
+ if str(path).endswith(".gguf"):
89
+ return out # quant type names differ; nothing truthful to compare
90
+ try:
91
+ header, tens = read_safetensors(str(path))
92
+ except Exception:
93
+ return [" ? Could not read the artifact header; showing voice.json as-is."]
94
+ names = [k for k in tens if k != "delta.voice.marker"]
95
+ claims, _synthetic = _voice_tensors(meta)
96
+ if claims:
97
+ for entry in claims:
98
+ tname = entry.get("name")
99
+ if tname not in tens:
100
+ out.append(f" ! MISMATCH tensor: voice.json lists '{tname}', file doesn't hold it.")
101
+ continue
102
+ info = tens[tname]
103
+ claimed_shape = entry.get("shape")
104
+ if claimed_shape is not None and list(claimed_shape) != list(info["shape"]):
105
+ out.append(f" ! MISMATCH shape '{tname}': voice.json says {list(claimed_shape)}, "
106
+ f"file holds {list(info['shape'])} β€” file wins.")
107
+ claimed_dtype = (entry.get("dtype") or "").upper()
108
+ if claimed_dtype and claimed_dtype != str(info["dtype"]).upper():
109
+ out.append(f" ! MISMATCH dtype '{tname}': voice.json says {entry.get('dtype')}, "
110
+ f"file holds {info['dtype']} β€” file wins.")
111
+ claimed_names = {e.get("name") for e in claims}
112
+ for extra in names:
113
+ if extra not in claimed_names:
114
+ out.append(f" ! File holds '{extra}' which voice.json doesn't list.")
115
+ if not names:
116
+ out.append(" ? No tensors found in the artifact.")
117
+ has_marker = "delta.voice.marker" in tens
118
+ if meta.get("type") == "delta" and not has_marker:
119
+ out.append(" ! voice.json claims delta but the file has no delta marker.")
120
+ if meta.get("type") != "delta" and has_marker:
121
+ out.append(" ! File carries a delta marker but voice.json type is "
122
+ f"'{meta.get('type', 'voice')}'.")
123
+ return out
124
+ # No tensor claims at all (minimal legacy json): only the delta-marker
125
+ # check below is truthful; shape/dtype have nothing to compare against.
126
+ if not names:
127
+ return [" ? No tensors found in the artifact."]
128
+ has_marker = "delta.voice.marker" in tens
129
+ if meta.get("type") == "delta" and not has_marker:
130
+ out.append(" ! voice.json claims delta but the file has no delta marker.")
131
+ if meta.get("type") != "delta" and has_marker:
132
+ out.append(" ! File carries a delta marker but voice.json type is "
133
+ f"'{meta.get('type', 'voice')}'.")
134
+ return out
135
+
136
+
137
+ def cmd_info(args):
138
+ voicename = args.voicename.strip()
139
+ voice_path, voice_meta = _registry_resolve(voicename)
140
+ if voice_path is None or not voice_path.exists():
141
+ bps = _base_paths(voicename)
142
+ if bps and bps[0].exists():
143
+ meta = _read_meta(bps[1]) or {}
144
+ _say(f" Name: {voicename}")
145
+ _say(f" Type: base")
146
+ _say(f" Source: https://huggingface.co/{meta.get('source_hf_model', voicename)}")
147
+ _say(f" Tensor: {meta.get('tensor_name', 'β€”')}")
148
+ _say(f" Shape: {meta.get('shape', 'β€”')}")
149
+ _say(f" Dtype: {meta.get('dtype', 'β€”')}")
150
+ _say(f" File: {bps[0]}")
151
+ return
152
+ _fail(f" βœ— Voice '{voicename}' not found. Try voice list")
153
+
154
+ meta = _read_meta(Path(voice_path).parent / "voice.json") or voice_meta or {}
155
+ name = meta.get("name", voicename)
156
+ source = meta.get("source_hf_model") or meta.get("source") or "β€”"
157
+ typ = meta.get("type", "voice")
158
+ _say(f" Name: {name}")
159
+ _say(f" Type: {typ}")
160
+ _say(f" Source: {source}")
161
+ if meta.get("family") or meta.get("architecture"):
162
+ _say(f" Family: {meta.get('family', 'β€”')} ({meta.get('architecture', 'β€”')})")
163
+ tensors, _synthetic = _voice_tensors(meta)
164
+ only = tensors[0] if len(tensors) == 1 else {}
165
+ _say(f" Tensor: {meta.get('tensor_name') or only.get('name', 'β€”')}")
166
+ _say(f" Shape: {meta.get('shape') or only.get('shape', 'β€”')}")
167
+ _say(f" Dtype: {meta.get('dtype') or only.get('dtype', 'β€”')}")
168
+ if len(tensors) > 1:
169
+ for entry in tensors:
170
+ _say(f" - {entry.get('name')}: {entry.get('shape')} Β· {entry.get('dtype')}")
171
+ b = meta.get("bytes")
172
+ if isinstance(b, int):
173
+ _say(f" Bytes: {b} ({b/1e6:.1f} MB)")
174
+ if meta.get("base"):
175
+ _say(f" Base: {meta.get('base')}")
176
+ if meta.get("license"):
177
+ _say(f" License: {_trunc(meta['license'], 80)}")
178
+ _say(f" Saved: {meta.get('downloaded_at', meta.get('fetched_at', 'β€”'))}")
179
+ _say(f" File: {voice_path}")
180
+ for w in _verify_voice_file(str(voice_path), meta):
181
+ _warn(w)
182
+
183
+
184
+ def cmd_remove(args):
185
+ name = args.voice.strip().lower()
186
+ target = next((d for d in _iter_voices() if d.name.lower() == name), None)
187
+ if target is None:
188
+ _fail(f" βœ— Voice '{args.voice}' not found.")
189
+ if _noninteractive(args) and not getattr(args, "yes", False):
190
+ _fail(f" βœ— Refusing to remove '{target.name}' in non-interactive mode without --yes.")
191
+ if not _confirm(f" Remove voice '{target.name}'? [y/N]: ", args):
192
+ _say(" Cancelled.")
193
+ sys.exit(0)
194
+ import shutil
195
+ shutil.rmtree(target)
196
+ log_operation({"op": "remove", "voice": target.name, "output": str(target), "status": "ok"})
197
+ _ok(f"Removed '{target.name}'.")
198
+
199
+
200
+ def cmd_cleanup(args):
201
+ parts_dir = ctx.VOICES_DIR / ".parts"
202
+ if not parts_dir.exists() or not any(parts_dir.iterdir()):
203
+ _ok("No incomplete downloads to clean.")
204
+ return
205
+ removed, reclaimed = 0, 0
206
+ import shutil
207
+ force = bool(getattr(args, "force", False))
208
+ for p in sorted(parts_dir.iterdir()):
209
+ try:
210
+ if p.is_dir():
211
+ lock = p / ".lock"
212
+ if lock.exists() and not force:
213
+ try:
214
+ if time.time() - lock.stat().st_mtime < 600:
215
+ _note(f"Skipping active download {p.name}")
216
+ continue
217
+ except Exception:
218
+ pass
219
+ sz = sum(f.stat().st_size for f in p.rglob("*") if f.is_file())
220
+ shutil.rmtree(p)
221
+ removed += 1
222
+ reclaimed += sz
223
+ _ok(f"Removed {p.name}/ ({sz/1e6:.1f} MB)")
224
+ else:
225
+ sz = p.stat().st_size if p.is_file() else 0
226
+ p.unlink()
227
+ removed += 1
228
+ reclaimed += sz
229
+ _ok(f"Removed {p.name}")
230
+ except Exception as e:
231
+ if ctx.VERBOSE:
232
+ _warn(f"Could not remove {p}: {e}")
233
+ _ok(f"Cleaned {removed} entry(s), reclaimed {reclaimed/1e6:.1f} MB.")
234
+ try:
235
+ if not any(parts_dir.iterdir()):
236
+ parts_dir.rmdir()
237
+ except Exception:
238
+ pass
239
+
240
+
241
+ def cmd_auth(args):
242
+ tok_path = ctx.HF_TOKEN_PATH
243
+ if getattr(args, "clear", False):
244
+ try:
245
+ tok_path.unlink(missing_ok=True)
246
+ _ok("Removed stored HF token.")
247
+ except Exception as e:
248
+ _fail(f" βœ— Could not remove token: {e}")
249
+ return
250
+ if getattr(args, "show", False):
251
+ tok = _get_hf_token()
252
+ if not tok:
253
+ _say(" βœ“ No HF token stored. Run: voice auth")
254
+ return
255
+ masked = tok[:4] + "Β·" * (len(tok) - 8) + tok[-4:] if len(tok) > 8 else "Β·" * len(tok)
256
+ src = "environment" if os.environ.get("HF_TOKEN") or os.environ.get("HF_HUB_TOKEN") else str(tok_path)
257
+ _say(f" βœ“ HF token present: {masked} ({src})")
258
+ return
259
+ _say(" Paste your HF token (hf_…): it stays on this machine, 0600, never logged.")
260
+ try:
261
+ try:
262
+ tok = getpass.getpass(" Paste HF token: ").strip()
263
+ except Exception:
264
+ tok = input(" Paste HF token: ").strip()
265
+ except (EOFError, KeyboardInterrupt):
266
+ _say(" ⚠ Cancelled.")
267
+ sys.exit(0)
268
+ if not tok:
269
+ _say(" ⚠ No token entered.")
270
+ sys.exit(0)
271
+ if not tok.startswith("hf_") or len(tok) < 10:
272
+ _fail(" βœ— That doesn't look like an HF token (should start with hf_…).")
273
+ _step("Verifying…")
274
+ try:
275
+ req = urllib.request.Request("https://huggingface.co/api/whoami-v2",
276
+ headers=_hf_auth_headers("https://huggingface.co/api/whoami-v2", {"Authorization": f"Bearer {tok}"}))
277
+ with urllib.request.urlopen(req, timeout=15) as resp:
278
+ data = json.loads(resp.read())
279
+ _ok(f"Verified as {data.get('name') or data.get('id') or 'ok'}.")
280
+ except urllib.error.HTTPError as e:
281
+ if e.code in (401, 403):
282
+ _fail(" βœ— Token rejected (401). Check it at https://huggingface.co/settings/tokens")
283
+ _warn(f"Verification skipped (HTTP {e.code}), saving anyway.")
284
+ except Exception:
285
+ _warn("Could not verify now β€” saving anyway.")
286
+ try:
287
+ ctx.VOICES_DIR.mkdir(parents=True, exist_ok=True)
288
+ try:
289
+ os.chmod(ctx.VOICES_DIR, 0o700)
290
+ except Exception:
291
+ pass
292
+ tmp = tok_path.with_suffix(".tmp")
293
+ # atomic 0600: create with restricted perms, avoid 0644 race
294
+ fd = os.open(str(tmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
295
+ try:
296
+ os.write(fd, (tok.strip() + "\n").encode("utf-8"))
297
+ try:
298
+ os.fsync(fd)
299
+ except Exception:
300
+ pass
301
+ finally:
302
+ os.close(fd)
303
+ try:
304
+ os.chmod(tmp, 0o600)
305
+ except Exception:
306
+ pass
307
+ tmp.replace(tok_path)
308
+ try:
309
+ os.chmod(tok_path, 0o600)
310
+ except Exception:
311
+ pass
312
+ _ok(f"Saved to {tok_path} (0600).")
313
+ except Exception as e:
314
+ _fail(f" βœ— Could not save token: {e}")
315
+
316
+
317
+ def cmd_path(_args):
318
+ script = _entry_script()
319
+ if script is None:
320
+ _fail(" βœ— Cannot locate the voice entry script in this interpreter. Run voice.py directly.")
321
+ script = Path(script).resolve()
322
+ vlib_src = script.parent / "vlib"
323
+ if sys.platform.startswith("win"):
324
+ import shutil as _sh
325
+ bindir = Path(os.environ.get("LOCALAPPDATA", "")) / "voice-cli"
326
+ if str(bindir) == "voice-cli":
327
+ _fail(" βœ— Couldn't find a Windows app-data folder to install into.")
328
+ bindir.mkdir(parents=True, exist_ok=True)
329
+ _copy_tree(vlib_src, bindir / "vlib")
330
+ _sh.copy2(str(script), str(bindir / "voice.py"))
331
+ if _sh.which("voice"):
332
+ _ok("Voice updated on your PATH.")
333
+ else:
334
+ _ok("Voice is ready. Add this to your PATH (PowerShell):")
335
+ _say(f' setx PATH "%PATH%;{bindir}"')
336
+ return
337
+ import shutil
338
+ existing = shutil.which("voice")
339
+ if existing:
340
+ try:
341
+ same = Path(existing).resolve() == script
342
+ except Exception:
343
+ same = False
344
+ if same:
345
+ _ok("Voice is already on your PATH.")
346
+ return
347
+ # Stale install: relink only when the old entry is our symlink
348
+ # (points at a voice.py); never touch a foreign binary.
349
+ try:
350
+ old_target = os.readlink(existing)
351
+ except OSError:
352
+ _fail(f" βœ— 'voice' on PATH isn't ours ({existing}). Remove or rename it, then retry.")
353
+ if Path(old_target).name != "voice.py":
354
+ _fail(f" βœ— '{existing}' isn't a voice install (points at {old_target}). Remove or rename it, then retry.")
355
+ try:
356
+ Path(existing).unlink()
357
+ Path(existing).symlink_to(str(script))
358
+ except (PermissionError, OSError):
359
+ _fail(f" βœ— Couldn't relink {existing}. Try: sudo ln -sf {script} {existing}")
360
+ _ok(f"Voice updated: {existing} now points at {script}. Open a new terminal and run: voice")
361
+ return
362
+ candidates = [Path("/usr/local/bin"), Path.home() / ".local" / "bin", Path("/usr/bin"), Path("/bin")]
363
+ on_path = next((d for d in candidates if d.is_dir() and str(d) in os.environ.get("PATH", "").split(":")), None)
364
+ if on_path is None:
365
+ _fail(" βœ— No writable directory on PATH found. Add one and retry.")
366
+ target = on_path / "voice"
367
+ try:
368
+ target.symlink_to(str(script))
369
+ except FileExistsError:
370
+ target.unlink()
371
+ target.symlink_to(str(script))
372
+ except (PermissionError, OSError):
373
+ _fail(f" βœ— Couldn't create a link in {on_path}. Try: sudo ln -s {script} {target}")
374
+ _ok(f"Voice is ready at {target}. Open a new terminal and run: voice")
375
+
376
+
377
+ def cmd_doctor(_args):
378
+ import platform
379
+ import shutil
380
+ _say(f" voice {ctx.VERSION} β€” doctor")
381
+ _say(f" Python {platform.python_version()} ({sys.executable})")
382
+ uv = shutil.which("uv")
383
+ _say(f" uv {'found: ' + uv if uv else 'NOT FOUND β€” curl -LsSf https://astral.sh/uv/install.sh | sh'}")
384
+ venv = ctx.VOICES_DIR / ".venv"
385
+ if (venv / "bin" / "python").exists():
386
+ _say(f" venv {venv} present")
387
+ else:
388
+ _say(f" venv not created yet (created on first get/cast/pack/delta)")
389
+ try:
390
+ free = shutil.disk_usage(ctx.VOICES_DIR if ctx.VOICES_DIR.exists() else Path.home()).free
391
+ _say(f" disk free {free/1e9:.1f} GB")
392
+ except Exception:
393
+ pass
394
+ tok = _get_hf_token()
395
+ _say(f" HF token {'present' if tok else 'not present'}")
396
+ if ctx.OPERATIONS_LOG.exists():
397
+ try:
398
+ n = sum(1 for _ in open(ctx.OPERATIONS_LOG))
399
+ _say(f" operations logged: {n} ({ctx.OPERATIONS_LOG})")
400
+ except Exception:
401
+ pass
402
+ if ctx.REPORTS_JSONL.exists():
403
+ try:
404
+ n = sum(1 for _ in open(ctx.REPORTS_JSONL))
405
+ pending = len(list(ctx.REPORTS_OUTBOX.glob("*.json"))) if ctx.REPORTS_OUTBOX.exists() else 0
406
+ _say(f" reports: {n} local, {pending} pending in outbox ({ctx.REPORTS_JSONL})")
407
+ if pending > 5000:
408
+ _warn(f" outbox >5000 pending β€” run: voice report --sync --yes")
409
+ # headroom
410
+ try:
411
+ sz = ctx.REPORTS_JSONL.stat().st_size
412
+ if sz > 50 * 1024 * 1024:
413
+ _warn(f" reports.jsonl {sz/1e6:.1f} MB β€” consider archiving")
414
+ except Exception:
415
+ pass
416
+ except Exception:
417
+ pass
418
+ else:
419
+ _say(f" reports: none yet β€” run: voice report")
420
+ try:
421
+ req = urllib.request.Request("https://huggingface.co/api/whoami-v2" if tok else "https://huggingface.co",
422
+ headers=_hf_auth_headers("https://huggingface.co"))
423
+ with urllib.request.urlopen(req, timeout=3) as resp:
424
+ _say(f" connectivity huggingface.co ok ({resp.status})")
425
+ except Exception as e:
426
+ _warn(f"connectivity failed: {e}")
427
+ # check push target
428
+ try:
429
+ _say(f" community reports: {ctx.REPORTS_DATASET_ID} (share prompt on voice report)")
430
+ except Exception:
431
+ pass
432
+
433
+
434
+ def _print_version():
435
+ import platform
436
+ _say(f" voice {ctx.VERSION}")
437
+ _say(f" Python {platform.python_version()} ({sys.executable})")
438
+ for mod in ("gguf", "numpy", "huggingface_hub", "hf_xet"):
439
+ try:
440
+ m = __import__(mod)
441
+ _say(f" {mod} {getattr(m, '__version__', 'installed')}")
442
+ except Exception:
443
+ _say(f" {mod} not installed")
444
+
445
+
446
+ def _print_completion(shell):
447
+ cmds = ["get", "cast", "delta", "list", "info", "remove", "cleanup", "auth", "report", "path", "doctor"]
448
+ voices = [d.name for d in _iter_voices() if (d / "voice.json").exists()]
449
+ vlist = " ".join(voices)
450
+ clist = " ".join(cmds)
451
+ if shell == "bash":
452
+ print(textwrap.dedent(f"""\
453
+ _voice_completions() {{
454
+ local cur="${{COMP_WORDS[COMP_CWORD]}}"
455
+ local cmds="{clist}"
456
+ if [[ $COMP_CWORD -eq 1 ]]; then
457
+ COMPREPLY=($(compgen -W "$cmds --help --version --completion" -- "$cur"))
458
+ return
459
+ fi
460
+ case "${{COMP_WORDS[1]}}" in
461
+ cast|delta|info|remove) COMPREPLY=($(compgen -W "{vlist}" -- "$cur")) ;;
462
+ list) COMPREPLY=($(compgen -W "base --base" -- "$cur")) ;;
463
+ *) COMPREPLY=() ;;
464
+ esac
465
+ }}
466
+ complete -F _voice_completions voice
467
+ """))
468
+ elif shell == "zsh":
469
+ print(textwrap.dedent(f"""\
470
+ #compdef voice
471
+ _voice() {{
472
+ local -a cmds; cmds=({clist})
473
+ local -a voices; voices=({vlist})
474
+ _arguments -C "1: :($cmds)" "*::arg:->args"
475
+ case $line[1] in
476
+ cast|delta|info|remove) _describe voices voices ;;
477
+ list) _values "target" base ;;
478
+ esac
479
+ }}
480
+ compdef _voice voice
481
+ """))
482
+ elif shell == "fish":
483
+ print(textwrap.dedent(f"""\
484
+ complete -c voice -f
485
+ complete -c voice -n "__fish_use_subcommand" -a "{clist}" -d "voice command"
486
+ complete -c voice -n "__fish_seen_subcommand_from cast delta info remove" -a "{vlist}"
487
+ """))
488
+
489
+
490
+ def _copy_tree(src, dst):
491
+ """Refresh-copy a directory (Windows PATH install needs vlib/ beside voice.py)."""
492
+ import shutil as _sh
493
+ if Path(src).is_dir():
494
+ _sh.copytree(str(src), str(dst), dirs_exist_ok=True,
495
+ ignore=_sh.ignore_patterns("__pycache__"))
496
+ return dst
497
+
498
+
499
+ def _entry_script():
500
+ """Absolute path of the CLI entry (voice.py), or None if unknown.
501
+ The split moved commands into vlib/, so __file__ here is the module β€”
502
+ resolve via __main__ instead. Single seam for every entry-path need."""
503
+ _main = getattr(sys.modules.get("__main__"), "__file__", None)
504
+ if not _main:
505
+ return None
506
+ return os.path.realpath(_main)
507
+
508
+
509
+ def _ensure_venv(cmd):
510
+ if os.environ.get("VOICE_NO_VENV") in ("1", "true", "True"):
511
+ return
512
+ if cmd not in ("get", "cast", "delta"):
513
+ return
514
+ venv = ctx.VOICES_DIR / ".venv"
515
+ venv_py = venv / "bin" / "python"
516
+ try:
517
+ if venv_py.exists() and (sys.prefix == str(venv) or Path(sys.executable).resolve().parent.parent == venv.resolve()):
518
+ return
519
+ except Exception:
520
+ pass
521
+ import shutil
522
+ if shutil.which("uv") is None:
523
+ _fail(" βœ— uv not found. Install it, then retry:\n curl -LsSf https://astral.sh/uv/install.sh | sh")
524
+ if venv_py.exists():
525
+ try:
526
+ _main = _entry_script()
527
+ if not _main:
528
+ _warn(" venv re-exec skipped: entry script unknown, continuing in-process.")
529
+ return
530
+ os.execv(str(venv_py), [str(venv_py), _main, *sys.argv[1:]])
531
+ except Exception as e:
532
+ if ctx.VERBOSE:
533
+ _warn(f"could not re-exec into venv: {e}")
534
+ return
535
+ _step(f"Creating a virtual environment at {venv}…")
536
+ import subprocess
537
+ try:
538
+ subprocess.run(["uv", "venv", str(venv), "--python", sys.executable], capture_output=True, check=True)
539
+ subprocess.run(["uv", "pip", "install", "--quiet", "--python", str(venv_py),
540
+ "gguf", "numpy", "huggingface_hub[hf_xet]", "hf_xet"], capture_output=True, check=True)
541
+ except Exception as e:
542
+ _fail(f" βœ— Failed to create venv: {e}")
543
+ _main = _entry_script()
544
+ if not _main:
545
+ _warn(" venv re-exec skipped: entry script unknown, continuing in-process.")
546
+ return
547
+ try:
548
+ os.execv(str(venv_py), [str(venv_py), _main, *sys.argv[1:]])
549
+ except Exception as e:
550
+ _fail(f" βœ— Could not re-exec into venv: {e}")
vlib/cmds_voices.py ADDED
@@ -0,0 +1,551 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Voice commands: get, cast, delta, add."""
2
+
3
+ from pathlib import Path
4
+ import argparse
5
+ import json
6
+ import os
7
+ import sys
8
+ import urllib.error
9
+ import urllib.request
10
+ from vlib import ctx
11
+ from vlib.ui import _fail, _noninteractive, _note, _now_iso, _ok, _say, _step
12
+ from vlib.net import _cleanup_tmp, _safe_id
13
+ from vlib.tensors import _is_delta_safetensors, _is_gguf_file, read_safetensors, write_safetensors
14
+ from vlib.sources import _NAME_RULES, _base_paths, _family, _hf_license, _nested, _public_source, _sanitize_voice_name, _slugify_voice_name, open_source
15
+ from vlib.fetch import _fetch_pack, _fetch_tensor, _resolve_output_name, _source_arch, _write_fetched, _write_voicepack_json
16
+ from vlib.registry import _assert_not_symlink, _check_overwrite, _out_path, _read_meta, _registry_folder, _registry_name_exists, _registry_resolve, _resolve_name, _write_voice, log_operation
17
+ from vlib.cache import _ensure_base_tensors
18
+ from vlib.compute import _cast_core, _cast_pack, _load_tensor, _load_voice, _parse_exclude_list, _parse_tensor_list, _resolve_one_tensor, _tensor_src_meta
19
+ from vlib.delta import _choose_voice, _compute_delta, _resolve_base
20
+
21
+
22
+ def cmd_get(args):
23
+ source_id = args.source.strip().rstrip("/")
24
+ if not source_id:
25
+ _fail(" βœ— Enter a model name β€” for example: voice get google/gemma-3-1b")
26
+ _step(f"Finding the voice in {source_id}…")
27
+
28
+ try:
29
+ source = open_source(source_id)
30
+ names = source.names()
31
+ if not names:
32
+ _fail(f" βœ— No tensors found in '{source_id}'.")
33
+ arch, config = _source_arch(source)
34
+ requested = _parse_tensor_list(args)
35
+ want_multi = bool(getattr(args, "multi", False)) or len(requested) > 1
36
+ if not requested and not want_multi:
37
+ tname = _resolve_output_name(source, config, names)
38
+ if tname is None:
39
+ _fail(f' βœ— "{source_id}" doesn\'t expose an output head. Try `voice get {source_id} <tensor>` with a specific tensor.')
40
+ requested = [tname]
41
+ elif not requested and want_multi:
42
+ _fail(" βœ— --multi needs tensor names (pass them as positionals).")
43
+ resolved = [_resolve_one_tensor(r, names) for r in requested]
44
+ # Dedupe preserve order.
45
+ seen = set()
46
+ uniq = []
47
+ for r in resolved:
48
+ if r not in seen:
49
+ seen.add(r)
50
+ uniq.append(r)
51
+ resolved = uniq
52
+ except urllib.error.HTTPError as e:
53
+ _fail(f" βœ— Model '{source_id}' was not found or is not accessible ({e.code}).")
54
+ except Exception as e:
55
+ if ctx.VERBOSE:
56
+ import traceback
57
+ _say(traceback.format_exc())
58
+ _fail(f" βœ— Could not reach the model. Check your connection ({type(e).__name__}).")
59
+
60
+ if want_multi or len(resolved) > 1:
61
+ # Multi-pack -> voicepack.safetensors + voicepack.json (file output, not the registry).
62
+ if getattr(args, "name", None):
63
+ _fail(" βœ— --name is for registry voices; packs save to files. Drop --name or the extra tensors.")
64
+ out = Path(getattr(args, "out", None) or (Path.cwd() / "voicepack.safetensors"))
65
+ out.parent.mkdir(parents=True, exist_ok=True)
66
+ _check_overwrite(str(out), args)
67
+ pack = _fetch_pack(source, source_id, resolved)
68
+ _step(f"Packing {len(pack)} tensors β†’ {out.name}…")
69
+ write_safetensors(pack, str(out))
70
+ _write_voicepack_json(out, source_id, pack)
71
+ log_operation({"op": "tensor_get", "source": source_id, "format": source.kind,
72
+ "tensors": list(pack.keys()), "output": str(out), "status": "ok"})
73
+ _ok(f"Done β†’ {out} ({len(pack)} tensors)")
74
+ return
75
+
76
+ # Single tensor from here.
77
+ tname = resolved[0]
78
+ ref = source.ref(tname)
79
+ if getattr(args, "out", None):
80
+ # Standalone file; --name has no meaning here.
81
+ if getattr(args, "name", None):
82
+ _fail(" βœ— --name is for registry voices; --out writes a standalone file. Drop one.")
83
+ out = Path(args.out)
84
+ out.parent.mkdir(parents=True, exist_ok=True)
85
+ _check_overwrite(str(out), args)
86
+ _step(f"Extracting {tname} ({ref.dtype} {tuple(ref.shape)})…")
87
+ tmp_dir = ctx.VOICES_DIR / ".parts" / _safe_id(source_id, tname)
88
+ try:
89
+ fetched = _fetch_tensor(source, tname, tmp_dir)
90
+ except Exception as e:
91
+ _fail(f" βœ— Could not extract tensor: {e}")
92
+ actual_out = _write_fetched(fetched, tname, out)
93
+ _cleanup_tmp(tmp_dir)
94
+ log_operation({"op": "tensor_get", "source": source_id, "format": source.kind, "tensor": tname,
95
+ "dtype": fetched[3], "shape": list(fetched[4]), "output": str(actual_out), "status": "ok"})
96
+ _ok(f"Done β†’ {actual_out}")
97
+ return
98
+
99
+ if ctx.VERBOSE:
100
+ _note(f"{_family(arch)} Β· {tname} Β· {ref.dtype} Β· {ref.shape}")
101
+
102
+ # name the voice
103
+ default_name = source_id.split("/")[-1].split("@")[0].split(":")[0].replace(".gguf", "").replace(".safetensors", "")
104
+ name = _resolve_name(args, default_name)
105
+ folder = _registry_folder(name)
106
+ if _registry_name_exists(name) and folder.exists() and ((folder / "voice.safetensors").exists() or (folder / "voice.gguf").exists()):
107
+ _check_overwrite(str(folder), args)
108
+
109
+ _step(f"Downloading {tname}…")
110
+ tmp_dir = ctx.VOICES_DIR / ".parts" / _safe_id(source_id, tname)
111
+ try:
112
+ fetched = _fetch_tensor(source, tname, tmp_dir)
113
+ except urllib.error.HTTPError as e:
114
+ _fail(f" βœ— Could not download the tensor ({e.code}). Check your connection.")
115
+ except Exception as e:
116
+ if ctx.VERBOSE:
117
+ import traceback
118
+ _say(traceback.format_exc())
119
+ _fail(f" βœ— Could not download the tensor: {e}")
120
+
121
+ vocab = _nested(config, "vocab_size") if config else None
122
+ hidden = _nested(config, "hidden_size", "n_embd") if config else None
123
+ license_text = None if getattr(args, "quiet", False) else (_hf_license(getattr(source, "repo", None)) if getattr(source, "repo", None) else None)
124
+
125
+ meta = {
126
+ "name": name,
127
+ "source": _public_source(source_id),
128
+ "source_format": source.kind,
129
+ "tensor_name": tname,
130
+ "tensors": [{"name": tname, "shape": list(fetched[4]), "dtype": fetched[3]}],
131
+ "vocab_size": vocab,
132
+ "hidden_dim": hidden,
133
+ "dtype": fetched[3],
134
+ "shape": list(fetched[4]),
135
+ "architecture": arch,
136
+ "family": _family(arch),
137
+ "license": license_text,
138
+ "downloaded_at": _now_iso(),
139
+ }
140
+ _write_voice(folder, tname, fetched, meta)
141
+ _cleanup_tmp(tmp_dir)
142
+
143
+ log_operation({"op": "get", "source": source_id, "format": source.kind, "tensor": tname,
144
+ "dtype": fetched[3], "shape": list(fetched[4]), "output": str(folder), "status": "ok"})
145
+ _ok("Done. Your new voice is ready.")
146
+ _say(f" Saved to: {folder}")
147
+ _say(f" Use it: voice cast <your-model.gguf> {name}")
148
+
149
+
150
+ def cmd_cast(args):
151
+ target = args.target
152
+ if not os.path.exists(target):
153
+ _fail(f' βœ— "{target}" couldn\'t be found.')
154
+ if os.path.isdir(target):
155
+ _fail(f' βœ— "{target}" is a folder, not a model file.')
156
+
157
+ voice_arg = args.voice
158
+ voice_path, voice_meta = _registry_resolve(voice_arg)
159
+ if voice_path is None or not voice_path.exists():
160
+ _fail(f" βœ— Voice '{voice_arg}' not found. Use 'voice list' or pass a .safetensors/.gguf path.")
161
+
162
+ target_format = "gguf" if _is_gguf_file(target) else "safetensors"
163
+ out_path = _out_path(args, target, voice_arg)
164
+ _check_overwrite(out_path, args)
165
+ # Voicepack in the registry (or a pack path): cast every tensor, not just the first.
166
+ try:
167
+ _probe = open_source(str(voice_path))
168
+ _pack_names = [n for n in _probe.names() if n != "delta.voice.marker"]
169
+ except Exception:
170
+ _pack_names = []
171
+ requested = _parse_tensor_list(args)
172
+ excludes = _parse_exclude_list(args)
173
+ if requested:
174
+ # Tensor filter: resolve each positional (exact or single substring), then cast the selection.
175
+ resolved = [_resolve_one_tensor(r, _pack_names) for r in requested]
176
+ seen = set()
177
+ selected = []
178
+ for r in resolved:
179
+ if r not in seen:
180
+ seen.add(r)
181
+ selected.append(r)
182
+ resolved_ex = set()
183
+ for e in excludes:
184
+ if e in _pack_names:
185
+ resolved_ex.add(e)
186
+ elif len([n for n in _pack_names if e in n]) == 1:
187
+ resolved_ex.add([n for n in _pack_names if e in n][0])
188
+ selected = [n for n in selected if n not in resolved_ex]
189
+ if not selected:
190
+ _fail(" βœ— --exclude removes all selected tensors. Nothing to cast.")
191
+ if len(selected) > 1:
192
+ _cast_pack(target, str(voice_path), out_path, args, op="cast",
193
+ excludes=[n for n in _pack_names if n not in selected], meta=voice_meta)
194
+ return
195
+ f32, ref, raw, qtype = _load_tensor(str(voice_path), selected[0])
196
+ src_meta = voice_meta if voice_meta is not None else _tensor_src_meta(str(voice_path))
197
+ _cast_core(target, target_format, out_path, f32, ref, src_meta, args, op="cast", vraw=raw, vqtype=qtype)
198
+ return
199
+ if len(_pack_names) > 1:
200
+ _cast_pack(target, str(voice_path), out_path, args, op="cast", excludes=excludes, meta=voice_meta)
201
+ return
202
+
203
+ # auto-detect delta file via marker even without voice.json (e.g. shipped style.safetensors)
204
+ # do this after _check_overwrite so prompt and Detected don't interleave
205
+ if (voice_meta is None or voice_meta.get("type") != "delta") and voice_path.is_file():
206
+ try:
207
+ if _is_delta_safetensors(str(voice_path)):
208
+ voice_meta = {"type": "delta", "source": str(voice_path)}
209
+ _note("Detected delta voice β€” fusing style (delta + head)…")
210
+ except Exception:
211
+ pass
212
+
213
+ vf32, vref, vraw, vqtype = _load_voice(voice_path)
214
+ _cast_core(target, target_format, out_path, vf32, vref, voice_meta, args, op="cast", vraw=vraw, vqtype=vqtype)
215
+
216
+
217
+
218
+
219
+ def cmd_delta(args):
220
+ import numpy as np
221
+
222
+ voicename = getattr(args, "voicename", None)
223
+ if not voicename or not str(voicename).strip():
224
+ voicename = _choose_voice(args)
225
+ voicename = voicename.strip()
226
+ # If voicename is an existing file/dir path, treat as direct voice file (e.g. /path/to/voice.safetensors)
227
+ _voice_file_mode = False
228
+ _voice_file_path = None
229
+ _candidate = Path(voicename).expanduser()
230
+ if _candidate.exists():
231
+ if _candidate.is_file():
232
+ _voice_file_mode = True
233
+ _voice_file_path = _candidate.resolve()
234
+ # derive default name: if file is inside ~/.voices/, use the voice folder name (e.g. ~/.voices/styletune-v2/voice.safetensors -> styletune-v2)
235
+ # otherwise use file stem. This lets delta-voice be named delta-<voice-name> when source is from registry
236
+ try:
237
+ is_inside_voices = str(_candidate.resolve()).startswith(str(ctx.VOICES_DIR.resolve()) + os.sep)
238
+ except Exception:
239
+ is_inside_voices = False
240
+ if is_inside_voices:
241
+ # e.g. ~/.voices/styletune-v2/voice.safetensors -> styletune-v2
242
+ raw = _candidate.parent.name
243
+ # if parent is base/ org dir, fallback to stem
244
+ if raw in ("base", ".voices") or raw.startswith("."):
245
+ raw = _candidate.stem
246
+ else:
247
+ raw = _candidate.stem
248
+ # prompt for delta name, default is raw (or voice.json name if available)
249
+ try:
250
+ meta_tmp = _read_meta(_candidate.parent / "voice.json")
251
+ default_raw = (meta_tmp or {}).get("name") or raw
252
+ except Exception:
253
+ default_raw = raw
254
+ default_raw = str(default_raw).strip() or raw
255
+ # ask user for name, default from meta or file
256
+ if _noninteractive(args):
257
+ sanitized = _sanitize_voice_name(default_raw) or _slugify_voice_name(default_raw)
258
+ else:
259
+ try:
260
+ prompt = f" Name the delta or press enter to use [{default_raw}]: "
261
+ entered = input(prompt).strip()
262
+ chosen = entered or default_raw
263
+ sanitized = _sanitize_voice_name(chosen) or _slugify_voice_name(chosen)
264
+ except (EOFError, KeyboardInterrupt):
265
+ _say(" ⚠ Cancelled.")
266
+ sys.exit(0)
267
+ if sanitized is None:
268
+ sanitized = "voice"
269
+ # keep original path for _compute_delta, but use sanitized name for delta naming / messages
270
+ voice_path = _voice_file_path
271
+ voice_meta = _read_meta(_candidate.parent / "voice.json")
272
+ # use sanitized as voicename for delta naming downstream (without delta- prefix, added later)
273
+ voicename = sanitized
274
+ elif _candidate.is_dir():
275
+ voice_path, voice_meta = _registry_resolve(str(_candidate))
276
+ if voice_path is None or not voice_path.exists():
277
+ _fail(f" βœ— Voice '{voicename}' not found. Try voice list")
278
+ # use directory name as voicename if it looks like a voice folder
279
+ dir_name = _candidate.name
280
+ if _sanitize_voice_name(dir_name.replace("delta-", "", 1) if dir_name.lower().startswith("delta-") else dir_name) is not None:
281
+ voicename = dir_name
282
+ # fall through to existing meta handling
283
+ else:
284
+ # exists but not file/dir? treat as registry name
285
+ if "/" in voicename or "\\" in voicename or "." in voicename or _sanitize_voice_name(voicename.replace("delta-", "", 1) if voicename.lower().startswith("delta-") else voicename) is None:
286
+ _fail(" βœ— Invalid voice name. Use letters, numbers, hyphens, underscores.")
287
+ voice_path, voice_meta = _registry_resolve(voicename)
288
+ if voice_path is None or not voice_path.exists():
289
+ _fail(f" βœ— Voice '{voicename}' not found. Try voice list")
290
+ else:
291
+ if "/" in voicename or "\\" in voicename or "." in voicename or _sanitize_voice_name(voicename.replace("delta-", "", 1) if voicename.lower().startswith("delta-") else voicename) is None:
292
+ _fail(" βœ— Invalid voice name. Use letters, numbers, hyphens, underscores.")
293
+ voice_path, voice_meta = _registry_resolve(voicename)
294
+ if voice_path is None or not voice_path.exists():
295
+ _fail(f" βœ— Voice '{voicename}' not found. Try voice list")
296
+ # handle file-mode delta naming already set; for registry mode voice_path/voice_meta already resolved
297
+ if _voice_file_mode:
298
+ # voice_path already set, ensure it exists
299
+ if not voice_path.exists():
300
+ _fail(f" βœ— Voice file '{_voice_file_path}' not found.")
301
+ else:
302
+ # registry mode already validated above
303
+ pass
304
+
305
+ source_hf = (voice_meta or {}).get("source_hf_model") or (voice_meta or {}).get("source") or "β€”"
306
+ _say(f" '{voicename}' is {source_hf}")
307
+
308
+ # --base may be an HF repo (org/model) or a local file/dir path (e.g. /path/to/lm_head.safetensors)
309
+ base_arg = getattr(args, "base", None)
310
+ base_is_local = False
311
+ base_file = None
312
+ base_json = None
313
+ base_id = None
314
+ if base_arg and Path(base_arg).expanduser().exists():
315
+ p = Path(base_arg).expanduser().resolve()
316
+ if p.is_file():
317
+ base_file = p
318
+ base_id = str(p)
319
+ base_is_local = True
320
+ elif p.is_dir():
321
+ # directory containing model.safetensors or any safetensors
322
+ cand = p / "model.safetensors"
323
+ if cand.exists():
324
+ base_file = cand.resolve()
325
+ base_is_local = True
326
+ base_id = str(base_file)
327
+ else:
328
+ safes = sorted(p.glob("*.safetensors"))
329
+ if safes:
330
+ base_file = safes[0].resolve()
331
+ base_is_local = True
332
+ base_id = str(base_file)
333
+ else:
334
+ _fail(f" βœ— No .safetensors found in {p}")
335
+ # if local, no base_json needed; will be used directly
336
+ if not base_is_local:
337
+ base_id = _resolve_base(args, source_hf)
338
+ # _resolve_base may return a local path if user typed a path at the prompt
339
+ _maybe_local = Path(str(base_id)).expanduser()
340
+ if _maybe_local.exists():
341
+ p = _maybe_local.resolve()
342
+ if p.is_file():
343
+ base_file = p
344
+ base_is_local = True
345
+ base_json = None
346
+ elif p.is_dir():
347
+ cand = p / "model.safetensors"
348
+ if cand.exists():
349
+ base_file = cand.resolve()
350
+ base_is_local = True
351
+ base_id = str(base_file)
352
+ base_json = None
353
+ else:
354
+ safes = sorted(p.glob("*.safetensors"))
355
+ if safes:
356
+ base_file = safes[0].resolve()
357
+ base_is_local = True
358
+ base_id = str(base_file)
359
+ base_json = None
360
+ else:
361
+ _fail(f" βœ— No .safetensors found in {p}")
362
+ if not base_is_local:
363
+ tmp = _base_paths(base_id)
364
+ if tmp is None:
365
+ _fail(f" βœ— Invalid base '{base_id}'")
366
+ base_file, base_json = tmp
367
+ try:
368
+ _vsrc = open_source(str(voice_path))
369
+ _need = [n for n in _vsrc.names() if n != "delta.voice.marker"]
370
+ except Exception:
371
+ _need = []
372
+ if not _need:
373
+ _fail(" βœ— No tensors found for delta.")
374
+ base_file = _ensure_base_tensors(base_id, _need, args)
375
+
376
+ delta_name = f"delta-{voicename.lower()}"
377
+ delta_folder = ctx.VOICES_DIR / delta_name
378
+ if delta_folder.exists() and ((delta_folder / "voice.safetensors").exists() or (delta_folder / "voice.gguf").exists()):
379
+ _check_overwrite(str(delta_folder), args)
380
+
381
+ _step("Crafting your style…")
382
+ _compute_delta(voice_path, base_file, delta_folder, delta_name, args)
383
+
384
+ meta = {
385
+ "name": delta_name, "type": "delta", "source": voicename,
386
+ "source_hf_model": _public_source(source_hf),
387
+ "base": _public_source(base_id),
388
+ "dtype": "F16",
389
+ "downloaded_at": _now_iso(),
390
+ }
391
+ # enrich with shape/dtype from the delta file (all tensors, not just the first)
392
+ try:
393
+ hdr, tens = read_safetensors(str(delta_folder / "voice.safetensors"))
394
+ keys = [k for k in tens.keys() if k != "delta.voice.marker"]
395
+ meta["tensors"] = [{"name": k, "shape": list(tens[k]["shape"]), "dtype": tens[k]["dtype"],
396
+ "bytes": tens[k]["data_offsets"][1] - tens[k]["data_offsets"][0]} for k in keys]
397
+ if len(keys) == 1:
398
+ # backward compat: single-tensor voices keep flat fields
399
+ meta["tensor_name"] = keys[0]
400
+ meta["shape"] = list(tens[keys[0]]["shape"])
401
+ meta["dtype"] = tens[keys[0]]["dtype"]
402
+ meta["bytes"] = tens[keys[0]]["data_offsets"][1]
403
+ except Exception:
404
+ pass
405
+ jtmp = delta_folder / "voice.json.tmp"
406
+ jtmp.write_text(json.dumps(meta, indent=2) + "\n")
407
+ jtmp.replace(delta_folder / "voice.json")
408
+
409
+ log_operation({"op": "delta", "voice": voicename, "base": base_id, "tensor": meta.get("tensor_name"),
410
+ "dtype": meta.get("dtype"), "shape": meta.get("shape"), "output": str(delta_folder), "status": "ok"})
411
+ _ok(f"Done β†’ {delta_folder}")
412
+ _say(f" Use it: voice cast <your-model.gguf> {delta_name}")
413
+
414
+
415
+ def cmd_add(args):
416
+ import shutil
417
+ st_path = Path(getattr(args, "safetensors", "") or "").expanduser()
418
+ js_path = Path(getattr(args, "json", "") or "").expanduser()
419
+ if not st_path.is_file():
420
+ _fail(f' βœ— "{st_path}" couldn\'t be found. Need voice.safetensors')
421
+ if not js_path.is_file():
422
+ _fail(f' βœ— "{js_path}" couldn\'t be found. Need voice.json β€” both are required')
423
+ # validate safetensors
424
+ try:
425
+ hdr, tens = read_safetensors(str(st_path))
426
+ except Exception as e:
427
+ _fail(f" βœ— {st_path} is not a valid .safetensors file: {e}")
428
+ if not tens:
429
+ _fail(f" βœ— No tensors found in {st_path}")
430
+ # validate json
431
+ try:
432
+ meta = json.loads(js_path.read_text(encoding="utf-8"))
433
+ except Exception as e:
434
+ _fail(f" βœ— {js_path} is not valid JSON: {e}")
435
+ if not isinstance(meta, dict):
436
+ _fail(f" βœ— {js_path} must contain a JSON object")
437
+ # determine name: --name > json name > prompt
438
+ default_name = (meta.get("name") or st_path.stem or "voice").strip()
439
+ # sanitize default for display
440
+ name_arg = getattr(args, "name", None)
441
+ if name_arg and str(name_arg).strip():
442
+ name = str(name_arg).strip()
443
+ elif _noninteractive(args):
444
+ name = default_name
445
+ else:
446
+ try:
447
+ # show default from json
448
+ prompt = f" Name the voice or press enter to use [{default_name}]: "
449
+ entered = input(prompt).strip()
450
+ name = entered or default_name
451
+ except (EOFError, KeyboardInterrupt):
452
+ _say(" ⚠ Cancelled.")
453
+ sys.exit(0)
454
+ if _sanitize_voice_name(name) is None:
455
+ _fail(f" βœ— Invalid name '{name}'. {_NAME_RULES}")
456
+ folder = _registry_folder(name)
457
+ if _registry_name_exists(name) and folder.exists() and ((folder / "voice.safetensors").exists() or (folder / "voice.gguf").exists()):
458
+ _check_overwrite(str(folder), args)
459
+ # remove old to allow replace
460
+ try:
461
+ shutil.rmtree(folder)
462
+ except Exception:
463
+ pass
464
+ # also handle case where folder exists as partial
465
+ _assert_not_symlink(folder)
466
+ folder.mkdir(parents=True, exist_ok=True)
467
+ try:
468
+ os.chmod(folder, 0o700)
469
+ except Exception:
470
+ pass
471
+ _assert_not_symlink(folder)
472
+ # copy files atomically with 0600 and fsync β€” tensor FIRST, then metadata,
473
+ # so a crash never leaves a valid voice.json pointing at a partial artifact.
474
+ dest_st = folder / "voice.safetensors"
475
+ dest_js = folder / "voice.json"
476
+ # ensure meta name matches sanitized folder name (lowercased)
477
+ meta = dict(meta)
478
+ meta["name"] = name.strip().lower() if _sanitize_voice_name(name) else name
479
+ # copy safetensors atomically: use tmp + fsync + replace, 0600
480
+ tmp_st = dest_st.with_suffix(".tmp")
481
+ # stream copy with fsync
482
+ with open(st_path, "rb") as src, open(tmp_st, "wb") as dst:
483
+ # ensure 0600
484
+ try:
485
+ os.chmod(tmp_st, 0o600)
486
+ except Exception:
487
+ pass
488
+ while True:
489
+ b = src.read(8 << 20)
490
+ if not b:
491
+ break
492
+ dst.write(b)
493
+ dst.flush()
494
+ try:
495
+ os.fsync(dst.fileno())
496
+ except Exception:
497
+ pass
498
+ try:
499
+ os.chmod(tmp_st, 0o600)
500
+ except Exception:
501
+ pass
502
+ tmp_st.replace(dest_st)
503
+ try:
504
+ os.chmod(dest_st, 0o600)
505
+ except Exception:
506
+ pass
507
+ # verify tensor before writing metadata
508
+ try:
509
+ rhdr, rtens = read_safetensors(str(dest_st))
510
+ if not rtens:
511
+ try:
512
+ dest_st.unlink(missing_ok=True)
513
+ except Exception:
514
+ pass
515
+ _fail(f" βœ— No tensors found in {st_path} β€” nothing added.")
516
+ except SystemExit:
517
+ raise
518
+ except Exception as e:
519
+ try:
520
+ dest_st.unlink(missing_ok=True)
521
+ except Exception:
522
+ pass
523
+ _fail(f" βœ— Validation failed for {st_path}: {e} β€” nothing added.")
524
+ # write json atomically 0600 (only after the tensor is known good)
525
+ tmp_js = dest_js.with_suffix(".json.tmp")
526
+ fd = os.open(str(tmp_js), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
527
+ try:
528
+ os.write(fd, (json.dumps(meta, indent=2) + "\n").encode("utf-8"))
529
+ try:
530
+ os.fsync(fd)
531
+ except Exception:
532
+ pass
533
+ finally:
534
+ os.close(fd)
535
+ try:
536
+ os.chmod(tmp_js, 0o600)
537
+ except Exception:
538
+ pass
539
+ tmp_js.replace(dest_js)
540
+ try:
541
+ dirfd = os.open(str(folder), os.O_DIRECTORY)
542
+ try:
543
+ os.fsync(dirfd)
544
+ finally:
545
+ os.close(dirfd)
546
+ except Exception:
547
+ pass
548
+ log_operation({"op": "add", "source_st": str(st_path), "source_json": str(js_path), "output": str(folder), "status": "ok"})
549
+ _ok(f"Added voice '{name}' β†’ {folder}")
550
+ _say(f" File: {dest_st} ({dest_st.stat().st_size/1e6:.1f} MB)")
551
+ _say(f" Use it: voice cast <your-model.gguf> {name}")
vlib/compute.py ADDED
@@ -0,0 +1,1186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cast math: orient, fuse, quantize, pack writes."""
2
+
3
+ from pathlib import Path
4
+ import os
5
+ import struct
6
+ import sys
7
+ import time
8
+ from vlib import ctx
9
+ from vlib.ui import _confirm, _fail, _noninteractive, _note, _ok, _progress_pref, _say, _step, _transform, _warn
10
+ from vlib.tensors import _gguf_raw_to_f32, _is_delta_safetensors, _is_ggml_quant, _is_gguf_file, _safetensors_write_repl, decode_to_f32, encode_from_f32, read_safetensors, rewrite_safetensors_tensor
11
+ from vlib.sources import OUTPUT_ALIASES, _nested, detect_output_tensor, largest_2d, open_source
12
+ from vlib.registry import log_operation
13
+
14
+
15
+ def _load_voice(voice_path):
16
+ """Load a voice artifact. Returns (f32_array_or_None, ref, raw_bytes_or_None, ggml_type_or_None).
17
+ Quantized GGUF artifacts carry raw blocks; safetensors artifacts decode to f32."""
18
+ src = open_source(str(voice_path))
19
+ names = [n for n in src.names() if n != "delta.voice.marker"]
20
+ if not names:
21
+ _fail(f" βœ— No tensors found in {voice_path}.")
22
+ tname = names[0] if len(names) == 1 else "output.weight" if "output.weight" in names else names[0]
23
+ ref = src.ref(tname)
24
+ if src.kind == "gguf" and _is_ggml_quant(ref.dtype):
25
+ return None, ref, src.read_raw(tname), ref.dtype
26
+ return src.read_f32(tname), ref, None, None
27
+
28
+
29
+ def _orient(arr, shape, target_shape):
30
+ import numpy as np
31
+ if tuple(shape) == tuple(target_shape):
32
+ return arr
33
+ if tuple(shape) == tuple(target_shape[::-1]):
34
+ if ctx.VERBOSE:
35
+ _note("Transposing to match the target layout…")
36
+ return np.ascontiguousarray(arr.T)
37
+ return None
38
+
39
+
40
+ def _cast_st_prepare(target, tensors, vf32, vref, meta, args, vraw=None, vqtype=None, strict_target=False, target_name=None):
41
+ """Per-tensor compute for a safetensors target. Returns (tname, data, tdtype, tshape).
42
+ target_name (from _resolve_pack_targets) overrides lookup; None keeps the
43
+ original single-tensor behavior byte-for-byte."""
44
+ import numpy as np
45
+ if vraw is not None:
46
+ # quant artifact -> safetensors target: dequant is the only requant path
47
+ from gguf.constants import GGMLQuantizationType as Q
48
+ from gguf.quants import quant_shape_to_byte_shape
49
+ vf32 = _gguf_raw_to_f32(vraw, Q[vqtype], tuple(reversed(tuple(int(x) for x in vref.shape))))
50
+ names = list(tensors.keys())
51
+ if target_name is not None:
52
+ tname = target_name if target_name in names else None
53
+ if tname is None:
54
+ _fail(f" βœ— Resolved target '{target_name}' for '{vref.name}' not in {target}.")
55
+ else:
56
+ tname = vref.name if vref.name in names else None
57
+ if tname is None and strict_target:
58
+ _fail(f" βœ— Tensor '{vref.name}' not found in target {target}. Voicepack requires exact match (not mapping to another tensor).")
59
+ if tname is None:
60
+ tname = detect_output_tensor(names, None) or largest_2d(names, {n: tensors[n]["shape"] for n in names})
61
+ if tname is None:
62
+ _fail(" βœ— Could not find a matching tensor in the target.")
63
+ tshape = tuple(tensors[tname]["shape"])
64
+ tdtype = tensors[tname]["dtype"]
65
+ # --quant override for safetensors target: only F32/F16/BF16/keep/target make sense (no GGUF quants).
66
+ _qo = _parse_quant_override(getattr(args, "quant", None) or "keep")
67
+ if _qo is None:
68
+ _fail(f" βœ— Invalid --quant '{getattr(args, 'quant', None)}' for safetensors target. Use keep|target|f32|f16|bf16.")
69
+ if _qo not in ("keep", "target", "f32", "f16", "bf16"):
70
+ _fail(f" βœ— --quant {_qo} needs GGUF target (safetensors has no quant {_qo}). Use keep|target|f32|f16|bf16.")
71
+ if _qo in ("f32", "f16", "bf16"):
72
+ _want = {"f32": "F32", "f16": "F16", "bf16": "BF16"}[_qo]
73
+ if _want != tdtype.upper():
74
+ _warn(f" Re-encoding target {tdtype} β†’ {_want} for --quant {_qo} (extra conversion).")
75
+ tdtype = _want
76
+ # Show quant/target by default for tensor commands (advanced) and verbose for voice.
77
+ try:
78
+ _svq = vqtype if vraw is not None else vref.dtype
79
+ except Exception:
80
+ _svq = "?"
81
+ if ctx.VERBOSE:
82
+ _note(f"Voice '{vref.name}' is {_svq} {tuple(vref.shape)} β†’ target '{tname}' is {tdtype} {tshape}")
83
+ if vraw is not None and ctx.VERBOSE:
84
+ _note(f"Dequanting {vqtype} once to F32 for safetensors target (unavoidable, safetensors has no quant).")
85
+ oriented = _orient(vf32, vref.shape, tshape)
86
+ if oriented is None:
87
+ _fail(f" βœ— Voice shape {vref.shape} doesn't fit target tensor {tname} {tshape}.")
88
+ if (meta or {}).get("type") == "delta":
89
+ _step(f"Fusing style into {tname} (delta + head β†’ {tdtype}, kept)…")
90
+ with open(target, "rb") as f:
91
+ _hs = struct.unpack("<Q", f.read(8))[0]
92
+ info = tensors[tname]
93
+ with open(target, "rb") as f:
94
+ f.seek(8 + _hs + info["data_offsets"][0])
95
+ head_f32 = decode_to_f32(f.read(info["data_offsets"][1] - info["data_offsets"][0]), tdtype, tshape)
96
+ oriented = np.ascontiguousarray(head_f32 + oriented)
97
+ data = encode_from_f32(oriented, tdtype)
98
+ return tname, data, tdtype, tshape
99
+
100
+
101
+ def _cast_core(target, target_format, out_path, vf32, vref, meta, args, op="cast", vraw=None, vqtype=None, strict_target=False):
102
+ if getattr(args, "speak", False) and target_format != "gguf":
103
+ _fail(" βœ— --speak needs a .gguf target.")
104
+ if target_format == "gguf":
105
+ _cast_gguf(target, out_path, vf32, vref, meta, args, op, vraw=vraw, vqtype=vqtype, strict_target=strict_target)
106
+ return
107
+ # safetensors target
108
+ header, tensors = read_safetensors(target)
109
+ t0s = time.time()
110
+ tname, data, tdtype, tshape = _cast_st_prepare(target, tensors, vf32, vref, meta, args,
111
+ vraw=vraw, vqtype=vqtype, strict_target=strict_target)
112
+ _step(f"Replacing {tname} ({tdtype} {tshape})…")
113
+ rewrite_safetensors_tensor(target, out_path, tname, data, tdtype, tshape)
114
+ file_size = os.path.getsize(out_path)
115
+ try:
116
+ _note(f"Kept {len(data)/1e6:.1f} MB β†’ {tdtype} @ {(len(data)/1e6)/max(time.time()-t0s,1e-6):.0f} MB/s")
117
+ except Exception:
118
+ pass
119
+ log_operation({"op": op, "target": target, "format": "safetensors", "tensor": tname,
120
+ "dtype": tdtype, "shape": list(tshape), "output": out_path, "status": "ok"})
121
+ _ok(f"Done β†’ {out_path}")
122
+ _say(f" Tensor replaced at {tdtype} ({file_size/1e6:.1f} MB).")
123
+
124
+
125
+ def _is_head_name(name):
126
+ if not name:
127
+ return False
128
+ try:
129
+ if name in OUTPUT_ALIASES or name in ("token_embd.weight",):
130
+ return True
131
+ for a in OUTPUT_ALIASES:
132
+ if name.endswith(a):
133
+ return True
134
+ except Exception:
135
+ pass
136
+ return False
137
+
138
+
139
+ def _parse_quant_override(s):
140
+ # Returns normalized key: keep|target or Q-type name upper (e.g. Q4_0). Trivial: accept all GGML quants.
141
+ if s is None:
142
+ return "keep"
143
+ k = str(s).strip().lower().replace("-", "_")
144
+ if k in ("keep", "target"):
145
+ return k
146
+ # Aliases for float family.
147
+ if k in ("fp32",):
148
+ k = "f32"
149
+ if k in ("fp16",):
150
+ k = "f16"
151
+ # Try any GGMLQuantizationType name.
152
+ try:
153
+ from gguf.constants import GGMLQuantizationType as _Qp
154
+ # Case-insensitive match against all Q names.
155
+ for _q in _Qp:
156
+ if _q.name.lower() == k:
157
+ return k
158
+ except Exception:
159
+ pass
160
+ # Fallback known list (when gguf import fails).
161
+ _known = {"f32", "f16", "bf16", "q8_0", "q8_1", "q8_k", "q1_0", "q4_0", "q4_1", "q5_0", "q5_1",
162
+ "q2_k", "q3_k", "q4_k", "q5_k", "q6_k", "iq1_s", "iq1_m", "iq2_xxs", "iq2_xs", "iq2_s",
163
+ "iq3_xxs", "iq3_s", "iq4_nl", "iq4_xs", "tq1_0", "tq2_0", "mxfp4", "nvfp4"}
164
+ if k in _known:
165
+ return k
166
+ return None
167
+
168
+
169
+ def _quant_from_key(key):
170
+ # key normalized via _parse_quant_override (lower). Returns Q type or None for keep/target.
171
+ if key in ("keep", "target", None):
172
+ return None
173
+ try:
174
+ from gguf.constants import GGMLQuantizationType as _Qq
175
+ for _q in _Qq:
176
+ if _q.name.lower() == key:
177
+ return _q
178
+ # Upper direct (e.g. Q4_0).
179
+ return _Qq[key.upper()]
180
+ except Exception:
181
+ return None
182
+
183
+
184
+ def _qerr(e):
185
+ """Readable lib error: gguf often raises bare NotImplementedError ('')."""
186
+ s = str(e).strip()
187
+ return f"{type(e).__name__}: {s}" if s else type(e).__name__
188
+
189
+
190
+ _NEAREST_CACHE = {}
191
+
192
+
193
+ def _nearest_encodable(target_q):
194
+ """Nearest gguf-encodable quant to target_q by bits/param (ties β†’ larger =
195
+ quality-preserving). The lib decodes everything but encodes only a subset
196
+ (no K/IQ quants); this keeps fused tensors near the original size instead
197
+ of ballooning to F16. Support probed once per process (version-proof)."""
198
+ import numpy as np
199
+ from gguf.constants import GGMLQuantizationType as Q
200
+ from gguf.constants import GGML_QUANT_SIZES as S
201
+ from gguf.quants import quantize
202
+ key = getattr(target_q, "name", str(target_q))
203
+ if key in _NEAREST_CACHE:
204
+ return _NEAREST_CACHE[key]
205
+ tbpp = S[target_q][1] * 8 / S[target_q][0]
206
+ probe = (np.arange(64 * 64, dtype=np.float32).reshape(64, 64) - 2048) / 64
207
+ scored = []
208
+ for name in ("Q8_0", "Q5_1", "Q5_0", "Q4_1", "Q4_0", "MXFP4", "F16"):
209
+ cand = getattr(Q, name, None)
210
+ if cand is None or cand == target_q:
211
+ continue
212
+ try:
213
+ quantize(probe, cand)
214
+ except Exception:
215
+ continue
216
+ cbpp = S[cand][1] * 8 / S[cand][0]
217
+ scored.append((abs(cbpp - tbpp), -cbpp, cand))
218
+ best = min(scored)[2] if scored else None
219
+ _NEAREST_CACHE[key] = best
220
+ return best
221
+
222
+ def _quantize_to_Q(fused, req_q):
223
+ # Quantize F32 fused array to any Q (F32/F16/BF16 via astype, else gguf quantize).
224
+ # Returns (q_head, out_qtype). Raises on unsupported (e.g. Q1_0/Q8_1/Q8_K NotImplemented).
225
+ import numpy as np
226
+ from gguf.constants import GGMLQuantizationType as Q
227
+ from gguf.quants import quantize
228
+ if req_q in (Q.F32,):
229
+ return fused.astype(np.float32), Q.F32
230
+ if req_q in (Q.F16,):
231
+ return fused.astype(np.float16), Q.F16
232
+ try:
233
+ _b = getattr(Q, "BF16", None)
234
+ except Exception:
235
+ _b = None
236
+ if _b is not None and req_q == _b:
237
+ _u32 = fused.astype(np.float32).view(np.uint32)
238
+ return (((_u32 + 0x7FFF + ((_u32 >> 16) & 1)) >> 16).astype(np.uint16), _b)
239
+ # All other GGML quants (Q4_0/Q4_K/Q6_K/IQ*/TQ*/MXFP4/...) via single quantize call (trivial).
240
+ return quantize(fused, req_q), req_q
241
+
242
+
243
+ def _fuse_delta(target_tensor, delta_f32, strict=False):
244
+ """Dequant the target head once to F32 and fuse the delta (head + delta).
245
+ Single seam for the three dequant→fuse sites in _gguf_fuse_one.
246
+ strict (safetensors delta path only): fail clean on shape mismatch, exactly as before."""
247
+ import numpy as np
248
+ from gguf.constants import GGMLQuantizationType as Q
249
+ if target_tensor.tensor_type in (Q.F32, Q.F16):
250
+ try:
251
+ head_f32 = target_tensor.data.astype(np.float32)
252
+ except Exception:
253
+ head_f32 = np.asarray(target_tensor.data, dtype=np.float32)
254
+ else:
255
+ from gguf.quants import dequantize as _deq
256
+ head_f32 = _deq(target_tensor.data, target_tensor.tensor_type)
257
+ if strict and head_f32.shape != delta_f32.shape:
258
+ sys.stdout.write("\n")
259
+ _fail(f" βœ— Style shape {delta_f32.shape} doesn't match model head {head_f32.shape}.")
260
+ return np.ascontiguousarray(head_f32 + delta_f32)
261
+
262
+
263
+ def _gguf_fuse_one(target, target_tensor, tname, logical, vf32, vref, meta, args, op, vraw=None, vqtype=None):
264
+ """Fuse one voice tensor into its target GGUF tensor form. Returns (q_head, out_qtype)."""
265
+ import numpy as np
266
+ from gguf.constants import GGMLQuantizationType as Q
267
+ from gguf.quants import quantize
268
+ # Philosophy: preserve quality, avoid dequant->quant cycles.
269
+ # - GGUF quant voice (vraw) is kept at its obtained quant (never upscale Q2->Q6, no inventing data).
270
+ # - Safetensors high-fidelity lm_head (voice) is quantized once to Q8_0.
271
+ # - Generic tensors keep target quant (F32 norms stay F32, never forced Q8_0).
272
+ # - All GGML quants supported (trivial passthrough + quantize where implemented).
273
+ _quant_override = _parse_quant_override(getattr(args, "quant", None) or "keep")
274
+ if _quant_override is None:
275
+ _fail(f" βœ— Invalid --quant '{getattr(args, 'quant', None)}'. Use keep|target|<any quant like q8_0,q4_0,q4_k,q6_k,iq4_nl,f16,f32,bf16>.")
276
+ _is_head = _is_head_name(vref.name) or _is_head_name(tname)
277
+ _is_delta = (meta or {}).get("type") == "delta"
278
+ # Show quant/target by default for advanced tensor commands (and verbose for voice)
279
+ try:
280
+ _tq = target_tensor.tensor_type.name
281
+ except Exception:
282
+ _tq = "?"
283
+ try:
284
+ _vq = vqtype if vraw is not None else vref.dtype
285
+ except Exception:
286
+ _vq = "?"
287
+ if ctx.VERBOSE:
288
+ _note(f"Voice '{vref.name}' is {_vq} {tuple(vref.shape)} β†’ target '{tname}' is {_tq} {logical}")
289
+ if vraw is not None:
290
+ # GGUF quant voice: keep obtained quant (no upscale, no extra cycle).
291
+ try:
292
+ _vqtype = Q[vqtype]
293
+ except Exception:
294
+ _fail(f" βœ— Unknown voice quant '{vqtype}'.")
295
+ if tuple(vref.shape) != logical:
296
+ _fail(f" βœ— Voice shape {vref.shape} doesn't fit target tensor {tname} {logical}.")
297
+ if _quant_override in ("keep",):
298
+ # Transplant quant type as-is (may differ from target type β€” intentional, preserves quality).
299
+ if _vqtype.name != target_tensor.tensor_type.name and ctx.VERBOSE:
300
+ _note(f"Keeping voice quant {_vqtype.name} (target was {target_tensor.tensor_type.name}, not upscaling).")
301
+ _step(f"Casting onto {os.path.basename(target)} ({_vqtype.name}, kept)…")
302
+ from gguf.quants import quant_shape_to_byte_shape as _q2b_keep
303
+ _keep_shape = _q2b_keep(tuple(int(x) for x in vref.shape), _vqtype)
304
+ _need = 1
305
+ for _d in _keep_shape:
306
+ _need *= _d
307
+ _raw = np.frombuffer(vraw, dtype=np.uint8)
308
+ if _raw.size < _need:
309
+ _fail(f" βœ— Short voice data for transplant ({_raw.size}/{_need} bytes).")
310
+ q_head, out_qtype = _raw[:_need].reshape(_keep_shape), _vqtype
311
+ elif _quant_override == "target":
312
+ # Explicit: dequant once then quant to target (extra cycle, warn).
313
+ _warn(f"Re-quanting voice {_vqtype.name} → target {target_tensor.tensor_type.name} (extra dequant→quant cycle).")
314
+ from gguf.quants import dequantize as _deq2
315
+ from gguf.quants import quant_shape_to_byte_shape as _q2b
316
+ # vref.shape is logical already checked
317
+ _rshape = tuple(vref.shape)
318
+ try:
319
+ _f32v = _deq2(np.frombuffer(vraw, dtype=np.uint8).reshape(_q2b(_rshape, _vqtype)), _vqtype)
320
+ except Exception as e:
321
+ _fail(f" βœ— Could not dequant voice for --quant target: {e}")
322
+ if _is_delta:
323
+ _f32v = _fuse_delta(target_tensor, _f32v)
324
+ t_quant0 = time.time()
325
+ _tq2 = target_tensor.tensor_type
326
+ if _tq2 in (Q.F32,):
327
+ q_head, out_qtype = _f32v.astype(np.float32), Q.F32
328
+ elif _tq2 in (Q.F16,):
329
+ q_head, out_qtype = _f32v.astype(np.float16), Q.F16
330
+ else:
331
+ try:
332
+ _bf = getattr(Q, "BF16", None)
333
+ except Exception:
334
+ _bf = None
335
+ if _bf is not None and _tq2 == _bf:
336
+ # BF16 target: gguf lib has no BF16 encoder here; keep F32 to avoid loss
337
+ q_head, out_qtype = _f32v.astype(np.float32), Q.F32
338
+ _warn(" BF16 target: keeping F32 to avoid loss (use --quant f16/bf16 explicitly if needed).")
339
+ else:
340
+ try:
341
+ q_head, out_qtype = quantize(_f32v, _tq2), _tq2
342
+ except Exception as e:
343
+ _fail(f" βœ— Could not quant to target {_tq2.name}: {e}")
344
+ _note(f"Re-quantized {_f32v.nbytes/1e6:.1f} MB β†’ {_tq2.name} @ {(_f32v.nbytes/1e6)/max(time.time()-t_quant0,1e-6):.0f} MB/s")
345
+ else:
346
+ # Explicit q8_0/f16/f32/bf16 for quant voice: dequant once then quant.
347
+ from gguf.quants import dequantize as _deq2
348
+ from gguf.quants import quant_shape_to_byte_shape as _q2b
349
+ _rshape = tuple(vref.shape)
350
+ try:
351
+ _f32v = _deq2(np.frombuffer(vraw, dtype=np.uint8).reshape(_q2b(_rshape, _vqtype)), _vqtype)
352
+ except Exception as e:
353
+ _fail(f" βœ— Could not dequant voice for --quant {_quant_override}: {e}")
354
+ if _is_delta:
355
+ _f32v = _fuse_delta(target_tensor, _f32v)
356
+ _qn = _quant_from_key(_quant_override)
357
+ if _qn is None:
358
+ _fail(f" βœ— Invalid --quant '{_quant_override}'.")
359
+ t_quant0 = time.time()
360
+ try:
361
+ q_head, out_qtype = _quantize_to_Q(_f32v, _qn)
362
+ except NotImplementedError as e:
363
+ _fail(f" βœ— Quant {getattr(_qn, 'name', _quant_override)} not implemented by gguf lib ({e}). Keeping original is recommended.")
364
+ except Exception as e:
365
+ _fail(f" βœ— Could not quant to {_quant_override}: {e}")
366
+ _note(f"Quantized {_f32v.nbytes/1e6:.1f} MB β†’ {out_qtype.name} @ {(_f32v.nbytes/1e6)/max(time.time()-t_quant0,1e-6):.0f} MB/s")
367
+ else:
368
+ oriented = _orient(vf32, vref.shape, logical)
369
+ if oriented is None:
370
+ _fail(f" βœ— Voice shape {vref.shape} doesn't fit target tensor {tname} {logical}.")
371
+ t_fuse0 = time.time()
372
+ # Decide out quant: Q8_0 once only for safetensors high-fidelity heads; else keep target.
373
+ # vref.dtype here is F32/F16/BF16 (high-fidelity, since vraw is None).
374
+ _target_q = target_tensor.tensor_type
375
+ # Explicit override wins (any quant trivially supported via _quantize_to_Q).
376
+ _req = None
377
+ if _quant_override != "keep":
378
+ if _quant_override == "target":
379
+ _req = _target_q
380
+ else:
381
+ _req = _quant_from_key(_quant_override)
382
+ if _req is None:
383
+ _fail(f" βœ— Invalid --quant '{_quant_override}'.")
384
+ t_fuse0 = time.time()
385
+ if _is_delta:
386
+ # Fuse first (F32 math preserves quality), decide quant after.
387
+ fused = _fuse_delta(target_tensor, oriented, strict=True)
388
+ if _req is not None:
389
+ # Explicit --quant for delta (all quants trivial via helper).
390
+ _step(f"Fusing style into {tname} (delta + head β†’ {_req.name})…")
391
+ t_quant0 = time.time()
392
+ try:
393
+ q_head, out_qtype = _quantize_to_Q(fused, _req)
394
+ except NotImplementedError as e:
395
+ _fail(f" βœ— Quant {_req.name} not implemented by gguf lib ({e}). Keeping original is recommended.")
396
+ except Exception as e:
397
+ _fail(f" βœ— Could not quant delta to {_req.name}: {e}")
398
+ _note(f"Quantized {fused.nbytes/1e6:.1f} MB β†’ {_req.name} @ {(fused.nbytes/1e6)/max(time.time()-t_quant0,1e-6):.0f} MB/s")
399
+ elif _is_head:
400
+ # Head delta from high-fidelity: Q8_0 once (old behavior, with %32 fallback).
401
+ if fused.shape[-1] % 32 != 0:
402
+ _warn(f"Head last dim {fused.shape[-1]} not Γ—32 β€” Q8_0 needs %32, keeping F16 (larger, compatible).")
403
+ _step(f"Fusing style into {tname} (delta + head β†’ F16 fallback)…")
404
+ q_head, out_qtype = fused.astype(np.float16), Q.F16
405
+ else:
406
+ _step(f"Fusing style into {tname} (delta + head β†’ Q8_0)…")
407
+ t_quant0 = time.time()
408
+ try:
409
+ q_head, out_qtype = quantize(fused, Q.Q8_0), Q.Q8_0
410
+ except Exception as e:
411
+ _fail(f" βœ— Could not quant delta to Q8_0: {e}")
412
+ _note(f"Quantized {fused.nbytes/1e6:.1f} MB β†’ Q8_0 @ {(fused.nbytes/1e6)/max(time.time()-t_quant0,1e-6):.0f} MB/s")
413
+ else:
414
+ # Generic delta: keep target quant (never force Q8_0). Never upscale implicitly.
415
+ _step(f"Fusing style into {tname} (delta + head β†’ {_target_q.name}, kept)…")
416
+ t_quant0 = time.time()
417
+ if _target_q in (Q.F32,):
418
+ q_head, out_qtype = fused.astype(np.float32), Q.F32
419
+ elif _target_q in (Q.F16,):
420
+ q_head, out_qtype = fused.astype(np.float16), Q.F16
421
+ else:
422
+ try:
423
+ _b = getattr(Q, "BF16", None)
424
+ except Exception:
425
+ _b = None
426
+ if _b is not None and _target_q == _b:
427
+ q_head, out_qtype = fused.astype(np.float32), Q.F32
428
+ _warn(" BF16 target: keeping F32 to avoid loss (use --quant bf16 explicitly if needed).")
429
+ out_qtype = Q.F32
430
+ else:
431
+ try:
432
+ q_head, out_qtype = quantize(fused, _target_q), _target_q
433
+ except Exception as e:
434
+ _near = _nearest_encodable(_target_q)
435
+ if _near is not None:
436
+ _warn(f" {_target_q.name} not encodable ({_qerr(e)}); keeping {_near.name} (nearest size) instead of F16.")
437
+ q_head, out_qtype = _quantize_to_Q(fused, _near)
438
+ else:
439
+ _warn(f" Could not quant to {_target_q.name} ({_qerr(e)}), keeping F16.")
440
+ q_head, out_qtype = fused.astype(np.float16), Q.F16
441
+ try:
442
+ _note(f"Fused {fused.nbytes/1e6:.1f} MB β†’ {out_qtype.name} @ {(fused.nbytes/1e6)/max(time.time()-t_quant0,1e-6):.0f} MB/s")
443
+ except Exception:
444
+ pass
445
+ else:
446
+ # Not delta: voice replace (all quants trivial).
447
+ if _req is not None:
448
+ _step(f"Casting onto {os.path.basename(target)} ({_req.name})…")
449
+ fused = oriented
450
+ t_quant0 = time.time()
451
+ try:
452
+ q_head, out_qtype = _quantize_to_Q(fused, _req)
453
+ except NotImplementedError as e:
454
+ _fail(f" βœ— Quant {_req.name} not implemented by gguf lib ({e}).")
455
+ except Exception as e:
456
+ _fail(f" βœ— Could not quant to {_req.name}: {e}")
457
+ _note(f"Quantized {fused.nbytes/1e6:.1f} MB β†’ {_req.name} @ {(fused.nbytes/1e6)/max(time.time()-t_quant0,1e-6):.0f} MB/s")
458
+ elif _is_head:
459
+ # lm_head high-fidelity: quant once to Q8_0 (preserve quality, Q4 saves little).
460
+ if oriented.shape[-1] % 32 != 0:
461
+ _warn(f"Head last dim {oriented.shape[-1]} not Γ—32 β€” Q8_0 needs %32, keeping F16 (larger, compatible).")
462
+ _step(f"Casting onto {os.path.basename(target)} (F16 fallback)…")
463
+ fused = oriented
464
+ q_head, out_qtype = fused.astype(np.float16), Q.F16
465
+ else:
466
+ _step(f"Casting onto {os.path.basename(target)} (Q8_0)…")
467
+ fused = oriented
468
+ t_quant0 = time.time()
469
+ try:
470
+ q_head, out_qtype = quantize(fused, Q.Q8_0), Q.Q8_0
471
+ except Exception as e:
472
+ sys.stdout.write("\n")
473
+ if ctx.VERBOSE:
474
+ _say(f" Details: {e}")
475
+ _fail(" βœ— Could not prepare the voiced model. Try again.")
476
+ _note(f"Quantized {fused.nbytes/1e6:.1f} MB β†’ Q8_0 @ {(fused.nbytes/1e6)/max(time.time()-t_quant0,1e-6):.0f} MB/s")
477
+ else:
478
+ # Generic tensor: keep target quant (fix crash: F32 norms stay F32, never forced Q8_0).
479
+ _tq = _target_q
480
+ try:
481
+ _tqn = _tq.name
482
+ except Exception:
483
+ _tqn = str(_tq)
484
+ try:
485
+ _vqn = vref.dtype
486
+ except Exception:
487
+ _vqn = "?"
488
+ # Mismatch prompt only for safetensors non-head high-fidelity differing from target.
489
+ _need_prompt = (_vqn.upper() in ("F32", "F16", "BF16", "FP32", "FP16") and _tqn.upper() != _vqn.upper())
490
+ _custom_done = False
491
+ if _need_prompt and _quant_override == "keep" and not _noninteractive(args):
492
+ try:
493
+ _say(f" Target '{tname}' is {_tqn}, fetched is {_vqn}.")
494
+ _ans = input(f" Press enter to quantize to {_tqn} or enter custom quant (any like Q8_0/Q4_0/Q6_K/F16/F32/BF16): ").strip()
495
+ except (EOFError, KeyboardInterrupt):
496
+ _say(" ⚠ Cancelled.")
497
+ sys.exit(0)
498
+ _ans_n = _ans.strip().upper().replace("-", "_")
499
+ if _ans.strip() == "" or _ans_n == _tqn.upper():
500
+ pass
501
+ else:
502
+ _ck = _parse_quant_override(_ans)
503
+ _cq = _quant_from_key(_ck) if _ck not in (None, "keep", "target") else None
504
+ if _ck is None or (_cq is None and _ck not in ("keep", "target")):
505
+ _warn(f" Unknown quant '{_ans}', using target {_tqn}.")
506
+ elif _ck == "target" or _ans_n == _tqn.upper():
507
+ pass
508
+ else:
509
+ _warn(f"Forcing {_ans} for generic tensor (extra quant cycle).")
510
+ fused = oriented
511
+ t_quant0 = time.time()
512
+ try:
513
+ q_head, out_qtype = _quantize_to_Q(fused, _cq)
514
+ except NotImplementedError as e:
515
+ _fail(f" βœ— Quant {_cq.name} not implemented ({e}).")
516
+ except Exception as e:
517
+ _fail(f" βœ— Could not quant to {_cq.name}: {e}")
518
+ _note(f"Quantized {fused.nbytes/1e6:.1f} MB β†’ {_cq.name} @ {(fused.nbytes/1e6)/max(time.time()-t_quant0,1e-6):.0f} MB/s")
519
+ _note(f"Fusing done in {time.time()-t_fuse0:.1f}s")
520
+ _custom_done = True
521
+ # keep-target path (default, silent when match, Enter=target when mismatch)
522
+ if not _custom_done:
523
+ _step(f"Casting '{vref.name}' ({_vqn}) onto '{tname}' ({_tqn}, kept)…")
524
+ fused = oriented
525
+ t_quant0 = time.time()
526
+ if _tq in (Q.F32,):
527
+ q_head, out_qtype = fused.astype(np.float32), Q.F32
528
+ elif _tq in (Q.F16,):
529
+ q_head, out_qtype = fused.astype(np.float16), Q.F16
530
+ else:
531
+ try:
532
+ _b = getattr(Q, "BF16", None)
533
+ except Exception:
534
+ _b = None
535
+ if _b is not None and _tq == _b:
536
+ q_head, out_qtype = fused.astype(np.float32), Q.F32
537
+ _warn(" BF16 target: keeping F32 to avoid loss (use --quant bf16 explicitly if needed).")
538
+ out_qtype = Q.F32
539
+ else:
540
+ try:
541
+ q_head, out_qtype = quantize(fused, _tq), _tq
542
+ except Exception as e:
543
+ _near = _nearest_encodable(_tq)
544
+ if _near is not None:
545
+ _warn(f" {_tqn} not encodable ({_qerr(e)}); keeping {_near.name} (nearest size) instead of F16.")
546
+ q_head, out_qtype = _quantize_to_Q(fused, _near)
547
+ else:
548
+ _warn(f" Could not quant to {_tqn} ({_qerr(e)}), keeping F16.")
549
+ q_head, out_qtype = fused.astype(np.float16), Q.F16
550
+ try:
551
+ _note(f"Kept {fused.nbytes/1e6:.1f} MB β†’ {out_qtype.name} @ {(fused.nbytes/1e6)/max(time.time()-t_quant0,1e-6):.0f} MB/s")
552
+ except Exception:
553
+ pass
554
+ try:
555
+ t_fuse0
556
+ except Exception:
557
+ t_fuse0 = time.time()
558
+ _note(f"Fusing done in {time.time()-t_fuse0:.1f}s")
559
+ return q_head, out_qtype
560
+
561
+
562
+ def _gguf_write_repl(target, reader, arch, repl, out_path, args, op, detail=None, _extra_names=()):
563
+ """Single-pass GGUF write. repl maps tensor name -> (array, ggml_type).
564
+ detail (single-tensor callers): (tname, logical, vref, vraw, vqtype, meta) for rich messages."""
565
+ from gguf import GGUFWriter
566
+ from gguf.gguf_writer import GGUFValue
567
+ from gguf.constants import GGMLQuantizationType as Q
568
+ if not arch:
569
+ _warn(" Target architecture unknown β€” labeling output 'llama' (GGUF requires an arch).")
570
+ writer = GGUFWriter(out_path, arch or "llama", use_temp_file=True)
571
+ for field in reader.fields.values():
572
+ if field.name in ("general.architecture",) or field.name.startswith("GGUF."):
573
+ continue
574
+ try:
575
+ val = field.contents()
576
+ except Exception:
577
+ continue
578
+ vtype = field.types[0] if field.types else None
579
+ if vtype is None:
580
+ continue
581
+ subtype = field.types[-1] if len(field.types) > 1 else None
582
+ try:
583
+ writer.kv_data[0][field.name] = GGUFValue(value=val, type=vtype, sub_type=subtype)
584
+ except Exception:
585
+ try:
586
+ writer.add_key_value(field.name, val, vtype, subtype)
587
+ except Exception:
588
+ if ctx.VERBOSE:
589
+ _warn(f"Could not copy field {field.name}")
590
+
591
+ for t in reader.tensors:
592
+ if t.name in repl:
593
+ _qh, _qt = repl[t.name]
594
+ writer.add_tensor(t.name, _qh, raw_dtype=_qt)
595
+ elif t.tensor_type in (Q.F32, Q.F16):
596
+ writer.add_tensor(t.name, t.data)
597
+ else:
598
+ writer.add_tensor(t.name, t.data, raw_dtype=t.tensor_type)
599
+ for name in _extra_names:
600
+ if name in repl and name not in [t.name for t in reader.tensors]:
601
+ _qh, _qt = repl[name]
602
+ writer.add_tensor(name, _qh, raw_dtype=_qt)
603
+
604
+ # writing GGUF is the long wait (often 5-30s for 26GB) β€” show bar with speed, internet or local
605
+ _step(f"Writing {out_path} …")
606
+ # estimate total bytes for progress β€” use data.nbytes
607
+ try:
608
+ total_write = 8192
609
+ for t in reader.tensors:
610
+ if t.name in repl:
611
+ continue
612
+ try:
613
+ total_write += int(t.data.nbytes)
614
+ except Exception as e:
615
+ if ctx.VERBOSE:
616
+ _warn(f"progress estimate for {t.name} failed: {e}")
617
+ try:
618
+ for _qh, _qt in repl.values():
619
+ total_write += int(_qh.nbytes)
620
+ except Exception:
621
+ try:
622
+ for _qh, _qt in repl.values():
623
+ total_write += int(_qh.size * _qh.itemsize)
624
+ except Exception:
625
+ pass
626
+ if ctx.VERBOSE:
627
+ _note(f"Estimated write {total_write/1e6:.1f} MB ({len(repl)} replaced + {len([t for t in reader.tensors if t.name not in repl])} copied)")
628
+ except Exception as e:
629
+ if ctx.VERBOSE:
630
+ _warn(f"progress estimate failed: {e}")
631
+ total_write = 0
632
+ # ensure total_write is at least header + heads for small models, never 0 for progress
633
+ if total_write < 1000:
634
+ try:
635
+ total_write = 8192
636
+ for _qh, _qt in repl.values():
637
+ try:
638
+ total_write += int(_qh.nbytes)
639
+ except Exception:
640
+ pass
641
+ for t in reader.tensors:
642
+ if t.name not in repl:
643
+ try:
644
+ total_write += int(t.data.nbytes)
645
+ except Exception:
646
+ pass
647
+ except Exception:
648
+ pass
649
+ if total_write < 1000:
650
+ total_write = 0
651
+ t_write0 = time.time()
652
+ last_w = [0.0]
653
+ # progress poll for GGUF write (local disk, but user is waiting)
654
+ import threading
655
+ _stop_poll = [False]
656
+ def _poll_gguf():
657
+ # poll output file (or temp file) size
658
+ target_path = getattr(writer, 'fname', out_path)
659
+ # GGUFWriter with use_temp_file=True writes to temp file with suffix
660
+ # fallback to out_path
661
+ poll_path = target_path if os.path.exists(target_path) else out_path
662
+ # try to find temp file if not yet at final path
663
+ while not _stop_poll[0]:
664
+ try:
665
+ cur = 0
666
+ if os.path.exists(poll_path):
667
+ cur = os.path.getsize(poll_path)
668
+ else:
669
+ # try temp file pattern
670
+ import glob
671
+ tmps = glob.glob(str(out_path) + "*")
672
+ for p in tmps:
673
+ if os.path.isfile(p):
674
+ try:
675
+ cur = max(cur, os.path.getsize(p))
676
+ except Exception:
677
+ pass
678
+ elapsed = time.time() - t_write0 or 1e-6
679
+ mbps = cur / elapsed / 1e6
680
+ pct = (cur / total_write * 100) if total_write else 0
681
+ if pct > 100:
682
+ pct = 100
683
+ if total_write:
684
+ msg = f" β†’ writing {pct:3.0f}% Β· {cur/1e6:.1f}/{total_write/1e6:.1f} MB Β· {mbps:.0f} MB/s "
685
+ else:
686
+ msg = f" β†’ writing {cur/1e6:.1f} MB Β· {mbps:.0f} MB/s "
687
+ sys.stdout.write(_transform(msg) + ("\n" if _progress_pref() else "\r"))
688
+ sys.stdout.flush()
689
+ except Exception:
690
+ pass
691
+ time.sleep(0.25)
692
+ _thr = threading.Thread(target=_poll_gguf, daemon=True)
693
+ _thr.start()
694
+ try:
695
+ writer.write_header_to_file()
696
+ writer.write_kv_data_to_file()
697
+ writer.write_tensors_to_file()
698
+ writer.close()
699
+ except Exception as e:
700
+ _stop_poll[0] = True
701
+ try:
702
+ _thr.join(timeout=0.5)
703
+ except Exception:
704
+ pass
705
+ sys.stdout.write("\n")
706
+ if ctx.VERBOSE:
707
+ _say(f" Details: {e}")
708
+ _fail(f" βœ— Could not save the voiced model to {out_path}.")
709
+ _stop_poll[0] = True
710
+ try:
711
+ _thr.join(timeout=0.5)
712
+ except Exception:
713
+ pass
714
+ sys.stdout.write("\n")
715
+ elapsed_w = time.time() - t_write0 or 1e-6
716
+ try:
717
+ final_sz = os.path.getsize(out_path)
718
+ _note(f"Wrote {final_sz/1e6:.1f} MB @ {final_sz/elapsed_w/1e6:.0f} MB/s")
719
+ except Exception:
720
+ pass
721
+
722
+ file_size = os.path.getsize(out_path)
723
+ if detail is None:
724
+ summed = [(n, _qt.name if hasattr(_qt, "name") else str(_qt)) for n, (_qh, _qt) in repl.items()]
725
+ log_operation({"op": op, "target": target, "format": "gguf", "tensors": summed,
726
+ "output": out_path, "status": "ok"})
727
+ _ok(f"Done β†’ {out_path}")
728
+ for n, _qt in summed:
729
+ _say(f" Cast '{n}' β†’ {_qt} ({file_size/1e6:.1f} MB total).")
730
+ _say(f" Load it: llama serve -m {out_path}")
731
+ return
732
+ tname, logical, vref, vraw, vqtype, meta = detail
733
+ _qh0, out_qtype = repl[tname]
734
+ log_operation({"op": op, "target": target, "format": "gguf", "tensor": tname,
735
+ "dtype": out_qtype.name, "shape": list(logical), "output": out_path, "status": "ok"})
736
+ _ok(f"Done β†’ {out_path}")
737
+ is_fallback = 'fallback_to_f16' in locals() and locals().get('fallback_to_f16')
738
+ _is_head_msg = _is_head_name(tname) or _is_head_name(getattr(vref, "name", None))
739
+ _label = "Head" if _is_head_msg else "Tensor"
740
+ if (meta or {}).get("type") == "delta" and is_fallback:
741
+ _say(f" Style fused and head kept as F16 (Q8_0 needs %32, fallback) ({file_size/1e6:.1f} MB).")
742
+ elif (meta or {}).get("type") == "delta":
743
+ # Delta head keeps Q8_0 for heads, keeps target for generic.
744
+ if _is_head_msg and out_qtype == Q.Q8_0:
745
+ _say(f" Style fused and head quantized to Q8_0 ({file_size/1e6:.1f} MB).")
746
+ else:
747
+ _say(f" Style fused into '{tname}' β†’ {out_qtype.name}, kept ({file_size/1e6:.1f} MB).")
748
+ elif is_fallback:
749
+ _say(f" Head kept as F16 (Q8_0 needs last dim %32, fallback) ({file_size/1e6:.1f} MB).")
750
+ elif out_qtype == Q.Q8_0 and _is_head_msg:
751
+ _qo = _parse_quant_override(getattr(args, "quant", None) or "keep")
752
+ _kept_q8 = (vraw is not None and isinstance(vqtype, str) and vqtype == Q.Q8_0.name and _qo in ("keep",))
753
+ if _kept_q8:
754
+ _say(f" Head kept as Q8_0 (from {vqtype}, {file_size/1e6:.1f} MB).")
755
+ else:
756
+ _say(f" Head quantized to Q8_0 ({file_size/1e6:.1f} MB).")
757
+ else:
758
+ _from = vqtype if vraw is not None else getattr(vref, "dtype", "?")
759
+ _say(f" {_label} '{tname}' kept as {out_qtype.name} (from {_from}, {file_size/1e6:.1f} MB).")
760
+ _say(f" Load it: llama serve -m {out_path}")
761
+
762
+
763
+ def _read_gguf_arch(reader):
764
+ """Single arch-reader for GGUF targets. Returns the general.architecture string or None."""
765
+ import numpy as np
766
+ for field in reader.fields.values():
767
+ if field.name == "general.architecture":
768
+ try:
769
+ raw = field.parts[field.data[0]]
770
+ return raw.tobytes().decode() if isinstance(raw, np.ndarray) else str(raw)
771
+ except Exception:
772
+ return None
773
+ return None
774
+
775
+
776
+ def _confirm_arch_mismatch(varch, arch, args):
777
+ """Single mismatch warn (+ confirm) shared by single and pack GGUF casts."""
778
+ if varch and arch and varch.lower() != arch.lower():
779
+ _warn(f"This voice was trained for a {varch} model, but the target is {arch}. It may not sound right.")
780
+ if not _confirm(" Continue anyway? [y/N]: ", args):
781
+ _note("Cancelled.")
782
+ sys.exit(0)
783
+
784
+
785
+ def _cast_gguf_speak(target, reader, arch, out_path, vf32, vref, meta, args, op, vraw=None, vqtype=None):
786
+ """--speak: voice onto output.weight, invented if absent. GGUF only."""
787
+ import numpy as np
788
+ from gguf.constants import GGMLQuantizationType as Q
789
+ from gguf.quants import quantize, quant_shape_to_byte_shape
790
+ if (meta or {}).get("type") == "delta":
791
+ _fail(" βœ— --speak cannot place a delta voice (differences are not weights).")
792
+ names = [t.name for t in reader.tensors]
793
+ ot = next((t for t in reader.tensors if t.name == "output.weight"), None)
794
+ et = next((t for t in reader.tensors if t.name == "token_embd.weight"), None)
795
+ if ot is not None:
796
+ olog = tuple(reversed(tuple(int(x) for x in ot.shape)))
797
+ invented = False
798
+ elif et is not None:
799
+ olog = tuple(reversed(tuple(int(x) for x in et.shape)))
800
+ invented = True
801
+ else:
802
+ _fail(" βœ— Target has no output.weight or token_embd.weight to mirror geometry from.")
803
+ varch = (meta or {}).get("architecture")
804
+ _confirm_arch_mismatch(varch, arch, args)
805
+ tname = "output.weight"
806
+ if vraw is not None:
807
+ _vqtype = Q[vqtype]
808
+ if tuple(vref.shape) != olog:
809
+ _fail(f" βœ— Voice shape {vref.shape} doesn't fit {tname} {olog}.")
810
+ _qo = _parse_quant_override(getattr(args, "quant", None) or "keep")
811
+ if _qo not in ("keep",):
812
+ fitted = _gguf_raw_to_f32(vraw, _vqtype, tuple(int(x) for x in vref.shape))
813
+ q_head, out_qtype = _quantize_to_Q(np.ascontiguousarray(fitted), Q[_qo])
814
+ else:
815
+ _need = 1
816
+ for _d in quant_shape_to_byte_shape(tuple(int(x) for x in vref.shape), _vqtype):
817
+ _need *= _d
818
+ _raw = np.frombuffer(vraw, dtype=np.uint8)
819
+ if _raw.size < _need:
820
+ _fail(f" βœ— Voice data too short for {vqtype} {tuple(vref.shape)}.")
821
+ q_head = np.ascontiguousarray(_raw[:_need].reshape(
822
+ quant_shape_to_byte_shape(tuple(int(x) for x in vref.shape), _vqtype)))
823
+ out_qtype = _vqtype
824
+ else:
825
+ fitted = _orient(vf32, vref.shape, olog)
826
+ if fitted is None:
827
+ _fail(f" βœ— Voice shape {vref.shape} doesn't fit {tname} {olog}.")
828
+ fitted = np.ascontiguousarray(fitted)
829
+ _qo = _parse_quant_override(getattr(args, "quant", None) or "keep")
830
+ if _qo is None:
831
+ _fail(f" βœ— Invalid --quant '{getattr(args, 'quant', None)}'.")
832
+ if _qo not in ("keep",):
833
+ q_head, out_qtype = _quantize_to_Q(fitted, Q[_qo])
834
+ elif fitted.shape[-1] % 32 == 0:
835
+ q_head, out_qtype = quantize(fitted, Q.Q8_0), Q.Q8_0
836
+ else:
837
+ _note(" last dim not x32 - added output kept F16")
838
+ q_head, out_qtype = fitted.astype(np.float16), Q.F16
839
+ if invented:
840
+ _note("speak: voice -> invented output.weight (file untied)")
841
+ else:
842
+ _note("speak: voice -> output.weight")
843
+ _gguf_write_repl(target, reader, arch, {tname: (q_head, out_qtype)}, out_path, args, op,
844
+ detail=(tname, olog, vref, vraw, vqtype, meta),
845
+ _extra_names=(tname,) if invented else ())
846
+
847
+ def _cast_gguf(target, out_path, vf32, vref, meta, args, op, vraw=None, vqtype=None, strict_target=False):
848
+ import numpy as np
849
+ from gguf import GGUFReader
850
+
851
+ reader = GGUFReader(target)
852
+ arch = _read_gguf_arch(reader)
853
+
854
+ if getattr(args, "speak", False):
855
+ _cast_gguf_speak(target, reader, arch, out_path, vf32, vref, meta, args, op, vraw=vraw, vqtype=vqtype)
856
+ return
857
+ target_tensor = None
858
+ if vref.name:
859
+ for t in reader.tensors:
860
+ if t.name == vref.name:
861
+ target_tensor = t
862
+ break
863
+ if target_tensor is None and strict_target:
864
+ _fail(f" βœ— Tensor '{vref.name}' not found in target {target}. Voicepack requires exact match (not mapping to another tensor).")
865
+ if target_tensor is None:
866
+ for preferred in ("output.weight", "token_embd.weight"):
867
+ for t in reader.tensors:
868
+ if t.name == preferred:
869
+ target_tensor = t
870
+ break
871
+ if target_tensor is not None:
872
+ break
873
+ if target_tensor is None:
874
+ _fail(" βœ— Target has no output.weight or token_embd.weight to receive the voice.")
875
+
876
+ tname = target_tensor.name
877
+ logical = tuple(reversed(tuple(int(x) for x in target_tensor.shape)))
878
+
879
+ # arch mismatch warning
880
+ varch = (meta or {}).get("architecture")
881
+ _confirm_arch_mismatch(varch, arch, args)
882
+
883
+ q_head, out_qtype = _gguf_fuse_one(target, target_tensor, tname, logical, vf32, vref, meta, args, op, vraw=vraw, vqtype=vqtype)
884
+ _gguf_write_repl(target, reader, arch, {tname: (q_head, out_qtype)}, out_path, args, op,
885
+ detail=(tname, logical, vref, vraw, vqtype, meta))
886
+
887
+
888
+ # GGUF (llama.cpp) <-> safetensors (HF) tensor-name aliases, matched by
889
+ # (block index, role). Exact name match always wins; aliases only fire on a
890
+ # miss, and only when exactly one candidate matches. Anything else fails loud
891
+ # at the call site β€” never a silent mapping to another tensor.
892
+ _ROLE_ALIASES = (
893
+ ("embed_tokens.weight", "token_embd.weight"),
894
+ ("word_embeddings.weight", "token_embd.weight"),
895
+ ("wte.weight", "token_embd.weight"),
896
+ ("output_layer.weight", "output.weight"),
897
+ ("lm_head.weight", "output.weight"),
898
+ ("final_layernorm.weight", "output_norm.weight"),
899
+ ("self_attention.dense.weight", "attn_output.weight"),
900
+ ("mlp.dense_4h_to_h.weight", "ffn_down.weight"),
901
+ ("model.norm.weight", "output_norm.weight"),
902
+ ("ln_f.weight", "output_norm.weight"),
903
+ ("input_layernorm.weight", "attn_norm.weight"),
904
+ ("self_attn.q_proj.weight", "attn_q.weight"),
905
+ ("self_attn.k_proj.weight", "attn_k.weight"),
906
+ ("self_attn.v_proj.weight", "attn_v.weight"),
907
+ ("self_attn.o_proj.weight", "attn_o.weight"),
908
+ ("self_attn.o_proj.weight", "attn_output.weight"),
909
+ ("post_attention_layernorm.weight", "post_attention_norm.weight"),
910
+ ("mlp.down_proj.weight", "ffn_down.weight"),
911
+ ("experts.down_proj", "ffn_down_exps.weight"),
912
+ ("mlp.up_proj.weight", "ffn_up.weight"),
913
+ )
914
+
915
+
916
+ def _block_index(name):
917
+ import re
918
+ m = re.search(r"(?<![A-Za-z0-9_])(?:layers|layer|blk|blks|blocks|h)\.(\d+)\b", name)
919
+ if m:
920
+ return int(m.group(1))
921
+ m = re.search(r"\.(\d+)\.", name)
922
+ return int(m.group(1)) if m else None
923
+
924
+
925
+ def _pack_aliases(vname, target_names):
926
+ """All alias matches for vname in target_names (either naming direction).
927
+ Exact match is handled by callers first; this only fires on a miss."""
928
+ hits = []
929
+ for st_suf, gg_suf in _ROLE_ALIASES:
930
+ for t in target_names:
931
+ st_v = vname.endswith(st_suf)
932
+ gg_v = vname.endswith(gg_suf)
933
+ st_t = t.endswith(st_suf)
934
+ gg_t = t.endswith(gg_suf)
935
+ if (st_v and gg_t) or (gg_v and st_t):
936
+ if _block_index(vname) == _block_index(t) and t not in hits:
937
+ hits.append(t)
938
+ return hits
939
+
940
+
941
+ def _role_key(name):
942
+ """(block_index, trailing_role) canonical key for same-format matching.
943
+ Trailing role is everything after the block number (e.g.
944
+ 'self_attn.o_proj.weight'); unblocked names fall back to their last two
945
+ components (e.g. 'embed_tokens.weight'). The .weight/.bias suffix stays
946
+ in the key so a bias never matches a weight. The block pattern is
947
+ left-guarded so it never fires inside a word (e.g. 'torch.0')."""
948
+ import re
949
+ m = re.search(r"(?<![A-Za-z0-9_])(?:layers|layer|blk|blks|blocks|h)\.(\d+)\b", name)
950
+ if m:
951
+ return (int(m.group(1)), name[m.end():].strip("."))
952
+ m = re.search(r"\.(\d+)\.", name)
953
+ if m:
954
+ return (int(m.group(1)), name[m.end():].strip("."))
955
+ parts = name.split(".")
956
+ return (None, ".".join(parts[-2:]))
957
+
958
+
959
+ def _same_role(vname, target_names):
960
+ """Targets sharing vname's (block, trailing role). Cross-format pairs
961
+ (ST suffix vs GGUF suffix) never collide here β€” _ROLE_ALIASES owns those.
962
+ Only fires on a miss; callers gate on elem-count and uniqueness."""
963
+ want = _role_key(vname)
964
+ return [t for t in target_names if _role_key(t) == want]
965
+
966
+
967
+ def _cast_gguf_pack(target, out_path, items, args, op):
968
+ """Single-pass pack cast onto GGUF. items: [(vf32, vref, meta, vraw, vqtype)]."""
969
+ import numpy as np
970
+ from gguf import GGUFReader
971
+ reader = GGUFReader(target)
972
+ arch = _read_gguf_arch(reader)
973
+ varch = (items[0][2] or {}).get("architecture") if items else None
974
+ _confirm_arch_mismatch(varch, arch, args)
975
+ _names = [t.name for t in reader.tensors]
976
+ _counts = {}
977
+ for t in reader.tensors:
978
+ try:
979
+ n = 1
980
+ for d in t.shape:
981
+ n *= int(d)
982
+ _counts[t.name] = n
983
+ except Exception:
984
+ pass
985
+ _map = _resolve_pack_targets([(ref.name, ref.shape) for (_, ref, _, _, _) in items],
986
+ _names, target, _counts)
987
+ repl = {}
988
+ for vf32, vref, meta, vraw, vqtype in items:
989
+ target_tensor = next(t for t in reader.tensors if t.name == _map[vref.name])
990
+ tname = target_tensor.name
991
+ logical = tuple(reversed(tuple(int(x) for x in target_tensor.shape)))
992
+ q_head, out_qtype = _gguf_fuse_one(target, target_tensor, tname, logical, vf32, vref, meta, args, op, vraw=vraw, vqtype=vqtype)
993
+ repl[tname] = (q_head, out_qtype)
994
+ _gguf_write_repl(target, reader, arch, repl, out_path, args, op)
995
+
996
+
997
+ def _resolve_pack_targets(pairs, target_names, target, counts=None):
998
+ """Map every voice tensor to a target tensor (exact, else one alias,
999
+ else one same-role same-format match).
1000
+ Resolves ALL up front so a pack fails once with every miss β€” never
1001
+ fix-forward one at a time. Same-size targets are offered as hints only.
1002
+ pairs: [(voice_name, voice_shape)]; counts: {target_name: elem_count}.
1003
+ Voice shapes are logical-as-is (GgufSource.ref already reverses);
1004
+ only elem-counts are compared, so transposes pass and nothing double-flips."""
1005
+ def _want(vshape):
1006
+ if vshape is None:
1007
+ return None
1008
+ n = 1
1009
+ for d in vshape:
1010
+ n *= int(d)
1011
+ return n
1012
+ mapping, misses = {}, []
1013
+ for vname, vshape in pairs:
1014
+ if vname in target_names:
1015
+ mapping[vname] = vname
1016
+ continue
1017
+ cands = _pack_aliases(vname, target_names)
1018
+ if len(cands) == 1:
1019
+ mapping[vname] = cands[0]
1020
+ _note(f" Aliased '{vname}' β†’ '{cands[0]}' ({target} naming).")
1021
+ elif len(cands) > 1:
1022
+ misses.append(f" '{vname}' is ambiguous: {', '.join(cands)}")
1023
+ else:
1024
+ roles = [t for t in _same_role(vname, target_names)
1025
+ if _want(vshape) is None or counts is None
1026
+ or counts.get(t, _want(vshape)) == _want(vshape)]
1027
+ if len(roles) == 1:
1028
+ mapping[vname] = roles[0]
1029
+ _note(f" Aliased '{vname}' β†’ '{roles[0]}' (same role).")
1030
+ continue
1031
+ if len(roles) > 1:
1032
+ misses.append(f" '{vname}' is ambiguous: {', '.join(roles)}")
1033
+ continue
1034
+ hint = ""
1035
+ if counts is not None and vshape is not None:
1036
+ same = [t for t in target_names if counts.get(t) == _want(vshape)][:3]
1037
+ if same:
1038
+ hint = f" (same size: {', '.join(same)})"
1039
+ misses.append(f" '{vname}' not found{hint}")
1040
+ if misses:
1041
+ _fail(f" βœ— {len(misses)} tensor(s) have no match in target {target}:\n"
1042
+ + "\n".join(misses)
1043
+ + "\n Voicepack requires exact match (not mapping to another tensor).")
1044
+ return mapping
1045
+
1046
+
1047
+ def _cast_pack(target, src_path, out_path, args, op, excludes=(), meta=None):
1048
+ """Cast every tensor in a pack file onto target (minus excludes).
1049
+ GGUF and safetensors targets each get one write pass (repl-dict fused first).
1050
+ Delta marker on the pack is detected once and threaded into every tensor."""
1051
+ src = open_source(str(src_path))
1052
+ all_names = [n for n in src.names() if n != "delta.voice.marker"]
1053
+ for e in excludes:
1054
+ if e not in all_names and not any(e in n for n in all_names):
1055
+ _warn(f" --exclude '{e}' not in pack (has {len(all_names)} tensors). Ignoring.")
1056
+
1057
+ resolved_ex = set()
1058
+ for e in excludes:
1059
+ if e in all_names:
1060
+ resolved_ex.add(e)
1061
+ else:
1062
+ _m = [n for n in all_names if e in n]
1063
+ if len(_m) == 1:
1064
+ resolved_ex.add(_m[0])
1065
+ to_cast = [n for n in all_names if n not in resolved_ex]
1066
+ if not to_cast:
1067
+ _fail(" βœ— --exclude removes all tensors. Nothing to cast.")
1068
+ _step(f"Casting {len(to_cast)} tensors from {Path(src_path).name} onto {Path(target).name}…")
1069
+ if resolved_ex:
1070
+ _note(f"Excluding {len(resolved_ex)}: {', '.join(sorted(resolved_ex))}")
1071
+ is_delta_file = str(src_path).endswith(".safetensors") and _is_delta_safetensors(str(src_path))
1072
+ if is_delta_file:
1073
+ _note("Detected delta voice β€” fusing style (delta + head)…")
1074
+ if meta is None and is_delta_file:
1075
+ meta = {"type": "delta", "source": str(src_path)}
1076
+ items = []
1077
+ for i, tname in enumerate(to_cast):
1078
+ f32, ref, raw, qtype = _load_tensor(src_path, tname, _src=src)
1079
+ _step(f"[{i+1}/{len(to_cast)}] {tname} ({ref.dtype} {tuple(ref.shape)})…")
1080
+ items.append((f32, ref, meta, raw, qtype))
1081
+ if _is_gguf_file(target):
1082
+ _cast_gguf_pack(target, out_path, items, args, op)
1083
+ return
1084
+ _header, _tensors = read_safetensors(target)
1085
+ _st_names = list(_tensors.keys())
1086
+ _st_counts = {}
1087
+ for _n, _info in _tensors.items():
1088
+ try:
1089
+ _c = 1
1090
+ for _d in _info["shape"]:
1091
+ _c *= int(_d)
1092
+ _st_counts[_n] = _c
1093
+ except Exception:
1094
+ pass
1095
+ _st_map = _resolve_pack_targets([(ref.name, ref.shape) for (_, ref, _, _, _) in items],
1096
+ _st_names, target, _st_counts)
1097
+ repl = {}
1098
+ for f32, ref, m, raw, qtype in items:
1099
+ tname, data, tdtype, tshape = _cast_st_prepare(target, _tensors, f32, ref, m, args,
1100
+ vraw=raw, vqtype=qtype, strict_target=True,
1101
+ target_name=_st_map[ref.name])
1102
+ repl[tname] = (data, tdtype, tshape)
1103
+ _step(f"Replacing {len(repl)} tensors in {Path(target).name}…")
1104
+ _safetensors_write_repl(target, repl, out_path)
1105
+ file_size = os.path.getsize(out_path)
1106
+ log_operation({"op": op, "target": target, "format": "safetensors",
1107
+ "tensors": [(n, d) for n, (_b, d, _s) in repl.items()],
1108
+ "output": out_path, "status": "ok"})
1109
+ _ok(f"Done β†’ {out_path}")
1110
+ for n, (_b, d, _s) in repl.items():
1111
+ _say(f" Tensor '{n}' replaced at {d} ({file_size/1e6:.1f} MB total).")
1112
+
1113
+
1114
+ def _parse_tensor_list(args):
1115
+ # Tensor positionals (get/cast tensors...), with comma-split for shell convenience.
1116
+ raw = getattr(args, "tensors", None) or []
1117
+ if isinstance(raw, str):
1118
+ raw = [raw]
1119
+ # Split comma-separated entries, strip, drop empties.
1120
+ out = []
1121
+ for r in raw:
1122
+ if r is None:
1123
+ continue
1124
+ for part in str(r).split(","):
1125
+ part = part.strip()
1126
+ if part:
1127
+ out.append(part)
1128
+ return out
1129
+
1130
+
1131
+ def _parse_exclude_list(args):
1132
+ raw = getattr(args, "excludes", None) or getattr(args, "exclude", None) or []
1133
+ if isinstance(raw, str):
1134
+ raw = [raw]
1135
+ out = []
1136
+ for r in (raw or []):
1137
+ for part in str(r).split(","):
1138
+ part = part.strip()
1139
+ if part:
1140
+ out.append(part)
1141
+ return out
1142
+
1143
+
1144
+ def _resolve_one_tensor(req, names):
1145
+ # Exact match first, then single substring match, else fail. Returns resolved name.
1146
+ if req in names:
1147
+ return req
1148
+ matches = [n for n in names if req and req in n]
1149
+ if len(matches) == 1:
1150
+ return matches[0]
1151
+ if len(matches) > 1:
1152
+ _fail(f" βœ— '{req}' is ambiguous: {', '.join(matches[:8])}…")
1153
+ _fail(f" βœ— Tensor '{req}' not found. Try one of: {', '.join(names[:10])}")
1154
+
1155
+
1156
+ def _tensor_src_meta(src_path):
1157
+ """File-level delta detection for the tensor path (the marker lives in the file; no voice.json)."""
1158
+ if str(src_path).endswith(".safetensors"):
1159
+ try:
1160
+ if _is_delta_safetensors(str(src_path)):
1161
+ return {"type": "delta", "source": str(src_path)}
1162
+ except Exception:
1163
+ pass
1164
+ return None
1165
+
1166
+
1167
+ def _load_tensor(path, name=None, _src=None):
1168
+ src = _src if _src is not None else open_source(str(path))
1169
+ names = src.names()
1170
+ if not names:
1171
+ _fail(f" βœ— No tensors found in {path}.")
1172
+ if name is None:
1173
+ name = "output.weight" if "output.weight" in names else names[0]
1174
+
1175
+ if name not in names:
1176
+ matches = [n for n in names if name in n]
1177
+ if len(matches) == 1:
1178
+ name = matches[0]
1179
+ else:
1180
+ _fail(f" βœ— Tensor '{name}' not found in {path}.")
1181
+ ref = src.ref(name)
1182
+ if src.kind == "gguf" and _is_ggml_quant(ref.dtype):
1183
+ return None, ref, src.read_raw(name), ref.dtype
1184
+ return src.read_f32(name), ref, None, None
1185
+
1186
+
vlib/ctx.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared constants and globals: single definition for paths, version, verbosity."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ VOICES_DIR = Path.home() / ".voices"
7
+
8
+
9
+ HF_TOKEN_PATH = VOICES_DIR / ".hf_token"
10
+
11
+
12
+ OPERATIONS_LOG = VOICES_DIR / "operations.log"
13
+
14
+
15
+ REPORTS_JSONL = VOICES_DIR / "reports.jsonl"
16
+
17
+
18
+ REPORTS_OUTBOX = VOICES_DIR / "outbox"
19
+
20
+
21
+ REPORTS_DATASET_ID = "Wiself/voice-reports"
22
+
23
+
24
+ REPORTS_REMOTE_PREFIX = "data/reports-"
25
+
26
+
27
+ VERSION = "3.0"
28
+
29
+
30
+ DTYPE_BYTES = {"F32": 4, "F16": 2, "BF16": 2}
31
+
32
+
33
+ VERBOSE = False
vlib/delta.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Delta computation (voice minus base) and base resolution."""
2
+
3
+ from pathlib import Path
4
+ import json
5
+ import os
6
+ import struct
7
+ import sys
8
+ import time
9
+ from vlib.ui import _fail, _noninteractive, _progress_pref, _say, _transform, _warn
10
+ from vlib.net import _fmt_eta
11
+ from vlib.tensors import _is_delta_safetensors, _is_ggml_quant, decode_to_f32, dtype_bytes, read_safetensors
12
+ from vlib.sources import _detect_base, _normalize_hf_repo, _public_source, open_source
13
+ from vlib.fetch import _resolve_output_name, _source_arch
14
+ from vlib.registry import _assert_not_symlink, _iter_voices, _read_meta
15
+
16
+
17
+ def _choose_voice(args):
18
+ voices = []
19
+ for d in _iter_voices():
20
+ meta = _read_meta(d / "voice.json")
21
+ if meta:
22
+ voices.append((d.name, meta))
23
+ if not voices:
24
+ _fail(" βœ— No voices stored yet. Use 'voice get <model>' to save one.")
25
+ _say(" Available voices:")
26
+ for i, (dirname, meta) in enumerate(voices, 1):
27
+ _say(f" {i}. {meta.get('name', dirname)} β€” {meta.get('source_hf_model', meta.get('source', 'β€”'))}")
28
+ try:
29
+ choice = input(f" Choose voice [1-{len(voices)}]: ").strip()
30
+ except (EOFError, KeyboardInterrupt):
31
+ _say(" ⚠ Cancelled.")
32
+ sys.exit(0)
33
+ if not choice.isdigit() or not (1 <= int(choice) <= len(voices)):
34
+ _fail(" βœ— Invalid choice.")
35
+ return voices[int(choice) - 1][1].get("name", voices[int(choice) - 1][0])
36
+
37
+
38
+ def _resolve_base(args, source_hf):
39
+ base_arg = getattr(args, "base", None)
40
+ if base_arg:
41
+ # allow local file/dir path as base (for manual delta)
42
+ p = Path(base_arg).expanduser()
43
+ if p.exists():
44
+ return str(p.resolve())
45
+ norm = _normalize_hf_repo(base_arg)
46
+ if norm is None:
47
+ _fail(f" βœ— Invalid HF repo '{base_arg}'. Use org/model or a local path.")
48
+ return norm
49
+ if source_hf not in ("β€”", "", None) and "/" in source_hf:
50
+ suggested, is_multi, count, _raw = _detect_base(source_hf)
51
+ else:
52
+ suggested, is_multi, count, _raw = None, False, 0, []
53
+ if suggested is None:
54
+ if _noninteractive(args):
55
+ _fail(" βœ— Could not detect the base model. Use --base <org/model> --yes")
56
+ try:
57
+ entered = input(" Enter base HF repo (org/model) or local path: ").strip()
58
+ except (EOFError, KeyboardInterrupt):
59
+ _say(" ⚠ Cancelled.")
60
+ sys.exit(0)
61
+ if not entered:
62
+ _fail(" βœ— Invalid base repo.")
63
+ p = Path(entered).expanduser()
64
+ if p.exists():
65
+ return str(p.resolve())
66
+ norm = _normalize_hf_repo(entered)
67
+ if norm is None:
68
+ _fail(" βœ— Invalid base repo.")
69
+ return norm
70
+ if is_multi:
71
+ _say(f" Base: {suggested} ({count} candidates)")
72
+ else:
73
+ _say(f" Base detected: {suggested}")
74
+ if _noninteractive(args):
75
+ return suggested
76
+ try:
77
+ entered = input(f" Press enter to use {suggested}, or type another repo/path: ").strip()
78
+ except (EOFError, KeyboardInterrupt):
79
+ _say(" ⚠ Cancelled.")
80
+ sys.exit(0)
81
+ if not entered:
82
+ return suggested
83
+ p = Path(entered).expanduser()
84
+ if p.exists():
85
+ return str(p.resolve())
86
+ norm = _normalize_hf_repo(entered)
87
+ return norm if norm else suggested
88
+
89
+
90
+ def _delta_gate(args, warn_lines, phrase, fail_hint):
91
+ """Confirm niche delta-on-delta cases. Fails fast when non-interactive."""
92
+ for i, line in enumerate(warn_lines):
93
+ (_warn if i == 0 else _say)(line)
94
+ if _noninteractive(args):
95
+ _fail(f" βœ— {fail_hint} (re-run interactively and type {phrase} to proceed).")
96
+ try:
97
+ ans = input(f" Type {phrase} to continue, Enter to cancel: ").strip()
98
+ except (EOFError, KeyboardInterrupt):
99
+ _say(" Cancelled."); sys.exit(0)
100
+ if ans.strip().lower() != phrase.lower():
101
+ _say(" Cancelled."); sys.exit(0)
102
+
103
+
104
+ def _check_delta_gate(args, voice_path, base_path):
105
+ """Single gate-checker for delta-on-delta niche cases. Computes the
106
+ is_voice/is_base flags internally (GGUF has no marker, so non-safetensors
107
+ sides stay False β€” matches both the guarded GGUF branch and the
108
+ safetensors branch, where both sides are .safetensors)."""
109
+ is_voice_delta = _is_delta_safetensors(str(voice_path)) if str(voice_path).endswith(".safetensors") else False
110
+ is_base_delta = _is_delta_safetensors(str(base_path)) if str(base_path).endswith(".safetensors") else False
111
+ if not is_voice_delta and is_base_delta:
112
+ _delta_gate(args, [" The base you gave is a delta-voice (has delta.voice.marker).",
113
+ " Deltas are meant to be applied, not used as a base.",
114
+ " normal βˆ’ delta = 2*normal βˆ’ style β€” nonsense, not a style."],
115
+ "UNDO THE UNDO", "delta-on-delta needs explicit confirmation")
116
+ elif is_voice_delta and not is_base_delta:
117
+ _delta_gate(args, [" The voice you gave is already a delta-voice.",
118
+ " Delta-on-normal is usually just style βˆ’ base. You are doing delta βˆ’ normal = style βˆ’ 2*normal."],
119
+ "DOUBLE DIP", "delta-on-delta needs explicit confirmation")
120
+ elif is_voice_delta and is_base_delta:
121
+ _delta_gate(args, [" ✨ Congratulations β€” you have discovered a hidden feature",
122
+ " The voice and base are both delta-voices.",
123
+ " delta βˆ’ delta = style difference of style differences."],
124
+ "BIG BRAIN", "delta-on-delta needs explicit confirmation")
125
+
126
+
127
+ def _compute_delta(voice_path, base_path, delta_folder, delta_name, args=None):
128
+ # Fast path: both safetensors (no dequant, chunked, efficient for 1.4GB).
129
+ # Slow path: either side GGUF quant -> dequant once to F32 (unavoidable), direct numpy (efficient for <50MB tests).
130
+ _v_gguf = _b_gguf = False
131
+ # Chunk seam resolves through the voice facade (not vlib.tensors): the suite
132
+ # patches voice._DELTA_CHUNK_ELEMS / voice._gguf_raw_slice_to_f32, so the
133
+ # slow path must honor the facade namespace. Local import: voice imports us.
134
+ import voice as _facade
135
+ try:
136
+ v_hdr, v_tensors = read_safetensors(str(voice_path))
137
+ except Exception:
138
+ _v_gguf = True
139
+ v_hdr = v_tensors = None
140
+ try:
141
+ b_hdr, b_tensors = read_safetensors(str(base_path))
142
+ except Exception:
143
+ _b_gguf = True
144
+ b_hdr = b_tensors = None
145
+ if _v_gguf or _b_gguf:
146
+ import numpy as np
147
+ # Resolve tensor names via open_source (handles GGUF + safetensors uniformly).
148
+ v_src = open_source(str(voice_path))
149
+ b_src = open_source(str(base_path))
150
+ v_names = [n for n in v_src.names() if n != "delta.voice.marker"]
151
+ b_names = [n for n in b_src.names() if n != "delta.voice.marker"]
152
+ if not v_names or not b_names:
153
+ _fail(" βœ— No tensors found for delta.")
154
+ # Match by exact name: every voice tensor needs a same-named base tensor.
155
+ # Output head first for deterministic order, then voice order.
156
+ try:
157
+ arch, cfg = _source_arch(v_src)
158
+ _cand = _resolve_output_name(v_src, cfg, v_names)
159
+ except Exception:
160
+ _cand = None
161
+ ordered = ([_cand] if _cand in v_names else []) + [n for n in v_names if n != _cand]
162
+ missing = [n for n in ordered if n not in b_names]
163
+ if missing:
164
+ shown = ", ".join(missing[:5]) + ("…" if len(missing) > 5 else "")
165
+ _fail(f" βœ— {len(missing)} voice tensor(s) have no match in base: {shown}. Delta needs same names on both sides.")
166
+ pairs = [(n, n) for n in ordered]
167
+ _check_delta_gate(args, voice_path, base_path)
168
+ _assert_not_symlink(delta_folder)
169
+ delta_folder.mkdir(parents=True, exist_ok=True)
170
+ try:
171
+ os.chmod(delta_folder, 0o700)
172
+ except Exception:
173
+ pass
174
+ out_path = delta_folder / "voice.safetensors"
175
+ tmp_out = delta_folder / "voice.safetensors.tmp"
176
+ # Header first (offsets need shapes only), then stream one pair at a
177
+ # time so packs never hold every tensor in RAM at once.
178
+ header, off = {}, 0
179
+ for vname, bname in pairs:
180
+ vref, bref = v_src.ref(vname), b_src.ref(bname)
181
+ if tuple(vref.shape) != tuple(bref.shape):
182
+ _fail(f" βœ— Voice tensor '{vname}' shape {tuple(vref.shape)} doesn't fit base {tuple(bref.shape)}.")
183
+ nbytes = 2
184
+ for d in vref.shape:
185
+ nbytes *= int(d)
186
+ header[vname] = {"dtype": "F16", "shape": [int(x) for x in vref.shape], "data_offsets": [off, off + nbytes]}
187
+ off += nbytes
188
+ header["delta.voice.marker"] = {"dtype": "U8", "shape": [1], "data_offsets": [off, off + 1]}
189
+ header["__metadata__"] = {"type": "delta", "base": _public_source(str(base_path)), "voice": _public_source(str(voice_path))}
190
+ hj = json.dumps(header).encode("utf-8")
191
+ with open(tmp_out, "wb") as out:
192
+ out.write(struct.pack("<Q", len(hj)))
193
+ out.write(hj)
194
+ for vname, bname in pairs:
195
+ vref, bref = v_src.ref(vname), b_src.ref(bname)
196
+ _warn(f" Dequanting {vname} ({vref.dtype}) and base ({bref.dtype}) once to F32 for delta math (unavoidable for GGUF quant).")
197
+ elems = 1
198
+ for d in vref.shape:
199
+ elems *= int(d)
200
+ # Within a pair, decode/dequant in _DELTA_CHUNK_ELEMS slices so peak
201
+ # stays O(chunk): plain sides stream raw slices; quant sides stream
202
+ # block-aligned raw slices (GGML_QUANT_SIZES units decode independently).
203
+ from gguf.constants import GGMLQuantizationType as _Q
204
+ from gguf.constants import GGML_QUANT_SIZES as _QS
205
+ v_q = _is_ggml_quant(vref.dtype)
206
+ b_q = _is_ggml_quant(bref.dtype)
207
+ v_qt = _Q[vref.dtype] if v_q else None
208
+ b_qt = _Q[bref.dtype] if b_q else None
209
+ v_bpe = 1 if v_q else dtype_bytes(vref.dtype)
210
+ b_bpe = 1 if b_q else dtype_bytes(bref.dtype)
211
+
212
+ def _quant_slice(src, name, qt, at, n):
213
+ bs, ts = _QS[qt]
214
+ b0 = at // bs
215
+ b1 = (at + n + bs - 1) // bs
216
+ seg = src.read_raw_slice(name, b0 * ts, (b1 - b0) * ts)
217
+ return _facade._gguf_raw_slice_to_f32(seg, qt, at - b0 * bs, n)
218
+
219
+ done = 0
220
+ while done < elems:
221
+ cur = min(_facade._DELTA_CHUNK_ELEMS, elems - done)
222
+ if v_q:
223
+ v_f32 = _quant_slice(v_src, vname, v_qt, done, cur)
224
+ else:
225
+ v_f32 = decode_to_f32(v_src.read_raw_slice(vname, done * v_bpe, cur * v_bpe),
226
+ vref.dtype, (cur,))
227
+ if b_q:
228
+ b_f32 = _quant_slice(b_src, bname, b_qt, done, cur)
229
+ else:
230
+ b_f32 = decode_to_f32(b_src.read_raw_slice(bname, done * b_bpe, cur * b_bpe),
231
+ bref.dtype, (cur,))
232
+ out.write((v_f32.astype(np.float32) - b_f32.astype(np.float32)).astype("float16").tobytes())
233
+ done += cur
234
+ out.write(b"\x01")
235
+ out.flush()
236
+ try:
237
+ os.fsync(out.fileno())
238
+ except Exception:
239
+ pass
240
+ tmp_out.replace(out_path)
241
+ return out_path
242
+ # Original chunked safetensors-safetensors path below (no dequant beyond F32 math per chunk).
243
+ # Match by exact name: every voice tensor needs a same-named base tensor.
244
+ v_keys = [k for k in v_tensors.keys() if k != "delta.voice.marker"]
245
+ if not v_keys:
246
+ _fail(" βœ— No tensors found for delta.")
247
+ missing = [k for k in v_keys if k not in b_tensors]
248
+ if missing:
249
+ shown = ", ".join(missing[:5]) + ("…" if len(missing) > 5 else "")
250
+ _fail(f" βœ— {len(missing)} voice tensor(s) have no match in base: {shown}. Delta needs same names on both sides.")
251
+ pairs = [(k, k) for k in v_keys]
252
+ _check_delta_gate(args, voice_path, base_path)
253
+ with open(voice_path, "rb") as f:
254
+ v_hs = struct.unpack("<Q", f.read(8))[0]
255
+ with open(base_path, "rb") as f:
256
+ b_hs = struct.unpack("<Q", f.read(8))[0]
257
+ specs = []
258
+ header, off = {}, 0
259
+ for vname, bname in pairs:
260
+ vinfo, binfo = v_tensors[vname], b_tensors[bname]
261
+ vshape, bshape = tuple(vinfo["shape"]), tuple(binfo["shape"])
262
+ if vshape != bshape:
263
+ _fail(f" βœ— Voice tensor '{vname}' shape {vshape} doesn't fit base {bshape}.")
264
+ elems = 1
265
+ for d in vshape:
266
+ elems *= int(d)
267
+ nbytes = elems * 2
268
+ header[vname] = {"dtype": "F16", "shape": list(vshape), "data_offsets": [off, off + nbytes]}
269
+ off += nbytes
270
+ specs.append({"name": vname, "vinfo": vinfo, "binfo": binfo,
271
+ "v_start": 8 + v_hs + vinfo["data_offsets"][0],
272
+ "b_start": 8 + b_hs + binfo["data_offsets"][0],
273
+ "v_bpe": dtype_bytes(vinfo["dtype"]), "b_bpe": dtype_bytes(binfo["dtype"]),
274
+ "elems": elems})
275
+ total_elems = off // 2
276
+ marker_name = "delta.voice.marker"
277
+ header[marker_name] = {"dtype": "U8", "shape": [1], "data_offsets": [off, off + 1]}
278
+ header["__metadata__"] = {"type": "delta", "base": _public_source(str(base_path)), "voice": _public_source(str(voice_path))}
279
+ hj = json.dumps(header).encode("utf-8")
280
+ _assert_not_symlink(delta_folder)
281
+ delta_folder.mkdir(parents=True, exist_ok=True)
282
+ try:
283
+ os.chmod(delta_folder, 0o700)
284
+ except Exception:
285
+ pass
286
+ out_path = delta_folder / "voice.safetensors"
287
+ tmp_out = delta_folder / "voice.safetensors.tmp"
288
+ chunk = 1 << 20
289
+ total_bytes = total_elems * 2
290
+ t0 = time.time()
291
+ last = [0.0]
292
+ def _delta_progress(force=False):
293
+ now = time.time()
294
+ if not force and now - last[0] < 0.25 and processed < total_elems:
295
+ return
296
+ last[0] = now
297
+ elapsed = now - t0 or 1e-6
298
+ mbps = (processed * 2) / elapsed / 1e6
299
+ pct = processed / total_elems * 100 if total_elems else 100
300
+ remain = total_bytes - processed * 2
301
+ eta = _fmt_eta(remain, mbps)
302
+ msg = f" β†’ crafting delta {pct:3.0f}% Β· {processed*2/1e6:.0f}/{total_bytes/1e6:.0f} MB Β· {mbps:.1f} MB/s Β· {eta} "
303
+ sys.stdout.write(_transform(msg) + ("\n" if _progress_pref() else "\r"))
304
+ sys.stdout.flush()
305
+ with open(voice_path, "rb") as fv, open(base_path, "rb") as fb, open(tmp_out, "wb") as out:
306
+ out.write(struct.pack("<Q", len(hj)))
307
+ out.write(hj)
308
+ processed = 0
309
+ _delta_progress(force=True)
310
+ for spec in specs:
311
+ fv.seek(spec["v_start"])
312
+ fb.seek(spec["b_start"])
313
+ vinfo, binfo = spec["vinfo"], spec["binfo"]
314
+ v_bpe, b_bpe = spec["v_bpe"], spec["b_bpe"]
315
+ done = 0
316
+ while done < spec["elems"]:
317
+ cur = min(chunk, spec["elems"] - done)
318
+ v_raw = fv.read(cur * v_bpe)
319
+ b_raw = fb.read(cur * b_bpe)
320
+ if len(v_raw) != cur * v_bpe or len(b_raw) != cur * b_bpe:
321
+ sys.stdout.write("\n")
322
+ _fail(f" βœ— Short read during delta compute ({spec['name']}).")
323
+ v_f32 = decode_to_f32(v_raw, vinfo["dtype"], (cur,))
324
+ b_f32 = decode_to_f32(b_raw, binfo["dtype"], (cur,))
325
+ delta_f16 = (v_f32 - b_f32).astype("float16")
326
+ out.write(delta_f16.tobytes())
327
+ done += cur
328
+ processed += cur
329
+ _delta_progress()
330
+ _delta_progress(force=True)
331
+ sys.stdout.write("\n")
332
+ # marker byte for auto-detect (delta.voice.marker)
333
+ out.write(b"\x01")
334
+ out.flush()
335
+ try:
336
+ os.fsync(out.fileno())
337
+ except Exception:
338
+ pass
339
+ tmp_out.replace(out_path)
340
+ return out_path
vlib/fetch.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fetching tensors from sources into local bytes."""
2
+
3
+ from pathlib import Path
4
+ import json
5
+ import os
6
+ from vlib import ctx
7
+ from vlib.ui import _fail, _now_iso, _step, _warn
8
+ from vlib.net import _cleanup_tmp, _safe_id, download_range
9
+ from vlib.tensors import _is_ggml_quant, _is_quant_fetched, _write_safetensors_streaming, _write_single_tensor_gguf, encode_from_f32, write_safetensors
10
+ from vlib.sources import _hf_config, _public_source, detect_output_tensor, largest_2d
11
+
12
+
13
+ def _source_arch(source):
14
+ if source.kind == "safetensors":
15
+ cfg = _hf_config(source.repo) if source.repo else None
16
+ if cfg:
17
+ arch = (cfg.get("architectures") or [None])[0] or cfg.get("model_type")
18
+ else:
19
+ arch = None
20
+ return arch, cfg
21
+ source._load()
22
+ if getattr(source, "_reader", None) is not None:
23
+ for f in source._reader.fields.values():
24
+ if f.name == "general.architecture":
25
+ try:
26
+ v = f.parts[f.data[0]]
27
+ return (v.decode() if isinstance(v, bytes) else str(v)), None
28
+ except Exception:
29
+ return None, None
30
+ return (source._remote[0] if source._remote else None), None
31
+
32
+
33
+ def _resolve_output_name(source, config, names):
34
+ if source.kind == "gguf":
35
+ for n in ("output.weight", "token_embd.weight"):
36
+ if n in names:
37
+ return n
38
+ n = detect_output_tensor(names, config)
39
+ if n:
40
+ return n
41
+ else:
42
+ n = detect_output_tensor(names, config)
43
+ if n:
44
+ return n
45
+ shapes = {n: source.ref(n).shape for n in names}
46
+ return largest_2d(names, shapes)
47
+
48
+
49
+ def _stream_safetensors_tensor(source, name, dest_path):
50
+ """Stream a safetensors tensor's raw bytes to dest_path. Returns byte count."""
51
+ if source.path:
52
+ hs, tensors = source._local()
53
+ begin, end = tensors[name]["data_offsets"]
54
+ with open(source.path, "rb") as src, open(dest_path, "wb") as out:
55
+ src.seek(8 + hs + begin)
56
+ remaining = end - begin
57
+ while remaining:
58
+ b = src.read(min(8 << 20, remaining))
59
+ if not b:
60
+ break
61
+ out.write(b)
62
+ remaining -= len(b)
63
+ return end - begin
64
+ url, hs, tensors = source._shard_info(name)
65
+ begin, end = tensors[name]["data_offsets"]
66
+ import shutil
67
+ combined = download_range(url, 8 + hs + begin, 8 + hs + end - 1, dest_path.parent, None, label=name)
68
+ shutil.move(str(combined), str(dest_path))
69
+ return end - begin
70
+
71
+
72
+ def _fetch_tensor(source, name, tmp_dir):
73
+ """Return (kind, raw_path, data, dtype, shape) for one tensor.
74
+ kind == 'file' β†’ raw bytes at raw_path; kind == 'bytes' β†’ data holds bytes.
75
+ GGML-quantized tensors keep their exact raw blocks (no requant)."""
76
+ ref = source.ref(name)
77
+ tmp_dir.mkdir(parents=True, exist_ok=True)
78
+ if source.kind == "safetensors":
79
+ raw_path = tmp_dir / "raw.bin"
80
+ n = _stream_safetensors_tensor(source, name, raw_path)
81
+ return ("file", raw_path, n, ref.dtype, ref.shape)
82
+ if _is_ggml_quant(ref.dtype):
83
+ # passthrough: exact quant blocks, stored in a single-tensor GGUF artifact
84
+ raw_path = tmp_dir / "raw.bin"
85
+ n = _stream_gguf_tensor(source, name, raw_path)
86
+ return ("file", raw_path, n, ref.dtype, ref.shape)
87
+ f32 = source.read_f32(name)
88
+ # Preserve original precision family: BF16 stays BF16 (not F16 downconvert),
89
+ # F32 stays F32, F16 stays F16. BF16->F32->BF16 round-trip is exact.
90
+ _rd = ref.dtype.upper()
91
+ if _rd in ("F32", "FP32"):
92
+ dtype = "F32"
93
+ elif _rd in ("BF16", "BF16E8M"):
94
+ dtype = "BF16"
95
+ else:
96
+ dtype = "F16"
97
+ return ("bytes", None, encode_from_f32(f32, dtype), dtype, ref.shape)
98
+
99
+
100
+ def _stream_gguf_tensor(source, name, dest_path):
101
+ """Stream a GGUF tensor's exact raw blocks to dest_path. Returns byte count."""
102
+ import shutil
103
+ import numpy as np
104
+ if source.path:
105
+ source._load()
106
+ for t in source._reader.tensors:
107
+ if t.name == name:
108
+ with open(dest_path, "wb") as out:
109
+ raw = t.data.tobytes() if t.data.dtype == np.uint8 else memoryview(t.data).cast("B").tobytes()
110
+ out.write(raw)
111
+ return t.n_bytes
112
+ raise KeyError(name)
113
+ dims, gtype, data_offset, n_bytes = source._remote[2][name]
114
+ start = source._remote[1] + data_offset
115
+ combined = download_range(source.url, start, start + n_bytes - 1, dest_path.parent, None, label=name)
116
+ shutil.move(str(combined), str(dest_path))
117
+ return n_bytes
118
+
119
+
120
+ def _write_fetched(fetched, tensor_name, out_path):
121
+ """Write a fetched tensor to a standalone artifact (gguf when quantized)."""
122
+ kind, raw_path, data, dtype, shape = fetched
123
+ if _is_quant_fetched(fetched):
124
+ if not str(out_path).endswith(".gguf"):
125
+ out_path = str(out_path)[:-len(".safetensors")] + ".gguf" if str(out_path).endswith(".safetensors") else str(out_path) + ".gguf"
126
+ _write_single_tensor_gguf(tensor_name, raw_path, raw_path.stat().st_size, dtype, shape, str(out_path))
127
+ return Path(out_path)
128
+ tmp = str(out_path) + ".tmp"
129
+ if kind == "file":
130
+ _write_safetensors_streaming(tensor_name, str(raw_path), raw_path.stat().st_size, dtype, shape, tmp)
131
+ else:
132
+ write_safetensors({tensor_name: (data, dtype, shape)}, tmp)
133
+ os.replace(tmp, str(out_path))
134
+ return Path(out_path)
135
+
136
+
137
+ def _write_voicepack_json(out_path, source_id, pack):
138
+ """Provenance sidecar for a voicepack: source + per-tensor names/shapes/dtypes.
139
+ Written next to the pack file (same stem, .json suffix)."""
140
+ meta = {
141
+ "source": _public_source(source_id),
142
+ "tensors": [{"name": n, "shape": [int(x) for x in v[2]], "dtype": v[1]}
143
+ for n, v in pack.items()],
144
+ "downloaded_at": _now_iso(),
145
+ }
146
+ jpath = Path(str(out_path)).with_suffix(".json")
147
+ jtmp = jpath.parent / (jpath.name + ".tmp")
148
+ jtmp.write_text(json.dumps(meta, indent=2) + "\n")
149
+ jtmp.replace(jpath)
150
+ return jpath
151
+
152
+
153
+ def _fetch_pack(source, source_id, resolved):
154
+ """Fetch each resolved tensor's raw bytes. Returns pack {name: (bytes, dtype, shape)}.
155
+ Shared by get --multi and pack: always safetensors (GGUF quants dequant once with a warning)."""
156
+ pack = {}
157
+ for tname in resolved:
158
+ ref = source.ref(tname)
159
+ _step(f"Extracting {tname} ({ref.dtype} {tuple(ref.shape)})…")
160
+ tmp_dir = ctx.VOICES_DIR / ".parts" / _safe_id(source_id, tname)
161
+ try:
162
+ fetched = _fetch_tensor(source, tname, tmp_dir)
163
+ except Exception as e:
164
+ _fail(f" βœ— Could not extract tensor {tname}: {e}")
165
+ kind, raw_path, data, dtype, shape = fetched
166
+ try:
167
+ if kind == "file":
168
+ # safetensors raw file: read bytes directly (preserves BF16/F16/F32).
169
+ with open(str(raw_path), "rb") as f:
170
+ b = f.read()
171
+ pack[tname] = (b, dtype, tuple(shape))
172
+ else:
173
+ # bytes already encoded (F32/F16/BF16 preserved).
174
+ pack[tname] = (data, dtype, tuple(shape))
175
+ # If fetched was GGUF quant passthrough, _fetch_tensor returns
176
+ # ("file", raw, n, qtype, shape) with quant dtype β€” dequant once for safetensors pack.
177
+ if _is_ggml_quant(dtype):
178
+ _warn(f" {tname} is {dtype} quant β€” dequanting once to F32 for voicepack.safetensors (unavoidable, safetensors has no quant).")
179
+ try:
180
+ f32 = source.read_f32(tname)
181
+ pack[tname] = (f32.astype("float32").tobytes(), "F32", tuple(shape))
182
+ except Exception as e:
183
+ _fail(f" βœ— Could not dequant {tname} for pack: {e}")
184
+ finally:
185
+ _cleanup_tmp(tmp_dir)
186
+ if not pack:
187
+ _fail(" βœ— Nothing to pack.")
188
+ return pack
vlib/net.py ADDED
@@ -0,0 +1,451 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HTTP, auth, retries, and ranged downloads."""
2
+
3
+ from pathlib import Path
4
+ import json
5
+ import os
6
+ import re
7
+ import sys
8
+ import time
9
+ import urllib.error
10
+ import urllib.request
11
+ from urllib.parse import urlparse
12
+ from vlib import ctx
13
+ from vlib.ui import _fail, _note, _progress_pref, _say, _transform
14
+
15
+
16
+ _HF_TOKEN_CACHE = {"tok": None, "ts": 0.0}
17
+
18
+
19
+ def _get_hf_token():
20
+ # cache 60s to avoid file thrash on hot path
21
+ now = time.time()
22
+ if now - _HF_TOKEN_CACHE["ts"] < 60 and _HF_TOKEN_CACHE["tok"] is not None:
23
+ return _HF_TOKEN_CACHE["tok"]
24
+ tok = None
25
+ for k in ("HF_TOKEN", "HF_HUB_TOKEN", "HUGGING_FACE_HUB_TOKEN"):
26
+ v = os.environ.get(k)
27
+ if v and v.strip().startswith("hf_"):
28
+ tok = v.strip()
29
+ break
30
+ if tok is None:
31
+ for p in (ctx.HF_TOKEN_PATH, Path.home() / ".cache" / "huggingface" / "token", Path.home() / ".huggingface" / "token"):
32
+ try:
33
+ if p.exists():
34
+ t = p.read_text().strip()
35
+ if t.startswith("hf_"):
36
+ tok = t
37
+ break
38
+ except Exception:
39
+ continue
40
+ _HF_TOKEN_CACHE["tok"] = tok
41
+ _HF_TOKEN_CACHE["ts"] = now
42
+ return tok
43
+
44
+
45
+ _HF_TRUSTED_HOSTS = frozenset({"huggingface.co", "cdn-lfs.huggingface.co", "cdn-lfs-us-1.huggingface.co"})
46
+
47
+
48
+ from functools import lru_cache
49
+
50
+
51
+ @lru_cache(maxsize=256)
52
+ def _is_hf_url(url):
53
+ try:
54
+ host = (urlparse(url).hostname or "").lower()
55
+ except Exception:
56
+ return False
57
+ if host in _HF_TRUSTED_HOSTS:
58
+ return True
59
+ return host == "huggingface.co" or host.endswith(".huggingface.co")
60
+
61
+
62
+ def _hf_auth_headers(url, extra=None):
63
+ """Auth + UA. Never sends Authorization to non-HF hosts; never overwrites caller header for HF either."""
64
+ h = {"User-Agent": "voice/2.0"}
65
+ if extra:
66
+ # Strip caller Authorization if target is not HF - prevents token leak to arbitrary URL
67
+ if not _is_hf_url(url) and "Authorization" in extra:
68
+ extra = {k: v for k, v in extra.items() if k != "Authorization"}
69
+ h.update(extra)
70
+ if "Authorization" not in h:
71
+ tok = _get_hf_token()
72
+ if tok and _is_hf_url(url):
73
+ h["Authorization"] = f"Bearer {tok}"
74
+ # If caller supplied Authorization for non-HF, drop it
75
+ if "Authorization" in h and not _is_hf_url(url):
76
+ del h["Authorization"]
77
+ return h
78
+
79
+
80
+ class _PreserveAuthRedirectHandler(urllib.request.HTTPRedirectHandler):
81
+ def redirect_request(self, req, fp, code, msg, hdrs, newurl):
82
+ result = super().redirect_request(req, fp, code, msg, hdrs, newurl)
83
+ if result is None:
84
+ return None
85
+ if _is_hf_url(newurl):
86
+ if "Authorization" not in result.headers:
87
+ tok = _get_hf_token()
88
+ if tok:
89
+ result.add_header("Authorization", f"Bearer {tok}")
90
+ else:
91
+ # stdlib forwards the original headers across hosts: strip the token
92
+ # (bridge/CDN URLs are pre-signed, so it is never needed there either)
93
+ try:
94
+ result.remove_header("Authorization")
95
+ except Exception:
96
+ pass
97
+ return result
98
+
99
+
100
+ urllib.request.install_opener(urllib.request.build_opener(_PreserveAuthRedirectHandler()))
101
+
102
+
103
+ def _parse_retry_after(headers):
104
+ try:
105
+ v = headers.get("Retry-After") or headers.get("retry-after")
106
+ return int(str(v).split(",")[0].strip()) if v is not None else None
107
+ except Exception:
108
+ return None
109
+
110
+
111
+ def _bounded_retry_after(headers, fallback, cap=30):
112
+ """Retry-After with a cap: a 429 must never look like a hang (e.g. Retry-After: 3600)."""
113
+ try:
114
+ ra = _parse_retry_after(headers)
115
+ wait = ra if ra is not None else fallback
116
+ except Exception:
117
+ wait = fallback
118
+ try:
119
+ wait = float(wait)
120
+ except Exception:
121
+ wait = float(fallback)
122
+ if wait < 0:
123
+ wait = float(fallback)
124
+ return min(wait, cap)
125
+
126
+
127
+ def _timeout(base):
128
+ try:
129
+ env = int(os.environ.get("HF_HUB_DOWNLOAD_TIMEOUT", "0"))
130
+ return max(base, env) if env > 0 else base
131
+ except Exception:
132
+ return base
133
+
134
+
135
+ def http_range(url, start, end, timeout=60):
136
+ for attempt in range(3):
137
+ hdrs = _hf_auth_headers(url, {"Range": f"bytes={start}-{end}"})
138
+ try:
139
+ with urllib.request.urlopen(urllib.request.Request(url, headers=hdrs), timeout=_timeout(timeout)) as resp:
140
+ if resp.status == 429 and attempt < 2:
141
+ wait = _bounded_retry_after(resp.headers, 1.5 * (2 ** attempt))
142
+ if wait >= 5:
143
+ _note(f" Rate limited (429) β€” waiting {wait:.0f}s before retry…")
144
+ time.sleep(wait)
145
+ continue
146
+ if resp.status not in (200, 206):
147
+ raise urllib.error.HTTPError(url, resp.status, f"status {resp.status}", resp.headers, None)
148
+ return resp.read(), resp.status, resp.headers.get("ETag")
149
+ except urllib.error.HTTPError as e:
150
+ if e.code == 429 and attempt < 2:
151
+ wait = _bounded_retry_after(getattr(e, "headers", None) or {}, 1.5 * (2 ** attempt))
152
+ if wait >= 5:
153
+ _note(f" Rate limited (429) β€” waiting {wait:.0f}s before retry…")
154
+ time.sleep(wait)
155
+ continue
156
+ raise
157
+
158
+
159
+ def http_get_json(url, timeout=30):
160
+ req = urllib.request.Request(url, headers=_hf_auth_headers(url))
161
+ try:
162
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
163
+ return json.loads(resp.read())
164
+ except urllib.error.HTTPError as e:
165
+ try:
166
+ e.close() # else the response fp dangles until GC (ResourceWarning)
167
+ except Exception:
168
+ pass
169
+ raise
170
+
171
+
172
+ def _safe_id(model_id, shard_url=""):
173
+ mid = re.sub(r"[^a-z0-9._-]", "_", model_id.strip().lower().replace("..", "_").replace("/", "__").replace("\\", "_").replace(":", "_"))
174
+ mid = re.sub(r"_+", "_", mid).strip("_")[:120]
175
+ if shard_url:
176
+ shard = re.sub(r"[^a-z0-9._-]", "_", shard_url.split("/")[-1].replace(".safetensors", "").replace(".gguf", "")).lower()[:40]
177
+ return f"{mid}__{shard}"
178
+ return mid
179
+
180
+
181
+ def _acquire_parts_lock(part_dir):
182
+ """Best-effort cross-process lock for .parts. Returns lock path or None."""
183
+ lock = part_dir / ".lock"
184
+ try:
185
+ # O_EXCL to avoid double acquisition
186
+ fd = os.open(str(lock), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
187
+ try:
188
+ os.write(fd, str(os.getpid()).encode())
189
+ finally:
190
+ os.close(fd)
191
+ return lock
192
+ except FileExistsError:
193
+ try:
194
+ # stale lock (>10 min) - reclaim
195
+ if time.time() - lock.stat().st_mtime > 600:
196
+ lock.unlink(missing_ok=True)
197
+ fd = os.open(str(lock), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
198
+ os.close(fd)
199
+ return lock
200
+ except Exception:
201
+ pass
202
+ return None
203
+ except Exception:
204
+ return None
205
+
206
+
207
+ def download_range(url, abs_start, abs_end, part_dir, etag, label="", n_parts=None):
208
+ import threading
209
+
210
+ total = abs_end - abs_start + 1
211
+ part_dir.mkdir(parents=True, exist_ok=True)
212
+ try:
213
+ os.chmod(part_dir.parent, 0o700)
214
+ except Exception:
215
+ pass
216
+ meta_path = part_dir / "meta.json"
217
+
218
+ old_etag = None
219
+ if meta_path.exists():
220
+ try:
221
+ old_etag = json.loads(meta_path.read_text()).get("etag")
222
+ except Exception:
223
+ old_etag = None
224
+ # acquire lock BEFORE writing meta so racers can't interleave etag/url
225
+ _parts_lock = _acquire_parts_lock(part_dir)
226
+ try:
227
+ meta_path.write_text(json.dumps({"url": url, "abs_start": abs_start, "total": total, "etag": etag}, indent=2))
228
+ except Exception:
229
+ pass
230
+
231
+ try:
232
+ import shutil
233
+ free = shutil.disk_usage(part_dir).free
234
+ if free < int(total * 1.25):
235
+ _fail(f" βœ— Not enough disk space: need {total*1.25/1e9:.1f} GB, free {free/1e9:.1f} GB.")
236
+ except SystemExit:
237
+ raise
238
+ except Exception:
239
+ pass
240
+
241
+ if n_parts is None:
242
+ n_parts = 8 if total > 512 * 1024 * 1024 else (4 if total > 128 * 1024 * 1024 else 1)
243
+ chunk_total = total // n_parts
244
+ ranges = []
245
+ for i in range(n_parts):
246
+ s = i * chunk_total
247
+ e = (s + chunk_total - 1) if i < n_parts - 1 else total - 1
248
+ ranges.append((abs_start + s, abs_start + e, part_dir / f"chunk_{i:03d}.part", e - s + 1))
249
+
250
+ downloaded = 0
251
+ for _rs, _re, p, csz in ranges:
252
+ if p.exists():
253
+ try:
254
+ sz = p.stat().st_size
255
+ # If ETag changed (or one side unknown), invalidate stale chunk
256
+ if old_etag != etag and (old_etag is not None or etag is not None):
257
+ p.unlink(missing_ok=True)
258
+ continue
259
+ if sz > csz:
260
+ p.unlink(missing_ok=True)
261
+ continue
262
+ downloaded += sz
263
+ except Exception:
264
+ pass
265
+
266
+ t0 = time.time()
267
+ lock = threading.RLock() # reentrant: progress() also takes it, and runs inside locked regions
268
+ stop = threading.Event() # set on fatal error / interrupt: workers wind down, partials kept
269
+ errors = []
270
+ last = [0.0]
271
+
272
+ def progress(force=False):
273
+ now = time.time()
274
+ with lock:
275
+ cur_downloaded = downloaded
276
+ if not force and now - last[0] < 0.25 and cur_downloaded < total:
277
+ return
278
+ last[0] = now
279
+ elapsed = now - t0 or 1e-6
280
+ mbps = cur_downloaded / elapsed / 1e6
281
+ pct = cur_downloaded / total * 100 if total else 100
282
+ remain = total - cur_downloaded
283
+ eta = _fmt_eta(remain, mbps)
284
+ msg = f" β†’ {label} {pct:3.0f}% Β· {cur_downloaded/1e6:.0f}/{total/1e6:.0f} MB Β· {mbps:.1f} MB/s Β· {eta} "
285
+ sys.stdout.write(_transform(msg) + ("\n" if _progress_pref() else "\r"))
286
+ sys.stdout.flush()
287
+
288
+ # show 0% immediately so user sees waiting + speed
289
+ progress(force=True)
290
+
291
+ def chunk(r_start, r_end, p, csz):
292
+ nonlocal downloaded
293
+ existing = p.stat().st_size if p.exists() else 0
294
+ if existing == csz:
295
+ return # already complete (counted in pre-scan); re-requesting would 416
296
+ eff = r_start + existing
297
+ for attempt in range(3):
298
+ if stop.is_set():
299
+ return
300
+ try:
301
+ hdrs = _hf_auth_headers(url, {"Range": f"bytes={eff}-{r_end}"})
302
+ if etag:
303
+ hdrs["If-Match"] = etag
304
+ with urllib.request.urlopen(urllib.request.Request(url, headers=hdrs), timeout=_timeout(120)) as resp:
305
+ if resp.status not in (200, 206):
306
+ raise urllib.error.HTTPError(url, resp.status, "bad status", None, None)
307
+ if resp.status == 200 and (r_end - r_start + 1) != total:
308
+ # multi-part chunk but server ignored Range: body is wrong data β€” fail fast
309
+ _fail(f" βœ— Server ignored the Range request for {label or url} (got 200, need 206). Resume unsupported β€” try again later.")
310
+ if existing and resp.status == 200:
311
+ # server ignored Range - we already counted existing in downloaded, correct it
312
+ with lock:
313
+ downloaded -= existing
314
+ p.unlink(missing_ok=True)
315
+ existing = 0
316
+ eff = r_start
317
+ mode = "ab" if existing else "wb"
318
+ with open(p, mode) as f:
319
+ while not stop.is_set():
320
+ b = resp.read(8 << 20)
321
+ if not b:
322
+ break
323
+ f.write(b)
324
+ with lock:
325
+ downloaded += len(b)
326
+ progress()
327
+ f.flush()
328
+ try:
329
+ os.fsync(f.fileno())
330
+ except Exception:
331
+ pass
332
+ final = p.stat().st_size if p.exists() else 0
333
+ if final != csz and attempt < 2:
334
+ existing = final
335
+ eff = r_start + final
336
+ time.sleep(0.5 * (attempt + 1))
337
+ continue
338
+ if final != csz:
339
+ raise OSError(f"{p.name} incomplete {final}/{csz}")
340
+ return
341
+ except urllib.error.HTTPError as e:
342
+ if attempt < 2:
343
+ wait = _bounded_retry_after(getattr(e, "headers", None) or {}, 0.7 * (2 ** attempt))
344
+ if wait >= 5:
345
+ _note(f" Rate limited ({e.code}) β€” waiting {wait:.0f}s before retry…")
346
+ time.sleep(wait)
347
+ if p.exists():
348
+ existing = p.stat().st_size
349
+ eff = r_start + existing
350
+ continue
351
+ raise
352
+ except (urllib.error.URLError, TimeoutError, OSError):
353
+ if attempt < 2:
354
+ time.sleep(0.7 * (2 ** attempt))
355
+ if p.exists():
356
+ existing = p.stat().st_size
357
+ eff = r_start + existing
358
+ continue
359
+ raise
360
+ raise OSError(f"{p.name} failed after 3 retries")
361
+
362
+ def worker(rs, re_, p, csz):
363
+ try:
364
+ chunk(rs, re_, p, csz)
365
+ except SystemExit as e:
366
+ errors.append(e) # _fail() already printed the reason
367
+ stop.set()
368
+ except Exception as e: # noqa: BLE001 β€” transported to main thread below
369
+ errors.append(e)
370
+ stop.set()
371
+
372
+ try:
373
+ if n_parts == 1:
374
+ rs, re_, p, csz = ranges[0]
375
+ chunk(rs, re_, p, csz)
376
+ else:
377
+ # daemon threads: Ctrl-C returns instantly, partial chunks kept for resume
378
+ threads = [threading.Thread(target=worker, args=(rs, re_, p, csz), daemon=True)
379
+ for rs, re_, p, csz in ranges]
380
+ for t in threads:
381
+ t.start()
382
+ while any(t.is_alive() for t in threads):
383
+ for t in threads:
384
+ t.join(timeout=0.25)
385
+ if stop.is_set():
386
+ break
387
+ if stop.is_set():
388
+ break
389
+ if errors:
390
+ raise errors[0]
391
+ except KeyboardInterrupt:
392
+ stop.set()
393
+ sys.stdout.write("\n")
394
+ _fail(f" βœ— Download cancelled ({downloaded/1e6:.0f}/{total/1e6:.0f} MB kept). Run again to resume.")
395
+ except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, OSError):
396
+ sys.stdout.write("\n")
397
+ _fail(f" βœ— Download interrupted ({downloaded/1e6:.0f}/{total/1e6:.0f} MB kept). Run again to resume.")
398
+ except Exception as e:
399
+ sys.stdout.write("\n")
400
+ if ctx.VERBOSE:
401
+ import traceback
402
+ _say(traceback.format_exc())
403
+ _fail(f" βœ— Download failed: {e}")
404
+
405
+ # ensure 100% with speed is shown before combining
406
+ progress(force=True)
407
+ for _rs, _re, p, csz in ranges:
408
+ try:
409
+ if p.stat().st_size != csz:
410
+ _fail(f" βœ— Incomplete download ({p.name}). Run again to resume.")
411
+ except Exception:
412
+ _fail(f" βœ— Missing chunk {p.name}. Run again to resume.")
413
+ combined = part_dir / "combined.part"
414
+ with open(combined, "wb") as out:
415
+ for _rs, _re, p, _csz in ranges:
416
+ with open(p, "rb") as inp:
417
+ while True:
418
+ b = inp.read(8 << 20)
419
+ if not b:
420
+ break
421
+ out.write(b)
422
+ out.flush()
423
+ try:
424
+ os.fsync(out.fileno())
425
+ except Exception:
426
+ pass
427
+ # clean up lock
428
+ try:
429
+ if '_parts_lock' in locals() and _parts_lock and _parts_lock.exists():
430
+ _parts_lock.unlink(missing_ok=True)
431
+ except Exception:
432
+ pass
433
+ sys.stdout.write("\n")
434
+ sys.stdout.flush()
435
+ return combined
436
+
437
+
438
+ def _fmt_eta(remaining, mbps):
439
+ if mbps < 1e-6:
440
+ return "--:--"
441
+ sec = int(remaining / (mbps * 1e6))
442
+ return f"{sec}s" if sec < 60 else f"{sec//60}m {sec%60}s"
443
+
444
+
445
+ def _cleanup_tmp(tmp_dir):
446
+ try:
447
+ if tmp_dir.exists():
448
+ import shutil
449
+ shutil.rmtree(tmp_dir)
450
+ except Exception:
451
+ pass
vlib/pack.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Head-only voice-pack preset (unused by the CLI)."""
2
+ import argparse
3
+ import json
4
+ import urllib.error
5
+ from pathlib import Path
6
+
7
+ from vlib import ctx
8
+ from vlib.fetch import _fetch_pack, _resolve_output_name, _source_arch
9
+ from vlib.sources import (_family, _hf_config, _public_source,
10
+ _slugify_voice_name, open_source)
11
+ from vlib.tensors import write_safetensors
12
+ from vlib.ui import _fail, _note, _now_iso, _say, _step
13
+
14
+
15
+ def _resolve_pack_tensors(source, repo, config, names):
16
+ """Head-only voice-pack preset. Embed/norm/blocks stay manual
17
+ (voice get <repo> <tensors...>).
18
+ Config is the source config with _hf_config fallback. Miss fails loud."""
19
+ if config is None and getattr(source, "repo", None):
20
+ config = _hf_config(source.repo)
21
+ head = _resolve_output_name(source, config, names)
22
+ if head is None:
23
+ _fail(f" βœ— pack: no output head found in '{repo}'.")
24
+ return [head]
25
+
26
+
27
+ def cmd_pack(args):
28
+ repo = args.repo.strip().rstrip("/")
29
+ if not repo:
30
+ _fail(" βœ— Enter a model name β€” for example: voice pack google/gemma-3-1b")
31
+ _step(f"Finding the voice pack in {repo}…")
32
+ try:
33
+ source = open_source(repo)
34
+ names = source.names()
35
+ if not names:
36
+ _fail(f" βœ— No tensors found in '{repo}'.")
37
+ arch, config = _source_arch(source)
38
+ except urllib.error.HTTPError as e:
39
+ _fail(f" βœ— Model '{repo}' was not found or is not accessible ({e.code}).")
40
+ except Exception as e:
41
+ if ctx.VERBOSE:
42
+ import traceback
43
+ _say(traceback.format_exc())
44
+ _fail(f" βœ— Could not reach the model. Check your connection ({type(e).__name__}).")
45
+
46
+ _step("Resolving voice-pack tensors…")
47
+ resolved = _resolve_pack_tensors(source, repo, config, names)
48
+ for tname in resolved:
49
+ _note(f" {tname}")
50
+ # Fetch via the same multi path as get --multi, then save flat in the
51
+ # registry through the existing add machinery (no new storage format).
52
+ pack = _fetch_pack(source, repo, resolved)
53
+ slug = _slugify_voice_name(repo.split("/")[-1].split("@")[0].split(":")[0].replace(".gguf", "").replace(".safetensors", ""))
54
+ name = f"pack-{slug}"
55
+ import tempfile, shutil
56
+ stage = Path(tempfile.mkdtemp(prefix="voicepack-stage-"))
57
+ try:
58
+ st_path = stage / "voice.safetensors"
59
+ write_safetensors(pack, str(st_path))
60
+ js_path = stage / "voice.json"
61
+ js_path.write_text(json.dumps({
62
+ "name": name,
63
+ "source": _public_source(repo),
64
+ "source_format": source.kind,
65
+ "tensors": [{"name": n, "shape": [int(x) for x in pack[n][2]], "dtype": pack[n][1]}
66
+ for n in resolved if n in pack],
67
+ "architecture": arch,
68
+ "family": _family(arch),
69
+ "downloaded_at": _now_iso(),
70
+ }, indent=2) + "\n")
71
+ from vlib.cmds_voices import cmd_add
72
+ cmd_add(argparse.Namespace(safetensors=str(st_path), json=str(js_path), name=name,
73
+ yes=getattr(args, "yes", False),
74
+ non_interactive=getattr(args, "non_interactive", False)))
75
+ finally:
76
+ shutil.rmtree(stage, ignore_errors=True)
vlib/registry.py ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Voice registry, ops log, and write guards."""
2
+
3
+ from pathlib import Path
4
+ import json
5
+ import os
6
+ import sys
7
+ import time
8
+ from vlib import ctx
9
+ from vlib.ui import _confirm, _fail, _noninteractive, _say, _warn
10
+ from vlib.tensors import _is_gguf_file, _is_quant_fetched, _write_safetensors_streaming, _write_single_tensor_gguf, write_safetensors
11
+ from vlib.sources import _NAME_RULES, _sanitize_voice_name, _slugify_voice_name
12
+
13
+
14
+ def log_operation(record):
15
+ record = {"ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), **record}
16
+ try:
17
+ ctx.VOICES_DIR.mkdir(parents=True, exist_ok=True)
18
+ try:
19
+ os.chmod(ctx.VOICES_DIR, 0o700)
20
+ except Exception:
21
+ pass
22
+ _assert_not_symlink(ctx.OPERATIONS_LOG)
23
+ with open(ctx.OPERATIONS_LOG, "a") as f:
24
+ f.write(json.dumps(record, ensure_ascii=False, separators=(",", ":")) + "\n")
25
+ try:
26
+ f.flush()
27
+ os.fsync(f.fileno())
28
+ except Exception:
29
+ pass
30
+ try:
31
+ os.chmod(ctx.OPERATIONS_LOG, 0o600)
32
+ except Exception:
33
+ pass
34
+ try:
35
+ dirfd = os.open(str(ctx.VOICES_DIR), os.O_DIRECTORY)
36
+ try:
37
+ os.fsync(dirfd)
38
+ finally:
39
+ os.close(dirfd)
40
+ except Exception:
41
+ pass
42
+ except Exception:
43
+ if ctx.VERBOSE:
44
+ _warn("could not write operation log")
45
+
46
+
47
+ def _atomic_jsonl_append(path, record):
48
+ """Append one JSON line atomically with flock + fsync. Optimised: single chmod, cached JSON."""
49
+ path = Path(path)
50
+ # fast path: parent exists, avoid mkdir/chmod churn
51
+ if not path.parent.exists():
52
+ path.parent.mkdir(parents=True, exist_ok=True)
53
+ try:
54
+ os.chmod(path.parent, 0o700)
55
+ except Exception:
56
+ pass
57
+ if not path.exists():
58
+ try:
59
+ fd = os.open(str(path), os.O_WRONLY | os.O_CREAT, 0o600)
60
+ os.close(fd)
61
+ except Exception:
62
+ pass
63
+ else:
64
+ # occasional chmod drift check, not every append
65
+ try:
66
+ if oct(path.stat().st_mode & 0o777) != "0o600":
67
+ os.chmod(path, 0o600)
68
+ except Exception:
69
+ pass
70
+ _assert_not_symlink(path)
71
+ line = json.dumps(record, ensure_ascii=False, separators=(",", ":")) + "\n"
72
+ with open(path, "a") as f:
73
+ try:
74
+ import fcntl
75
+ fcntl.flock(f, fcntl.LOCK_EX)
76
+ locked = True
77
+ except Exception:
78
+ locked = False
79
+ try:
80
+ f.write(line)
81
+ f.flush()
82
+ try:
83
+ os.fsync(f.fileno())
84
+ except Exception:
85
+ pass
86
+ finally:
87
+ if locked:
88
+ try:
89
+ import fcntl
90
+ fcntl.flock(f, fcntl.LOCK_UN)
91
+ except Exception:
92
+ pass
93
+ try:
94
+ dirfd = os.open(str(path.parent), os.O_DIRECTORY)
95
+ try:
96
+ os.fsync(dirfd)
97
+ finally:
98
+ os.close(dirfd)
99
+ except Exception:
100
+ pass
101
+
102
+
103
+ def _write_outbox(report_id, record):
104
+ ctx.REPORTS_OUTBOX.mkdir(parents=True, exist_ok=True)
105
+ try:
106
+ os.chmod(ctx.REPORTS_OUTBOX, 0o700)
107
+ except Exception:
108
+ pass
109
+ _assert_not_symlink(ctx.REPORTS_OUTBOX)
110
+ out_path = ctx.REPORTS_OUTBOX / f"{report_id}.json"
111
+ _assert_not_symlink(out_path)
112
+ tmp = out_path.with_suffix(".tmp")
113
+ fd = os.open(str(tmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
114
+ try:
115
+ os.write(fd, (json.dumps(record, ensure_ascii=False) + "\n").encode("utf-8"))
116
+ try:
117
+ os.fsync(fd)
118
+ except Exception:
119
+ pass
120
+ finally:
121
+ os.close(fd)
122
+ tmp.replace(out_path)
123
+ try:
124
+ dirfd = os.open(str(ctx.REPORTS_OUTBOX), os.O_DIRECTORY)
125
+ try:
126
+ os.fsync(dirfd)
127
+ finally:
128
+ os.close(dirfd)
129
+ except Exception:
130
+ pass
131
+ return out_path
132
+
133
+
134
+ def _append_report(record):
135
+ _atomic_jsonl_append(ctx.REPORTS_JSONL, record)
136
+ _write_outbox(record["report_id"], record)
137
+ log_operation({"op": "report", "report_id": record["report_id"], "status": "ok", "voice": record.get("voice"), "target": record.get("target")})
138
+
139
+
140
+ def _iter_voices():
141
+ if not ctx.VOICES_DIR.exists():
142
+ return []
143
+ return [d for d in sorted(ctx.VOICES_DIR.iterdir(), key=lambda p: p.name.lower())
144
+ if d.is_dir() and not d.name.startswith(".") and d.name != "base"]
145
+
146
+
147
+ def _registry_resolve(name_or_path):
148
+ p = Path(name_or_path).expanduser()
149
+ if p.is_file():
150
+ return p, None
151
+ if p.is_dir():
152
+ for fname in ("voice.safetensors", "voice.gguf"):
153
+ if (p / fname).exists():
154
+ return p / fname, _read_meta(p / "voice.json")
155
+ target = name_or_path.strip().lower()
156
+ for d in _iter_voices():
157
+ if d.name.lower() == target:
158
+ for fname in ("voice.safetensors", "voice.gguf"):
159
+ if (d / fname).exists():
160
+ return d / fname, _read_meta(d / "voice.json")
161
+ return None, None
162
+
163
+
164
+ def _read_meta(json_path):
165
+ try:
166
+ return json.loads(json_path.read_text()) if json_path.exists() else None
167
+ except Exception:
168
+ return None
169
+
170
+
171
+ def _registry_name_exists(name):
172
+ target = name.strip().lower()
173
+ return any(d.name.lower() == target for d in _iter_voices())
174
+
175
+
176
+ def _registry_folder(name):
177
+ return ctx.VOICES_DIR / name.strip().lower()
178
+
179
+
180
+ def _assert_not_symlink(path):
181
+ # Resolve-first, fail-closed. The old code compared raw strings
182
+ # (str.startswith): a sibling such as ".voices2" counted as "inside"
183
+ # ".voices", uninspected parents were skipped by the same prefix test,
184
+ # and any resolve failure was swallowed (fail-open). Now: resolve before
185
+ # any comparison, test membership with real path relations, walk every
186
+ # component below VOICES_DIR, and refuse when resolution itself fails.
187
+ # Scope: registry paths (under VOICES_DIR). Unrelated user paths (e.g. an
188
+ # explicit --out in /tmp) keep the old behavior: direct-symlink check only,
189
+ # since the user chose them and system roots above them may legitimately
190
+ # be links. TOCTOU (link swapped between check and write) is out of scope:
191
+ # local CLI, self-owned dir.
192
+ try:
193
+ p = Path(path)
194
+ abs_p = p.absolute()
195
+ norm = Path(os.path.normpath(abs_p))
196
+ base_abs = ctx.VOICES_DIR.absolute()
197
+ base_res = ctx.VOICES_DIR.resolve()
198
+ rp = p.resolve()
199
+ if p.is_symlink():
200
+ _fail(f" βœ— Refusing to write through symlink: {path}")
201
+ prefix_hit = str(abs_p).startswith(str(base_abs))
202
+ inside = norm == base_abs or base_abs in norm.parents
203
+ if inside or prefix_hit:
204
+ # every component from the base down must be symlink-free; a
205
+ # prefix-sibling (".voices2") has no relative leg to walk, but it
206
+ # still faces the containment check below.
207
+ if inside:
208
+ cur = base_abs
209
+ for part in norm.relative_to(base_abs).parts:
210
+ cur = cur / part
211
+ if cur.is_symlink():
212
+ _fail(f" βœ— Refusing to write through symlink: {cur}")
213
+ if rp != base_res and base_res not in rp.parents:
214
+ _fail(f" βœ— Refusing to write outside VOICES_DIR: {path}")
215
+ except SystemExit:
216
+ raise
217
+ except Exception:
218
+ _fail(f" βœ— Refusing to write through unresolvable path: {path}")
219
+
220
+
221
+ def _write_voice(folder, tensor_name, fetched, meta):
222
+ _assert_not_symlink(folder)
223
+ folder.mkdir(parents=True, exist_ok=True)
224
+ try:
225
+ os.chmod(folder, 0o700)
226
+ except Exception:
227
+ pass
228
+ _assert_not_symlink(folder)
229
+ kind, raw_path, data, dtype, shape = fetched
230
+ if _is_quant_fetched(fetched):
231
+ fpath = folder / "voice.gguf"
232
+ tmp = folder / "voice.gguf.tmp"
233
+ _write_single_tensor_gguf(tensor_name, raw_path, raw_path.stat().st_size, dtype, shape, str(tmp))
234
+ else:
235
+ fpath = folder / "voice.safetensors"
236
+ tmp = folder / "voice.safetensors.tmp"
237
+ if kind == "file":
238
+ _write_safetensors_streaming(tensor_name, str(raw_path), raw_path.stat().st_size, dtype, shape, str(tmp))
239
+ else:
240
+ write_safetensors({tensor_name: (data, dtype, shape)}, str(tmp))
241
+ tmp.replace(fpath)
242
+ try:
243
+ os.chmod(fpath, 0o600)
244
+ except Exception:
245
+ pass
246
+ meta = dict(meta)
247
+ meta["artifact_format"] = "gguf" if _is_quant_fetched(fetched) else "safetensors"
248
+ meta["ggml_type"] = dtype if _is_quant_fetched(fetched) else None
249
+ meta["bytes"] = fpath.stat().st_size
250
+ jtmp = folder / "voice.json.tmp"
251
+ # atomic 0600 json write
252
+ fd = os.open(str(jtmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
253
+ try:
254
+ os.write(fd, (json.dumps(meta, indent=2) + "\n").encode("utf-8"))
255
+ try:
256
+ os.fsync(fd)
257
+ except Exception:
258
+ pass
259
+ finally:
260
+ os.close(fd)
261
+ jtmp.replace(folder / "voice.json")
262
+ return fpath
263
+
264
+
265
+ def _resolve_name(args, default_name):
266
+ provided = getattr(args, "name", None)
267
+ if provided and str(provided).strip():
268
+ name = str(provided).strip()
269
+ if _sanitize_voice_name(name) is None:
270
+ _fail(f" βœ— Invalid name '{name}'. {_NAME_RULES}")
271
+ return name
272
+ default_slug = _slugify_voice_name(default_name)
273
+ if _noninteractive(args):
274
+ return default_slug
275
+ try:
276
+ entered = input(f" β†’ Name the voice or press enter to use [{default_slug}]: ").strip()
277
+ name = entered or default_slug
278
+ except (EOFError, KeyboardInterrupt):
279
+ _say(" ⚠ Cancelled.")
280
+ sys.exit(0)
281
+ if _sanitize_voice_name(name) is None:
282
+ _fail(f" βœ— Invalid name '{name}'. {_NAME_RULES}")
283
+ return name
284
+
285
+
286
+ def _out_path(args, target, voice_arg):
287
+ if getattr(args, "out", None):
288
+ out = args.out
289
+ # jail: resolve but allow any absolute path user explicitly requested; block symlink traversal
290
+ if out.endswith(("/", os.sep)) or os.path.isdir(out) or not os.path.splitext(out)[1]:
291
+ out = os.path.join(out, f"{_stem(voice_arg)}-{_stem(target)}{'.gguf' if _is_gguf_file(target) else '.safetensors'}")
292
+ # normalize
293
+ out = os.path.abspath(out)
294
+ return out
295
+ # sane default: ~/Voiced/<voice>-<model>/<voice>-<model>.gguf (not alongside target)
296
+ # avoids overwriting and keeps Voiced folder tidy
297
+ voiced_base = Path.home() / "Voiced"
298
+ sub = f"{_stem(voice_arg)}-{_stem(target)}"
299
+ ext = ".gguf" if _is_gguf_file(target) else ".safetensors"
300
+ return str(voiced_base / sub / f"{sub}{ext}")
301
+
302
+
303
+ def _stem(p):
304
+ return os.path.splitext(os.path.basename(p))[0]
305
+
306
+
307
+ def _check_overwrite(out_path, args):
308
+ if os.path.islink(out_path):
309
+ _fail(f" βœ— Refusing to write through symlink: {out_path}")
310
+ if os.path.exists(out_path):
311
+ if _noninteractive(args) and not getattr(args, "yes", False):
312
+ _fail(f" βœ— Output '{out_path}' exists. Use --yes to overwrite in non-interactive mode.")
313
+ if not _confirm(f" Output '{out_path}' already exists. Overwrite? [y/N]: ", args):
314
+ _say(" Cancelled.")
315
+ sys.exit(0)
316
+ out_dir = os.path.dirname(os.path.abspath(out_path))
317
+ if out_dir and os.path.islink(out_dir):
318
+ _fail(f" βœ— Refusing to write through symlink: {out_dir}")
319
+ if out_dir and not os.path.isdir(out_dir):
320
+ try:
321
+ os.makedirs(out_dir, exist_ok=True)
322
+ except Exception:
323
+ _fail(f" βœ— Could not create folder \"{out_dir}\".")
vlib/reports.py ADDED
@@ -0,0 +1,469 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Local reports and community sharing."""
2
+
3
+ from pathlib import Path
4
+ import json
5
+ import os
6
+ import re
7
+ import sys
8
+ import urllib.error
9
+ import urllib.request
10
+ from vlib import ctx
11
+ from vlib.ui import _noninteractive, _now_iso, _ok, _say, _step, _warn
12
+ from vlib.net import _get_hf_token, _parse_retry_after
13
+ from vlib.tensors import _is_gguf_file, read_safetensors
14
+ from vlib.sources import _family, _public_source, detect_output_tensor, largest_2d
15
+ from vlib.registry import _append_report, _iter_voices, _read_meta, _registry_resolve
16
+
17
+
18
+ def _hash_file(path):
19
+ import hashlib
20
+ h = hashlib.sha256()
21
+ with open(path, "rb") as f:
22
+ while True:
23
+ b = f.read(8 << 20)
24
+ if not b:
25
+ break
26
+ h.update(b)
27
+ return h.hexdigest()
28
+
29
+
30
+ def _sanitize_note(s):
31
+ if s is None:
32
+ return None
33
+ s = str(s).replace("\r", " ").replace("\n", " ").replace("\x00", " ")
34
+ s = re.sub(r"\s+", " ", s).strip()
35
+ if not s:
36
+ return None
37
+ # strip CSV injection prefixes
38
+ if s and s[0] in "=+@-":
39
+ s = "'" + s
40
+ if len(s) > 240:
41
+ s = s[:239] + "…"
42
+ return s
43
+
44
+
45
+ def _sanitize_reporter(s):
46
+ if not s:
47
+ return None
48
+ s = re.sub(r"[^A-Za-z0-9._-]", "_", str(s).strip())[:64].strip("_")
49
+ return s or None
50
+
51
+
52
+ def _prompt_share(args, preview=None):
53
+ if getattr(args, "no_push", False):
54
+ return False
55
+ if getattr(args, "yes", False):
56
+ return True
57
+ if _noninteractive(args):
58
+ return False
59
+ if preview:
60
+ _say(f" Will share: {preview}")
61
+ try:
62
+ ans = input(" Leave a breadcrumb to help the next person? [y/N] (N): ").strip().lower()
63
+ except (EOFError, KeyboardInterrupt):
64
+ _say(" ⚠ Cancelled.")
65
+ sys.exit(0)
66
+ return ans in ("y", "yes")
67
+
68
+
69
+ def _push_reports_batch(records):
70
+ """Batch push: group by shard, single fetch+upload per shard. Returns pushed count.
71
+ Non-collaborators lack write access to Wiself/voice-reports β†’ fallback to PR (create_pr=True).
72
+ Fixes tempfile.mktemp racy API β†’ mkstemp 0600; 3 retries on 429/409/412.
73
+ Note: download-modify-upload still racy for direct pushes (two owners racing may lose);
74
+ dedup on report_id softens it, and PR path eliminates race for crowd (each PR is isolated)."""
75
+ import time, tempfile
76
+ if not records:
77
+ return 0
78
+ tok = _get_hf_token()
79
+ if not tok:
80
+ _warn(" No HF token β€” saved to outbox only. Run: voice auth")
81
+ return 0
82
+ try:
83
+ from huggingface_hub import HfApi
84
+ from huggingface_hub import CommitOperationAdd
85
+ api = HfApi(token=tok)
86
+ except Exception as e:
87
+ _warn(f" huggingface_hub not available ({e}) β€” kept in outbox.")
88
+ return 0
89
+ try:
90
+ api.create_repo(repo_id=ctx.REPORTS_DATASET_ID, repo_type="dataset", exist_ok=True)
91
+ except Exception:
92
+ pass
93
+ from collections import defaultdict
94
+ by_shard = defaultdict(list)
95
+ for r in records:
96
+ shard = f"{ctx.REPORTS_REMOTE_PREFIX}{r['ts'][:7]}.jsonl"
97
+ by_shard[shard].append(r)
98
+ pushed = 0
99
+ for shard, recs in by_shard.items():
100
+ for attempt in range(3):
101
+ try:
102
+ existing = ""
103
+ try:
104
+ from huggingface_hub import hf_hub_download
105
+ tmp = hf_hub_download(repo_id=ctx.REPORTS_DATASET_ID, repo_type="dataset", filename=shard, token=tok)
106
+ existing = Path(tmp).read_text(encoding="utf-8", errors="replace")
107
+ except Exception:
108
+ existing = ""
109
+ # parse existing_ids properly (not substring) for correct dedup
110
+ existing_ids = set()
111
+ if existing:
112
+ for line in existing.splitlines():
113
+ if not line.strip():
114
+ continue
115
+ try:
116
+ existing_ids.add(json.loads(line).get("report_id"))
117
+ except Exception:
118
+ continue
119
+ # headroom check per Tim Cook: warn if shard near 10k
120
+ if len(existing_ids) > 10000:
121
+ _warn(f" Shard {shard} has {len(existing_ids)} reports β€” consider rotation (reports-YYYY-MM-2).")
122
+ todo = [r for r in recs if r["report_id"] not in existing_ids]
123
+ if not todo:
124
+ for r in recs:
125
+ try:
126
+ (ctx.REPORTS_OUTBOX / f"{r['report_id']}.json").unlink(missing_ok=True)
127
+ except Exception:
128
+ pass
129
+ pushed += len(recs)
130
+ break
131
+ if not existing:
132
+ new_content = "\n".join(json.dumps(r, ensure_ascii=False, separators=(",", ":")) for r in todo) + "\n"
133
+ else:
134
+ if not existing.endswith("\n"):
135
+ existing += "\n"
136
+ new_content = existing + "\n".join(json.dumps(r, ensure_ascii=False, separators=(",", ":")) for r in todo) + "\n"
137
+ # mkstemp 0600, not deprecated mktemp
138
+ fd_tmp, tmp_path = tempfile.mkstemp(prefix="voice-report-")
139
+ tmpf = Path(tmp_path)
140
+ try:
141
+ os.write(fd_tmp, new_content.encode("utf-8"))
142
+ try:
143
+ os.fsync(fd_tmp)
144
+ except Exception:
145
+ pass
146
+ finally:
147
+ try:
148
+ os.close(fd_tmp)
149
+ except Exception:
150
+ pass
151
+ try:
152
+ os.chmod(tmpf, 0o600)
153
+ except Exception:
154
+ pass
155
+ # try direct push first; on 403 (no write access) fallback to PR
156
+ try:
157
+ api.upload_file(path_or_fileobj=str(tmpf), path_in_repo=shard, repo_id=ctx.REPORTS_DATASET_ID, repo_type="dataset", commit_message=f"report batch {len(todo)} -> {shard}")
158
+ except Exception as e_direct:
159
+ is_403 = False
160
+ try:
161
+ from huggingface_hub.utils import HfHubHTTPError as _HE
162
+ if isinstance(e_direct, _HE) and getattr(e_direct.response, "status_code", None) == 403:
163
+ is_403 = True
164
+ elif "403" in str(e_direct) or "Forbidden" in str(e_direct):
165
+ is_403 = True
166
+ except Exception:
167
+ pass
168
+ if is_403:
169
+ _step(" No write access β€” creating pull request…")
170
+ api.create_commit(
171
+ repo_id=ctx.REPORTS_DATASET_ID,
172
+ repo_type="dataset",
173
+ operations=[CommitOperationAdd(path_in_repo=shard, path_or_fileobj=str(tmpf))],
174
+ commit_message=f"report batch {len(todo)} -> {shard} ({todo[0]['report_id'][:8]}…)",
175
+ create_pr=True,
176
+ )
177
+ _ok(" PR opened β€” thank you! Maintainer will merge shortly.")
178
+ else:
179
+ raise
180
+ try:
181
+ tmpf.unlink(missing_ok=True)
182
+ except Exception:
183
+ pass
184
+ for r in todo:
185
+ try:
186
+ (ctx.REPORTS_OUTBOX / f"{r['report_id']}.json").unlink(missing_ok=True)
187
+ except Exception:
188
+ pass
189
+ pushed += len(todo)
190
+ break
191
+ except Exception as e:
192
+ try:
193
+ from huggingface_hub.utils import HfHubHTTPError as _HE
194
+ code = getattr(getattr(e, "response", None), "status_code", None)
195
+ if isinstance(e, _HE) and code in (429, 409, 412):
196
+ ra = _parse_retry_after(getattr(e.response, "headers", {}) or {})
197
+ time.sleep(ra or (0.7 * (2 ** attempt)))
198
+ continue
199
+ # 403 already handled via PR above; other 403 should not retry as direct
200
+ if isinstance(e, _HE) and code == 403:
201
+ # if PR also failed, keep in outbox
202
+ _warn(f" Push failed for {shard} (403 Forbidden) β€” kept in outbox. PR fallback also failed.")
203
+ break
204
+ except Exception:
205
+ pass
206
+ if attempt < 2 and isinstance(e, (urllib.error.URLError, TimeoutError, OSError)):
207
+ time.sleep(0.7 * (2 ** attempt))
208
+ continue
209
+ if attempt >= 2:
210
+ if ctx.VERBOSE:
211
+ import traceback
212
+ _say(traceback.format_exc())
213
+ _warn(f" Push failed for {shard} ({type(e).__name__}: {e}) β€” kept in outbox.")
214
+ break
215
+ time.sleep(0.7 * (2 ** attempt))
216
+ return pushed
217
+
218
+
219
+ def _push_report(record):
220
+ return _push_reports_batch([record]) == 1
221
+
222
+
223
+ def _last_operation():
224
+ try:
225
+ if not ctx.OPERATIONS_LOG.exists():
226
+ return None
227
+ # read last non-empty line
228
+ with open(ctx.OPERATIONS_LOG, "rb") as f:
229
+ f.seek(0, 2)
230
+ size = f.tell()
231
+ if size == 0:
232
+ return None
233
+ # read tail 8KB
234
+ read = min(size, 8192)
235
+ f.seek(size - read)
236
+ tail = f.read().decode("utf-8", "replace").strip().splitlines()
237
+ for line in reversed(tail):
238
+ if not line.strip():
239
+ continue
240
+ try:
241
+ rec = json.loads(line)
242
+ op = rec.get("op")
243
+ if op:
244
+ return op
245
+ except Exception:
246
+ continue
247
+ except Exception:
248
+ pass
249
+ return None
250
+
251
+
252
+ def cmd_report(args):
253
+ msg = _sanitize_note(getattr(args, "message", None))
254
+ voice_arg = getattr(args, "voice", None)
255
+ import uuid, platform
256
+ report_id = uuid.uuid4().hex
257
+ ts = _now_iso()
258
+ # sanitize voice/target early for CSV safety
259
+ voice_name = _sanitize_note(voice_arg.strip()) if voice_arg and voice_arg.strip() else None
260
+ target_raw = getattr(args, "target", None)
261
+ target_basename = _sanitize_note(Path(target_raw).name) if target_raw else None
262
+ # auto-pull voice provenance
263
+ voice_meta = None
264
+ try:
265
+ if voice_arg:
266
+ vp, vm = _registry_resolve(voice_arg)
267
+ if vp and vp.exists():
268
+ if vm is None:
269
+ vm = _read_meta(Path(vp).parent / "voice.json")
270
+ voice_meta = dict(vm or {})
271
+ if "tensor_sha256" not in voice_meta and vp.exists():
272
+ try:
273
+ voice_meta["tensor_sha256"] = _hash_file(str(vp))
274
+ except Exception:
275
+ pass
276
+ else:
277
+ # fallback: most recent voice if requested name not found
278
+ voices = sorted([d for d in _iter_voices() if (d / "voice.json").exists()], key=lambda p: p.stat().st_mtime, reverse=True)
279
+ if voices:
280
+ vm = _read_meta(voices[0] / "voice.json")
281
+ if vm:
282
+ voice_meta = dict(vm)
283
+ if not voice_name:
284
+ voice_name = _sanitize_note(vm.get("name") or voices[0].name)
285
+ else:
286
+ # zero flags: auto-pull most recent voice for full traceability
287
+ voices = sorted([d for d in _iter_voices() if (d / "voice.json").exists()], key=lambda p: p.stat().st_mtime, reverse=True)
288
+ if voices:
289
+ vm = _read_meta(voices[0] / "voice.json")
290
+ if vm:
291
+ voice_meta = dict(vm)
292
+ if not voice_name:
293
+ voice_name = _sanitize_note(vm.get("name") or voices[0].name)
294
+ except Exception:
295
+ pass
296
+ # auto-probe target file if exists
297
+ target_info = None
298
+ if target_raw and Path(target_raw).expanduser().exists():
299
+ try:
300
+ p = Path(target_raw).expanduser()
301
+ if _is_gguf_file(str(p)):
302
+ from gguf import GGUFReader
303
+ r = GGUFReader(str(p))
304
+ arch = None
305
+ for fld in r.fields.values():
306
+ if fld.name == "general.architecture":
307
+ try:
308
+ v = fld.parts[fld.data[0]]
309
+ if isinstance(v, bytes):
310
+ arch = v.decode()
311
+ elif hasattr(v, "tobytes"):
312
+ try:
313
+ arch = v.tobytes().decode()
314
+ except Exception:
315
+ arch = str(v)
316
+ else:
317
+ arch = str(v)
318
+ arch = arch.strip("\x00").strip()
319
+ except Exception:
320
+ pass
321
+ break
322
+ # find output tensor
323
+ tname = None
324
+ for pref in ("output.weight", "token_embd.weight"):
325
+ for t in r.tensors:
326
+ if t.name == pref:
327
+ tname = t.name
328
+ break
329
+ if tname:
330
+ break
331
+ shape = None
332
+ dtype = None
333
+ if tname:
334
+ for t in r.tensors:
335
+ if t.name == tname:
336
+ shape = list(reversed([int(x) for x in t.shape]))
337
+ dtype = t.tensor_type.name
338
+ break
339
+ target_info = {"basename": target_basename, "format": "gguf", "tensor_name": tname, "architecture": arch, "family": _family(arch), "shape": shape, "dtype": dtype}
340
+ else:
341
+ # try safetensors probe (header only)
342
+ try:
343
+ hdr, tens = read_safetensors(str(p))
344
+ # pick output tensor
345
+ tname = detect_output_tensor(list(tens.keys()), None) or largest_2d(list(tens.keys()), {k: v["shape"] for k, v in tens.items()})
346
+ info = tens.get(tname) if tname else None
347
+ target_info = {"basename": target_basename, "format": "safetensors", "tensor_name": tname, "shape": list(info["shape"]) if info else None, "dtype": info["dtype"] if info else None}
348
+ except Exception:
349
+ target_info = {"basename": target_basename, "format": "safetensors"}
350
+ except Exception:
351
+ pass
352
+ if target_info is None and target_basename:
353
+ target_info = {"basename": target_basename}
354
+ # env enrichment
355
+ try:
356
+ import numpy as _np
357
+ np_ver = getattr(_np, "__version__", None)
358
+ except Exception:
359
+ np_ver = None
360
+ try:
361
+ import gguf as _gg
362
+ gg_ver = getattr(_gg, "__version__", "installed")
363
+ except Exception:
364
+ gg_ver = None
365
+ env_info = {"voice_ver": ctx.VERSION, "python": platform.python_version(), "platform": platform.platform(), "numpy": np_ver, "gguf": gg_ver}
366
+ operation = getattr(args, "operation", None) or _last_operation() or "report"
367
+ record = {
368
+ "v": 2,
369
+ "report_id": report_id,
370
+ "ts": ts,
371
+ "voice": voice_name,
372
+ "voice_meta": voice_meta,
373
+ "target": target_basename,
374
+ "target_meta": target_info,
375
+ "operation": operation,
376
+ "status": "ok",
377
+ "message": msg,
378
+ "env": env_info,
379
+ }
380
+ # keep backward compat flat fields for old dataset readers
381
+ if voice_meta:
382
+ record["voice_source"] = _public_source(voice_meta.get("source_hf_model") or voice_meta.get("source"))
383
+ record["voice_tensor"] = voice_meta.get("tensor_name")
384
+ record["voice_dtype"] = voice_meta.get("dtype")
385
+ record["voice_shape"] = voice_meta.get("shape")
386
+ reporter = _sanitize_reporter(getattr(args, "reporter", None))
387
+ if reporter:
388
+ record["reporter"] = reporter
389
+ # auto-reporter from HF token if available (opt-in via --reporter not set)
390
+ if "reporter" not in record:
391
+ # do not auto-fill PII; keep null unless user opted
392
+ pass
393
+ # validate size
394
+ try:
395
+ rec_json = json.dumps(record, ensure_ascii=False)
396
+ if len(rec_json) > 4096:
397
+ _warn(" Note too large β€” truncating.")
398
+ record["message"] = _sanitize_note((msg or "")[:240])
399
+ except Exception:
400
+ pass
401
+ _append_report(record)
402
+ _ok(f"Thanks for noting β€” saved locally ({report_id[:8]}…) β†’ {ctx.REPORTS_JSONL}")
403
+ _say(f" Outbox: {ctx.REPORTS_OUTBOX / (report_id + '.json')} (private, stays until you choose to share)")
404
+ # also handle --sync of pending
405
+ if getattr(args, "sync", False):
406
+ pending = sorted(ctx.REPORTS_OUTBOX.glob("*.json"))
407
+ if not pending:
408
+ _say(" No pending reports to sync.")
409
+ return
410
+ _step(f"Syncing {len(pending)} pending report(s)…")
411
+ recs = []
412
+ for p in pending:
413
+ try:
414
+ recs.append(json.loads(p.read_text()))
415
+ except Exception as e:
416
+ _warn(f" Could not read {p.name}: {e}")
417
+ if recs:
418
+ n = _push_reports_batch(recs)
419
+ _ok(f"Synced {n}/{len(recs)} report(s).")
420
+ return
421
+ # warm preview β€” what will be shared
422
+ preview = None
423
+ try:
424
+ pv = voice_name or (voice_meta.get("name") if voice_meta else None) or "β€”"
425
+ pt = (voice_meta.get("tensor_name") if voice_meta else None) or (target_info.get("tensor_name") if target_info else None) or "β€”"
426
+ ps = voice_meta.get("shape") if voice_meta and voice_meta.get("shape") else (target_info.get("shape") if target_info else None)
427
+ pd = voice_meta.get("dtype") if voice_meta and voice_meta.get("dtype") else (target_info.get("dtype") if target_info else None)
428
+ pv_str = f"voice: {pv}, tensor: {pt}"
429
+ if ps:
430
+ pv_str += f" {ps}"
431
+ if pd:
432
+ pv_str += f" {pd}"
433
+ if target_basename:
434
+ pv_str += f", target: {target_basename}"
435
+ if target_info and target_info.get("family"):
436
+ pv_str += f" {target_info.get('family')}"
437
+ preview = pv_str
438
+ except Exception:
439
+ preview = None
440
+ should_push = _prompt_share(args, preview=preview)
441
+ if should_push:
442
+ _step("Sharing to community to help others…")
443
+ ok = _push_report(record)
444
+ if ok:
445
+ _ok("Shared to community β€” thank you! Helps the next person.")
446
+ pending = sorted(ctx.REPORTS_OUTBOX.glob("*.json"))
447
+ if pending:
448
+ recs = []
449
+ for p in pending:
450
+ try:
451
+ recs.append(json.loads(p.read_text()))
452
+ except Exception:
453
+ continue
454
+ if recs:
455
+ _step(f"Syncing {len(recs)} pending report(s)…")
456
+ _push_reports_batch(recs)
457
+ # headroom warning
458
+ try:
459
+ sz = ctx.REPORTS_JSONL.stat().st_size if ctx.REPORTS_JSONL.exists() else 0
460
+ if sz > 50 * 1024 * 1024:
461
+ _warn(f" reports.jsonl {sz/1e6:.1f} MB β€” consider archiving (rotation to reports-YYYY-MM.jsonl).")
462
+ if len(list(ctx.REPORTS_OUTBOX.glob("*.json"))) > 5000:
463
+ _warn(" Outbox >5000 pending β€” run voice report --sync --yes to flush.")
464
+ except Exception:
465
+ pass
466
+ else:
467
+ _warn(" Share pending β€” will retry on next report or voice doctor.")
468
+ else:
469
+ _say(" Saved locally. Outbox retained for later push (rerun with --yes to share).")
vlib/sources.py ADDED
@@ -0,0 +1,507 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Model sources: naming heuristics, HF metadata, source objects."""
2
+
3
+ from pathlib import Path
4
+ import json
5
+ import re
6
+ import struct
7
+ import urllib.error
8
+ import urllib.request
9
+ from urllib.parse import urlparse
10
+ from vlib import ctx
11
+ from vlib.ui import _fail, _note
12
+ from vlib.net import _hf_auth_headers, http_get_json, http_range
13
+ from vlib.tensors import _gguf_raw_to_f32, _gguf_tensor_f32, _is_gguf_file, _parse_gguf_header, decode_to_f32
14
+
15
+
16
+ OUTPUT_ALIASES = (
17
+ "lm_head.weight", "output.weight", "transformer.output_layer.weight",
18
+ "output_layer.weight", "embed_out.weight",
19
+ )
20
+
21
+
22
+ EMBEDDING_ALIASES = (
23
+ "model.embed_tokens.weight", "embed_tokens.weight",
24
+ "transformer.embedding.word_embeddings.weight", "wte.weight", "word_embeddings.weight",
25
+ )
26
+
27
+
28
+ def _family(arch):
29
+ a = (arch or "").lower()
30
+ if "glm" in a or "chatglm" in a:
31
+ return "GLM"
32
+ if "gemma4" in a:
33
+ return "Gemma 4"
34
+ if "gemma" in a:
35
+ return "Gemma"
36
+ if "qwen" in a:
37
+ return "Qwen"
38
+ if "llama" in a:
39
+ return "Llama"
40
+ if "gpt2" in a or "gptneo" in a:
41
+ return "GPT"
42
+ if "opt" in a:
43
+ return "OPT"
44
+ return arch or "unknown"
45
+
46
+
47
+ def _nested(cfg, *keys):
48
+ for k in keys:
49
+ if isinstance(cfg.get(k), (int, str, list)):
50
+ return cfg[k]
51
+ tc = cfg.get("text_config") or {}
52
+ for k in keys:
53
+ if isinstance(tc.get(k), (int, str, list)):
54
+ return tc[k]
55
+ return None
56
+
57
+
58
+ def detect_output_tensor(names, config):
59
+ if not names:
60
+ return None
61
+ for alias in OUTPUT_ALIASES:
62
+ if alias in names:
63
+ return alias
64
+ for n in names:
65
+ if any(a in n for a in ("lm_head", "output_layer", "embed_out")) and n.endswith("weight"):
66
+ return n
67
+ tie = config.get("tie_word_embeddings") if config else None
68
+ if tie is True:
69
+ for alias in EMBEDDING_ALIASES:
70
+ if alias in names:
71
+ return alias
72
+ # any embedding-like tensor (tied layouts, incl. transformer.wte.weight)
73
+ for n in names:
74
+ if n.endswith(("embed_tokens.weight", "word_embeddings.weight", "wte.weight")):
75
+ return n
76
+ return None
77
+
78
+
79
+ def largest_2d(names, shapes):
80
+ best, best_elems = None, 0
81
+ for n in names:
82
+ s = shapes.get(n)
83
+ if s is not None and len(s) == 2:
84
+ elems = int(s[0]) * int(s[1])
85
+ if elems > best_elems:
86
+ best, best_elems = n, elems
87
+ return best
88
+
89
+
90
+ def _hf_config(model_id):
91
+ try:
92
+ return http_get_json(f"https://huggingface.co/{model_id}/resolve/main/config.json", timeout=30)
93
+ except Exception:
94
+ return None
95
+
96
+
97
+ def _hf_license(model_id):
98
+ for url in (f"https://huggingface.co/{model_id}/resolve/main/LICENSE", f"https://huggingface.co/{model_id}/raw/main/LICENSE"):
99
+ try:
100
+ req = urllib.request.Request(url, headers=_hf_auth_headers(url))
101
+ with urllib.request.urlopen(req, timeout=15) as resp:
102
+ text = resp.read(2000).decode("utf-8", "replace").strip()
103
+ if len(text) > 10:
104
+ return text[:200]
105
+ except Exception:
106
+ continue
107
+ return None
108
+
109
+
110
+ def _hf_model_index(model_id, rev="main"):
111
+ try:
112
+ return http_get_json(f"https://huggingface.co/{model_id}/resolve/{rev}/model.safetensors.index.json", timeout=20)
113
+ except Exception:
114
+ return None
115
+
116
+
117
+ def _normalize_hf_repo(s):
118
+ if not s or not s.strip():
119
+ return None
120
+ s = s.strip().rstrip("/")
121
+ if s.startswith(("http://", "https://")):
122
+ try:
123
+ parts = urlparse(s).path.strip("/").split("/")
124
+ if len(parts) >= 2 and parts[0] and parts[1]:
125
+ return f"{parts[0]}/{parts[1]}"
126
+ except Exception:
127
+ return None
128
+ return None
129
+ s = s.split("@")[0].split(":")[0].strip()
130
+ parts = s.split("/")
131
+ if len(parts) >= 2 and parts[0] and parts[1]:
132
+ return f"{parts[0]}/{parts[1]}"
133
+ return None
134
+
135
+
136
+ def _base_paths(base_id):
137
+ norm = _normalize_hf_repo(base_id)
138
+ if norm is None:
139
+ return None
140
+ org, model = (x.lower() for x in norm.split("/"))
141
+ file_path = ctx.VOICES_DIR / "base" / org / f"{model}.safetensors"
142
+ return file_path, ctx.VOICES_DIR / "base" / org / f"{model}.json"
143
+
144
+
145
+ def _hf_api_base_model(source_id):
146
+ try:
147
+ data = http_get_json(f"https://huggingface.co/api/models/{source_id}", timeout=15)
148
+ card = data.get("cardData") or {}
149
+ bm = card.get("base_model") or data.get("base_model")
150
+ if bm is None:
151
+ return None
152
+ if isinstance(bm, str):
153
+ return [bm]
154
+ if isinstance(bm, list):
155
+ return [x for x in bm if isinstance(x, str) and x.strip()]
156
+ except Exception:
157
+ return None
158
+ return None
159
+
160
+
161
+ def _hf_mergekit_base(source_id):
162
+ for tmpl in (
163
+ f"https://huggingface.co/{source_id}/raw/main/mergekit_config.yml",
164
+ f"https://huggingface.co/{source_id}/raw/main/mergekit_config.yaml",
165
+ ):
166
+ try:
167
+ req = urllib.request.Request(tmpl, headers=_hf_auth_headers(tmpl))
168
+ with urllib.request.urlopen(req, timeout=15) as resp:
169
+ text = resp.read().decode("utf-8", "replace")
170
+ for line in text.splitlines():
171
+ line = line.strip()
172
+ if line.startswith("#"):
173
+ continue
174
+ m = re.search(r'base_model\s*:\s*["\']?\s*([A-Za-z0-9._-]+/[A-Za-z0-9._-]+)', line)
175
+ if m:
176
+ return m.group(1).strip()
177
+ except Exception:
178
+ continue
179
+ return None
180
+
181
+
182
+ def _detect_base(source_id):
183
+ raw = _hf_api_base_model(source_id)
184
+ if not raw:
185
+ return None, False, 0, []
186
+ if len(raw) == 1:
187
+ return raw[0], False, 1, raw
188
+ tried = _hf_mergekit_base(source_id)
189
+ if tried:
190
+ return tried, True, len(raw), raw
191
+ return raw[0], True, len(raw), raw
192
+
193
+
194
+ class TensorRef:
195
+ __slots__ = ("name", "dtype", "shape")
196
+
197
+ def __init__(self, name, dtype, shape):
198
+ self.name = name
199
+ self.dtype = dtype
200
+ self.shape = tuple(int(x) for x in shape)
201
+
202
+ def __repr__(self):
203
+ return f"TensorRef({self.name!r}, {self.dtype}, {self.shape})"
204
+
205
+
206
+ class SafetensorsSource:
207
+ kind = "safetensors"
208
+
209
+ def __init__(self, path=None, repo=None, url=None, rev=None):
210
+ self.path = str(path) if path else None
211
+ self.repo = repo
212
+ self.url = url
213
+ self.rev = rev or "main"
214
+ self._header = None
215
+ self._header_size = None
216
+ self._index = None # weight_map for sharded repos
217
+ self._shard_headers = {} # shard file -> (header_size, tensors)
218
+
219
+ def _local(self):
220
+ """(header_size, tensors) for a local file β€” read once, then cached."""
221
+ if self.path and self._header_size is None:
222
+ with open(self.path, "rb") as f:
223
+ self._header_size = struct.unpack("<Q", f.read(8))[0]
224
+ self._header = json.loads(f.read(self._header_size))
225
+ return self._header_size, {k: v for k, v in self._header.items() if k != "__metadata__"}
226
+
227
+ def _single_url(self):
228
+ if self.url:
229
+ return self.url
230
+ return f"https://huggingface.co/{self.repo}/resolve/{self.rev}/model.safetensors"
231
+
232
+ def _load_index(self):
233
+ if self._index is not None or self.path or self.url:
234
+ return
235
+ idx = _hf_model_index(self.repo, self.rev)
236
+ self._index = idx.get("weight_map") if idx else None
237
+
238
+ def names(self):
239
+ if self.path:
240
+ return list(self._local()[1].keys())
241
+ if self.repo and self.url is None:
242
+ self._load_index()
243
+ if self._index is not None:
244
+ return list(self._index.keys())
245
+ # single remote file
246
+ self._load_single_header()
247
+ return list(self._tensors().keys())
248
+
249
+ def _load_single_header(self):
250
+ if self._header is not None:
251
+ return
252
+ raw, _st, _e = http_range(self._single_url(), 0, 7)
253
+ self._header_size = struct.unpack("<Q", raw)[0]
254
+ raw, _st, _e = http_range(self._single_url(), 8, 8 + self._header_size - 1)
255
+ self._header = json.loads(raw)
256
+
257
+ def _tensors(self):
258
+ return {k: v for k, v in self._header.items() if k != "__metadata__"}
259
+
260
+ def _shard_info(self, name):
261
+ """Return (url, header_size, tensors) for the shard containing `name`."""
262
+ if self.path:
263
+ hs, tensors = self._local()
264
+ return None, hs, tensors
265
+ if self.url:
266
+ self._load_single_header()
267
+ return self._single_url(), self._header_size, self._tensors()
268
+ # repo
269
+ self._load_index()
270
+ if self._index is None:
271
+ self._load_single_header()
272
+ return self._single_url(), self._header_size, self._tensors()
273
+ shard = self._index.get(name)
274
+ if shard is None:
275
+ raise KeyError(name)
276
+ if shard not in self._shard_headers:
277
+ url = f"https://huggingface.co/{self.repo}/resolve/{self.rev}/{shard}"
278
+ raw, _st, _e = http_range(url, 0, 7)
279
+ hs = struct.unpack("<Q", raw)[0]
280
+ raw, _st, _e = http_range(url, 8, 8 + hs - 1)
281
+ tensors = {k: v for k, v in json.loads(raw).items() if k != "__metadata__"}
282
+ self._shard_headers[shard] = (url, hs, tensors)
283
+ return self._shard_headers[shard]
284
+
285
+ def ref(self, name):
286
+ if self.path:
287
+ info = self._local()[1][name]
288
+ return TensorRef(name, info["dtype"], info["shape"])
289
+ url, hs, tensors = self._shard_info(name)
290
+ info = tensors[name]
291
+ return TensorRef(name, info["dtype"], info["shape"])
292
+
293
+ def read_raw(self, name):
294
+ if self.path:
295
+ hs, tensors = self._local()
296
+ info = tensors[name]
297
+ begin, end = info["data_offsets"]
298
+ with open(self.path, "rb") as f:
299
+ f.seek(8 + hs + begin)
300
+ return f.read(end - begin)
301
+ url, hs, tensors = self._shard_info(name)
302
+ info = tensors[name]
303
+ begin, end = info["data_offsets"]
304
+ data, _st, _e = http_range(url, 8 + hs + begin, 8 + hs + end - 1, timeout=300)
305
+ return data
306
+
307
+ def read_raw_slice(self, name, byte_off, byte_len):
308
+ """Minimal slice read: byte_off/byte_len relative to the tensor's data."""
309
+ if self.path:
310
+ hs, tensors = self._local()
311
+ info = tensors[name]
312
+ begin = info["data_offsets"][0] + byte_off
313
+ with open(self.path, "rb") as f:
314
+ f.seek(8 + hs + begin)
315
+ return f.read(byte_len)
316
+ url, hs, tensors = self._shard_info(name)
317
+ info = tensors[name]
318
+ begin = info["data_offsets"][0] + byte_off
319
+ data, _st, _e = http_range(url, 8 + hs + begin, 8 + hs + begin + byte_len - 1, timeout=300)
320
+ return data
321
+
322
+ def read_f32(self, name):
323
+ info = self.ref(name)
324
+ return decode_to_f32(self.read_raw(name), info.dtype, info.shape)
325
+
326
+
327
+ class GgufSource:
328
+ kind = "gguf"
329
+
330
+ def __init__(self, path=None, url=None, rev=None):
331
+ self.path = str(path) if path else None
332
+ self.url = url
333
+ self.rev = rev or "main"
334
+ self._reader = None
335
+ self._remote = None
336
+
337
+ def _load(self):
338
+ if self.path:
339
+ if self._reader is None:
340
+ from gguf import GGUFReader
341
+ self._reader = GGUFReader(self.path)
342
+ elif self._remote is None:
343
+ self._remote = _parse_gguf_header(self.url)
344
+
345
+ def names(self):
346
+ self._load()
347
+ if self._reader is not None:
348
+ return [t.name for t in self._reader.tensors]
349
+ return list(self._remote[2].keys())
350
+
351
+ def ref(self, name):
352
+ from gguf.constants import GGMLQuantizationType as Q
353
+ self._load()
354
+ if self._reader is not None:
355
+ for t in self._reader.tensors:
356
+ if t.name == name:
357
+ return TensorRef(name, t.tensor_type.name, tuple(reversed(t.shape)))
358
+ raise KeyError(name)
359
+ dims, gtype, _off, _nbytes = self._remote[2][name]
360
+ return TensorRef(name, Q(gtype).name, tuple(reversed(dims)))
361
+
362
+ def read_f32(self, name):
363
+ from gguf.constants import GGMLQuantizationType as Q
364
+ self._load()
365
+ if self._reader is not None:
366
+ for t in self._reader.tensors:
367
+ if t.name == name:
368
+ return _gguf_tensor_f32(t)
369
+ raise KeyError(name)
370
+ dims, gtype, data_offset, n_bytes = self._remote[2][name]
371
+ start = self._remote[1] + data_offset
372
+ raw, _st, _e = http_range(self.url, start, start + n_bytes - 1, timeout=300)
373
+ return _gguf_raw_to_f32(raw, Q(gtype), dims)
374
+
375
+ def read_raw(self, name):
376
+ """Exact tensor bytes in file order (any quant, incl. K/IQ blocks)."""
377
+ import numpy as np
378
+ if self.path:
379
+ self._load()
380
+ for t in self._reader.tensors:
381
+ if t.name == name:
382
+ return t.data.tobytes() if t.data.dtype == np.uint8 else memoryview(t.data).cast("B").tobytes()
383
+ raise KeyError(name)
384
+ dims, gtype, data_offset, n_bytes = self._remote[2][name]
385
+ start = self._remote[1] + data_offset
386
+ raw, _st, _e = http_range(self.url, start, start + n_bytes - 1, timeout=300)
387
+ return raw
388
+
389
+ def read_raw_slice(self, name, byte_off, byte_len):
390
+ """Minimal slice read: byte_off/byte_len relative to the tensor's raw bytes."""
391
+ import numpy as np
392
+ if self.path:
393
+ self._load()
394
+ for t in self._reader.tensors:
395
+ if t.name == name:
396
+ try:
397
+ # flatten to bytes FIRST: bare mv[i:j] slices dim-0 rows, not bytes
398
+ mv = memoryview(t.data).cast("B")
399
+ return mv[byte_off:byte_off + byte_len].tobytes()
400
+ except Exception:
401
+ return self.read_raw(name)[byte_off:byte_off + byte_len]
402
+ raise KeyError(name)
403
+ dims, gtype, data_offset, n_bytes = self._remote[2][name]
404
+ start = self._remote[1] + data_offset + byte_off
405
+ raw, _st, _e = http_range(self.url, start, start + byte_len - 1, timeout=300)
406
+ return raw
407
+
408
+
409
+ def open_source(arg, rev=None):
410
+ p = Path(arg).expanduser()
411
+ if p.exists():
412
+ if p.is_dir():
413
+ ggufs = sorted(p.glob("*.gguf"))
414
+ if ggufs:
415
+ return GgufSource(path=str(ggufs[0]))
416
+ if (p / "model.safetensors").exists():
417
+ return SafetensorsSource(path=str(p / "model.safetensors"))
418
+ _fail(f" βœ— No model.safetensors or .gguf found in {arg}.")
419
+ if p.is_file():
420
+ return GgufSource(path=str(p)) if _is_gguf_file(str(p)) else SafetensorsSource(path=str(p))
421
+ if arg.startswith(("http://", "https://")):
422
+ if arg.rstrip("/").split("/")[-1].endswith(".gguf"):
423
+ return GgufSource(url=arg)
424
+ return SafetensorsSource(url=arg)
425
+ return _source_from_repo(arg.strip().rstrip("/"), rev=rev)
426
+
427
+
428
+ def _source_from_repo(repo, rev=None):
429
+ rev = rev or "main"
430
+ try:
431
+ tree = http_get_json(f"https://huggingface.co/api/models/{repo}/tree/{rev}", timeout=20)
432
+ files = [f.get("path", "") for f in tree if f.get("path")]
433
+ ggufs = [f for f in files if f.endswith(".gguf")]
434
+ if ggufs:
435
+ chosen = _pick_gguf(ggufs)
436
+ if ctx.VERBOSE:
437
+ _note(f"GGUF repo β€” using {chosen}")
438
+ return GgufSource(url=f"https://huggingface.co/{repo}/resolve/{rev}/{chosen}", rev=rev)
439
+ if any(f.endswith(".safetensors") for f in files):
440
+ return SafetensorsSource(repo=repo, rev=rev)
441
+ if files:
442
+ _fail(f" βœ— '{repo}' has no .safetensors or .gguf files (found: {', '.join(files[:6])}{'…' if len(files) > 6 else ''}).")
443
+ except SystemExit:
444
+ raise
445
+ except Exception:
446
+ pass
447
+ return SafetensorsSource(repo=repo, rev=rev)
448
+
449
+
450
+ def _pick_gguf(files):
451
+ files = sorted(files)
452
+ for key in ("fp16", "f16", "q8_0"):
453
+ for f in files:
454
+ if key in f.lower():
455
+ return f
456
+ return files[0]
457
+
458
+
459
+ def _sanitize_voice_name(name):
460
+ if not name or not name.strip():
461
+ return None
462
+ name = name.strip()
463
+ if name.lower().startswith("delta-") or name in (".", ".."):
464
+ return None
465
+ bad = set("/\\.: ")
466
+ for c in name:
467
+ if c in bad or ord(c) < 32:
468
+ return None
469
+ return name
470
+
471
+
472
+ _NAME_RULES = "Use letters, numbers, hyphens, underscores (no spaces, /, \\, dots, colons; not starting with delta-)."
473
+
474
+
475
+ def _public_source(ref):
476
+ """Share-safe form of a model reference for voice.json/reports.
477
+ HF ids pass through; URLs lose userinfo/query/fragment; local paths
478
+ collapse to basename (directories leak usernames)."""
479
+ s = (ref or "").strip()
480
+ if not s or s == "β€”":
481
+ return s
482
+ try:
483
+ u = urlparse(s)
484
+ if u.scheme in ("http", "https"):
485
+ net = u.hostname or ""
486
+ if u.port:
487
+ net += f":{u.port}"
488
+ return f"{u.scheme}://{net}{u.path}"
489
+ if u.scheme == "file":
490
+ return Path(u.path).name or s
491
+ except Exception:
492
+ pass
493
+ if (s.startswith(("/", "~", ".")) or "\\" in s or len(s) > 2 and s[1] == ":"
494
+ or Path(s).expanduser().exists()):
495
+ return Path(s).name or s
496
+ return s
497
+
498
+
499
+ def _slugify_voice_name(raw, fallback="voice"):
500
+ """Derive a valid voice name from arbitrary input (filenames, model IDs).
501
+ Lowercases, maps runs of invalid chars to '-', collapses, trims to 64 chars."""
502
+ s = str(raw or "").strip().lower()
503
+ if not s:
504
+ return fallback
505
+ s = re.sub(r"[^a-z0-9_-]+", "-", s)
506
+ s = re.sub(r"-+", "-", s).strip("-_")[:64]
507
+ return s or fallback
vlib/tensors.py ADDED
@@ -0,0 +1,488 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tensor file-format primitives: safetensors, GGUF headers, dtype math, quant predicates."""
2
+
3
+ from pathlib import Path
4
+ import json
5
+ import os
6
+ import struct
7
+ import urllib.error
8
+ import urllib.request
9
+ from vlib.ui import _fail
10
+ from vlib.net import _hf_auth_headers
11
+
12
+
13
+ def _validate_safetensors_header(header, file_size, header_size):
14
+ """Validate data_offsets are sane. Raises ValueError on corruption."""
15
+ tensors = {k: v for k, v in header.items() if k != "__metadata__"}
16
+ data_len = file_size - 8 - header_size
17
+ if data_len < 0:
18
+ raise ValueError(f"header {header_size} exceeds file {file_size}")
19
+ # collect and validate offsets
20
+ intervals = []
21
+ for name, info in tensors.items():
22
+ if not isinstance(info.get("data_offsets"), (list, tuple)) or len(info["data_offsets"]) != 2:
23
+ raise ValueError(f"tensor {name!r} missing data_offsets")
24
+ begin, end = info["data_offsets"]
25
+ if not isinstance(begin, int) or not isinstance(end, int) or begin < 0 or end < begin or end > data_len:
26
+ raise ValueError(f"tensor {name!r} offsets [{begin},{end}) out of bounds (data_len {data_len})")
27
+ shape = info.get("shape")
28
+ dtype = info.get("dtype")
29
+ if dtype is not None:
30
+ try:
31
+ bpe = dtype_bytes(dtype)
32
+ except ValueError:
33
+ raise ValueError(f"tensor {name!r} unknown dtype {dtype!r}")
34
+ if isinstance(shape, (list, tuple)):
35
+ prod = 1
36
+ for d in shape:
37
+ prod *= int(d)
38
+ expected = prod * bpe
39
+ if expected != (end - begin):
40
+ raise ValueError(f"tensor {name!r} shape {shape} dtype {dtype} expects {expected} bytes, got {end-begin}")
41
+ intervals.append((begin, end, name))
42
+ intervals.sort()
43
+ for i in range(1, len(intervals)):
44
+ if intervals[i][0] < intervals[i-1][1]:
45
+ raise ValueError(f"overlapping offsets: {intervals[i-1][2]!r} and {intervals[i][2]!r}")
46
+
47
+
48
+ def read_safetensors(path):
49
+ try:
50
+ file_size = os.path.getsize(path)
51
+ with open(path, "rb") as f:
52
+ raw = f.read(8)
53
+ if len(raw) < 8:
54
+ raise ValueError("file too short")
55
+ header_size = struct.unpack("<Q", raw)[0]
56
+ if header_size <= 0 or header_size + 8 > file_size:
57
+ raise ValueError(f"header_size {header_size} not sane for {file_size} bytes")
58
+ header = json.loads(f.read(header_size))
59
+ _validate_safetensors_header(header, file_size, header_size)
60
+ except (json.JSONDecodeError, ValueError, OSError, struct.error) as exc:
61
+ raise ValueError(f"{path} is not a valid .safetensors file.") from exc
62
+ return header, {k: v for k, v in header.items() if k != "__metadata__"}
63
+
64
+
65
+ def write_safetensors(tensors, out_path):
66
+ header, offset = {}, 0
67
+ for name, (data, dtype, shape) in tensors.items():
68
+ n = len(data)
69
+ header[name] = {"dtype": dtype, "shape": list(shape), "data_offsets": [offset, offset + n]}
70
+ offset += n
71
+ hj = json.dumps(header).encode("utf-8")
72
+ # atomic write via tmp + fsync
73
+ tmp_path = str(out_path) + ".tmp"
74
+ with open(tmp_path, "wb") as f:
75
+ f.write(struct.pack("<Q", len(hj)))
76
+ f.write(hj)
77
+ for name in tensors:
78
+ f.write(tensors[name][0])
79
+ f.flush()
80
+ try:
81
+ os.fsync(f.fileno())
82
+ except Exception:
83
+ pass
84
+ os.replace(tmp_path, str(out_path))
85
+ try:
86
+ # fsync parent dir for durability
87
+ dirfd = os.open(os.path.dirname(os.path.abspath(str(out_path))) or ".", os.O_DIRECTORY)
88
+ try:
89
+ os.fsync(dirfd)
90
+ finally:
91
+ os.close(dirfd)
92
+ except Exception:
93
+ pass
94
+ return os.path.getsize(out_path)
95
+
96
+
97
+ def _write_safetensors_streaming(tensor_name, src_path, src_total, dtype, shape, out_path):
98
+ header = {tensor_name: {"dtype": dtype, "shape": list(shape), "data_offsets": [0, src_total]}}
99
+ hj = json.dumps(header).encode("utf-8")
100
+ with open(out_path, "wb") as out, open(src_path, "rb") as inp:
101
+ out.write(struct.pack("<Q", len(hj)))
102
+ out.write(hj)
103
+ while True:
104
+ b = inp.read(8 << 20)
105
+ if not b:
106
+ break
107
+ out.write(b)
108
+ out.flush()
109
+ try:
110
+ os.fsync(out.fileno())
111
+ except Exception:
112
+ pass
113
+
114
+
115
+ def rewrite_safetensors_tensor(target_path, out_path, replace_name, replace_bytes, replace_dtype, replace_shape):
116
+ """Copy target_path -> out_path, replacing one tensor's bytes. The tensor's
117
+ dtype/shape may change but its byte size must stay identical (offsets fixed)."""
118
+ with open(target_path, "rb") as f:
119
+ header_size = struct.unpack("<Q", f.read(8))[0]
120
+ raw_hdr = f.read(header_size)
121
+ if len(raw_hdr) != header_size:
122
+ _fail(f" βœ— Truncated header in {target_path}.")
123
+ header = json.loads(raw_hdr)
124
+ _validate_safetensors_header(header, os.path.getsize(target_path), header_size)
125
+ tensors = {k: v for k, v in header.items() if k != "__metadata__"}
126
+ if replace_name not in tensors:
127
+ _fail(f" βœ— Tensor '{replace_name}' not found in {target_path}.")
128
+ old_n = tensors[replace_name]["data_offsets"][1] - tensors[replace_name]["data_offsets"][0]
129
+ if len(replace_bytes) != old_n:
130
+ _fail(f" βœ— Byte-size mismatch replacing '{replace_name}' ({len(replace_bytes)} vs {old_n}).")
131
+ new_header = dict(header)
132
+ offset = 0
133
+ for name in header:
134
+ if name == "__metadata__":
135
+ continue
136
+ if name == replace_name:
137
+ new_header[name] = {"dtype": replace_dtype, "shape": list(replace_shape), "data_offsets": [offset, offset + len(replace_bytes)]}
138
+ else:
139
+ n = tensors[name]["data_offsets"][1] - tensors[name]["data_offsets"][0]
140
+ new_header[name] = {"dtype": tensors[name]["dtype"], "shape": tensors[name]["shape"], "data_offsets": [offset, offset + n]}
141
+ offset += new_header[name]["data_offsets"][1] - new_header[name]["data_offsets"][0]
142
+ hj = json.dumps(new_header).encode("utf-8")
143
+ data_start = 8 + header_size
144
+ with open(target_path, "rb") as src, open(out_path, "wb") as out:
145
+ out.write(struct.pack("<Q", len(hj)))
146
+ out.write(hj)
147
+ for name in header:
148
+ if name == "__metadata__":
149
+ continue
150
+ begin, end = tensors[name]["data_offsets"]
151
+ if name == replace_name:
152
+ out.write(replace_bytes)
153
+ src.seek(data_start + end)
154
+ else:
155
+ src.seek(data_start + begin)
156
+ remaining = end - begin
157
+ while remaining:
158
+ b = src.read(min(8 << 20, remaining))
159
+ if not b:
160
+ _fail(f" βœ— Truncated read for tensor '{name}' in {target_path} (missing {remaining} bytes).")
161
+ out.write(b)
162
+ remaining -= len(b)
163
+ out.flush()
164
+ try:
165
+ os.fsync(out.fileno())
166
+ except Exception:
167
+ pass
168
+
169
+
170
+ def _safetensors_write_repl(target_path, repl, out_path):
171
+ """Single-pass safetensors rewrite. repl maps tensor name -> (bytes, dtype, shape).
172
+ Streams the target once: replacement bytes spliced in, unchanged bytes pass through.
173
+ Atomic via tmp+fsync+replace. Same per-tensor guards as rewrite_safetensors_tensor."""
174
+ with open(target_path, "rb") as f:
175
+ header_size = struct.unpack("<Q", f.read(8))[0]
176
+ raw_hdr = f.read(header_size)
177
+ if len(raw_hdr) != header_size:
178
+ _fail(f" βœ— Truncated header in {target_path}.")
179
+ header = json.loads(raw_hdr)
180
+ _validate_safetensors_header(header, os.path.getsize(target_path), header_size)
181
+ tensors = {k: v for k, v in header.items() if k != "__metadata__"}
182
+ for name in repl:
183
+ if name not in tensors:
184
+ _fail(f" βœ— Tensor '{name}' not found in {target_path}.")
185
+ old_n = tensors[name]["data_offsets"][1] - tensors[name]["data_offsets"][0]
186
+ if len(repl[name][0]) != old_n:
187
+ _fail(f" βœ— Byte-size mismatch replacing '{name}' ({len(repl[name][0])} vs {old_n}).")
188
+ new_header = dict(header)
189
+ offset = 0
190
+ for name in header:
191
+ if name == "__metadata__":
192
+ continue
193
+ if name in repl:
194
+ _rb, _rd, _rs = repl[name]
195
+ new_header[name] = {"dtype": _rd, "shape": list(_rs), "data_offsets": [offset, offset + len(_rb)]}
196
+ else:
197
+ n = tensors[name]["data_offsets"][1] - tensors[name]["data_offsets"][0]
198
+ new_header[name] = {"dtype": tensors[name]["dtype"], "shape": tensors[name]["shape"], "data_offsets": [offset, offset + n]}
199
+ offset += new_header[name]["data_offsets"][1] - new_header[name]["data_offsets"][0]
200
+ hj = json.dumps(new_header).encode("utf-8")
201
+ data_start = 8 + header_size
202
+ tmp_path = str(out_path) + ".tmp"
203
+ with open(target_path, "rb") as src, open(tmp_path, "wb") as out:
204
+ out.write(struct.pack("<Q", len(hj)))
205
+ out.write(hj)
206
+ for name in header:
207
+ if name == "__metadata__":
208
+ continue
209
+ begin, end = tensors[name]["data_offsets"]
210
+ if name in repl:
211
+ out.write(repl[name][0])
212
+ src.seek(data_start + end)
213
+ else:
214
+ src.seek(data_start + begin)
215
+ remaining = end - begin
216
+ while remaining:
217
+ b = src.read(min(8 << 20, remaining))
218
+ if not b:
219
+ _fail(f" βœ— Truncated read for tensor '{name}' in {target_path} (missing {remaining} bytes).")
220
+ out.write(b)
221
+ remaining -= len(b)
222
+ out.flush()
223
+ try:
224
+ os.fsync(out.fileno())
225
+ except Exception:
226
+ pass
227
+ os.replace(tmp_path, str(out_path))
228
+
229
+
230
+ _DTYPE_BYTES_MAP = {
231
+ "F32": 4, "FP32": 4, "F64": 8,
232
+ "F16": 2, "FP16": 2, "BF16": 2, "BF16E8M": 2, "F16E5M6": 2,
233
+ "I8": 1, "U8": 1, "BOOL": 1, "I16": 2, "I32": 4, "I64": 8,
234
+ }
235
+
236
+
237
+ def dtype_bytes(dtype):
238
+ try:
239
+ return _DTYPE_BYTES_MAP[dtype.upper()]
240
+ except KeyError:
241
+ raise ValueError(f"unsupported dtype {dtype}")
242
+
243
+
244
+ def bf16_to_f32(u16):
245
+ import numpy as np
246
+ return (u16.astype(np.uint32) << 16).view(np.float32).copy()
247
+
248
+
249
+ def decode_to_f32(raw, dtype, shape):
250
+ import numpy as np
251
+ d = dtype.upper()
252
+ # length sanity: prevent silent truncation or over-read
253
+ try:
254
+ bpe = dtype_bytes(d)
255
+ except ValueError:
256
+ raise
257
+ prod = 1
258
+ for dim in shape:
259
+ prod *= int(dim)
260
+ expected = prod * bpe
261
+ if len(raw) != expected:
262
+ raise ValueError(f"decode length mismatch for {dtype} {shape}: got {len(raw)} bytes, expected {expected}")
263
+ if d in ("BF16", "BF16E8M"):
264
+ return bf16_to_f32(np.frombuffer(raw, dtype=np.uint16)).reshape(shape)
265
+ if d in ("F16", "FP16", "F16E5M6"):
266
+ return np.frombuffer(raw, dtype=np.float16).reshape(shape).astype(np.float32)
267
+ if d in ("F32", "FP32"):
268
+ return np.frombuffer(raw, dtype=np.float32).reshape(shape).astype(np.float32)
269
+ raise ValueError(f"unsupported dtype {dtype}")
270
+
271
+
272
+ def encode_from_f32(arr, dtype):
273
+ import numpy as np
274
+ d = dtype.upper()
275
+ if d in ("F32", "FP32"):
276
+ return arr.astype(np.float32).tobytes()
277
+ if d in ("F16", "FP16", "F16E5M6"):
278
+ return arr.astype(np.float16).tobytes()
279
+ if d in ("BF16", "BF16E8M"):
280
+ u32 = arr.astype(np.float32).view(np.uint32)
281
+ # round-to-nearest-even (not truncation): matches standard BF16 cast
282
+ rounded = (u32 + 0x7FFF + ((u32 >> 16) & 1)) >> 16
283
+ return rounded.astype(np.uint16).tobytes()
284
+ raise ValueError(f"unsupported dtype {dtype}")
285
+
286
+
287
+ def _gguf_tensor_f32(t):
288
+ import numpy as np
289
+ from gguf.constants import GGMLQuantizationType as Q
290
+ from gguf.quants import dequantize
291
+ if t.tensor_type == Q.F32:
292
+ return np.ascontiguousarray(t.data, dtype=np.float32)
293
+ if t.tensor_type == Q.F16:
294
+ return t.data.astype(np.float32)
295
+ return dequantize(t.data, t.tensor_type)
296
+
297
+
298
+ def _gguf_raw_to_f32(raw, ggml_type, dims):
299
+ import numpy as np
300
+ from gguf.constants import GGMLQuantizationType as Q
301
+ from gguf.quants import dequantize, quant_shape_to_byte_shape
302
+ rshape = tuple(reversed(dims))
303
+ if ggml_type == Q.F32:
304
+ return np.frombuffer(raw, dtype=np.float32).reshape(rshape).astype(np.float32)
305
+ if ggml_type == Q.F16:
306
+ return np.frombuffer(raw, dtype=np.float16).reshape(rshape).astype(np.float32)
307
+ return dequantize(np.frombuffer(raw, dtype=np.uint8).reshape(quant_shape_to_byte_shape(rshape, ggml_type)), ggml_type)
308
+
309
+
310
+ def _gguf_raw_slice_to_f32(raw, ggml_type, off, n):
311
+ """Dequantize one element slice [off, off+n) of a quant tensor's raw bytes.
312
+ Expands to block alignment (blocks decode independently), dequantizes only the
313
+ covering blocks, then cuts the exact slice. Peak F32 is O(chunk), not O(tensor)."""
314
+ import numpy as np
315
+ from gguf.constants import GGML_QUANT_SIZES
316
+ from gguf.quants import dequantize
317
+ block_size, type_size = GGML_QUANT_SIZES[ggml_type]
318
+ u8 = np.frombuffer(raw, dtype=np.uint8)
319
+ b0 = off // block_size
320
+ b1 = (off + n + block_size - 1) // block_size
321
+ f32 = dequantize(u8[b0 * type_size:b1 * type_size].reshape(-1, type_size), ggml_type).reshape(-1)
322
+ return f32[off - b0 * block_size:off - b0 * block_size + n].astype(np.float32)
323
+
324
+
325
+ _DELTA_CHUNK_ELEMS = 1 << 20 # 1M-elem (~4MB F32) streaming seam for delta math; mirrors the fast path
326
+
327
+
328
+ def _gguf_nbytes(dims, ggml_type):
329
+ from gguf.constants import GGML_QUANT_SIZES
330
+ n = 1
331
+ for d in dims:
332
+ n *= int(d)
333
+ block_size, type_size = GGML_QUANT_SIZES[ggml_type]
334
+ return n * type_size // block_size
335
+
336
+
337
+ def _parse_gguf_header(url):
338
+ """Fetch a remote GGUF's metadata + tensor table.
339
+ Returns (arch, header_size, {name: (dims, ggml_type, data_offset, n_bytes)}),
340
+ where header_size is the aligned offset of the data section."""
341
+ from gguf.constants import GGMLQuantizationType as Q
342
+ buf, base = b"", 0
343
+
344
+ def need(n):
345
+ nonlocal buf, base
346
+ while len(buf) < n:
347
+ a = base + len(buf)
348
+ b = a + max(n - len(buf), 256 * 1024) - 1
349
+ req = urllib.request.Request(url, headers=_hf_auth_headers(url, {"Range": f"bytes={a}-{b}"}))
350
+ with urllib.request.urlopen(req, timeout=120) as r:
351
+ chunk = r.read()
352
+ if not chunk:
353
+ break
354
+ buf += chunk
355
+
356
+ def take(n):
357
+ nonlocal buf, base
358
+ need(n)
359
+ out, buf, base = buf[:n], buf[n:], base + n
360
+ return out
361
+
362
+ u32 = lambda: struct.unpack("<I", take(4))[0]
363
+ u64 = lambda: struct.unpack("<Q", take(8))[0]
364
+ string = lambda: take(u64()).decode("utf-8", "replace")
365
+ sizes = {0: 1, 1: 1, 2: 2, 3: 2, 4: 4, 5: 4, 6: 4, 7: 1, 10: 8, 11: 8, 12: 8}
366
+
367
+ def skip_value(t):
368
+ if t == 8:
369
+ take(u64()); return
370
+ if t == 9:
371
+ et = u32(); n = u64()
372
+ if et not in sizes and et != 8:
373
+ raise ValueError(f"unknown GGUF array element type {et}")
374
+ if et == 8:
375
+ for _ in range(n):
376
+ take(u64())
377
+ else:
378
+ take(n * sizes[et])
379
+ return
380
+ if t not in sizes:
381
+ raise ValueError(f"unknown GGUF value type {t}")
382
+ take(sizes[t])
383
+
384
+ if u32() != 0x46554747:
385
+ raise ValueError("not a GGUF file")
386
+ u32() # version
387
+ tensor_count = u64()
388
+ kv_count = u64()
389
+ arch = None
390
+ alignment = 32
391
+ for _ in range(kv_count):
392
+ key = string()
393
+ t = u32()
394
+ if key == "general.architecture" and t == 8:
395
+ arch = take(u64()).decode("utf-8", "replace")
396
+ elif key == "general.alignment" and t == 4:
397
+ alignment = max(1, u32())
398
+ else:
399
+ skip_value(t)
400
+ tensors = {}
401
+ for _ in range(tensor_count):
402
+ name = string()
403
+ nd = u32()
404
+ dims = [u64() for _ in range(nd)]
405
+ gtype = u32()
406
+ data_offset = u64()
407
+ tensors[name] = (dims, Q(gtype), data_offset, _gguf_nbytes(dims, Q(gtype)))
408
+ # Tensor data_offsets are relative to the data section, which starts after
409
+ # the header (+ tensor infos), padded up to `general.alignment`.
410
+ header_size = base + ((-base) % alignment)
411
+ return arch, header_size, tensors
412
+
413
+
414
+ def _is_gguf_file(path):
415
+ try:
416
+ with open(path, "rb") as f:
417
+ data = f.read(4)
418
+ return len(data) == 4 and data == b"GGUF"
419
+ except Exception:
420
+ return False
421
+
422
+
423
+ _QUANT_DTYPES = frozenset()
424
+
425
+
426
+ try:
427
+ from gguf.constants import GGMLQuantizationType as _QALL
428
+ _QUANT_DTYPES = frozenset(t.name for t in _QALL) - {"F32", "F16", "F64", "BF16",
429
+ "I8", "I16", "I32", "I64"}
430
+ except Exception:
431
+ _QUANT_DTYPES = frozenset({"Q4_0", "Q4_1", "Q5_0", "Q5_1", "Q8_0", "Q8_1", "Q8_K", "Q1_0",
432
+ "Q2_K", "Q3_K", "Q3_K_XS", "Q3_K_S", "Q3_K_M", "Q3_K_L",
433
+ "Q4_K", "Q4_K_S", "Q4_K_M", "Q5_K", "Q5_K_M", "Q6_K", "Q8_K",
434
+ "IQ1_S", "IQ1_M", "IQ2_XXS", "IQ2_XS", "IQ2_S", "IQ3_XXS", "IQ3_S", "IQ4_XS", "IQ4_NL",
435
+ "TQ1_0", "TQ2_0", "MXFP4", "NVFP4"})
436
+
437
+
438
+ def _is_ggml_quant(dtype):
439
+ return dtype.upper() in _QUANT_DTYPES
440
+
441
+
442
+ def _is_quant_fetched(fetched):
443
+ return _is_ggml_quant(fetched[3])
444
+
445
+
446
+ def _is_delta_safetensors(header_or_path):
447
+ """Check if file/header indicates delta via marker or metadata."""
448
+ try:
449
+ if isinstance(header_or_path, (str, Path)):
450
+ with open(header_or_path, "rb") as f:
451
+ hs = struct.unpack("<Q", f.read(8))[0]
452
+ hdr = json.loads(f.read(hs))
453
+ else:
454
+ hdr = header_or_path
455
+ if "delta.voice.marker" in hdr:
456
+ return True
457
+ meta = hdr.get("__metadata__")
458
+ if isinstance(meta, dict) and meta.get("type") == "delta":
459
+ return True
460
+ except Exception:
461
+ pass
462
+ return False
463
+
464
+
465
+ def _write_single_tensor_gguf(tensor_name, raw_path, n_bytes, ggml_type, shape, out_path):
466
+ """Write one tensor's exact raw quant blocks as a minimal standalone GGUF."""
467
+ from gguf import GGUFWriter
468
+ from gguf.constants import GGMLQuantizationType as Q
469
+ from gguf.quants import quant_shape_to_byte_shape
470
+ import numpy as np
471
+ qtype = Q[ggml_type]
472
+ # byte blocks are laid out in logical order (no reversal: the writer stores
473
+ # reversed file dims itself, and the reader exposes them back the same way)
474
+ byte_shape = quant_shape_to_byte_shape(tuple(int(x) for x in shape), qtype)
475
+ need = 1
476
+ for _d in byte_shape:
477
+ need *= _d
478
+ raw = np.fromfile(str(raw_path), dtype=np.uint8, count=need)
479
+ if raw.size != need:
480
+ _fail(f" βœ— Short tensor data for '{tensor_name}' ({raw.size}/{need} bytes).")
481
+ tmp = str(out_path) + ".tmp"
482
+ writer = GGUFWriter(tmp, "voice", use_temp_file=False)
483
+ writer.add_tensor(tensor_name, raw.reshape(byte_shape), raw_dtype=qtype)
484
+ writer.write_header_to_file()
485
+ writer.write_kv_data_to_file()
486
+ writer.write_tensors_to_file()
487
+ writer.close()
488
+ os.replace(tmp, str(out_path))
vlib/ui.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Terminal output, prompts, and formatting."""
2
+
3
+ import os
4
+ import sys
5
+
6
+
7
+ _PLAIN_MAP = {"βœ“": "[OK]", "βœ—": "[ERR]", "⚠": "[WARN]", "β†’": ">", "↳": "->", "Β·": "-", "…": "...", "─": "-", "β€”": "-", "←": "<-", "↔": "<->"}
8
+
9
+
10
+ def _plain():
11
+ return os.environ.get("VOICE_PLAIN") == "1"
12
+
13
+
14
+ def _transform(msg):
15
+ if not _plain():
16
+ return msg
17
+ for k, v in _PLAIN_MAP.items():
18
+ msg = msg.replace(k, v)
19
+ return msg
20
+
21
+
22
+ def _now_iso():
23
+ from datetime import datetime, timezone
24
+ return datetime.now(timezone.utc).isoformat()
25
+
26
+
27
+ def _say(msg):
28
+ print(_transform(msg), flush=True)
29
+
30
+
31
+ def _ok(msg):
32
+ _say(f" βœ“ {msg}")
33
+
34
+
35
+ def _warn(msg):
36
+ _say(f" ⚠ {msg}")
37
+
38
+
39
+ def _step(msg):
40
+ _say(f" β†’ {msg}")
41
+
42
+
43
+ def _note(msg):
44
+ _say(f" ↳ {msg}")
45
+
46
+
47
+ def _fail(msg):
48
+ sys.stdout.flush()
49
+ msg = str(msg).lstrip()
50
+ if msg.startswith("βœ—"):
51
+ msg = msg[1:].lstrip()
52
+ _say(f" βœ— {msg}")
53
+ sys.exit(1)
54
+
55
+
56
+ def _noninteractive(args):
57
+ if getattr(args, "non_interactive", False) or getattr(args, "yes", False):
58
+ return True
59
+ try:
60
+ return not sys.stdin.isatty()
61
+ except Exception:
62
+ return True
63
+
64
+
65
+ def _progress_pref():
66
+ if _plain() or os.environ.get("REDUCE_MOTION") == "1":
67
+ return True
68
+ try:
69
+ return not sys.stdout.isatty()
70
+ except Exception:
71
+ return True
72
+
73
+
74
+ def _confirm(question, args):
75
+ if getattr(args, "yes", False):
76
+ return True
77
+ if _noninteractive(args):
78
+ return False
79
+ try:
80
+ ans = input(question).strip().lower()
81
+ except (EOFError, KeyboardInterrupt):
82
+ _say(" ⚠ Cancelled.")
83
+ sys.exit(0)
84
+ return ans in ("y", "yes")
85
+
86
+
87
+ def _trunc(s, w):
88
+ s = str(s)
89
+ return s[: w - 1] + "…" if len(s) > w else s
90
+
91
+
92
+ def _term_width():
93
+ try:
94
+ import shutil
95
+ return shutil.get_terminal_size().columns
96
+ except Exception:
97
+ return 80
voice-cli.tar.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b49d8f2c7bed7be28fa5c650fe9f0f892641bb1fa35236cb524aa4b760162a7a
3
+ size 66864
voice.py CHANGED
The diff for this file is too large to render. See raw diff
 
voice.pyz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2148e2de112c40fe5eaaf2ad880c72e4ac925775fc3d6f51dbd8d6d36da7a103
3
+ size 245086