Spaces:
Running on Zero
Running on Zero
Deploy latest Objectverse Diary from fa09aac
Browse files- .playwright-cli/console-2026-06-08T11-55-41-257Z.log +1 -0
- data/train/objectverse_sft_curated_v2.jsonl +0 -0
- docs/07-development-plan.md +3 -3
- docs/DATASET.md +85 -14
- docs/DEMO_VIDEO_SCRIPT.md +2 -2
- docs/DEVELOPMENT_STATUS.md +19 -9
- docs/FAILURES.md +21 -9
- docs/FIELD_NOTES.md +5 -5
- docs/FINAL_VERIFICATION_REPORT.md +6 -5
- docs/MODEL_CARD.md +25 -18
- docs/SOCIAL_POST.md +5 -5
- docs/SUBMISSION_GUIDE.md +7 -5
- docs/architecture-diagram.html +347 -0
- requirements-training.txt +1 -0
- scripts/README.md +103 -16
- scripts/finetune_lora.py +325 -32
- scripts/merge_lora_adapter.py +155 -0
- scripts/prepare_curated_dataset.py +315 -39
- scripts/publish_hf_dataset.py +155 -0
- scripts/publish_hf_gguf.py +100 -0
- src/examples.py +2 -1
- src/renderer/share_card.py +2 -2
- src/ui/copy.py +15 -15
- src/ui/layout.py +354 -211
- src/ui/styles.css +647 -616
- src/utils/json_repair.py +55 -11
- tests/test_dataset_tooling.py +21 -1
- tests/test_finetune_lora_tooling.py +102 -0
- tests/test_json_repair.py +34 -0
- tests/test_llama_cpp_smoke.py +1 -4
- tests/test_merge_lora_adapter.py +46 -0
- tests/test_publish_hf_dataset.py +57 -0
- tests/test_publish_hf_gguf.py +45 -0
.playwright-cli/console-2026-06-08T11-55-41-257Z.log
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
[ 101248ms] [ERROR] Failed to load resource: net::ERR_INCOMPLETE_CHUNKED_ENCODING @ http://127.0.0.1:7861/gradio_api/heartbeat/ybhxcw7jgrp:0
|
data/train/objectverse_sft_curated_v2.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
docs/07-development-plan.md
CHANGED
|
@@ -143,7 +143,7 @@ Verification:
|
|
| 143 |
|
| 144 |
Goal: make persona, diary, and chat generation use a small local text model runtime.
|
| 145 |
|
| 146 |
-
Status: optional runtime wiring complete;
|
| 147 |
|
| 148 |
Scope:
|
| 149 |
|
|
@@ -158,12 +158,12 @@ Scope:
|
|
| 158 |
Exit criteria:
|
| 159 |
|
| 160 |
- Text generation can run through llama.cpp or documented local fallback.
|
| 161 |
-
- README documents runtime path
|
| 162 |
- Trace records include runtime metadata.
|
| 163 |
|
| 164 |
Verification:
|
| 165 |
|
| 166 |
-
- Local runtime smoke test with
|
| 167 |
- JSON schema validation.
|
| 168 |
- Compare at least three object generations for persona consistency.
|
| 169 |
|
|
|
|
| 143 |
|
| 144 |
Goal: make persona, diary, and chat generation use a small local text model runtime.
|
| 145 |
|
| 146 |
+
Status: optional runtime wiring complete; published LoRA v2 Q4_K_M GGUF passed local llama.cpp smoke. Hosted Space text runtime validation is still pending.
|
| 147 |
|
| 148 |
Scope:
|
| 149 |
|
|
|
|
| 158 |
Exit criteria:
|
| 159 |
|
| 160 |
- Text generation can run through llama.cpp or documented local fallback.
|
| 161 |
+
- README documents runtime path and published GGUF selection.
|
| 162 |
- Trace records include runtime metadata.
|
| 163 |
|
| 164 |
Verification:
|
| 165 |
|
| 166 |
+
- Local runtime smoke test with `objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf`.
|
| 167 |
- JSON schema validation.
|
| 168 |
- Compare at least three object generations for persona consistency.
|
| 169 |
|
docs/DATASET.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
|
| 5 |
The project now has a deterministic SFT preview generator for local planning and schema validation.
|
| 6 |
|
| 7 |
-
Current
|
| 8 |
|
| 9 |
```bash
|
| 10 |
.venv/bin/python -B scripts/generate_dataset.py
|
|
@@ -18,9 +18,9 @@ data/train/objectverse_sft_preview.jsonl
|
|
| 18 |
|
| 19 |
This preview is mock-generated. It is not a final training dataset and should not be described as real model output.
|
| 20 |
|
| 21 |
-
The
|
| 22 |
|
| 23 |
-
|
| 24 |
|
| 25 |
```bash
|
| 26 |
.venv/bin/python -B scripts/prepare_curated_dataset.py \
|
|
@@ -36,11 +36,22 @@ Published synthetic curated dataset:
|
|
| 36 |
https://huggingface.co/datasets/qqyule/objectverse-diary-sft-curated
|
| 37 |
```
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
## Target Dataset
|
| 40 |
|
| 41 |
-
|
| 42 |
|
| 43 |
-
- 200-500 generated object-persona-diary samples
|
| 44 |
- at least 50 manually curated high-quality samples
|
| 45 |
- no private user photos
|
| 46 |
- no emails, tokens, serial numbers, or other sensitive identifiers
|
|
@@ -82,7 +93,7 @@ Full candidate pool later:
|
|
| 82 |
.venv/bin/python -B scripts/generate_dataset.py --count 300 --output data/train/objectverse_sft_candidates.jsonl
|
| 83 |
```
|
| 84 |
|
| 85 |
-
Manual curation should happen after generation. Do not publish the full candidate file until it has been reviewed.
|
| 86 |
|
| 87 |
Space VLM validation traces under `data/traces/space-vlm/` are failure evidence because they include `vision-fallback-to-mock`. Do not mix them into curated training data or describe them as successful real VLM outputs.
|
| 88 |
|
|
@@ -105,27 +116,77 @@ Validate the local JSONL shape without Modal auth or GPU usage:
|
|
| 105 |
--run-name objectverse-diary-qwen15b-curated-test
|
| 106 |
```
|
| 107 |
|
| 108 |
-
|
| 109 |
|
| 110 |
```bash
|
| 111 |
-
|
| 112 |
-
--
|
| 113 |
-
--
|
| 114 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
```
|
| 116 |
|
| 117 |
-
Current Modal status: the
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
Default training scaffold settings:
|
| 120 |
|
| 121 |
- base model: `Qwen/Qwen2.5-1.5B-Instruct`
|
| 122 |
- LoRA adapter target: persona and diary JSON output
|
|
|
|
|
|
|
| 123 |
- GPU: Modal `A10G`
|
| 124 |
- output: Modal Volume artifacts, not committed files
|
| 125 |
|
| 126 |
The current `objectverse_sft_preview.jsonl` file is mock-generated and should only be used to validate the training pipeline. It is not final Well-Tuned evidence. Do not store Modal credit codes, tokens, Hugging Face tokens, or private datasets in the repo.
|
| 127 |
|
| 128 |
-
The published `
|
| 129 |
|
| 130 |
## Curation Checklist
|
| 131 |
|
|
@@ -139,10 +200,20 @@ The published `objectverse_sft_curated.jsonl` dataset is synthetic curated train
|
|
| 139 |
|
| 140 |
## Publishing Notes
|
| 141 |
|
| 142 |
-
|
| 143 |
|
| 144 |
- create a dataset card
|
| 145 |
- document that mock preview rows are synthetic
|
| 146 |
- separate curated rows from raw candidates
|
| 147 |
- include license and privacy notes
|
| 148 |
- keep private images out of the repo
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
The project now has a deterministic SFT preview generator for local planning and schema validation.
|
| 6 |
|
| 7 |
+
Current preview artifact:
|
| 8 |
|
| 9 |
```bash
|
| 10 |
.venv/bin/python -B scripts/generate_dataset.py
|
|
|
|
| 18 |
|
| 19 |
This preview is mock-generated. It is not a final training dataset and should not be described as real model output.
|
| 20 |
|
| 21 |
+
The preview JSONL file is evidence for schema and workflow readiness only.
|
| 22 |
|
| 23 |
+
Curated v1 training-test artifact:
|
| 24 |
|
| 25 |
```bash
|
| 26 |
.venv/bin/python -B scripts/prepare_curated_dataset.py \
|
|
|
|
| 36 |
https://huggingface.co/datasets/qqyule/objectverse-diary-sft-curated
|
| 37 |
```
|
| 38 |
|
| 39 |
+
Current curated v2 artifact:
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
.venv/bin/python -B scripts/prepare_curated_dataset.py \
|
| 43 |
+
--version v2 \
|
| 44 |
+
--count 200 \
|
| 45 |
+
--output data/train/objectverse_sft_curated_v2.jsonl
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
The published dataset repo now includes `objectverse_sft_curated_v2.jsonl`: 200 synthetic curated rows covering 40 everyday objects and 5 personality modes, with exactly 40 rows per mode and no repeated object-mode pair. The v1 file remains preserved through repository history.
|
| 49 |
+
|
| 50 |
## Target Dataset
|
| 51 |
|
| 52 |
+
Target before stronger fine-tuning:
|
| 53 |
|
| 54 |
+
- 200-500 generated or curated object-persona-diary samples
|
| 55 |
- at least 50 manually curated high-quality samples
|
| 56 |
- no private user photos
|
| 57 |
- no emails, tokens, serial numbers, or other sensitive identifiers
|
|
|
|
| 93 |
.venv/bin/python -B scripts/generate_dataset.py --count 300 --output data/train/objectverse_sft_candidates.jsonl
|
| 94 |
```
|
| 95 |
|
| 96 |
+
Manual curation should happen after generation. For a stronger LoRA run, curate 150-300 rows from a broader object/mode/scene pool and leave 10-15% for evaluation. Do not publish the full candidate file until it has been reviewed.
|
| 97 |
|
| 98 |
Space VLM validation traces under `data/traces/space-vlm/` are failure evidence because they include `vision-fallback-to-mock`. Do not mix them into curated training data or describe them as successful real VLM outputs.
|
| 99 |
|
|
|
|
| 116 |
--run-name objectverse-diary-qwen15b-curated-test
|
| 117 |
```
|
| 118 |
|
| 119 |
+
The first badge-evidence run used 20 steps on 50 synthetic curated rows. For a higher-quality v2 run, validate the larger curated file first:
|
| 120 |
|
| 121 |
```bash
|
| 122 |
+
.venv/bin/python -B scripts/finetune_lora.py \
|
| 123 |
+
--dry-run \
|
| 124 |
+
--dataset data/train/objectverse_sft_curated_v2.jsonl \
|
| 125 |
+
--run-name objectverse-diary-qwen15b-lora-v2 \
|
| 126 |
+
--max-steps 120 \
|
| 127 |
+
--learning-rate 1e-4 \
|
| 128 |
+
--max-seq-length 1536 \
|
| 129 |
+
--lora-r 32 \
|
| 130 |
+
--lora-alpha 64 \
|
| 131 |
+
--per-device-train-batch-size 2 \
|
| 132 |
+
--gradient-accumulation-steps 4 \
|
| 133 |
+
--eval-ratio 0.1 \
|
| 134 |
+
--eval-steps 20
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
Executed v2 training command:
|
| 138 |
+
|
| 139 |
+
```bash
|
| 140 |
+
modal run --timestamps -n objectverse-diary-qwen15b-lora-v2 scripts/finetune_lora.py \
|
| 141 |
+
--dataset data/train/objectverse_sft_curated_v2.jsonl \
|
| 142 |
+
--run-name objectverse-diary-qwen15b-lora-v2 \
|
| 143 |
+
--max-steps 120 \
|
| 144 |
+
--learning-rate 1e-4 \
|
| 145 |
+
--max-seq-length 1536 \
|
| 146 |
+
--lora-r 32 \
|
| 147 |
+
--lora-alpha 64 \
|
| 148 |
+
--per-device-train-batch-size 2 \
|
| 149 |
+
--gradient-accumulation-steps 4 \
|
| 150 |
+
--eval-ratio 0.1 \
|
| 151 |
+
--eval-steps 20
|
| 152 |
```
|
| 153 |
|
| 154 |
+
Current Modal status: the v2 job completed successfully and produced the published LoRA adapter at `https://huggingface.co/qqyule/objectverse-diary-qwen15b-lora`.
|
| 155 |
+
|
| 156 |
+
Current v2 run summary:
|
| 157 |
+
|
| 158 |
+
- run name: `objectverse-diary-qwen15b-lora-v2`
|
| 159 |
+
- dataset: `data/train/objectverse_sft_curated_v2.jsonl`
|
| 160 |
+
- dataset repo path: `objectverse_sft_curated_v2.jsonl`
|
| 161 |
+
- records: 200 total, 180 train, 20 eval
|
| 162 |
+
- base model: `Qwen/Qwen2.5-1.5B-Instruct`
|
| 163 |
+
- max steps: 120
|
| 164 |
+
- learning rate: `1e-4`
|
| 165 |
+
- max sequence length: 1536
|
| 166 |
+
- LoRA rank / alpha / dropout: 32 / 64 / 0.05
|
| 167 |
+
- effective batch size: 8
|
| 168 |
+
- assistant-output-only loss: enabled
|
| 169 |
+
- train loss: 0.3240
|
| 170 |
+
- eval loss: 0.0162
|
| 171 |
+
- train runtime: 140.3364s
|
| 172 |
+
- epoch: 5.2222
|
| 173 |
+
- local adapter export: ignored `exports/objectverse-diary-qwen15b-lora-v2-adapter-dir/`
|
| 174 |
+
- model repo: `https://huggingface.co/qqyule/objectverse-diary-qwen15b-lora`
|
| 175 |
+
|
| 176 |
+
Additional v2 scaffold validation run: `objectverse-diary-qwen15b-lora-v2-curated50-retry1` completed on Modal with the existing 50-row curated dataset, using assistant-output-only loss, 45 train rows, 5 eval rows, `max_steps=120`, `learning_rate=1e-4`, `max_seq_length=1536`, LoRA `r=32`, `alpha=64`, and effective batch size 8. Metrics: `train_loss=0.2551`, `eval_loss=0.0093`, `train_runtime=146.5398s`, `epoch=20.0`. The adapter was downloaded to ignored local `exports/`; it has not been published to Hugging Face Hub.
|
| 177 |
|
| 178 |
Default training scaffold settings:
|
| 179 |
|
| 180 |
- base model: `Qwen/Qwen2.5-1.5B-Instruct`
|
| 181 |
- LoRA adapter target: persona and diary JSON output
|
| 182 |
+
- default loss: assistant-output-only labels, with prompt tokens masked
|
| 183 |
+
- default eval split: 10% when the dataset has at least two rows
|
| 184 |
- GPU: Modal `A10G`
|
| 185 |
- output: Modal Volume artifacts, not committed files
|
| 186 |
|
| 187 |
The current `objectverse_sft_preview.jsonl` file is mock-generated and should only be used to validate the training pipeline. It is not final Well-Tuned evidence. Do not store Modal credit codes, tokens, Hugging Face tokens, or private datasets in the repo.
|
| 188 |
|
| 189 |
+
The published `objectverse_sft_curated_v2.jsonl` dataset is synthetic curated training data. It is suitable for hackathon training evidence, but it should still be described honestly as deterministic synthetic curation rather than real user trace data.
|
| 190 |
|
| 191 |
## Curation Checklist
|
| 192 |
|
|
|
|
| 200 |
|
| 201 |
## Publishing Notes
|
| 202 |
|
| 203 |
+
When publishing to Hugging Face Datasets:
|
| 204 |
|
| 205 |
- create a dataset card
|
| 206 |
- document that mock preview rows are synthetic
|
| 207 |
- separate curated rows from raw candidates
|
| 208 |
- include license and privacy notes
|
| 209 |
- keep private images out of the repo
|
| 210 |
+
|
| 211 |
+
Curated v2 was published with:
|
| 212 |
+
|
| 213 |
+
```bash
|
| 214 |
+
.venv/bin/python -B scripts/publish_hf_dataset.py \
|
| 215 |
+
--dataset-file data/train/objectverse_sft_curated_v2.jsonl \
|
| 216 |
+
--repo-id qqyule/objectverse-diary-sft-curated \
|
| 217 |
+
--path-in-repo objectverse_sft_curated_v2.jsonl \
|
| 218 |
+
--commit-message "Upload Objectverse Diary curated v2 dataset"
|
| 219 |
+
```
|
docs/DEMO_VIDEO_SCRIPT.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
|
| 5 |
Record a 90-second stable demo for Objectverse Diary using the mock-safe Hugging Face Space or local Gradio app.
|
| 6 |
|
| 7 |
-
Do not claim that
|
| 8 |
|
| 9 |
## Recording Setup
|
| 10 |
|
|
@@ -104,6 +104,6 @@ Screen:
|
|
| 104 |
## Notes For Submission
|
| 105 |
|
| 106 |
- Mention MiniCPM-V as hosted-validated for object understanding, while the public demo defaults to mock for reliability.
|
| 107 |
-
- Mention the published synthetic curated dataset
|
| 108 |
- Mention public traces and failure notes if the submission form asks for reproducibility.
|
| 109 |
- Keep the final video under 2 minutes.
|
|
|
|
| 4 |
|
| 5 |
Record a 90-second stable demo for Objectverse Diary using the mock-safe Hugging Face Space or local Gradio app.
|
| 6 |
|
| 7 |
+
Do not claim that live Space LoRA/GGUF runtime wiring is complete. Hosted MiniCPM-V validation is complete for the vision path and local GGUF text smoke is complete, but the stable demo should still emphasize the mock-safe product loop, Gradio Off-Brand UI, public traces, published dataset/model evidence, and no commercial AI APIs.
|
| 8 |
|
| 9 |
## Recording Setup
|
| 10 |
|
|
|
|
| 104 |
## Notes For Submission
|
| 105 |
|
| 106 |
- Mention MiniCPM-V as hosted-validated for object understanding, while the public demo defaults to mock for reliability.
|
| 107 |
+
- Mention the published synthetic curated dataset, LoRA adapter, and Q4_K_M GGUF as model evidence, not live Space runtime.
|
| 108 |
- Mention public traces and failure notes if the submission form asks for reproducibility.
|
| 109 |
- Keep the final video under 2 minutes.
|
docs/DEVELOPMENT_STATUS.md
CHANGED
|
@@ -38,21 +38,22 @@ Last updated: 2026-06-08
|
|
| 38 |
- social post draft
|
| 39 |
- stable submission guide
|
| 40 |
- Well-Tuned evidence:
|
| 41 |
-
-
|
| 42 |
-
- Modal Qwen 1.5B LoRA
|
| 43 |
-
- LoRA adapter published at https://huggingface.co/qqyule/objectverse-diary-qwen15b-lora
|
| 44 |
-
- GGUF
|
| 45 |
- `scripts/check_llama_cpp_smoke.py`
|
| 46 |
-
-
|
|
|
|
|
|
|
|
|
|
| 47 |
- trace runtime no longer records literal `TEXT_MODEL_PATH`
|
| 48 |
- Local tests and initial acceptance currently pass.
|
| 49 |
|
| 50 |
## Not Completed
|
| 51 |
|
| 52 |
-
-
|
| 53 |
-
-
|
| 54 |
-
- Real text model traces from non-mock runtime.
|
| 55 |
-
- GGUF conversion and runtime wiring for the published LoRA adapter.
|
| 56 |
- Published Field Notes URL, recorded demo video URL, social post URL, and final public submission.
|
| 57 |
|
| 58 |
## Current Safe Defaults
|
|
@@ -68,6 +69,15 @@ For a stable public baseline, keep the mock-safe Space as the demo path and only
|
|
| 68 |
|
| 69 |
Next model gate:
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
Optional rerun gate if Space variables, secrets, or dependencies change:
|
| 72 |
|
| 73 |
```bash
|
|
|
|
| 38 |
- social post draft
|
| 39 |
- stable submission guide
|
| 40 |
- Well-Tuned evidence:
|
| 41 |
+
- 200-row synthetic curated v2 SFT dataset published at https://huggingface.co/datasets/qqyule/objectverse-diary-sft-curated
|
| 42 |
+
- Modal Qwen 1.5B LoRA v2 run completed with 120 steps, 180 train rows, and 20 eval rows
|
| 43 |
+
- LoRA v2 adapter published at https://huggingface.co/qqyule/objectverse-diary-qwen15b-lora
|
| 44 |
+
- LoRA v2 GGUF runtime evidence:
|
| 45 |
- `scripts/check_llama_cpp_smoke.py`
|
| 46 |
+
- adapter merged into `Qwen/Qwen2.5-1.5B-Instruct`
|
| 47 |
+
- pinned `llama.cpp` commit: `8f83d6c271d194bde2d410145a0ce73bc42e85cd`
|
| 48 |
+
- published Q4_K_M GGUF: https://huggingface.co/qqyule/objectverse-diary-qwen15b-lora/blob/main/objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf
|
| 49 |
+
- local smoke passed with `llama-cpp text generation`, schema-valid persona/diary, non-empty chat, and no `text-fallback-to-mock`
|
| 50 |
- trace runtime no longer records literal `TEXT_MODEL_PATH`
|
| 51 |
- Local tests and initial acceptance currently pass.
|
| 52 |
|
| 53 |
## Not Completed
|
| 54 |
|
| 55 |
+
- Hosted Space text runtime validation with the published GGUF. The public Space still uses mock-safe text until this passes.
|
| 56 |
+
- Real text model traces from the hosted non-mock text runtime.
|
|
|
|
|
|
|
| 57 |
- Published Field Notes URL, recorded demo video URL, social post URL, and final public submission.
|
| 58 |
|
| 59 |
## Current Safe Defaults
|
|
|
|
| 69 |
|
| 70 |
Next model gate:
|
| 71 |
|
| 72 |
+
Download or mount the published GGUF on the target runtime, set:
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
OBJECTVERSE_TEXT_BACKEND=llama-cpp
|
| 76 |
+
TEXT_MODEL_PATH=/absolute/path/to/objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
Then rerun the local or Space smoke path before claiming live text runtime.
|
| 80 |
+
|
| 81 |
Optional rerun gate if Space variables, secrets, or dependencies change:
|
| 82 |
|
| 83 |
```bash
|
docs/FAILURES.md
CHANGED
|
@@ -12,10 +12,10 @@ MiniCPM-V 2.6 is wired as an optional vision backend. Hosted Space ZeroGPU valid
|
|
| 12 |
|
| 13 |
The app includes a hidden `/vision_runtime_probe` API and `scripts/check_space_vlm.py` writes probe output into the Space VLM report before image validation. This probe identified the previous failure as a gated-model access issue rather than a GPU or dependency issue.
|
| 14 |
|
| 15 |
-
The
|
| 16 |
|
| 17 |
-
- repo: `
|
| 18 |
-
- file: `
|
| 19 |
- helper: `scripts/check_llama_cpp_smoke.py`
|
| 20 |
|
| 21 |
Known non-blocking warning:
|
|
@@ -70,15 +70,27 @@ Known non-blocking warning:
|
|
| 70 |
- Fallback used: mock object understanding plus mock text runtime if validation reaches generation.
|
| 71 |
- Resolution: unresolved; keep the public Space mock-safe until this section reports a passing VLM validation.
|
| 72 |
|
| 73 |
-
## 2026-06-08 -
|
| 74 |
|
| 75 |
- Area: llama.cpp text runtime evidence.
|
| 76 |
-
- Reproduction: Run `scripts/check_llama_cpp_smoke.py` with
|
| 77 |
- Expected: trace records `llama-cpp text generation`, persona/diary/chat run without `text-fallback-to-mock`.
|
| 78 |
-
- Actual:
|
| 79 |
-
- Impact:
|
| 80 |
-
- Fallback used:
|
| 81 |
-
- Resolution:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
## Anticipated Failure Areas
|
| 84 |
|
|
|
|
| 12 |
|
| 13 |
The app includes a hidden `/vision_runtime_probe` API and `scripts/check_space_vlm.py` writes probe output into the Space VLM report before image validation. This probe identified the previous failure as a gated-model access issue rather than a GPU or dependency issue.
|
| 14 |
|
| 15 |
+
The published LoRA v2 GGUF for local text smoke testing is available and has passed local llama.cpp smoke:
|
| 16 |
|
| 17 |
+
- repo: `qqyule/objectverse-diary-qwen15b-lora`
|
| 18 |
+
- file: `objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf`
|
| 19 |
- helper: `scripts/check_llama_cpp_smoke.py`
|
| 20 |
|
| 21 |
Known non-blocking warning:
|
|
|
|
| 70 |
- Fallback used: mock object understanding plus mock text runtime if validation reaches generation.
|
| 71 |
- Resolution: unresolved; keep the public Space mock-safe until this section reports a passing VLM validation.
|
| 72 |
|
| 73 |
+
## 2026-06-08 - LoRA v2 GGUF Local Smoke Passed
|
| 74 |
|
| 75 |
- Area: llama.cpp text runtime evidence.
|
| 76 |
+
- Reproduction: Run `scripts/check_llama_cpp_smoke.py` with `models/objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf` after optional `llama-cpp-python` installation.
|
| 77 |
- Expected: trace records `llama-cpp text generation`, persona/diary/chat run without `text-fallback-to-mock`.
|
| 78 |
+
- Actual: passed locally; trace included only `mock-vision-runtime` because text was real and vision remained mock for the smoke input.
|
| 79 |
+
- Impact: local llama.cpp text runtime evidence is ready. Public Space text runtime is still not validated with this GGUF.
|
| 80 |
+
- Fallback used: none for text.
|
| 81 |
+
- Resolution: resolved locally by using the merged LoRA v2 Q4_K_M GGUF and conservative JSON extraction / decoding settings.
|
| 82 |
+
- Evidence: `scripts/check_llama_cpp_smoke.py`, `docs/RUNTIME.md`, and the Hub file in `qqyule/objectverse-diary-qwen15b-lora`.
|
| 83 |
+
|
| 84 |
+
## 2026-06-08 - Hugging Face Xet GGUF Upload Stalled
|
| 85 |
+
|
| 86 |
+
- Area: Hugging Face model file upload.
|
| 87 |
+
- Reproduction: Upload `models/objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf` with the default Hub client path.
|
| 88 |
+
- Expected: upload completes and commits `objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf`.
|
| 89 |
+
- Actual: the first upload stalled with Xet TLS EOF / `CLOSE_WAIT` after partial progress.
|
| 90 |
+
- Impact: upload needed a retry; local GGUF file was unaffected.
|
| 91 |
+
- Fallback used: stopped the stalled upload process and retried with `HF_HUB_DISABLE_XET=1`.
|
| 92 |
+
- Resolution: resolved; ordinary Hub/LFS upload succeeded.
|
| 93 |
+
- Evidence: Hub file `https://huggingface.co/qqyule/objectverse-diary-qwen15b-lora/blob/main/objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf`.
|
| 94 |
|
| 95 |
## Anticipated Failure Areas
|
| 96 |
|
docs/FIELD_NOTES.md
CHANGED
|
@@ -137,10 +137,10 @@ For text runtime evidence, the project now includes a local smoke helper for an
|
|
| 137 |
|
| 138 |
```bash
|
| 139 |
.venv/bin/python -B scripts/check_llama_cpp_smoke.py \
|
| 140 |
-
--model-path models/
|
| 141 |
```
|
| 142 |
|
| 143 |
-
The
|
| 144 |
|
| 145 |
## 10. Privacy And Safety
|
| 146 |
|
|
@@ -150,12 +150,12 @@ Trace logging anonymizes text inputs before public export. The current public tr
|
|
| 150 |
|
| 151 |
## 11. What I Would Improve Next
|
| 152 |
|
| 153 |
-
The next model-focused step is to
|
| 154 |
|
| 155 |
After that:
|
| 156 |
|
| 157 |
-
-
|
| 158 |
-
-
|
| 159 |
- generate real non-mock traces if hosted/local model validation passes
|
| 160 |
- record a final demo video from the stable Space
|
| 161 |
|
|
|
|
| 137 |
|
| 138 |
```bash
|
| 139 |
.venv/bin/python -B scripts/check_llama_cpp_smoke.py \
|
| 140 |
+
--model-path models/objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf
|
| 141 |
```
|
| 142 |
|
| 143 |
+
The published local-smoke file is `objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf` from `qqyule/objectverse-diary-qwen15b-lora`. It is intentionally not committed. Local smoke passed on June 8, 2026; Space text runtime still needs a separate validation before it should be described as live.
|
| 144 |
|
| 145 |
## 10. Privacy And Safety
|
| 146 |
|
|
|
|
| 150 |
|
| 151 |
## 11. What I Would Improve Next
|
| 152 |
|
| 153 |
+
The next model-focused step is to validate the published GGUF in the hosted Space runtime, or keep it as local llama.cpp evidence while the public demo remains mock-safe.
|
| 154 |
|
| 155 |
After that:
|
| 156 |
|
| 157 |
+
- download or mount the published GGUF in the target runtime
|
| 158 |
+
- set `OBJECTVERSE_TEXT_BACKEND=llama-cpp` and `TEXT_MODEL_PATH` for that runtime
|
| 159 |
- generate real non-mock traces if hosted/local model validation passes
|
| 160 |
- record a final demo video from the stable Space
|
| 161 |
|
docs/FINAL_VERIFICATION_REPORT.md
CHANGED
|
@@ -8,15 +8,16 @@
|
|
| 8 |
|
| 9 |
## Summary
|
| 10 |
|
| 11 |
-
Objectverse Diary's stable mock-safe baseline remains locally verifiable. This update adds non-secret MiniCPM-V runtime diagnostics through a hidden Gradio API, probe-aware Space VLM reporting, a latest-failure-note updater, and
|
| 12 |
|
| 13 |
-
This report does not claim
|
| 14 |
|
| 15 |
## Implementation Additions
|
| 16 |
|
| 17 |
- Hidden `/vision_runtime_probe` Gradio API returns sanitized backend, dependency, GPU, and MiniCPM-V load diagnostics.
|
| 18 |
- `scripts/check_space_vlm.py` can include probe output in markdown/JSON reports and update the latest failure section in `docs/FAILURES.md`.
|
| 19 |
- `scripts/check_llama_cpp_smoke.py` validates persona, diary, and chat through an externally configured GGUF without committing model files.
|
|
|
|
| 20 |
- Runtime status no longer records literal `TEXT_MODEL_PATH`; traces only record whether an external GGUF path is configured.
|
| 21 |
- Submission docs now distinguish final-draft materials from published URLs.
|
| 22 |
|
|
@@ -88,11 +89,11 @@ No GGUF file, real token, private key, credential, or `.env` file was added by t
|
|
| 88 |
- Demo video URL is still pending recording/publication.
|
| 89 |
- Field Notes URL is still pending publication.
|
| 90 |
- Social post URL is still pending publication.
|
| 91 |
-
-
|
| 92 |
-
-
|
| 93 |
|
| 94 |
## Verdict
|
| 95 |
|
| 96 |
PASS for the stable mock-safe local submission baseline plus local diagnostics/smoke-helper implementation.
|
| 97 |
|
| 98 |
-
The project is ready for explicit-confirmation external steps: push `main`, sync the Space, rerun probe-aware Space VLM validation,
|
|
|
|
| 8 |
|
| 9 |
## Summary
|
| 10 |
|
| 11 |
+
Objectverse Diary's stable mock-safe baseline remains locally verifiable. This update adds non-secret MiniCPM-V runtime diagnostics through a hidden Gradio API, probe-aware Space VLM reporting, a latest-failure-note updater, and local llama.cpp GGUF smoke-test support. A later local run merged the LoRA v2 adapter, produced a Q4_K_M GGUF, uploaded it to the model repo, and passed local llama.cpp smoke.
|
| 12 |
|
| 13 |
+
This report does not claim live Space LoRA/GGUF runtime wiring, Field Notes publication, demo video publication, social post publication, or final public submission URLs are complete.
|
| 14 |
|
| 15 |
## Implementation Additions
|
| 16 |
|
| 17 |
- Hidden `/vision_runtime_probe` Gradio API returns sanitized backend, dependency, GPU, and MiniCPM-V load diagnostics.
|
| 18 |
- `scripts/check_space_vlm.py` can include probe output in markdown/JSON reports and update the latest failure section in `docs/FAILURES.md`.
|
| 19 |
- `scripts/check_llama_cpp_smoke.py` validates persona, diary, and chat through an externally configured GGUF without committing model files.
|
| 20 |
+
- LoRA v2 GGUF tooling now covers merge, publish, and local smoke for `objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf`.
|
| 21 |
- Runtime status no longer records literal `TEXT_MODEL_PATH`; traces only record whether an external GGUF path is configured.
|
| 22 |
- Submission docs now distinguish final-draft materials from published URLs.
|
| 23 |
|
|
|
|
| 89 |
- Demo video URL is still pending recording/publication.
|
| 90 |
- Field Notes URL is still pending publication.
|
| 91 |
- Social post URL is still pending publication.
|
| 92 |
+
- Hosted Space text runtime validation with the published GGUF remains pending.
|
| 93 |
+
- Live Space runtime wiring for the published LoRA/GGUF remains future work.
|
| 94 |
|
| 95 |
## Verdict
|
| 96 |
|
| 97 |
PASS for the stable mock-safe local submission baseline plus local diagnostics/smoke-helper implementation.
|
| 98 |
|
| 99 |
+
The project is ready for explicit-confirmation external steps: push `main`, sync the Space, rerun probe-aware Space VLM validation if needed, validate the published GGUF in the Space runtime before claiming live text generation, record/publish the demo video, publish Field Notes/social post, and fill final submission URLs.
|
docs/MODEL_CARD.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
| 2 |
|
| 3 |
## Status
|
| 4 |
|
| 5 |
-
Stable submission baseline plus one published text LoRA
|
| 6 |
|
| 7 |
-
The app defaults to deterministic mock backends. MiniCPM-V 2.6 vision is wired as an optional runtime backend for GPU environments, with a hidden non-secret probe for hosted diagnostics. Text generation has optional llama.cpp wiring for an externally configured GGUF model via `TEXT_MODEL_PATH`. A Modal LoRA
|
| 8 |
|
| 9 |
Hosted MiniCPM-V validation passed on June 8, 2026 after adding an `HF_TOKEN` Space secret with access to the gated `openbmb/MiniCPM-V-2_6` model. The validation used public mug, keyboard, and shoe images on ZeroGPU, while text generation intentionally remained mock. See `docs/SPACE_VLM_REPORT.md`.
|
| 10 |
|
|
@@ -19,8 +19,8 @@ Hosted MiniCPM-V validation passed on June 8, 2026 after adding an `HF_TOKEN` Sp
|
|
| 19 |
| Component | Candidate | Notes |
|
| 20 |
| --- | --- | --- |
|
| 21 |
| Vision | `openbmb/MiniCPM-V-2_6` or mock fallback | Wired as optional backend; hosted ZeroGPU validation passed, then Space rolled back to mock-safe defaults. |
|
| 22 |
-
| Text | deterministic mock text; published `Qwen/Qwen2.5-1.5B-Instruct` LoRA
|
| 23 |
-
| Runtime | optional GGUF through llama.cpp / llama-cpp-python | Wired with mock fallback;
|
| 24 |
| UI | Gradio Blocks | Required by the hackathon and project rules. |
|
| 25 |
|
| 26 |
## Parameter Budget
|
|
@@ -34,7 +34,7 @@ Record final numbers here before submission:
|
|
| 34 |
| Vision | MiniCPM-V 2.6 optional path | ~8B | yes, when enabled |
|
| 35 |
| Text base | Stable baseline mock text | 0 | no model parameters |
|
| 36 |
| Optional text base | `Qwen/Qwen2.5-1.5B-Instruct` | ~1.5B | yes, when enabled |
|
| 37 |
-
|
|
| 38 |
| Published LoRA adapter | `qqyule/objectverse-diary-qwen15b-lora` | small adapter over base model | yes, when enabled |
|
| 39 |
| Stable baseline total | Mock text + optional wired vision not active by default | 0 active model parameters by default | <= 32B |
|
| 40 |
|
|
@@ -63,7 +63,7 @@ Dataset planning lives in `docs/DATASET.md`.
|
|
| 63 |
|
| 64 |
Current preview data is deterministic and mock-generated. It should only be used for schema validation, dry-run validation, and workflow planning until real candidate samples are generated and curated.
|
| 65 |
|
| 66 |
-
The Modal training scaffold defaults to `Qwen/Qwen2.5-1.5B-Instruct` and saves adapter artifacts to a Modal Volume. `data/train/
|
| 67 |
|
| 68 |
Published adapter:
|
| 69 |
|
|
@@ -71,25 +71,32 @@ Published adapter:
|
|
| 71 |
https://huggingface.co/qqyule/objectverse-diary-qwen15b-lora
|
| 72 |
```
|
| 73 |
|
| 74 |
-
|
| 75 |
|
| 76 |
- Platform: Modal
|
| 77 |
-
- Run name: `objectverse-diary-qwen15b-
|
| 78 |
- Base model: `Qwen/Qwen2.5-1.5B-Instruct`
|
| 79 |
-
- Dataset:
|
| 80 |
-
-
|
| 81 |
-
-
|
| 82 |
-
-
|
| 83 |
-
-
|
| 84 |
-
-
|
| 85 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
GGUF smoke status:
|
| 88 |
|
| 89 |
-
-
|
| 90 |
-
-
|
| 91 |
- Local helper: `scripts/check_llama_cpp_smoke.py`
|
| 92 |
-
-
|
|
|
|
| 93 |
|
| 94 |
## Safety And Privacy
|
| 95 |
|
|
|
|
| 2 |
|
| 3 |
## Status
|
| 4 |
|
| 5 |
+
Stable submission baseline plus one published text LoRA v2 adapter and one published Q4_K_M GGUF. The public Gradio Space still defaults to deterministic mock text; the GGUF has passed local llama.cpp smoke, but it has not been switched into the live Space runtime.
|
| 6 |
|
| 7 |
+
The app defaults to deterministic mock backends. MiniCPM-V 2.6 vision is wired as an optional runtime backend for GPU environments, with a hidden non-secret probe for hosted diagnostics. Text generation has optional llama.cpp wiring for an externally configured GGUF model via `TEXT_MODEL_PATH`. A Modal LoRA v2 run completed, the adapter is published at `https://huggingface.co/qqyule/objectverse-diary-qwen15b-lora`, and the merged Q4_K_M GGUF is published in the same repo.
|
| 8 |
|
| 9 |
Hosted MiniCPM-V validation passed on June 8, 2026 after adding an `HF_TOKEN` Space secret with access to the gated `openbmb/MiniCPM-V-2_6` model. The validation used public mug, keyboard, and shoe images on ZeroGPU, while text generation intentionally remained mock. See `docs/SPACE_VLM_REPORT.md`.
|
| 10 |
|
|
|
|
| 19 |
| Component | Candidate | Notes |
|
| 20 |
| --- | --- | --- |
|
| 21 |
| Vision | `openbmb/MiniCPM-V-2_6` or mock fallback | Wired as optional backend; hosted ZeroGPU validation passed, then Space rolled back to mock-safe defaults. |
|
| 22 |
+
| Text | deterministic mock text by default; published `Qwen/Qwen2.5-1.5B-Instruct` LoRA v2 Q4_K_M GGUF for local runtime | Adapter and GGUF published; Space text runtime remains mock-safe. |
|
| 23 |
+
| Runtime | optional GGUF through llama.cpp / llama-cpp-python | Wired with mock fallback; local GGUF smoke passed on 2026-06-08. |
|
| 24 |
| UI | Gradio Blocks | Required by the hackathon and project rules. |
|
| 25 |
|
| 26 |
## Parameter Budget
|
|
|
|
| 34 |
| Vision | MiniCPM-V 2.6 optional path | ~8B | yes, when enabled |
|
| 35 |
| Text base | Stable baseline mock text | 0 | no model parameters |
|
| 36 |
| Optional text base | `Qwen/Qwen2.5-1.5B-Instruct` | ~1.5B | yes, when enabled |
|
| 37 |
+
| Published LoRA v2 GGUF | `qqyule/objectverse-diary-qwen15b-lora` / `objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf` | ~1.5B base, quantized file | yes, if enabled |
|
| 38 |
| Published LoRA adapter | `qqyule/objectverse-diary-qwen15b-lora` | small adapter over base model | yes, when enabled |
|
| 39 |
| Stable baseline total | Mock text + optional wired vision not active by default | 0 active model parameters by default | <= 32B |
|
| 40 |
|
|
|
|
| 63 |
|
| 64 |
Current preview data is deterministic and mock-generated. It should only be used for schema validation, dry-run validation, and workflow planning until real candidate samples are generated and curated.
|
| 65 |
|
| 66 |
+
The Modal training scaffold defaults to `Qwen/Qwen2.5-1.5B-Instruct` and saves adapter artifacts to a Modal Volume. `data/train/objectverse_sft_curated_v2.jsonl` contains 200 synthetic curated rows covering 40 everyday objects and 5 personality modes. It is published at `https://huggingface.co/datasets/qqyule/objectverse-diary-sft-curated` as `objectverse_sft_curated_v2.jsonl`.
|
| 67 |
|
| 68 |
Published adapter:
|
| 69 |
|
|
|
|
| 71 |
https://huggingface.co/qqyule/objectverse-diary-qwen15b-lora
|
| 72 |
```
|
| 73 |
|
| 74 |
+
Current v2 training run summary:
|
| 75 |
|
| 76 |
- Platform: Modal
|
| 77 |
+
- Run name: `objectverse-diary-qwen15b-lora-v2`
|
| 78 |
- Base model: `Qwen/Qwen2.5-1.5B-Instruct`
|
| 79 |
+
- Dataset: 200 synthetic curated v2 rows
|
| 80 |
+
- Train / eval rows: 180 / 20
|
| 81 |
+
- Steps: 120
|
| 82 |
+
- Max sequence length: 1536
|
| 83 |
+
- Learning rate: 0.0001
|
| 84 |
+
- Effective batch size: 8
|
| 85 |
+
- LoRA rank / alpha / dropout: 32 / 64 / 0.05
|
| 86 |
+
- Assistant-output-only loss: enabled
|
| 87 |
+
- Train loss: 0.3240
|
| 88 |
+
- Eval loss: 0.0162
|
| 89 |
+
- Epoch: 5.2222
|
| 90 |
+
- GGUF conversion: completed with pinned `llama.cpp` commit `8f83d6c271d194bde2d410145a0ce73bc42e85cd`
|
| 91 |
+
- Published GGUF: `objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf`
|
| 92 |
|
| 93 |
GGUF smoke status:
|
| 94 |
|
| 95 |
+
- Repo: `qqyule/objectverse-diary-qwen15b-lora`
|
| 96 |
+
- File: `objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf`
|
| 97 |
- Local helper: `scripts/check_llama_cpp_smoke.py`
|
| 98 |
+
- Local result: passed on 2026-06-08 with `llama-cpp text generation`, no `text-fallback-to-mock`, schema-valid persona and diary, and non-empty chat reply.
|
| 99 |
+
- Space result: not run; do not claim live Space text runtime until a separate Space validation passes.
|
| 100 |
|
| 101 |
## Safety And Privacy
|
| 102 |
|
docs/SOCIAL_POST.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
| 5 |
I built Objectverse Diary for Build Small Hackathon: a Gradio app where everyday objects wake up, get secret personas, write diaries, chat with you, and generate share cards.
|
| 6 |
|
| 7 |
Stable demo: mock-safe, reproducible, no commercial AI APIs.
|
| 8 |
-
MiniCPM-V hosted validation now passes for the vision path; llama.cpp
|
| 9 |
-
Synthetic curated dataset + Qwen 1.5B LoRA adapter are published as
|
| 10 |
|
| 11 |
Space: https://huggingface.co/spaces/build-small-hackathon/ObjectverseDiary
|
| 12 |
|
|
@@ -23,9 +23,9 @@ Objectverse Diary is my Build Small Hackathon project: a strange little object a
|
|
| 23 |
- a shareable personality card
|
| 24 |
- an anonymized trace record
|
| 25 |
|
| 26 |
-
The stable submission baseline is mock-safe and reproducible, with no commercial AI APIs. MiniCPM-V vision is wired and hosted-validated on ZeroGPU, while llama.cpp text
|
| 27 |
|
| 28 |
-
I also published a
|
| 29 |
|
| 30 |
Space:
|
| 31 |
https://huggingface.co/spaces/build-small-hackathon/ObjectverseDiary
|
|
@@ -38,4 +38,4 @@ https://huggingface.co/spaces/build-small-hackathon/ObjectverseDiary
|
|
| 38 |
|
| 39 |
- Add GitHub URL after push is confirmed.
|
| 40 |
- Add demo video URL after recording.
|
| 41 |
-
- Do not claim
|
|
|
|
| 5 |
I built Objectverse Diary for Build Small Hackathon: a Gradio app where everyday objects wake up, get secret personas, write diaries, chat with you, and generate share cards.
|
| 6 |
|
| 7 |
Stable demo: mock-safe, reproducible, no commercial AI APIs.
|
| 8 |
+
MiniCPM-V hosted validation now passes for the vision path; local llama.cpp smoke passes with the published LoRA v2 Q4_K_M GGUF.
|
| 9 |
+
Synthetic curated v2 dataset + Qwen 1.5B LoRA v2 adapter/GGUF are published as model evidence.
|
| 10 |
|
| 11 |
Space: https://huggingface.co/spaces/build-small-hackathon/ObjectverseDiary
|
| 12 |
|
|
|
|
| 23 |
- a shareable personality card
|
| 24 |
- an anonymized trace record
|
| 25 |
|
| 26 |
+
The stable submission baseline is mock-safe and reproducible, with no commercial AI APIs. MiniCPM-V vision is wired and hosted-validated on ZeroGPU, while llama.cpp text is validated locally through an optional GGUF path.
|
| 27 |
|
| 28 |
+
I also published a 200-row synthetic curated v2 SFT dataset, a Qwen 1.5B LoRA v2 adapter, and a Q4_K_M GGUF for model evidence. The GGUF is not wired into the public Space runtime yet; the live demo stays intentionally reliable.
|
| 29 |
|
| 30 |
Space:
|
| 31 |
https://huggingface.co/spaces/build-small-hackathon/ObjectverseDiary
|
|
|
|
| 38 |
|
| 39 |
- Add GitHub URL after push is confirmed.
|
| 40 |
- Add demo video URL after recording.
|
| 41 |
+
- Do not claim live Space LoRA/GGUF runtime wiring is complete.
|
docs/SUBMISSION_GUIDE.md
CHANGED
|
@@ -24,6 +24,7 @@
|
|
| 24 |
- Public mock traces: `data/traces/samples/`
|
| 25 |
- Stable demo baseline: Gradio example buttons replay committed sample traces first, then fall back to the live generation pipeline if a cached trace is missing.
|
| 26 |
- Optional llama.cpp runtime wiring: `src/models/llama_cpp_runner.py`
|
|
|
|
| 27 |
|
| 28 |
## Completed Locally
|
| 29 |
|
|
@@ -33,15 +34,16 @@
|
|
| 33 |
- Optional llama.cpp text runtime wiring through `TEXT_MODEL_PATH`.
|
| 34 |
- Hosted Space VLM validation script, report, JSON summary, and trace evidence export.
|
| 35 |
- Hosted Space VLM probe support, latest failure-note update support, and passing MiniCPM-V ZeroGPU validation after adding an `HF_TOKEN` Space secret for gated model access.
|
| 36 |
-
- Local GGUF smoke-test helper
|
| 37 |
-
- Synthetic curated SFT dataset published to Hugging Face Datasets.
|
| 38 |
-
- Modal Qwen 1.5B LoRA
|
|
|
|
| 39 |
- Field Notes draft, demo video script, and social post draft for the stable submission package.
|
| 40 |
|
| 41 |
## Not Completed Yet
|
| 42 |
|
| 43 |
-
-
|
| 44 |
-
- Real model traces
|
| 45 |
- Field Notes publication URL, recorded demo video URL, social post URL, and final public submission.
|
| 46 |
|
| 47 |
## Final Checks
|
|
|
|
| 24 |
- Public mock traces: `data/traces/samples/`
|
| 25 |
- Stable demo baseline: Gradio example buttons replay committed sample traces first, then fall back to the live generation pipeline if a cached trace is missing.
|
| 26 |
- Optional llama.cpp runtime wiring: `src/models/llama_cpp_runner.py`
|
| 27 |
+
- Published LoRA v2 Q4_K_M GGUF: https://huggingface.co/qqyule/objectverse-diary-qwen15b-lora/blob/main/objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf
|
| 28 |
|
| 29 |
## Completed Locally
|
| 30 |
|
|
|
|
| 34 |
- Optional llama.cpp text runtime wiring through `TEXT_MODEL_PATH`.
|
| 35 |
- Hosted Space VLM validation script, report, JSON summary, and trace evidence export.
|
| 36 |
- Hosted Space VLM probe support, latest failure-note update support, and passing MiniCPM-V ZeroGPU validation after adding an `HF_TOKEN` Space secret for gated model access.
|
| 37 |
+
- Local GGUF smoke-test helper passed with `models/objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf`; trace text runtime was `llama-cpp text generation` and no `text-fallback-to-mock` was present.
|
| 38 |
+
- Synthetic curated v2 SFT dataset published to Hugging Face Datasets: 200 rows, 40 objects, 5 personality modes.
|
| 39 |
+
- Modal Qwen 1.5B LoRA v2 run completed and adapter published to Hugging Face Models.
|
| 40 |
+
- LoRA v2 adapter merged into `Qwen/Qwen2.5-1.5B-Instruct`, converted with pinned `llama.cpp`, quantized to Q4_K_M, and uploaded to the same model repo.
|
| 41 |
- Field Notes draft, demo video script, and social post draft for the stable submission package.
|
| 42 |
|
| 43 |
## Not Completed Yet
|
| 44 |
|
| 45 |
+
- Hosted Space text runtime validation with the published GGUF. The local runtime passed, but the public Space has not been switched from mock-safe text.
|
| 46 |
+
- Real text-model traces from the hosted Space.
|
| 47 |
- Field Notes publication URL, recorded demo video URL, social post URL, and final public submission.
|
| 48 |
|
| 49 |
## Final Checks
|
docs/architecture-diagram.html
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Objectverse Diary Architecture Diagram</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 8 |
+
<style>
|
| 9 |
+
* {
|
| 10 |
+
margin: 0;
|
| 11 |
+
padding: 0;
|
| 12 |
+
box-sizing: border-box;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
body {
|
| 16 |
+
font-family: 'JetBrains Mono', monospace;
|
| 17 |
+
background: #020617;
|
| 18 |
+
min-height: 100vh;
|
| 19 |
+
padding: 2rem;
|
| 20 |
+
color: white;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.container {
|
| 24 |
+
max-width: 1200px;
|
| 25 |
+
margin: 0 auto;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.header {
|
| 29 |
+
margin-bottom: 2rem;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.header-row {
|
| 33 |
+
display: flex;
|
| 34 |
+
align-items: center;
|
| 35 |
+
gap: 1rem;
|
| 36 |
+
margin-bottom: 0.5rem;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
.pulse-dot {
|
| 40 |
+
width: 12px;
|
| 41 |
+
height: 12px;
|
| 42 |
+
background: #22d3ee;
|
| 43 |
+
border-radius: 50%;
|
| 44 |
+
animation: pulse 2s infinite;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
@keyframes pulse {
|
| 48 |
+
0%, 100% { opacity: 1; transform: scale(1); }
|
| 49 |
+
50% { opacity: 0.4; transform: scale(0.9); }
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
h1 {
|
| 53 |
+
font-size: 1.5rem;
|
| 54 |
+
font-weight: 700;
|
| 55 |
+
letter-spacing: -0.025em;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.subtitle {
|
| 59 |
+
color: #94a3b8;
|
| 60 |
+
font-size: 0.875rem;
|
| 61 |
+
margin-left: 1.75rem;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.diagram-container {
|
| 65 |
+
background: rgba(15, 23, 42, 0.5);
|
| 66 |
+
border-radius: 1rem;
|
| 67 |
+
border: 1px solid #1e293b;
|
| 68 |
+
padding: 1.5rem;
|
| 69 |
+
overflow-x: auto;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
svg {
|
| 73 |
+
width: 100%;
|
| 74 |
+
min-width: 950px;
|
| 75 |
+
display: block;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.cards {
|
| 79 |
+
display: grid;
|
| 80 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 81 |
+
gap: 1rem;
|
| 82 |
+
margin-top: 2rem;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.card {
|
| 86 |
+
background: rgba(15, 23, 42, 0.5);
|
| 87 |
+
border-radius: 0.75rem;
|
| 88 |
+
border: 1px solid #1e293b;
|
| 89 |
+
padding: 1.25rem;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.card-header {
|
| 93 |
+
display: flex;
|
| 94 |
+
align-items: center;
|
| 95 |
+
gap: 0.5rem;
|
| 96 |
+
margin-bottom: 0.75rem;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.card-dot {
|
| 100 |
+
width: 8px;
|
| 101 |
+
height: 8px;
|
| 102 |
+
border-radius: 50%;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.card-dot.cyan { background: #22d3ee; }
|
| 106 |
+
.card-dot.emerald { background: #34d399; }
|
| 107 |
+
.card-dot.violet { background: #a78bfa; }
|
| 108 |
+
.card-dot.amber { background: #fbbf24; }
|
| 109 |
+
.card-dot.rose { background: #fb7185; }
|
| 110 |
+
|
| 111 |
+
.card h3 {
|
| 112 |
+
font-size: 0.875rem;
|
| 113 |
+
font-weight: 600;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.card ul {
|
| 117 |
+
list-style: none;
|
| 118 |
+
color: #94a3b8;
|
| 119 |
+
font-size: 0.75rem;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.card li {
|
| 123 |
+
margin-bottom: 0.375rem;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.footer {
|
| 127 |
+
text-align: center;
|
| 128 |
+
margin-top: 2rem;
|
| 129 |
+
color: #475569;
|
| 130 |
+
font-size: 0.75rem;
|
| 131 |
+
}
|
| 132 |
+
</style>
|
| 133 |
+
</head>
|
| 134 |
+
<body>
|
| 135 |
+
<div class="container">
|
| 136 |
+
<!-- Header -->
|
| 137 |
+
<div class="header">
|
| 138 |
+
<div class="header-row">
|
| 139 |
+
<div class="pulse-dot"></div>
|
| 140 |
+
<h1>Objectverse Diary Architecture</h1>
|
| 141 |
+
</div>
|
| 142 |
+
<p class="subtitle">Multi-layered small model pipeline for the Build Small Hackathon (An Adventure in Thousand Token Wood)</p>
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
<!-- Main Diagram -->
|
| 146 |
+
<div class="diagram-container">
|
| 147 |
+
<svg viewBox="0 0 1000 680">
|
| 148 |
+
<!-- Definitions -->
|
| 149 |
+
<defs>
|
| 150 |
+
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
| 151 |
+
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
|
| 152 |
+
</marker>
|
| 153 |
+
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
| 154 |
+
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#1e293b" stroke-width="0.5"/>
|
| 155 |
+
</pattern>
|
| 156 |
+
</defs>
|
| 157 |
+
|
| 158 |
+
<!-- Background Grid -->
|
| 159 |
+
<rect width="100%" height="100%" fill="url(#grid)" />
|
| 160 |
+
|
| 161 |
+
<!-- Region/Cloud Boundary (Hugging Face Space Sandbox) -->
|
| 162 |
+
<rect x="160" y="40" width="700" height="600" rx="12" fill="rgba(251, 191, 36, 0.02)" stroke="#fbbf24" stroke-width="1.2" stroke-dasharray="8,4"/>
|
| 163 |
+
<text x="175" y="62" fill="#fbbf24" font-size="10" font-weight="600">Hugging Face Space Runtime Environment</text>
|
| 164 |
+
|
| 165 |
+
<!-- Dynamic GPU Security Boundary (ZeroGPU Space Context) -->
|
| 166 |
+
<rect x="630" y="90" width="210" height="400" rx="10" fill="rgba(244, 63, 94, 0.02)" stroke="#fb7185" stroke-width="1" stroke-dasharray="4,4"/>
|
| 167 |
+
<text x="640" y="110" fill="#fb7185" font-size="8" font-weight="600">ZeroGPU Allocation Sandbox</text>
|
| 168 |
+
|
| 169 |
+
<!-- Connections (Drawn early so they layer behind components) -->
|
| 170 |
+
<!-- User -> UI Layer -->
|
| 171 |
+
<line x1="120" y1="315" x2="198" y2="315" stroke="#22d3ee" stroke-width="1.5" marker-end="url(#arrowhead)"/>
|
| 172 |
+
<text x="159" y="304" fill="#94a3b8" font-size="8" font-weight="600" text-anchor="middle">HTTPS</text>
|
| 173 |
+
|
| 174 |
+
<!-- UI Layer -> Pipeline Coordinator -->
|
| 175 |
+
<line x1="340" y1="315" x2="418" y2="315" stroke="#22d3ee" stroke-width="1.5" marker-end="url(#arrowhead)"/>
|
| 176 |
+
<text x="379" y="304" fill="#94a3b8" font-size="8" text-anchor="middle">WS/Post</text>
|
| 177 |
+
|
| 178 |
+
<!-- Coordinator -> Vision Runner -->
|
| 179 |
+
<path d="M 550 280 L 590 280 Q 610 280 610 210 L 638 210" fill="none" stroke="#34d399" stroke-width="1.5" stroke-dasharray="2,2" marker-end="url(#arrowhead)"/>
|
| 180 |
+
<text x="590" y="240" fill="#fb7185" font-size="8" text-anchor="middle">@zero_gpu</text>
|
| 181 |
+
|
| 182 |
+
<!-- Coordinator -> Llama.cpp Runner -->
|
| 183 |
+
<path d="M 550 330 L 590 330 Q 610 330 610 395 L 638 395" fill="none" stroke="#34d399" stroke-width="1.5" marker-end="url(#arrowhead)"/>
|
| 184 |
+
<text x="595" y="360" fill="#34d399" font-size="8" text-anchor="middle">Local GGUF</text>
|
| 185 |
+
|
| 186 |
+
<!-- Coordinator -> Renderer/Traces -->
|
| 187 |
+
<line x1="475" y1="365" x2="475" y2="473" stroke="#34d399" stroke-width="1.5" marker-end="url(#arrowhead)"/>
|
| 188 |
+
<text x="483" y="420" fill="#94a3b8" font-size="8">Local Save</text>
|
| 189 |
+
|
| 190 |
+
<!-- Vision Model -> HF Hub -->
|
| 191 |
+
<line x1="810" y1="210" x2="888" y2="280" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="4,4" marker-end="url(#arrowhead)"/>
|
| 192 |
+
<text x="850" y="235" fill="#94a3b8" font-size="7" text-anchor="middle">gated pull</text>
|
| 193 |
+
|
| 194 |
+
<!-- Text Model -> HF Hub -->
|
| 195 |
+
<line x1="810" y1="395" x2="888" y2="330" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="4,4" marker-end="url(#arrowhead)"/>
|
| 196 |
+
<text x="850" y="370" fill="#94a3b8" font-size="7" text-anchor="middle">GGUF pull</text>
|
| 197 |
+
|
| 198 |
+
<!-- =================================================================
|
| 199 |
+
COMPONENTS (Opaque backgrounds + Semi-transparent borders)
|
| 200 |
+
================================================================= -->
|
| 201 |
+
|
| 202 |
+
<!-- 1. Users / Browser -->
|
| 203 |
+
<rect x="20" y="270" width="100" height="90" rx="6" fill="#0f172a" />
|
| 204 |
+
<rect x="20" y="270" width="100" height="90" rx="6" fill="rgba(30, 41, 59, 0.5)" stroke="#94a3b8" stroke-width="1.5"/>
|
| 205 |
+
<text x="70" y="295" fill="white" font-size="11" font-weight="700" text-anchor="middle">Users</text>
|
| 206 |
+
<text x="70" y="315" fill="#94a3b8" font-size="8" text-anchor="middle">Upload Image</text>
|
| 207 |
+
<text x="70" y="330" fill="#94a3b8" font-size="8" text-anchor="middle">Chat Session</text>
|
| 208 |
+
<text x="70" y="345" fill="#22d3ee" font-size="8" text-anchor="middle">Web/Mobile</text>
|
| 209 |
+
|
| 210 |
+
<!-- 2. UI Layer (Gradio) -->
|
| 211 |
+
<rect x="200" y="190" width="140" height="250" rx="8" fill="#0f172a" />
|
| 212 |
+
<rect x="200" y="190" width="140" height="250" rx="8" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1.5"/>
|
| 213 |
+
<text x="270" y="215" fill="white" font-size="12" font-weight="700" text-anchor="middle">Gradio Web UI</text>
|
| 214 |
+
<text x="270" y="235" fill="#94a3b8" font-size="8" text-anchor="middle">app.py / src/ui/</text>
|
| 215 |
+
<line x1="210" y1="245" x2="330" y2="245" stroke="#1e293b" stroke-width="1"/>
|
| 216 |
+
<text x="215" y="265" fill="#22d3ee" font-size="8" font-weight="600">• Image Drag-Drop</text>
|
| 217 |
+
<text x="215" y="285" fill="#22d3ee" font-size="8" font-weight="600">• Persona Selector</text>
|
| 218 |
+
<text x="215" y="305" fill="#22d3ee" font-size="8" font-weight="600">• Typewriter Diary</text>
|
| 219 |
+
<text x="215" y="325" fill="#22d3ee" font-size="8" font-weight="600">• Character Chat</text>
|
| 220 |
+
<text x="215" y="345" fill="#22d3ee" font-size="8" font-weight="600">• SVG Card Render</text>
|
| 221 |
+
<line x1="210" y1="365" x2="330" y2="365" stroke="#1e293b" stroke-width="1"/>
|
| 222 |
+
<text x="270" y="385" fill="#94a3b8" font-size="8" text-anchor="middle">Example Gallery Cache</text>
|
| 223 |
+
<text x="270" y="400" fill="#22d3ee" font-size="8" text-anchor="middle">(Deterministic Baseline)</text>
|
| 224 |
+
|
| 225 |
+
<!-- 3. Pipeline Coordinator -->
|
| 226 |
+
<rect x="420" y="240" width="130" height="125" rx="6" fill="#0f172a" />
|
| 227 |
+
<rect x="420" y="240" width="130" height="125" rx="6" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1.5"/>
|
| 228 |
+
<text x="485" y="265" fill="white" font-size="11" font-weight="700" text-anchor="middle">Pipeline Core</text>
|
| 229 |
+
<text x="485" y="280" fill="#94a3b8" font-size="8" text-anchor="middle">src/pipeline.py</text>
|
| 230 |
+
<line x1="430" y1="290" x2="540" y2="290" stroke="#1e293b" stroke-width="1"/>
|
| 231 |
+
<text x="435" y="305" fill="#34d399" font-size="8">• State Routing</text>
|
| 232 |
+
<text x="435" y="320" fill="#34d399" font-size="8">• Fallback Logic</text>
|
| 233 |
+
<text x="435" y="335" fill="#34d399" font-size="8">• Parse Schemas</text>
|
| 234 |
+
<text x="485" y="352" fill="#e2e8f0" font-size="7" text-anchor="middle">Pydantic validation</text>
|
| 235 |
+
|
| 236 |
+
<!-- 4. Vision Runner -->
|
| 237 |
+
<rect x="645" y="150" width="165" height="100" rx="6" fill="#0f172a" />
|
| 238 |
+
<rect x="645" y="150" width="165" height="100" rx="6" fill="rgba(120, 53, 15, 0.3)" stroke="#fbbf24" stroke-width="1.5"/>
|
| 239 |
+
<text x="727" y="175" fill="white" font-size="11" font-weight="700" text-anchor="middle">Vision Backend</text>
|
| 240 |
+
<text x="727" y="190" fill="#94a3b8" font-size="8" text-anchor="middle">src/models/vision_runner</text>
|
| 241 |
+
<line x1="655" y1="200" x2="800" y2="200" stroke="#1e293b" stroke-width="1"/>
|
| 242 |
+
<text x="660" y="215" fill="#fbbf24" font-size="8" font-weight="600">MiniCPM-V 2.6 (8B)</text>
|
| 243 |
+
<text x="660" y="230" fill="#94a3b8" font-size="8">ZeroGPU compatible</text>
|
| 244 |
+
<text x="660" y="242" fill="#fb7185" font-size="7">Fallback to mock on failure</text>
|
| 245 |
+
|
| 246 |
+
<!-- 5. Llama.cpp Runner -->
|
| 247 |
+
<rect x="645" y="335" width="165" height="110" rx="6" fill="#0f172a" />
|
| 248 |
+
<rect x="645" y="335" width="165" height="110" rx="6" fill="rgba(76, 29, 149, 0.4)" stroke="#a78bfa" stroke-width="1.5"/>
|
| 249 |
+
<text x="727" y="360" fill="white" font-size="11" font-weight="700" text-anchor="middle">Text Backend</text>
|
| 250 |
+
<text x="727" y="375" fill="#94a3b8" font-size="8" text-anchor="middle">llama_cpp_runner.py</text>
|
| 251 |
+
<line x1="655" y1="385" x2="800" y2="385" stroke="#1e293b" stroke-width="1"/>
|
| 252 |
+
<text x="660" y="400" fill="#a78bfa" font-size="8" font-weight="600">Qwen 1.5B GGUF</text>
|
| 253 |
+
<text x="660" y="415" fill="#94a3b8" font-size="8">Merged LoRA v2 Adapter</text>
|
| 254 |
+
<text x="660" y="428" fill="#fbbf24" font-size="7">Deterministic fallback runtime</text>
|
| 255 |
+
|
| 256 |
+
<!-- 6. Card Renderer & Traces (Opaque + Slate border) -->
|
| 257 |
+
<rect x="420" y="480" width="130" height="115" rx="6" fill="#0f172a" />
|
| 258 |
+
<rect x="420" y="480" width="130" height="115" rx="6" fill="rgba(30, 41, 59, 0.5)" stroke="#94a3b8" stroke-width="1.5"/>
|
| 259 |
+
<text x="485" y="505" fill="white" font-size="11" font-weight="700" text-anchor="middle">Output Services</text>
|
| 260 |
+
<text x="485" y="520" fill="#94a3b8" font-size="8" text-anchor="middle">renderer/ & traces/</text>
|
| 261 |
+
<line x1="430" y1="530" x2="540" y2="530" stroke="#1e293b" stroke-width="1"/>
|
| 262 |
+
<text x="435" y="545" fill="#94a3b8" font-size="8">• Card HTML Gen</text>
|
| 263 |
+
<text x="435" y="560" fill="#94a3b8" font-size="8">• Anonymizer Traces</text>
|
| 264 |
+
<text x="435" y="575" fill="#94a3b8" font-size="8">• data/traces/*.jsonl</text>
|
| 265 |
+
|
| 266 |
+
<!-- 7. Hugging Face Hub (External Repository) -->
|
| 267 |
+
<rect x="890" y="240" width="90" height="180" rx="8" fill="#0f172a" />
|
| 268 |
+
<rect x="890" y="240" width="90" height="180" rx="8" fill="rgba(30, 41, 59, 0.5)" stroke="#94a3b8" stroke-width="1.5"/>
|
| 269 |
+
<text x="935" y="270" fill="white" font-size="11" font-weight="700" text-anchor="middle">HF Hub</text>
|
| 270 |
+
<text x="935" y="285" fill="#94a3b8" font-size="8" text-anchor="middle">Remote Assets</text>
|
| 271 |
+
<line x1="900" y1="298" x2="970" y2="298" stroke="#1e293b" stroke-width="1"/>
|
| 272 |
+
<text x="905" y="315" fill="#94a3b8" font-size="8">• SFT Dataset</text>
|
| 273 |
+
<text x="905" y="335" fill="#94a3b8" font-size="8">• LoRA Weights</text>
|
| 274 |
+
<text x="905" y="355" fill="#94a3b8" font-size="8">• GGUF Files</text>
|
| 275 |
+
<text x="905" y="375" fill="#fb7185" font-size="8">• Gate models</text>
|
| 276 |
+
|
| 277 |
+
<!-- =================================================================
|
| 278 |
+
DIAGRAM LEGEND
|
| 279 |
+
================================================================= -->
|
| 280 |
+
<text x="180" y="515" fill="white" font-size="10" font-weight="600">Legend</text>
|
| 281 |
+
|
| 282 |
+
<rect x="180" y="527" width="16" height="10" rx="2" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1"/>
|
| 283 |
+
<text x="202" y="535" fill="#94a3b8" font-size="8">UI Layer (Gradio)</text>
|
| 284 |
+
|
| 285 |
+
<rect x="180" y="543" width="16" height="10" rx="2" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1"/>
|
| 286 |
+
<text x="202" y="551" fill="#94a3b8" font-size="8">Controller Layer (Python)</text>
|
| 287 |
+
|
| 288 |
+
<rect x="180" y="559" width="16" height="10" rx="2" fill="rgba(120, 53, 15, 0.3)" stroke="#fbbf24" stroke-width="1"/>
|
| 289 |
+
<text x="202" y="567" fill="#94a3b8" font-size="8">Vision Engine (MiniCPM-V)</text>
|
| 290 |
+
|
| 291 |
+
<rect x="180" y="575" width="16" height="10" rx="2" fill="rgba(76, 29, 149, 0.4)" stroke="#a78bfa" stroke-width="1"/>
|
| 292 |
+
<text x="202" y="583" fill="#94a3b8" font-size="8">Text Engine (Llama.cpp GGUF)</text>
|
| 293 |
+
|
| 294 |
+
<rect x="180" y="591" width="16" height="10" rx="2" fill="rgba(30, 41, 59, 0.5)" stroke="#94a3b8" stroke-width="1"/>
|
| 295 |
+
<text x="202" y="599" fill="#94a3b8" font-size="8">External & File Outputs</text>
|
| 296 |
+
|
| 297 |
+
<rect x="180" y="607" width="16" height="10" rx="2" fill="transparent" stroke="#fb7185" stroke-width="1" stroke-dasharray="2,2"/>
|
| 298 |
+
<text x="202" y="615" fill="#94a3b8" font-size="8">Security/Dynamic Hardware Group</text>
|
| 299 |
+
</svg>
|
| 300 |
+
</div>
|
| 301 |
+
|
| 302 |
+
<!-- Info Cards -->
|
| 303 |
+
<div class="cards">
|
| 304 |
+
<div class="card">
|
| 305 |
+
<div class="card-header">
|
| 306 |
+
<div class="card-dot cyan"></div>
|
| 307 |
+
<h3>UI & Frontend Layer</h3>
|
| 308 |
+
</div>
|
| 309 |
+
<ul>
|
| 310 |
+
<li>• <b>English-First Copy</b>: Retro archive design, warm paper, mystery vibe</li>
|
| 311 |
+
<li>• <b>Deterministic Fallback</b>: Example gallery reads committed mock records</li>
|
| 312 |
+
<li>• <b>Interactive Sandbox</b>: Full chat session maintaining object persona</li>
|
| 313 |
+
</ul>
|
| 314 |
+
</div>
|
| 315 |
+
|
| 316 |
+
<div class="card">
|
| 317 |
+
<div class="card-header">
|
| 318 |
+
<div class="card-dot emerald"></div>
|
| 319 |
+
<h3>Pipeline Coordinator</h3>
|
| 320 |
+
</div>
|
| 321 |
+
<ul>
|
| 322 |
+
<li>• <b>Modular Routing</b>: Vision descriptions trigger first-person diaries</li>
|
| 323 |
+
<li>• <b>Pydantic Validation</b>: Strict checks on JSON output from LLM</li>
|
| 324 |
+
<li>• <b>Trace Compliance</b>: Generates anonymized session logs to JSONL files</li>
|
| 325 |
+
</ul>
|
| 326 |
+
</div>
|
| 327 |
+
|
| 328 |
+
<div class="card">
|
| 329 |
+
<div class="card-header">
|
| 330 |
+
<div class="card-dot violet"></div>
|
| 331 |
+
<h3>Dual-Engine Model Execution</h3>
|
| 332 |
+
</div>
|
| 333 |
+
<ul>
|
| 334 |
+
<li>• <b>MiniCPM-V 2.6 (8B)</b>: Runs via HF Spaces ZeroGPU dynamically</li>
|
| 335 |
+
<li>• <b>Llama.cpp (1.5B)</b>: Runs highly optimized GGUF adapter locally</li>
|
| 336 |
+
<li>• <b>Flexible Mock Fallback</b>: Ensures 100% runtime uptime for judges</li>
|
| 337 |
+
</ul>
|
| 338 |
+
</div>
|
| 339 |
+
</div>
|
| 340 |
+
|
| 341 |
+
<!-- Footer -->
|
| 342 |
+
<p class="footer">
|
| 343 |
+
Objectverse Diary • Created for Build Small Hackathon • June 2026
|
| 344 |
+
</p>
|
| 345 |
+
</div>
|
| 346 |
+
</body>
|
| 347 |
+
</html>
|
requirements-training.txt
CHANGED
|
@@ -1,2 +1,3 @@
|
|
| 1 |
modal>=1,<2
|
| 2 |
huggingface_hub>=0.34,<1
|
|
|
|
|
|
| 1 |
modal>=1,<2
|
| 2 |
huggingface_hub>=0.34,<1
|
| 3 |
+
peft>=0.19,<1
|
scripts/README.md
CHANGED
|
@@ -7,12 +7,15 @@ Implemented initial scripts:
|
|
| 7 |
- `check_initial_stage.py`: verifies required files, runtime defaults, sample traces, pipeline, and Gradio build.
|
| 8 |
- `generate_sample_traces.py`: creates six stable public mock traces under `data/traces/samples/`.
|
| 9 |
- `generate_dataset.py`: creates deterministic SFT preview JSONL for schema and curation planning.
|
| 10 |
-
- `prepare_curated_dataset.py`: creates
|
| 11 |
- `export_traces.py`: exports validated public sample traces to JSONL for dataset-style publishing.
|
| 12 |
- `check_space_vlm.py`: validates MiniCPM-V object understanding on the hosted Hugging Face Space with three temporary public test images.
|
| 13 |
- `check_llama_cpp_smoke.py`: smoke-tests the optional llama.cpp text runtime with an external GGUF model.
|
| 14 |
-
- `finetune_lora.py`: validates SFT JSONL locally and defines the Modal LoRA training scaffold
|
|
|
|
| 15 |
- `publish_hf_adapter.py`: uploads a downloaded LoRA adapter folder to Hugging Face Hub.
|
|
|
|
|
|
|
| 16 |
|
| 17 |
Expected files during implementation:
|
| 18 |
|
|
@@ -28,15 +31,60 @@ Modal LoRA dry-run:
|
|
| 28 |
--run-name objectverse-diary-qwen15b-curated-test
|
| 29 |
```
|
| 30 |
|
| 31 |
-
Modal LoRA
|
| 32 |
|
| 33 |
```bash
|
| 34 |
-
|
| 35 |
-
--
|
| 36 |
-
--
|
| 37 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
```
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
Training dependencies are intentionally separate from the Space runtime:
|
| 41 |
|
| 42 |
```bash
|
|
@@ -56,22 +104,61 @@ or configure `MODAL_TOKEN_ID` and `MODAL_TOKEN_SECRET` through your shell/secret
|
|
| 56 |
After a successful Modal run, download the adapter from the output volume into ignored local exports. Modal's directory download behavior can vary; downloading individual adapter files into a directory is the safest path.
|
| 57 |
|
| 58 |
```bash
|
| 59 |
-
mkdir -p exports/objectverse-diary-qwen15b-
|
| 60 |
for file in vocab.json tokenizer_config.json tokenizer.json special_tokens_map.json merges.txt chat_template.jinja added_tokens.json adapter_model.safetensors adapter_config.json README.md; do
|
| 61 |
modal volume get objectverse-diary-lora-output \
|
| 62 |
-
"objectverse-diary-qwen15b-
|
| 63 |
-
"exports/objectverse-diary-qwen15b-
|
| 64 |
done
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
```
|
| 66 |
|
| 67 |
Then upload the adapter to Hugging Face Hub:
|
| 68 |
|
| 69 |
```bash
|
| 70 |
.venv/bin/python -B scripts/publish_hf_adapter.py \
|
| 71 |
-
--adapter-dir exports/objectverse-diary-qwen15b-
|
| 72 |
-
--repo-id qqyule/objectverse-diary-qwen15b-lora
|
|
|
|
| 73 |
```
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
Space VLM validation:
|
| 76 |
|
| 77 |
```bash
|
|
@@ -88,13 +175,13 @@ External Space changes are explicit:
|
|
| 88 |
.venv/bin/python -B scripts/check_space_vlm.py --configure-space --rollback-to-mock
|
| 89 |
```
|
| 90 |
|
| 91 |
-
Local GGUF smoke test
|
| 92 |
|
| 93 |
```bash
|
| 94 |
.venv/bin/python -B scripts/check_llama_cpp_smoke.py \
|
| 95 |
-
--model-path models/
|
| 96 |
```
|
| 97 |
|
| 98 |
-
|
| 99 |
|
| 100 |
-
Current status: mock trace generation, trace JSONL export, SFT preview generation, synthetic curated dataset publishing, optional MiniCPM-V wiring, optional llama.cpp wiring, hosted Space VLM validation tooling with non-secret probe support, local GGUF smoke helper, Modal LoRA training scaffolding,
|
|
|
|
| 7 |
- `check_initial_stage.py`: verifies required files, runtime defaults, sample traces, pipeline, and Gradio build.
|
| 8 |
- `generate_sample_traces.py`: creates six stable public mock traces under `data/traces/samples/`.
|
| 9 |
- `generate_dataset.py`: creates deterministic SFT preview JSONL for schema and curation planning.
|
| 10 |
+
- `prepare_curated_dataset.py`: creates deterministic synthetic curated SFT rows; v1 defaults to 50 rows, v2 defaults to 200 rows across 40 objects and 5 modes.
|
| 11 |
- `export_traces.py`: exports validated public sample traces to JSONL for dataset-style publishing.
|
| 12 |
- `check_space_vlm.py`: validates MiniCPM-V object understanding on the hosted Hugging Face Space with three temporary public test images.
|
| 13 |
- `check_llama_cpp_smoke.py`: smoke-tests the optional llama.cpp text runtime with an external GGUF model.
|
| 14 |
+
- `finetune_lora.py`: validates SFT JSONL locally and defines the Modal LoRA training scaffold with optional eval split, assistant-output-only loss, and tunable LoRA/batch settings.
|
| 15 |
+
- `publish_hf_dataset.py`: validates and uploads curated JSONL files to a Hugging Face Dataset repository.
|
| 16 |
- `publish_hf_adapter.py`: uploads a downloaded LoRA adapter folder to Hugging Face Hub.
|
| 17 |
+
- `merge_lora_adapter.py`: merges a local PEFT LoRA adapter into a Hugging Face base model and saves tokenizer files.
|
| 18 |
+
- `publish_hf_gguf.py`: validates and uploads a local GGUF file to a Hugging Face model repository.
|
| 19 |
|
| 20 |
Expected files during implementation:
|
| 21 |
|
|
|
|
| 31 |
--run-name objectverse-diary-qwen15b-curated-test
|
| 32 |
```
|
| 33 |
|
| 34 |
+
Modal LoRA v2 dry-run for a larger curated dataset:
|
| 35 |
|
| 36 |
```bash
|
| 37 |
+
.venv/bin/python -B scripts/prepare_curated_dataset.py \
|
| 38 |
+
--version v2 \
|
| 39 |
+
--count 200 \
|
| 40 |
+
--output data/train/objectverse_sft_curated_v2.jsonl
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
Publish curated v2 dataset:
|
| 44 |
+
|
| 45 |
+
```bash
|
| 46 |
+
.venv/bin/python -B scripts/publish_hf_dataset.py \
|
| 47 |
+
--dataset-file data/train/objectverse_sft_curated_v2.jsonl \
|
| 48 |
+
--repo-id qqyule/objectverse-diary-sft-curated \
|
| 49 |
+
--path-in-repo objectverse_sft_curated_v2.jsonl
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
```bash
|
| 53 |
+
.venv/bin/python -B scripts/finetune_lora.py \
|
| 54 |
+
--dry-run \
|
| 55 |
+
--dataset data/train/objectverse_sft_curated_v2.jsonl \
|
| 56 |
+
--run-name objectverse-diary-qwen15b-lora-v2 \
|
| 57 |
+
--max-steps 120 \
|
| 58 |
+
--learning-rate 1e-4 \
|
| 59 |
+
--max-seq-length 1536 \
|
| 60 |
+
--lora-r 32 \
|
| 61 |
+
--lora-alpha 64 \
|
| 62 |
+
--per-device-train-batch-size 2 \
|
| 63 |
+
--gradient-accumulation-steps 4 \
|
| 64 |
+
--eval-ratio 0.1 \
|
| 65 |
+
--eval-steps 20
|
| 66 |
```
|
| 67 |
|
| 68 |
+
Modal LoRA v2 training:
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
modal run --timestamps -n objectverse-diary-qwen15b-lora-v2 scripts/finetune_lora.py \
|
| 72 |
+
--dataset data/train/objectverse_sft_curated_v2.jsonl \
|
| 73 |
+
--run-name objectverse-diary-qwen15b-lora-v2 \
|
| 74 |
+
--max-steps 120 \
|
| 75 |
+
--learning-rate 1e-4 \
|
| 76 |
+
--max-seq-length 1536 \
|
| 77 |
+
--lora-r 32 \
|
| 78 |
+
--lora-alpha 64 \
|
| 79 |
+
--per-device-train-batch-size 2 \
|
| 80 |
+
--gradient-accumulation-steps 4 \
|
| 81 |
+
--eval-ratio 0.1 \
|
| 82 |
+
--eval-steps 20
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
For epoch-based experiments, set `--max-steps 0` and provide `--num-train-epochs`.
|
| 86 |
+
Assistant-output-only loss is enabled by default; pass `--no-assistant-only-loss` only for debugging full-text loss behavior.
|
| 87 |
+
|
| 88 |
Training dependencies are intentionally separate from the Space runtime:
|
| 89 |
|
| 90 |
```bash
|
|
|
|
| 104 |
After a successful Modal run, download the adapter from the output volume into ignored local exports. Modal's directory download behavior can vary; downloading individual adapter files into a directory is the safest path.
|
| 105 |
|
| 106 |
```bash
|
| 107 |
+
mkdir -p exports/objectverse-diary-qwen15b-lora-v2-adapter-dir
|
| 108 |
for file in vocab.json tokenizer_config.json tokenizer.json special_tokens_map.json merges.txt chat_template.jinja added_tokens.json adapter_model.safetensors adapter_config.json README.md; do
|
| 109 |
modal volume get objectverse-diary-lora-output \
|
| 110 |
+
"objectverse-diary-qwen15b-lora-v2/adapter/$file" \
|
| 111 |
+
"exports/objectverse-diary-qwen15b-lora-v2-adapter-dir/$file"
|
| 112 |
done
|
| 113 |
+
modal volume get objectverse-diary-lora-output \
|
| 114 |
+
objectverse-diary-qwen15b-lora-v2/metrics.json \
|
| 115 |
+
exports/objectverse-diary-qwen15b-lora-v2-adapter-dir/training_metrics.json
|
| 116 |
+
modal volume get objectverse-diary-lora-output \
|
| 117 |
+
objectverse-diary-qwen15b-lora-v2/training_config.json \
|
| 118 |
+
exports/objectverse-diary-qwen15b-lora-v2-adapter-dir/training_config.json
|
| 119 |
```
|
| 120 |
|
| 121 |
Then upload the adapter to Hugging Face Hub:
|
| 122 |
|
| 123 |
```bash
|
| 124 |
.venv/bin/python -B scripts/publish_hf_adapter.py \
|
| 125 |
+
--adapter-dir exports/objectverse-diary-qwen15b-lora-v2-adapter-dir \
|
| 126 |
+
--repo-id qqyule/objectverse-diary-qwen15b-lora \
|
| 127 |
+
--commit-message "Upload Objectverse Diary Qwen 1.5B LoRA v2"
|
| 128 |
```
|
| 129 |
|
| 130 |
+
LoRA v2 GGUF conversion and upload:
|
| 131 |
+
|
| 132 |
+
```bash
|
| 133 |
+
.venv/bin/python -B scripts/merge_lora_adapter.py \
|
| 134 |
+
--base-model Qwen/Qwen2.5-1.5B-Instruct \
|
| 135 |
+
--adapter exports/objectverse-diary-qwen15b-lora-v2-adapter-dir \
|
| 136 |
+
--output exports/objectverse-diary-qwen15b-lora-v2-merged-hf
|
| 137 |
+
|
| 138 |
+
git clone https://github.com/ggml-org/llama.cpp.git .tmp/llama.cpp
|
| 139 |
+
git -C .tmp/llama.cpp checkout 8f83d6c271d194bde2d410145a0ce73bc42e85cd
|
| 140 |
+
cmake -S .tmp/llama.cpp -B .tmp/llama.cpp/build -DCMAKE_BUILD_TYPE=Release
|
| 141 |
+
cmake --build .tmp/llama.cpp/build --target llama-quantize -j
|
| 142 |
+
|
| 143 |
+
.venv/bin/python .tmp/llama.cpp/convert_hf_to_gguf.py \
|
| 144 |
+
exports/objectverse-diary-qwen15b-lora-v2-merged-hf \
|
| 145 |
+
--outfile models/objectverse-diary-qwen15b-lora-v2-f16.gguf \
|
| 146 |
+
--outtype f16
|
| 147 |
+
|
| 148 |
+
.tmp/llama.cpp/build/bin/llama-quantize \
|
| 149 |
+
models/objectverse-diary-qwen15b-lora-v2-f16.gguf \
|
| 150 |
+
models/objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf \
|
| 151 |
+
Q4_K_M
|
| 152 |
+
|
| 153 |
+
.venv/bin/python -B scripts/publish_hf_gguf.py \
|
| 154 |
+
--gguf-file models/objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf \
|
| 155 |
+
--repo-id qqyule/objectverse-diary-qwen15b-lora \
|
| 156 |
+
--path-in-repo objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf \
|
| 157 |
+
--commit-message "Upload Objectverse Diary Qwen 1.5B LoRA v2 Q4_K_M GGUF"
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
The final Q4_K_M GGUF is ignored under `models/`. After upload, remove only generated intermediates such as the merged HF folder and F16 GGUF.
|
| 161 |
+
|
| 162 |
Space VLM validation:
|
| 163 |
|
| 164 |
```bash
|
|
|
|
| 175 |
.venv/bin/python -B scripts/check_space_vlm.py --configure-space --rollback-to-mock
|
| 176 |
```
|
| 177 |
|
| 178 |
+
Local LoRA v2 GGUF smoke test:
|
| 179 |
|
| 180 |
```bash
|
| 181 |
.venv/bin/python -B scripts/check_llama_cpp_smoke.py \
|
| 182 |
+
--model-path models/objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf
|
| 183 |
```
|
| 184 |
|
| 185 |
+
Published GGUF source: `qqyule/objectverse-diary-qwen15b-lora`, file `objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf`. Do not commit the downloaded file.
|
| 186 |
|
| 187 |
+
Current status: mock trace generation, trace JSONL export, SFT preview generation, synthetic curated v2 dataset publishing, optional MiniCPM-V wiring, optional llama.cpp wiring, hosted Space VLM validation tooling with non-secret probe support, local GGUF smoke helper, Modal LoRA training scaffolding, Modal LoRA v2 training, HF adapter publishing, GGUF conversion, GGUF upload, and local llama.cpp smoke are implemented. Real text-model validation on Space is not completed yet.
|
scripts/finetune_lora.py
CHANGED
|
@@ -3,7 +3,9 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import argparse
|
|
|
|
| 6 |
import json
|
|
|
|
| 7 |
import sys
|
| 8 |
from collections.abc import Callable, Mapping, Sequence
|
| 9 |
from dataclasses import asdict, dataclass, field
|
|
@@ -46,13 +48,48 @@ class TrainingConfig:
|
|
| 46 |
run_name: str = DEFAULT_RUN_NAME
|
| 47 |
base_model: str = DEFAULT_BASE_MODEL
|
| 48 |
max_steps: int = 80
|
|
|
|
| 49 |
learning_rate: float = 2e-4
|
| 50 |
max_seq_length: int = 1024
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
lora_r: int = 16
|
| 52 |
lora_alpha: int = 32
|
| 53 |
lora_dropout: float = 0.05
|
| 54 |
target_modules: tuple[str, ...] = field(default_factory=lambda: LORA_TARGET_MODULES)
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
def as_remote_dict(self) -> dict[str, object]:
|
| 57 |
payload = asdict(self)
|
| 58 |
payload["target_modules"] = list(self.target_modules)
|
|
@@ -86,11 +123,21 @@ def record_to_training_text(record: Mapping[str, object]) -> str:
|
|
| 86 |
"""Convert one validated chat record into a simple fallback training string."""
|
| 87 |
|
| 88 |
messages = _validate_messages(record.get("messages"), line_number=None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
blocks = []
|
| 90 |
for message in messages:
|
| 91 |
role = str(message["role"]).strip().lower()
|
| 92 |
content = str(message["content"]).strip()
|
| 93 |
blocks.append(f"{role}:\n{content}")
|
|
|
|
|
|
|
| 94 |
return "\n\n".join(blocks).strip()
|
| 95 |
|
| 96 |
|
|
@@ -145,15 +192,29 @@ def _dry_run_summary(
|
|
| 145 |
config: TrainingConfig,
|
| 146 |
) -> dict[str, object]:
|
| 147 |
first_text = record_to_training_text(records[0])
|
|
|
|
| 148 |
return {
|
| 149 |
"mode": "dry-run",
|
| 150 |
"dataset": str(dataset),
|
| 151 |
"record_count": len(records),
|
|
|
|
|
|
|
| 152 |
"base_model": config.base_model,
|
| 153 |
"run_name": config.run_name,
|
| 154 |
"max_steps": config.max_steps,
|
|
|
|
| 155 |
"learning_rate": config.learning_rate,
|
| 156 |
"max_seq_length": config.max_seq_length,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
"lora": {
|
| 158 |
"r": config.lora_r,
|
| 159 |
"alpha": config.lora_alpha,
|
|
@@ -184,7 +245,6 @@ def _train_lora_impl(
|
|
| 184 |
from transformers import (
|
| 185 |
AutoModelForCausalLM,
|
| 186 |
AutoTokenizer,
|
| 187 |
-
DataCollatorForLanguageModeling,
|
| 188 |
Trainer,
|
| 189 |
TrainingArguments,
|
| 190 |
)
|
|
@@ -218,49 +278,46 @@ def _train_lora_impl(
|
|
| 218 |
model.print_trainable_parameters()
|
| 219 |
|
| 220 |
dataset = Dataset.from_list(
|
| 221 |
-
[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
)
|
|
|
|
| 223 |
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
padding=False,
|
| 230 |
-
)
|
| 231 |
-
|
| 232 |
-
tokenized = dataset.map(
|
| 233 |
-
tokenize_batch,
|
| 234 |
-
batched=True,
|
| 235 |
-
remove_columns=["text"],
|
| 236 |
-
desc="Tokenize Objectverse Diary SFT records",
|
| 237 |
)
|
|
|
|
| 238 |
|
| 239 |
-
training_args = TrainingArguments(
|
| 240 |
-
output_dir=str(output_path / "trainer"),
|
| 241 |
-
max_steps=config.max_steps,
|
| 242 |
-
per_device_train_batch_size=1,
|
| 243 |
-
gradient_accumulation_steps=4,
|
| 244 |
-
learning_rate=config.learning_rate,
|
| 245 |
-
logging_steps=5,
|
| 246 |
-
save_strategy="no",
|
| 247 |
-
fp16=torch.cuda.is_available(),
|
| 248 |
-
report_to=[],
|
| 249 |
-
optim="adamw_torch",
|
| 250 |
-
)
|
| 251 |
trainer = Trainer(
|
| 252 |
model=model,
|
| 253 |
args=training_args,
|
| 254 |
-
train_dataset=
|
| 255 |
-
|
|
|
|
| 256 |
)
|
| 257 |
train_result = trainer.train()
|
|
|
|
|
|
|
|
|
|
| 258 |
|
| 259 |
model.save_pretrained(adapter_path)
|
| 260 |
tokenizer.save_pretrained(adapter_path)
|
| 261 |
|
| 262 |
metrics = dict(train_result.metrics)
|
| 263 |
-
metrics
|
|
|
|
|
|
|
| 264 |
metrics["base_model"] = config.base_model
|
| 265 |
(output_path / "metrics.json").write_text(
|
| 266 |
json.dumps(metrics, indent=2, sort_keys=True),
|
|
@@ -278,11 +335,157 @@ def _train_lora_impl(
|
|
| 278 |
"mode": "modal-training",
|
| 279 |
"run_name": config.run_name,
|
| 280 |
"record_count": len(records),
|
|
|
|
|
|
|
| 281 |
"adapter_path": str(adapter_path),
|
| 282 |
"metrics_path": str(output_path / "metrics.json"),
|
| 283 |
}
|
| 284 |
|
| 285 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
def _training_config_from_payload(payload: Mapping[str, object]) -> TrainingConfig:
|
| 287 |
target_modules = payload.get("target_modules", LORA_TARGET_MODULES)
|
| 288 |
if not isinstance(target_modules, Sequence) or isinstance(target_modules, (str, bytes)):
|
|
@@ -291,8 +494,19 @@ def _training_config_from_payload(payload: Mapping[str, object]) -> TrainingConf
|
|
| 291 |
run_name=str(payload.get("run_name", DEFAULT_RUN_NAME)),
|
| 292 |
base_model=str(payload.get("base_model", DEFAULT_BASE_MODEL)),
|
| 293 |
max_steps=int(payload.get("max_steps", 80)),
|
|
|
|
| 294 |
learning_rate=float(payload.get("learning_rate", 2e-4)),
|
| 295 |
max_seq_length=int(payload.get("max_seq_length", 1024)),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
lora_r=int(payload.get("lora_r", 16)),
|
| 297 |
lora_alpha=int(payload.get("lora_alpha", 32)),
|
| 298 |
lora_dropout=float(payload.get("lora_dropout", 0.05)),
|
|
@@ -302,16 +516,39 @@ def _training_config_from_payload(payload: Mapping[str, object]) -> TrainingConf
|
|
| 302 |
|
| 303 |
def _format_training_text(record: Mapping[str, object], tokenizer: Any) -> str:
|
| 304 |
messages = _validate_messages(record.get("messages"), line_number=None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
if hasattr(tokenizer, "apply_chat_template"):
|
| 306 |
try:
|
| 307 |
return tokenizer.apply_chat_template(
|
| 308 |
messages,
|
| 309 |
tokenize=False,
|
| 310 |
-
add_generation_prompt=
|
| 311 |
)
|
| 312 |
except Exception:
|
| 313 |
pass
|
| 314 |
-
return
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
|
| 317 |
def _print_json(payload: Mapping[str, object]) -> None:
|
|
@@ -323,8 +560,22 @@ def _build_config_from_args(args: argparse.Namespace) -> TrainingConfig:
|
|
| 323 |
run_name=args.run_name,
|
| 324 |
base_model=args.base_model,
|
| 325 |
max_steps=args.max_steps,
|
|
|
|
| 326 |
learning_rate=args.learning_rate,
|
| 327 |
max_seq_length=args.max_seq_length,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
)
|
| 329 |
|
| 330 |
|
|
@@ -334,8 +585,22 @@ def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
|
| 334 |
parser.add_argument("--run-name", default=DEFAULT_RUN_NAME)
|
| 335 |
parser.add_argument("--base-model", default=DEFAULT_BASE_MODEL)
|
| 336 |
parser.add_argument("--max-steps", type=int, default=80)
|
|
|
|
| 337 |
parser.add_argument("--learning-rate", type=float, default=2e-4)
|
| 338 |
parser.add_argument("--max-seq-length", type=int, default=1024)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
parser.add_argument("--dry-run", action="store_true")
|
| 340 |
return parser.parse_args(argv)
|
| 341 |
|
|
@@ -390,8 +655,22 @@ if modal is not None:
|
|
| 390 |
run_name: str = DEFAULT_RUN_NAME,
|
| 391 |
base_model: str = DEFAULT_BASE_MODEL,
|
| 392 |
max_steps: int = 80,
|
|
|
|
| 393 |
learning_rate: float = 2e-4,
|
| 394 |
max_seq_length: int = 1024,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
dry_run: bool = False,
|
| 396 |
) -> None:
|
| 397 |
result = run_training_entrypoint(
|
|
@@ -400,8 +679,22 @@ if modal is not None:
|
|
| 400 |
run_name=run_name,
|
| 401 |
base_model=base_model,
|
| 402 |
max_steps=max_steps,
|
|
|
|
| 403 |
learning_rate=learning_rate,
|
| 404 |
max_seq_length=max_seq_length,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
),
|
| 406 |
dry_run=dry_run,
|
| 407 |
allow_remote=True,
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import argparse
|
| 6 |
+
import inspect
|
| 7 |
import json
|
| 8 |
+
import math
|
| 9 |
import sys
|
| 10 |
from collections.abc import Callable, Mapping, Sequence
|
| 11 |
from dataclasses import asdict, dataclass, field
|
|
|
|
| 48 |
run_name: str = DEFAULT_RUN_NAME
|
| 49 |
base_model: str = DEFAULT_BASE_MODEL
|
| 50 |
max_steps: int = 80
|
| 51 |
+
num_train_epochs: float = 3.0
|
| 52 |
learning_rate: float = 2e-4
|
| 53 |
max_seq_length: int = 1024
|
| 54 |
+
per_device_train_batch_size: int = 1
|
| 55 |
+
gradient_accumulation_steps: int = 4
|
| 56 |
+
eval_ratio: float = 0.1
|
| 57 |
+
eval_steps: int = 10
|
| 58 |
+
warmup_ratio: float = 0.03
|
| 59 |
+
weight_decay: float = 0.0
|
| 60 |
+
logging_steps: int = 5
|
| 61 |
+
save_total_limit: int = 2
|
| 62 |
+
seed: int = 42
|
| 63 |
+
assistant_only_loss: bool = True
|
| 64 |
lora_r: int = 16
|
| 65 |
lora_alpha: int = 32
|
| 66 |
lora_dropout: float = 0.05
|
| 67 |
target_modules: tuple[str, ...] = field(default_factory=lambda: LORA_TARGET_MODULES)
|
| 68 |
|
| 69 |
+
def __post_init__(self) -> None:
|
| 70 |
+
if self.max_steps < 0:
|
| 71 |
+
raise ValueError("max_steps must be 0 or greater.")
|
| 72 |
+
if self.max_steps == 0 and self.num_train_epochs <= 0:
|
| 73 |
+
raise ValueError("num_train_epochs must be greater than 0 when max_steps is 0.")
|
| 74 |
+
if self.per_device_train_batch_size < 1:
|
| 75 |
+
raise ValueError("per_device_train_batch_size must be at least 1.")
|
| 76 |
+
if self.gradient_accumulation_steps < 1:
|
| 77 |
+
raise ValueError("gradient_accumulation_steps must be at least 1.")
|
| 78 |
+
if not 0 <= self.eval_ratio < 1:
|
| 79 |
+
raise ValueError("eval_ratio must be between 0 and 1.")
|
| 80 |
+
if self.eval_steps < 1:
|
| 81 |
+
raise ValueError("eval_steps must be at least 1.")
|
| 82 |
+
if self.logging_steps < 1:
|
| 83 |
+
raise ValueError("logging_steps must be at least 1.")
|
| 84 |
+
if self.save_total_limit < 1:
|
| 85 |
+
raise ValueError("save_total_limit must be at least 1.")
|
| 86 |
+
if self.lora_r < 1:
|
| 87 |
+
raise ValueError("lora_r must be at least 1.")
|
| 88 |
+
if self.lora_alpha < 1:
|
| 89 |
+
raise ValueError("lora_alpha must be at least 1.")
|
| 90 |
+
if not 0 <= self.lora_dropout < 1:
|
| 91 |
+
raise ValueError("lora_dropout must be between 0 and 1.")
|
| 92 |
+
|
| 93 |
def as_remote_dict(self) -> dict[str, object]:
|
| 94 |
payload = asdict(self)
|
| 95 |
payload["target_modules"] = list(self.target_modules)
|
|
|
|
| 123 |
"""Convert one validated chat record into a simple fallback training string."""
|
| 124 |
|
| 125 |
messages = _validate_messages(record.get("messages"), line_number=None)
|
| 126 |
+
return _messages_to_training_text(messages)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _messages_to_training_text(
|
| 130 |
+
messages: Sequence[Mapping[str, str]],
|
| 131 |
+
*,
|
| 132 |
+
add_generation_prompt: bool = False,
|
| 133 |
+
) -> str:
|
| 134 |
blocks = []
|
| 135 |
for message in messages:
|
| 136 |
role = str(message["role"]).strip().lower()
|
| 137 |
content = str(message["content"]).strip()
|
| 138 |
blocks.append(f"{role}:\n{content}")
|
| 139 |
+
if add_generation_prompt:
|
| 140 |
+
blocks.append("assistant:\n")
|
| 141 |
return "\n\n".join(blocks).strip()
|
| 142 |
|
| 143 |
|
|
|
|
| 192 |
config: TrainingConfig,
|
| 193 |
) -> dict[str, object]:
|
| 194 |
first_text = record_to_training_text(records[0])
|
| 195 |
+
eval_count = _eval_record_count(len(records), config.eval_ratio)
|
| 196 |
return {
|
| 197 |
"mode": "dry-run",
|
| 198 |
"dataset": str(dataset),
|
| 199 |
"record_count": len(records),
|
| 200 |
+
"train_record_count": len(records) - eval_count,
|
| 201 |
+
"eval_record_count": eval_count,
|
| 202 |
"base_model": config.base_model,
|
| 203 |
"run_name": config.run_name,
|
| 204 |
"max_steps": config.max_steps,
|
| 205 |
+
"num_train_epochs": config.num_train_epochs,
|
| 206 |
"learning_rate": config.learning_rate,
|
| 207 |
"max_seq_length": config.max_seq_length,
|
| 208 |
+
"per_device_train_batch_size": config.per_device_train_batch_size,
|
| 209 |
+
"gradient_accumulation_steps": config.gradient_accumulation_steps,
|
| 210 |
+
"effective_batch_size": (
|
| 211 |
+
config.per_device_train_batch_size * config.gradient_accumulation_steps
|
| 212 |
+
),
|
| 213 |
+
"eval_ratio": config.eval_ratio,
|
| 214 |
+
"eval_steps": config.eval_steps,
|
| 215 |
+
"warmup_ratio": config.warmup_ratio,
|
| 216 |
+
"weight_decay": config.weight_decay,
|
| 217 |
+
"assistant_only_loss": config.assistant_only_loss,
|
| 218 |
"lora": {
|
| 219 |
"r": config.lora_r,
|
| 220 |
"alpha": config.lora_alpha,
|
|
|
|
| 245 |
from transformers import (
|
| 246 |
AutoModelForCausalLM,
|
| 247 |
AutoTokenizer,
|
|
|
|
| 248 |
Trainer,
|
| 249 |
TrainingArguments,
|
| 250 |
)
|
|
|
|
| 278 |
model.print_trainable_parameters()
|
| 279 |
|
| 280 |
dataset = Dataset.from_list(
|
| 281 |
+
[
|
| 282 |
+
_tokenize_training_example(
|
| 283 |
+
record,
|
| 284 |
+
tokenizer,
|
| 285 |
+
max_length=config.max_seq_length,
|
| 286 |
+
assistant_only_loss=config.assistant_only_loss,
|
| 287 |
+
)
|
| 288 |
+
for record in records
|
| 289 |
+
]
|
| 290 |
)
|
| 291 |
+
train_dataset, eval_dataset = _split_dataset(dataset, config)
|
| 292 |
|
| 293 |
+
training_kwargs = _training_arguments_kwargs(
|
| 294 |
+
output_dir=output_path / "trainer",
|
| 295 |
+
config=config,
|
| 296 |
+
has_eval=eval_dataset is not None,
|
| 297 |
+
training_arguments_cls=TrainingArguments,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
)
|
| 299 |
+
training_kwargs["fp16"] = torch.cuda.is_available()
|
| 300 |
|
| 301 |
+
training_args = TrainingArguments(**training_kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
trainer = Trainer(
|
| 303 |
model=model,
|
| 304 |
args=training_args,
|
| 305 |
+
train_dataset=train_dataset,
|
| 306 |
+
eval_dataset=eval_dataset,
|
| 307 |
+
data_collator=_build_supervised_data_collator(tokenizer, torch),
|
| 308 |
)
|
| 309 |
train_result = trainer.train()
|
| 310 |
+
eval_metrics: dict[str, object] = {}
|
| 311 |
+
if eval_dataset is not None:
|
| 312 |
+
eval_metrics = dict(trainer.evaluate())
|
| 313 |
|
| 314 |
model.save_pretrained(adapter_path)
|
| 315 |
tokenizer.save_pretrained(adapter_path)
|
| 316 |
|
| 317 |
metrics = dict(train_result.metrics)
|
| 318 |
+
metrics.update(eval_metrics)
|
| 319 |
+
metrics["train_records"] = len(train_dataset)
|
| 320 |
+
metrics["eval_records"] = len(eval_dataset) if eval_dataset is not None else 0
|
| 321 |
metrics["base_model"] = config.base_model
|
| 322 |
(output_path / "metrics.json").write_text(
|
| 323 |
json.dumps(metrics, indent=2, sort_keys=True),
|
|
|
|
| 335 |
"mode": "modal-training",
|
| 336 |
"run_name": config.run_name,
|
| 337 |
"record_count": len(records),
|
| 338 |
+
"train_record_count": len(train_dataset),
|
| 339 |
+
"eval_record_count": len(eval_dataset) if eval_dataset is not None else 0,
|
| 340 |
"adapter_path": str(adapter_path),
|
| 341 |
"metrics_path": str(output_path / "metrics.json"),
|
| 342 |
}
|
| 343 |
|
| 344 |
|
| 345 |
+
def _tokenize_training_example(
|
| 346 |
+
record: Mapping[str, object],
|
| 347 |
+
tokenizer: Any,
|
| 348 |
+
*,
|
| 349 |
+
max_length: int,
|
| 350 |
+
assistant_only_loss: bool,
|
| 351 |
+
) -> dict[str, list[int]]:
|
| 352 |
+
full_text = _format_training_text(record, tokenizer)
|
| 353 |
+
encoded = tokenizer(
|
| 354 |
+
full_text,
|
| 355 |
+
truncation=True,
|
| 356 |
+
max_length=max_length,
|
| 357 |
+
padding=False,
|
| 358 |
+
add_special_tokens=False,
|
| 359 |
+
)
|
| 360 |
+
input_ids = list(encoded["input_ids"])
|
| 361 |
+
labels = list(input_ids)
|
| 362 |
+
|
| 363 |
+
if assistant_only_loss:
|
| 364 |
+
prompt_text = _format_prompt_text(record, tokenizer)
|
| 365 |
+
prompt_encoded = tokenizer(
|
| 366 |
+
prompt_text,
|
| 367 |
+
truncation=True,
|
| 368 |
+
max_length=max_length,
|
| 369 |
+
padding=False,
|
| 370 |
+
add_special_tokens=False,
|
| 371 |
+
)
|
| 372 |
+
mask_count = min(len(prompt_encoded["input_ids"]), len(labels))
|
| 373 |
+
labels[:mask_count] = [-100] * mask_count
|
| 374 |
+
if not any(label != -100 for label in labels):
|
| 375 |
+
raise ValueError(
|
| 376 |
+
"max_seq_length truncates all assistant labels; increase max_seq_length."
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
return {
|
| 380 |
+
"input_ids": input_ids,
|
| 381 |
+
"attention_mask": list(encoded["attention_mask"]),
|
| 382 |
+
"labels": labels,
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
def _split_dataset(dataset: Any, config: TrainingConfig) -> tuple[Any, Any | None]:
|
| 387 |
+
eval_count = _eval_record_count(len(dataset), config.eval_ratio)
|
| 388 |
+
if eval_count == 0:
|
| 389 |
+
return dataset, None
|
| 390 |
+
split = dataset.train_test_split(test_size=eval_count, shuffle=True, seed=config.seed)
|
| 391 |
+
return split["train"], split["test"]
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
def _eval_record_count(record_count: int, eval_ratio: float) -> int:
|
| 395 |
+
if record_count < 2 or eval_ratio <= 0:
|
| 396 |
+
return 0
|
| 397 |
+
return max(1, min(record_count - 1, math.ceil(record_count * eval_ratio)))
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
def _training_arguments_kwargs(
|
| 401 |
+
*,
|
| 402 |
+
output_dir: Path,
|
| 403 |
+
config: TrainingConfig,
|
| 404 |
+
has_eval: bool,
|
| 405 |
+
training_arguments_cls: Any | None = None,
|
| 406 |
+
) -> dict[str, object]:
|
| 407 |
+
kwargs: dict[str, object] = {
|
| 408 |
+
"output_dir": str(output_dir),
|
| 409 |
+
"per_device_train_batch_size": config.per_device_train_batch_size,
|
| 410 |
+
"gradient_accumulation_steps": config.gradient_accumulation_steps,
|
| 411 |
+
"learning_rate": config.learning_rate,
|
| 412 |
+
"logging_steps": config.logging_steps,
|
| 413 |
+
"warmup_ratio": config.warmup_ratio,
|
| 414 |
+
"weight_decay": config.weight_decay,
|
| 415 |
+
"report_to": [],
|
| 416 |
+
"optim": "adamw_torch",
|
| 417 |
+
"seed": config.seed,
|
| 418 |
+
"data_seed": config.seed,
|
| 419 |
+
}
|
| 420 |
+
if config.max_steps > 0:
|
| 421 |
+
kwargs["max_steps"] = config.max_steps
|
| 422 |
+
else:
|
| 423 |
+
kwargs["num_train_epochs"] = config.num_train_epochs
|
| 424 |
+
|
| 425 |
+
if has_eval:
|
| 426 |
+
kwargs.update(
|
| 427 |
+
{
|
| 428 |
+
"eval_steps": config.eval_steps,
|
| 429 |
+
"save_steps": config.eval_steps,
|
| 430 |
+
"save_strategy": "steps",
|
| 431 |
+
"save_total_limit": config.save_total_limit,
|
| 432 |
+
"load_best_model_at_end": True,
|
| 433 |
+
"metric_for_best_model": "eval_loss",
|
| 434 |
+
"greater_is_better": False,
|
| 435 |
+
}
|
| 436 |
+
)
|
| 437 |
+
if training_arguments_cls is None:
|
| 438 |
+
kwargs["eval_strategy"] = "steps"
|
| 439 |
+
else:
|
| 440 |
+
_set_eval_strategy_kwarg(kwargs, training_arguments_cls, "steps")
|
| 441 |
+
else:
|
| 442 |
+
kwargs["save_strategy"] = "no"
|
| 443 |
+
|
| 444 |
+
return kwargs
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
def _set_eval_strategy_kwarg(
|
| 448 |
+
kwargs: dict[str, object],
|
| 449 |
+
training_arguments_cls: Any,
|
| 450 |
+
strategy: str,
|
| 451 |
+
) -> None:
|
| 452 |
+
parameters = inspect.signature(training_arguments_cls.__init__).parameters
|
| 453 |
+
if "eval_strategy" in parameters:
|
| 454 |
+
kwargs["eval_strategy"] = strategy
|
| 455 |
+
elif "evaluation_strategy" in parameters:
|
| 456 |
+
kwargs["evaluation_strategy"] = strategy
|
| 457 |
+
else:
|
| 458 |
+
kwargs["do_eval"] = strategy != "no"
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def _build_supervised_data_collator(tokenizer: Any, torch_module: Any) -> Callable:
|
| 462 |
+
def collate(features: list[Mapping[str, list[int]]]) -> dict[str, object]:
|
| 463 |
+
labels = [list(feature["labels"]) for feature in features]
|
| 464 |
+
model_features = [
|
| 465 |
+
{
|
| 466 |
+
"input_ids": list(feature["input_ids"]),
|
| 467 |
+
"attention_mask": list(feature["attention_mask"]),
|
| 468 |
+
}
|
| 469 |
+
for feature in features
|
| 470 |
+
]
|
| 471 |
+
batch = tokenizer.pad(model_features, padding=True, return_tensors="pt")
|
| 472 |
+
max_length = batch["input_ids"].shape[1]
|
| 473 |
+
label_tensor = torch_module.full(
|
| 474 |
+
(len(labels), max_length),
|
| 475 |
+
-100,
|
| 476 |
+
dtype=torch_module.long,
|
| 477 |
+
)
|
| 478 |
+
for index, label in enumerate(labels):
|
| 479 |
+
label_tensor[index, : len(label)] = torch_module.tensor(
|
| 480 |
+
label,
|
| 481 |
+
dtype=torch_module.long,
|
| 482 |
+
)
|
| 483 |
+
batch["labels"] = label_tensor
|
| 484 |
+
return batch
|
| 485 |
+
|
| 486 |
+
return collate
|
| 487 |
+
|
| 488 |
+
|
| 489 |
def _training_config_from_payload(payload: Mapping[str, object]) -> TrainingConfig:
|
| 490 |
target_modules = payload.get("target_modules", LORA_TARGET_MODULES)
|
| 491 |
if not isinstance(target_modules, Sequence) or isinstance(target_modules, (str, bytes)):
|
|
|
|
| 494 |
run_name=str(payload.get("run_name", DEFAULT_RUN_NAME)),
|
| 495 |
base_model=str(payload.get("base_model", DEFAULT_BASE_MODEL)),
|
| 496 |
max_steps=int(payload.get("max_steps", 80)),
|
| 497 |
+
num_train_epochs=float(payload.get("num_train_epochs", 3.0)),
|
| 498 |
learning_rate=float(payload.get("learning_rate", 2e-4)),
|
| 499 |
max_seq_length=int(payload.get("max_seq_length", 1024)),
|
| 500 |
+
per_device_train_batch_size=int(payload.get("per_device_train_batch_size", 1)),
|
| 501 |
+
gradient_accumulation_steps=int(payload.get("gradient_accumulation_steps", 4)),
|
| 502 |
+
eval_ratio=float(payload.get("eval_ratio", 0.1)),
|
| 503 |
+
eval_steps=int(payload.get("eval_steps", 10)),
|
| 504 |
+
warmup_ratio=float(payload.get("warmup_ratio", 0.03)),
|
| 505 |
+
weight_decay=float(payload.get("weight_decay", 0.0)),
|
| 506 |
+
logging_steps=int(payload.get("logging_steps", 5)),
|
| 507 |
+
save_total_limit=int(payload.get("save_total_limit", 2)),
|
| 508 |
+
seed=int(payload.get("seed", 42)),
|
| 509 |
+
assistant_only_loss=bool(payload.get("assistant_only_loss", True)),
|
| 510 |
lora_r=int(payload.get("lora_r", 16)),
|
| 511 |
lora_alpha=int(payload.get("lora_alpha", 32)),
|
| 512 |
lora_dropout=float(payload.get("lora_dropout", 0.05)),
|
|
|
|
| 516 |
|
| 517 |
def _format_training_text(record: Mapping[str, object], tokenizer: Any) -> str:
|
| 518 |
messages = _validate_messages(record.get("messages"), line_number=None)
|
| 519 |
+
return _format_messages(messages, tokenizer, add_generation_prompt=False)
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
def _format_prompt_text(record: Mapping[str, object], tokenizer: Any) -> str:
|
| 523 |
+
messages = _validate_messages(record.get("messages"), line_number=None)
|
| 524 |
+
assistant_indices = [
|
| 525 |
+
index for index, message in enumerate(messages) if message["role"].lower() == "assistant"
|
| 526 |
+
]
|
| 527 |
+
if not assistant_indices:
|
| 528 |
+
raise ValueError("assistant_only_loss requires at least one assistant message.")
|
| 529 |
+
prompt_messages = messages[: assistant_indices[-1]]
|
| 530 |
+
return _format_messages(prompt_messages, tokenizer, add_generation_prompt=True)
|
| 531 |
+
|
| 532 |
+
|
| 533 |
+
def _format_messages(
|
| 534 |
+
messages: Sequence[Mapping[str, str]],
|
| 535 |
+
tokenizer: Any,
|
| 536 |
+
*,
|
| 537 |
+
add_generation_prompt: bool,
|
| 538 |
+
) -> str:
|
| 539 |
if hasattr(tokenizer, "apply_chat_template"):
|
| 540 |
try:
|
| 541 |
return tokenizer.apply_chat_template(
|
| 542 |
messages,
|
| 543 |
tokenize=False,
|
| 544 |
+
add_generation_prompt=add_generation_prompt,
|
| 545 |
)
|
| 546 |
except Exception:
|
| 547 |
pass
|
| 548 |
+
return _messages_to_training_text(
|
| 549 |
+
messages,
|
| 550 |
+
add_generation_prompt=add_generation_prompt,
|
| 551 |
+
)
|
| 552 |
|
| 553 |
|
| 554 |
def _print_json(payload: Mapping[str, object]) -> None:
|
|
|
|
| 560 |
run_name=args.run_name,
|
| 561 |
base_model=args.base_model,
|
| 562 |
max_steps=args.max_steps,
|
| 563 |
+
num_train_epochs=args.num_train_epochs,
|
| 564 |
learning_rate=args.learning_rate,
|
| 565 |
max_seq_length=args.max_seq_length,
|
| 566 |
+
per_device_train_batch_size=args.per_device_train_batch_size,
|
| 567 |
+
gradient_accumulation_steps=args.gradient_accumulation_steps,
|
| 568 |
+
eval_ratio=args.eval_ratio,
|
| 569 |
+
eval_steps=args.eval_steps,
|
| 570 |
+
warmup_ratio=args.warmup_ratio,
|
| 571 |
+
weight_decay=args.weight_decay,
|
| 572 |
+
logging_steps=args.logging_steps,
|
| 573 |
+
save_total_limit=args.save_total_limit,
|
| 574 |
+
seed=args.seed,
|
| 575 |
+
assistant_only_loss=args.assistant_only_loss,
|
| 576 |
+
lora_r=args.lora_r,
|
| 577 |
+
lora_alpha=args.lora_alpha,
|
| 578 |
+
lora_dropout=args.lora_dropout,
|
| 579 |
)
|
| 580 |
|
| 581 |
|
|
|
|
| 585 |
parser.add_argument("--run-name", default=DEFAULT_RUN_NAME)
|
| 586 |
parser.add_argument("--base-model", default=DEFAULT_BASE_MODEL)
|
| 587 |
parser.add_argument("--max-steps", type=int, default=80)
|
| 588 |
+
parser.add_argument("--num-train-epochs", type=float, default=3.0)
|
| 589 |
parser.add_argument("--learning-rate", type=float, default=2e-4)
|
| 590 |
parser.add_argument("--max-seq-length", type=int, default=1024)
|
| 591 |
+
parser.add_argument("--per-device-train-batch-size", type=int, default=1)
|
| 592 |
+
parser.add_argument("--gradient-accumulation-steps", type=int, default=4)
|
| 593 |
+
parser.add_argument("--eval-ratio", type=float, default=0.1)
|
| 594 |
+
parser.add_argument("--eval-steps", type=int, default=10)
|
| 595 |
+
parser.add_argument("--warmup-ratio", type=float, default=0.03)
|
| 596 |
+
parser.add_argument("--weight-decay", type=float, default=0.0)
|
| 597 |
+
parser.add_argument("--logging-steps", type=int, default=5)
|
| 598 |
+
parser.add_argument("--save-total-limit", type=int, default=2)
|
| 599 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 600 |
+
parser.add_argument("--assistant-only-loss", action=argparse.BooleanOptionalAction, default=True)
|
| 601 |
+
parser.add_argument("--lora-r", type=int, default=16)
|
| 602 |
+
parser.add_argument("--lora-alpha", type=int, default=32)
|
| 603 |
+
parser.add_argument("--lora-dropout", type=float, default=0.05)
|
| 604 |
parser.add_argument("--dry-run", action="store_true")
|
| 605 |
return parser.parse_args(argv)
|
| 606 |
|
|
|
|
| 655 |
run_name: str = DEFAULT_RUN_NAME,
|
| 656 |
base_model: str = DEFAULT_BASE_MODEL,
|
| 657 |
max_steps: int = 80,
|
| 658 |
+
num_train_epochs: float = 3.0,
|
| 659 |
learning_rate: float = 2e-4,
|
| 660 |
max_seq_length: int = 1024,
|
| 661 |
+
per_device_train_batch_size: int = 1,
|
| 662 |
+
gradient_accumulation_steps: int = 4,
|
| 663 |
+
eval_ratio: float = 0.1,
|
| 664 |
+
eval_steps: int = 10,
|
| 665 |
+
warmup_ratio: float = 0.03,
|
| 666 |
+
weight_decay: float = 0.0,
|
| 667 |
+
logging_steps: int = 5,
|
| 668 |
+
save_total_limit: int = 2,
|
| 669 |
+
seed: int = 42,
|
| 670 |
+
assistant_only_loss: bool = True,
|
| 671 |
+
lora_r: int = 16,
|
| 672 |
+
lora_alpha: int = 32,
|
| 673 |
+
lora_dropout: float = 0.05,
|
| 674 |
dry_run: bool = False,
|
| 675 |
) -> None:
|
| 676 |
result = run_training_entrypoint(
|
|
|
|
| 679 |
run_name=run_name,
|
| 680 |
base_model=base_model,
|
| 681 |
max_steps=max_steps,
|
| 682 |
+
num_train_epochs=num_train_epochs,
|
| 683 |
learning_rate=learning_rate,
|
| 684 |
max_seq_length=max_seq_length,
|
| 685 |
+
per_device_train_batch_size=per_device_train_batch_size,
|
| 686 |
+
gradient_accumulation_steps=gradient_accumulation_steps,
|
| 687 |
+
eval_ratio=eval_ratio,
|
| 688 |
+
eval_steps=eval_steps,
|
| 689 |
+
warmup_ratio=warmup_ratio,
|
| 690 |
+
weight_decay=weight_decay,
|
| 691 |
+
logging_steps=logging_steps,
|
| 692 |
+
save_total_limit=save_total_limit,
|
| 693 |
+
seed=seed,
|
| 694 |
+
assistant_only_loss=assistant_only_loss,
|
| 695 |
+
lora_r=lora_r,
|
| 696 |
+
lora_alpha=lora_alpha,
|
| 697 |
+
lora_dropout=lora_dropout,
|
| 698 |
),
|
| 699 |
dry_run=dry_run,
|
| 700 |
allow_remote=True,
|
scripts/merge_lora_adapter.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Merge an Objectverse Diary LoRA adapter into its base Hugging Face model."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import json
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
ADAPTER_WEIGHT_FILES = ("adapter_model.safetensors", "adapter_model.bin")
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def validate_adapter_source(adapter: str | Path, *, base_model: str) -> dict[str, object]:
|
| 15 |
+
adapter_text = str(adapter)
|
| 16 |
+
adapter_path = Path(adapter_text)
|
| 17 |
+
if adapter_path.exists():
|
| 18 |
+
if not adapter_path.is_dir():
|
| 19 |
+
raise ValueError(f"Adapter path is not a directory: {adapter_path}")
|
| 20 |
+
config_path = adapter_path / "adapter_config.json"
|
| 21 |
+
if not config_path.exists():
|
| 22 |
+
raise ValueError(f"Adapter directory is missing adapter_config.json: {adapter_path}")
|
| 23 |
+
if not any((adapter_path / name).exists() for name in ADAPTER_WEIGHT_FILES):
|
| 24 |
+
raise ValueError(
|
| 25 |
+
"Adapter directory is missing adapter_model.safetensors or adapter_model.bin."
|
| 26 |
+
)
|
| 27 |
+
config = _read_adapter_config(config_path)
|
| 28 |
+
configured_base = config.get("base_model_name_or_path")
|
| 29 |
+
if configured_base and str(configured_base) != base_model:
|
| 30 |
+
raise ValueError(
|
| 31 |
+
f"Adapter base model is {configured_base!r}, expected {base_model!r}."
|
| 32 |
+
)
|
| 33 |
+
return {
|
| 34 |
+
"adapter": str(adapter_path),
|
| 35 |
+
"adapter_type": "local",
|
| 36 |
+
"adapter_base_model": configured_base or "",
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
if "/" not in adapter_text:
|
| 40 |
+
raise FileNotFoundError(f"Adapter source does not exist: {adapter_text}")
|
| 41 |
+
return {
|
| 42 |
+
"adapter": adapter_text,
|
| 43 |
+
"adapter_type": "hub",
|
| 44 |
+
"adapter_base_model": "",
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def plan_merge(
|
| 49 |
+
*,
|
| 50 |
+
base_model: str,
|
| 51 |
+
adapter: str | Path,
|
| 52 |
+
output: Path,
|
| 53 |
+
dry_run: bool,
|
| 54 |
+
) -> dict[str, object]:
|
| 55 |
+
summary = validate_adapter_source(adapter, base_model=base_model)
|
| 56 |
+
summary.update(
|
| 57 |
+
{
|
| 58 |
+
"base_model": base_model,
|
| 59 |
+
"output": str(output),
|
| 60 |
+
"dry_run": dry_run,
|
| 61 |
+
}
|
| 62 |
+
)
|
| 63 |
+
if dry_run:
|
| 64 |
+
summary["merged"] = False
|
| 65 |
+
return summary
|
| 66 |
+
|
| 67 |
+
merge_lora_adapter(
|
| 68 |
+
base_model=base_model,
|
| 69 |
+
adapter=str(adapter),
|
| 70 |
+
output=output,
|
| 71 |
+
)
|
| 72 |
+
summary["merged"] = True
|
| 73 |
+
summary["files"] = sorted(path.name for path in output.iterdir() if path.is_file())
|
| 74 |
+
return summary
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def merge_lora_adapter(
|
| 78 |
+
*,
|
| 79 |
+
base_model: str,
|
| 80 |
+
adapter: str,
|
| 81 |
+
output: Path,
|
| 82 |
+
) -> None:
|
| 83 |
+
from peft import PeftModel
|
| 84 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 85 |
+
|
| 86 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 87 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 88 |
+
base_model,
|
| 89 |
+
torch_dtype="auto",
|
| 90 |
+
device_map={"": "cpu"},
|
| 91 |
+
low_cpu_mem_usage=True,
|
| 92 |
+
)
|
| 93 |
+
peft_model = PeftModel.from_pretrained(model, adapter)
|
| 94 |
+
merged = peft_model.merge_and_unload(safe_merge=True)
|
| 95 |
+
merged.save_pretrained(
|
| 96 |
+
output,
|
| 97 |
+
safe_serialization=True,
|
| 98 |
+
max_shard_size="2GB",
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
tokenizer = AutoTokenizer.from_pretrained(adapter if Path(adapter).exists() else base_model)
|
| 102 |
+
tokenizer.save_pretrained(output)
|
| 103 |
+
|
| 104 |
+
metadata = {
|
| 105 |
+
"base_model": base_model,
|
| 106 |
+
"adapter": adapter,
|
| 107 |
+
"output": str(output),
|
| 108 |
+
"format": "merged-hf",
|
| 109 |
+
}
|
| 110 |
+
(output / "objectverse_merge_metadata.json").write_text(
|
| 111 |
+
json.dumps(metadata, indent=2, sort_keys=True),
|
| 112 |
+
encoding="utf-8",
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def _read_adapter_config(config_path: Path) -> dict[str, object]:
|
| 117 |
+
try:
|
| 118 |
+
payload = json.loads(config_path.read_text(encoding="utf-8"))
|
| 119 |
+
except json.JSONDecodeError as exc:
|
| 120 |
+
raise ValueError(f"Invalid adapter_config.json: {exc.msg}") from exc
|
| 121 |
+
if not isinstance(payload, dict):
|
| 122 |
+
raise ValueError("adapter_config.json must contain a JSON object.")
|
| 123 |
+
return payload
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def _print_json(payload: dict[str, Any]) -> None:
|
| 127 |
+
print(json.dumps(payload, indent=2, sort_keys=True), flush=True)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _parse_args() -> argparse.Namespace:
|
| 131 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 132 |
+
parser.add_argument("--base-model", required=True)
|
| 133 |
+
parser.add_argument("--adapter", required=True)
|
| 134 |
+
parser.add_argument("--output", type=Path, required=True)
|
| 135 |
+
parser.add_argument("--dry-run", action="store_true")
|
| 136 |
+
return parser.parse_args()
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def main() -> None:
|
| 140 |
+
args = _parse_args()
|
| 141 |
+
_print_json(
|
| 142 |
+
plan_merge(
|
| 143 |
+
base_model=args.base_model,
|
| 144 |
+
adapter=args.adapter,
|
| 145 |
+
output=args.output,
|
| 146 |
+
dry_run=args.dry_run,
|
| 147 |
+
)
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
if __name__ == "__main__":
|
| 152 |
+
try:
|
| 153 |
+
main()
|
| 154 |
+
except Exception as exc:
|
| 155 |
+
raise SystemExit(str(exc)) from exc
|
scripts/prepare_curated_dataset.py
CHANGED
|
@@ -16,8 +16,11 @@ from src.models.schema import DiaryEntry, ObjectInfo, ObjectUnderstanding, Perso
|
|
| 16 |
|
| 17 |
|
| 18 |
DEFAULT_OUTPUT_PATH = Path("data/train/objectverse_sft_curated.jsonl")
|
|
|
|
| 19 |
DEFAULT_COUNT = 50
|
| 20 |
-
|
|
|
|
|
|
|
| 21 |
|
| 22 |
SYSTEM_PROMPT = (
|
| 23 |
"You are Objectverse Diary, an English-first small-model assistant. "
|
|
@@ -91,6 +94,226 @@ OBJECTS = [
|
|
| 91 |
},
|
| 92 |
]
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
MODE_PROFILES = {
|
| 95 |
"Cynical": {
|
| 96 |
"mood": "tired but sharply observant",
|
|
@@ -125,15 +348,24 @@ MODE_PROFILES = {
|
|
| 125 |
}
|
| 126 |
|
| 127 |
|
| 128 |
-
def build_curated_records(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
if count < 1:
|
| 130 |
raise ValueError("count must be at least 1")
|
| 131 |
|
|
|
|
|
|
|
| 132 |
records: list[dict[str, object]] = []
|
| 133 |
for index in range(count):
|
| 134 |
-
obj =
|
| 135 |
-
mode = MODES[(index // len(
|
| 136 |
-
record_id =
|
| 137 |
understanding = _build_object_understanding(obj)
|
| 138 |
persona = _build_persona(obj, mode)
|
| 139 |
diary = _build_diary(obj, mode, persona.persona, index)
|
|
@@ -141,31 +373,29 @@ def build_curated_records(count: int = DEFAULT_COUNT) -> list[dict[str, object]]
|
|
| 141 |
"persona": persona.persona.model_dump(mode="json"),
|
| 142 |
"diary": diary.model_dump(mode="json"),
|
| 143 |
}
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
}
|
| 168 |
-
)
|
| 169 |
return records
|
| 170 |
|
| 171 |
|
|
@@ -176,8 +406,48 @@ def write_jsonl(records: Sequence[Mapping[str, object]], output_path: Path) -> P
|
|
| 176 |
return output_path
|
| 177 |
|
| 178 |
|
| 179 |
-
def prepare_curated_dataset(
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
|
| 183 |
def _build_object_understanding(obj: Mapping[str, object]) -> ObjectUnderstanding:
|
|
@@ -212,18 +482,19 @@ def _build_diary(obj: Mapping[str, object], mode: str, persona: Persona, index:
|
|
| 212 |
profile = MODE_PROFILES[mode]
|
| 213 |
object_name = str(obj["name"])
|
| 214 |
features = ", ".join(str(feature) for feature in obj["features"][:2])
|
|
|
|
| 215 |
day_number = 300 + index + len(object_name)
|
| 216 |
english = (
|
| 217 |
f"Today I waited in the {obj['context']} wearing my {features} like official records. "
|
| 218 |
f"The humans moved around me with the confidence of temporary weather. "
|
| 219 |
f"I remembered how I {obj['memory']}, and I answered in my own way: {profile['voice']}. "
|
| 220 |
-
f"My mood is {persona.mood}, but I am still here,
|
| 221 |
)
|
| 222 |
chinese = (
|
| 223 |
f"今天我待在 {obj['context']},带着 {features},像一份安静的档案。"
|
| 224 |
f"人类从我身边经过,好像自己不是短暂天气。"
|
| 225 |
f"我记得自己曾经 {obj['memory']},于是用自己的方式回应:{profile['voice']}。"
|
| 226 |
-
f"我的情绪是 {persona.mood},但我仍在这里,
|
| 227 |
)
|
| 228 |
return DiaryEntry(
|
| 229 |
title=f"Secret Diary - Day {day_number}",
|
|
@@ -246,7 +517,10 @@ def _character_name(object_name: str, mode: str) -> str:
|
|
| 246 |
|
| 247 |
def _object_description(obj: Mapping[str, object]) -> str:
|
| 248 |
features = ", ".join(str(feature) for feature in obj["features"])
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
|
| 252 |
def _user_prompt(object_understanding: Mapping[str, object], mode: str) -> str:
|
|
@@ -260,15 +534,17 @@ def _user_prompt(object_understanding: Mapping[str, object], mode: str) -> str:
|
|
| 260 |
|
| 261 |
def _parse_args() -> argparse.Namespace:
|
| 262 |
parser = argparse.ArgumentParser(description=__doc__)
|
| 263 |
-
parser.add_argument("--
|
| 264 |
-
parser.add_argument("--
|
|
|
|
| 265 |
return parser.parse_args()
|
| 266 |
|
| 267 |
|
| 268 |
def main() -> None:
|
| 269 |
args = _parse_args()
|
| 270 |
-
output_path = prepare_curated_dataset(args.output, args.count)
|
| 271 |
-
|
|
|
|
| 272 |
|
| 273 |
|
| 274 |
if __name__ == "__main__":
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
DEFAULT_OUTPUT_PATH = Path("data/train/objectverse_sft_curated.jsonl")
|
| 19 |
+
DEFAULT_V2_OUTPUT_PATH = Path("data/train/objectverse_sft_curated_v2.jsonl")
|
| 20 |
DEFAULT_COUNT = 50
|
| 21 |
+
DEFAULT_V2_COUNT = 200
|
| 22 |
+
SOURCE_V1 = "objectverse-diary-synthetic-curated-v1"
|
| 23 |
+
SOURCE_V2 = "objectverse-diary-synthetic-curated-v2"
|
| 24 |
|
| 25 |
SYSTEM_PROMPT = (
|
| 26 |
"You are Objectverse Diary, an English-first small-model assistant. "
|
|
|
|
| 94 |
},
|
| 95 |
]
|
| 96 |
|
| 97 |
+
OBJECTS_V2 = [
|
| 98 |
+
*(
|
| 99 |
+
dict(
|
| 100 |
+
obj,
|
| 101 |
+
scene_detail=f"resting in the {obj['context']} with a history no one inventoried",
|
| 102 |
+
)
|
| 103 |
+
for obj in OBJECTS
|
| 104 |
+
),
|
| 105 |
+
{
|
| 106 |
+
"name": "wireless earbud case",
|
| 107 |
+
"features": ["rounded white shell", "tiny hinge", "charging light"],
|
| 108 |
+
"context": "commuter bag",
|
| 109 |
+
"memory": "held two small arguments against silence through a crowded train",
|
| 110 |
+
"scene_detail": "buried beside lint, receipts, and one forgotten mint",
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
"name": "transit card",
|
| 114 |
+
"features": ["scuffed plastic", "faded corner", "thin blue stripe"],
|
| 115 |
+
"context": "wallet slot",
|
| 116 |
+
"memory": "opened gates for mornings that were already late",
|
| 117 |
+
"scene_detail": "pressed flat under coins and expired coupons",
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"name": "canvas tote bag",
|
| 121 |
+
"features": ["creased cotton", "ink logo", "soft handles"],
|
| 122 |
+
"context": "entryway floor",
|
| 123 |
+
"memory": "carried groceries, books, and ambitions heavier than both",
|
| 124 |
+
"scene_detail": "slumped open with a receipt still clinging inside",
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"name": "cracked phone case",
|
| 128 |
+
"features": ["clear plastic", "corner crack", "fingerprint haze"],
|
| 129 |
+
"context": "bedside table",
|
| 130 |
+
"memory": "took the impact so the glowing rectangle could remain innocent",
|
| 131 |
+
"scene_detail": "lying face down after another nervous scroll",
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"name": "lip balm tube",
|
| 135 |
+
"features": ["twisted cap", "pocket scratches", "worn label"],
|
| 136 |
+
"context": "coat pocket",
|
| 137 |
+
"memory": "answered every small weather emergency without being thanked",
|
| 138 |
+
"scene_detail": "rolling between keys and a folded train ticket",
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
"name": "medicine organizer",
|
| 142 |
+
"features": ["clear lids", "weekday letters", "plastic hinges"],
|
| 143 |
+
"context": "bathroom shelf",
|
| 144 |
+
"memory": "sorted tiny promises into seven obedient compartments",
|
| 145 |
+
"scene_detail": "waiting under fluorescent light with Monday already open",
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"name": "travel toothbrush",
|
| 149 |
+
"features": ["folding handle", "blue bristles", "vented cap"],
|
| 150 |
+
"context": "hotel sink",
|
| 151 |
+
"memory": "kept a mouth honest in rooms that forgot every guest",
|
| 152 |
+
"scene_detail": "balanced near a wrapped soap and a paper cup",
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"name": "passport cover",
|
| 156 |
+
"features": ["navy leather", "creased spine", "stitched edge"],
|
| 157 |
+
"context": "carry-on pocket",
|
| 158 |
+
"memory": "guarded borders, delays, and a face trying to look awake",
|
| 159 |
+
"scene_detail": "wedged beside boarding papers and a silent pen",
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
"name": "boarding pass stub",
|
| 163 |
+
"features": ["thermal paper", "torn edge", "gate code"],
|
| 164 |
+
"context": "jacket pocket",
|
| 165 |
+
"memory": "proved a journey happened after the airport swallowed the day",
|
| 166 |
+
"scene_detail": "softened by rain and folded into four tired rectangles",
|
| 167 |
+
},
|
| 168 |
+
{
|
| 169 |
+
"name": "hotel keycard",
|
| 170 |
+
"features": ["matte plastic", "blank stripe", "room-number sleeve"],
|
| 171 |
+
"context": "nightstand",
|
| 172 |
+
"memory": "opened a temporary room for a temporary version of its human",
|
| 173 |
+
"scene_detail": "resting beside a glass of water no one trusted",
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"name": "remote control",
|
| 177 |
+
"features": ["rubber buttons", "battery door scar", "dusty edges"],
|
| 178 |
+
"context": "sofa cushion",
|
| 179 |
+
"memory": "changed channels while nobody changed their mind",
|
| 180 |
+
"scene_detail": "half-sunk between cushions with one crumb for company",
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"name": "reading glasses",
|
| 184 |
+
"features": ["thin frames", "smudged lenses", "bent temple"],
|
| 185 |
+
"context": "book stack",
|
| 186 |
+
"memory": "made small letters confess their meaning at midnight",
|
| 187 |
+
"scene_detail": "left open across a page that was never finished",
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"name": "glasses case",
|
| 191 |
+
"features": ["hard shell", "soft lining", "snap hinge"],
|
| 192 |
+
"context": "desk drawer",
|
| 193 |
+
"memory": "protected fragile clarity from the tyranny of keys",
|
| 194 |
+
"scene_detail": "waiting in darkness with a paperclip pressed to its side",
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
"name": "wristwatch",
|
| 198 |
+
"features": ["scratched face", "brown strap", "small crown"],
|
| 199 |
+
"context": "dresser tray",
|
| 200 |
+
"memory": "measured days while humans pretended not to be measured",
|
| 201 |
+
"scene_detail": "stopped beside coins and a single loose button",
|
| 202 |
+
},
|
| 203 |
+
{
|
| 204 |
+
"name": "hair clip",
|
| 205 |
+
"features": ["amber plastic", "tiny teeth", "curved spring"],
|
| 206 |
+
"context": "bathroom counter",
|
| 207 |
+
"memory": "held chaos together for meetings, errands, and almost-crying",
|
| 208 |
+
"scene_detail": "resting near a fogged mirror and stray strands",
|
| 209 |
+
},
|
| 210 |
+
{
|
| 211 |
+
"name": "laundry token",
|
| 212 |
+
"features": ["round brass", "machine number", "dulled rim"],
|
| 213 |
+
"context": "laundry room",
|
| 214 |
+
"memory": "bought one more spin for clothes that knew too much",
|
| 215 |
+
"scene_detail": "cool in a palm smelling faintly of detergent",
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
"name": "refrigerator magnet",
|
| 219 |
+
"features": ["painted souvenir", "flat magnet back", "chipped corner"],
|
| 220 |
+
"context": "kitchen door",
|
| 221 |
+
"memory": "held reminders in place while everyone forgot the reason",
|
| 222 |
+
"scene_detail": "pinning a grocery list under a blue-white hum",
|
| 223 |
+
},
|
| 224 |
+
{
|
| 225 |
+
"name": "grocery receipt",
|
| 226 |
+
"features": ["curled paper", "faded ink", "long total"],
|
| 227 |
+
"context": "kitchen counter",
|
| 228 |
+
"memory": "itemized hunger, soap, and one unnecessary chocolate bar",
|
| 229 |
+
"scene_detail": "curling beside fruit that ripened too quickly",
|
| 230 |
+
},
|
| 231 |
+
{
|
| 232 |
+
"name": "spice jar",
|
| 233 |
+
"features": ["glass body", "red powder", "metal lid"],
|
| 234 |
+
"context": "kitchen shelf",
|
| 235 |
+
"memory": "made bland evenings briefly remember a warmer country",
|
| 236 |
+
"scene_detail": "standing in a row of louder labels",
|
| 237 |
+
},
|
| 238 |
+
{
|
| 239 |
+
"name": "cutting board",
|
| 240 |
+
"features": ["wood grain", "knife marks", "rounded corner"],
|
| 241 |
+
"context": "kitchen island",
|
| 242 |
+
"memory": "received every chopped plan without flinching",
|
| 243 |
+
"scene_detail": "drying upright after a meal nobody photographed",
|
| 244 |
+
},
|
| 245 |
+
{
|
| 246 |
+
"name": "ceramic bowl",
|
| 247 |
+
"features": ["blue rim", "tiny chip", "glazed curve"],
|
| 248 |
+
"context": "dish rack",
|
| 249 |
+
"memory": "held soup, cereal, and one quiet apology",
|
| 250 |
+
"scene_detail": "tilted beside plates still warm from rinse water",
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
"name": "reusable chopsticks",
|
| 254 |
+
"features": ["dark bamboo", "tapered tips", "cloth sleeve"],
|
| 255 |
+
"context": "lunch bag",
|
| 256 |
+
"memory": "lifted noodles through ordinary hunger and office gossip",
|
| 257 |
+
"scene_detail": "tucked into a sleeve with a soy sauce stain",
|
| 258 |
+
},
|
| 259 |
+
{
|
| 260 |
+
"name": "tea tin",
|
| 261 |
+
"features": ["green metal", "tight lid", "leaf dust"],
|
| 262 |
+
"context": "pantry shelf",
|
| 263 |
+
"memory": "kept rain-colored leaves ready for small recoveries",
|
| 264 |
+
"scene_detail": "quiet behind cereal boxes and a jar of almonds",
|
| 265 |
+
},
|
| 266 |
+
{
|
| 267 |
+
"name": "sticky note stack",
|
| 268 |
+
"features": ["yellow pages", "curled edge", "faint adhesive"],
|
| 269 |
+
"context": "monitor base",
|
| 270 |
+
"memory": "accepted urgent thoughts that became decorative fossils",
|
| 271 |
+
"scene_detail": "leaning under a monitor's cold rectangular sun",
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
"name": "binder clip",
|
| 275 |
+
"features": ["black steel", "silver arms", "pinched mouth"],
|
| 276 |
+
"context": "paper tray",
|
| 277 |
+
"memory": "held loose pages together when ideas tried to scatter",
|
| 278 |
+
"scene_detail": "biting a stack marked later in blue ink",
|
| 279 |
+
},
|
| 280 |
+
{
|
| 281 |
+
"name": "fountain pen",
|
| 282 |
+
"features": ["black barrel", "gold nib", "ink stain"],
|
| 283 |
+
"context": "notebook margin",
|
| 284 |
+
"memory": "turned hesitation into lines that looked deliberate",
|
| 285 |
+
"scene_detail": "uncapped beside a sentence crossed out twice",
|
| 286 |
+
},
|
| 287 |
+
{
|
| 288 |
+
"name": "old ticket stub",
|
| 289 |
+
"features": ["creased paper", "seat number", "torn perforation"],
|
| 290 |
+
"context": "memory box",
|
| 291 |
+
"memory": "survived the event after the applause became dust",
|
| 292 |
+
"scene_detail": "pressed under postcards and a dried ribbon",
|
| 293 |
+
},
|
| 294 |
+
{
|
| 295 |
+
"name": "candle jar",
|
| 296 |
+
"features": ["smoked glass", "wax tunnel", "blackened wick"],
|
| 297 |
+
"context": "window ledge",
|
| 298 |
+
"memory": "made one room pretend to be softer than it was",
|
| 299 |
+
"scene_detail": "cooled beside a window with rain on the other side",
|
| 300 |
+
},
|
| 301 |
+
{
|
| 302 |
+
"name": "alarm clock",
|
| 303 |
+
"features": ["round face", "plastic feet", "stubborn button"],
|
| 304 |
+
"context": "bedside shelf",
|
| 305 |
+
"memory": "tore people from dreams and was hated for being correct",
|
| 306 |
+
"scene_detail": "facing a bed that negotiated with every morning",
|
| 307 |
+
},
|
| 308 |
+
{
|
| 309 |
+
"name": "tape measure",
|
| 310 |
+
"features": ["yellow tape", "lock switch", "metal hook"],
|
| 311 |
+
"context": "tool drawer",
|
| 312 |
+
"memory": "proved shelves, windows, and ambitions were smaller than claimed",
|
| 313 |
+
"scene_detail": "coiled beside screws and one pencil shaved short",
|
| 314 |
+
},
|
| 315 |
+
]
|
| 316 |
+
|
| 317 |
MODE_PROFILES = {
|
| 318 |
"Cynical": {
|
| 319 |
"mood": "tired but sharply observant",
|
|
|
|
| 348 |
}
|
| 349 |
|
| 350 |
|
| 351 |
+
def build_curated_records(
|
| 352 |
+
count: int | None = None,
|
| 353 |
+
*,
|
| 354 |
+
version: str = "v1",
|
| 355 |
+
) -> list[dict[str, object]]:
|
| 356 |
+
version = _validate_version(version)
|
| 357 |
+
if count is None:
|
| 358 |
+
count = DEFAULT_V2_COUNT if version == "v2" else DEFAULT_COUNT
|
| 359 |
if count < 1:
|
| 360 |
raise ValueError("count must be at least 1")
|
| 361 |
|
| 362 |
+
objects = _objects_for_version(version)
|
| 363 |
+
source = _source_for_version(version)
|
| 364 |
records: list[dict[str, object]] = []
|
| 365 |
for index in range(count):
|
| 366 |
+
obj = objects[index % len(objects)]
|
| 367 |
+
mode = MODES[(index // len(objects)) % len(MODES)]
|
| 368 |
+
record_id = _record_id(version, index)
|
| 369 |
understanding = _build_object_understanding(obj)
|
| 370 |
persona = _build_persona(obj, mode)
|
| 371 |
diary = _build_diary(obj, mode, persona.persona, index)
|
|
|
|
| 373 |
"persona": persona.persona.model_dump(mode="json"),
|
| 374 |
"diary": diary.model_dump(mode="json"),
|
| 375 |
}
|
| 376 |
+
record = {
|
| 377 |
+
"id": record_id,
|
| 378 |
+
"source": source,
|
| 379 |
+
"split": "train",
|
| 380 |
+
"mode": mode,
|
| 381 |
+
"object_description": _object_description(obj),
|
| 382 |
+
"object_understanding": understanding.model_dump(mode="json"),
|
| 383 |
+
"curation_notes": _curation_notes(version),
|
| 384 |
+
"messages": [
|
| 385 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 386 |
+
{
|
| 387 |
+
"role": "user",
|
| 388 |
+
"content": _user_prompt(understanding.model_dump(mode="json"), mode),
|
| 389 |
+
},
|
| 390 |
+
{
|
| 391 |
+
"role": "assistant",
|
| 392 |
+
"content": json.dumps(assistant_payload, ensure_ascii=False),
|
| 393 |
+
},
|
| 394 |
+
],
|
| 395 |
+
}
|
| 396 |
+
if version == "v2":
|
| 397 |
+
record["scene_detail"] = str(obj["scene_detail"])
|
| 398 |
+
records.append(record)
|
|
|
|
|
|
|
| 399 |
return records
|
| 400 |
|
| 401 |
|
|
|
|
| 406 |
return output_path
|
| 407 |
|
| 408 |
|
| 409 |
+
def prepare_curated_dataset(
|
| 410 |
+
output_path: Path | None = None,
|
| 411 |
+
count: int | None = None,
|
| 412 |
+
*,
|
| 413 |
+
version: str = "v1",
|
| 414 |
+
) -> Path:
|
| 415 |
+
version = _validate_version(version)
|
| 416 |
+
if output_path is None:
|
| 417 |
+
output_path = DEFAULT_V2_OUTPUT_PATH if version == "v2" else DEFAULT_OUTPUT_PATH
|
| 418 |
+
return write_jsonl(build_curated_records(count, version=version), output_path)
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
def _validate_version(version: str) -> str:
|
| 422 |
+
if version not in {"v1", "v2"}:
|
| 423 |
+
raise ValueError("version must be 'v1' or 'v2'.")
|
| 424 |
+
return version
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
def _objects_for_version(version: str) -> Sequence[Mapping[str, object]]:
|
| 428 |
+
return OBJECTS_V2 if version == "v2" else OBJECTS
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
def _source_for_version(version: str) -> str:
|
| 432 |
+
return SOURCE_V2 if version == "v2" else SOURCE_V1
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def _record_id(version: str, index: int) -> str:
|
| 436 |
+
if version == "v2":
|
| 437 |
+
return f"curated-v2-synthetic-{index + 1:04d}"
|
| 438 |
+
return f"curated-synthetic-{index + 1:04d}"
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
def _curation_notes(version: str) -> str:
|
| 442 |
+
if version == "v2":
|
| 443 |
+
return (
|
| 444 |
+
"Synthetic curated v2 row: no private photo, no personal identifier, "
|
| 445 |
+
"broader object and scene coverage, English-first output with Chinese helper text."
|
| 446 |
+
)
|
| 447 |
+
return (
|
| 448 |
+
"Synthetic curated row: no private photo, no personal identifier, "
|
| 449 |
+
"English-first output with Chinese helper text."
|
| 450 |
+
)
|
| 451 |
|
| 452 |
|
| 453 |
def _build_object_understanding(obj: Mapping[str, object]) -> ObjectUnderstanding:
|
|
|
|
| 482 |
profile = MODE_PROFILES[mode]
|
| 483 |
object_name = str(obj["name"])
|
| 484 |
features = ", ".join(str(feature) for feature in obj["features"][:2])
|
| 485 |
+
scene = str(obj.get("scene_detail", "collecting proof that ordinary things notice everything"))
|
| 486 |
day_number = 300 + index + len(object_name)
|
| 487 |
english = (
|
| 488 |
f"Today I waited in the {obj['context']} wearing my {features} like official records. "
|
| 489 |
f"The humans moved around me with the confidence of temporary weather. "
|
| 490 |
f"I remembered how I {obj['memory']}, and I answered in my own way: {profile['voice']}. "
|
| 491 |
+
f"My mood is {persona.mood}, but I am still here, {scene}."
|
| 492 |
)
|
| 493 |
chinese = (
|
| 494 |
f"今天我待在 {obj['context']},带着 {features},像一份安静的档案。"
|
| 495 |
f"人类从我身边经过,好像自己不是短暂天气。"
|
| 496 |
f"我记得自己曾经 {obj['memory']},于是用自己的方式回应:{profile['voice']}。"
|
| 497 |
+
f"我的情绪是 {persona.mood},但我仍在这里,{scene}。"
|
| 498 |
)
|
| 499 |
return DiaryEntry(
|
| 500 |
title=f"Secret Diary - Day {day_number}",
|
|
|
|
| 517 |
|
| 518 |
def _object_description(obj: Mapping[str, object]) -> str:
|
| 519 |
features = ", ".join(str(feature) for feature in obj["features"])
|
| 520 |
+
description = f"{obj['name']} in a {obj['context']} with {features}"
|
| 521 |
+
if "scene_detail" in obj:
|
| 522 |
+
description = f"{description}, {obj['scene_detail']}"
|
| 523 |
+
return description
|
| 524 |
|
| 525 |
|
| 526 |
def _user_prompt(object_understanding: Mapping[str, object], mode: str) -> str:
|
|
|
|
| 534 |
|
| 535 |
def _parse_args() -> argparse.Namespace:
|
| 536 |
parser = argparse.ArgumentParser(description=__doc__)
|
| 537 |
+
parser.add_argument("--version", choices=("v1", "v2"), default="v1")
|
| 538 |
+
parser.add_argument("--count", type=int, default=None)
|
| 539 |
+
parser.add_argument("--output", type=Path, default=None)
|
| 540 |
return parser.parse_args()
|
| 541 |
|
| 542 |
|
| 543 |
def main() -> None:
|
| 544 |
args = _parse_args()
|
| 545 |
+
output_path = prepare_curated_dataset(args.output, args.count, version=args.version)
|
| 546 |
+
record_count = args.count or (DEFAULT_V2_COUNT if args.version == "v2" else DEFAULT_COUNT)
|
| 547 |
+
print(f"wrote {record_count} synthetic curated SFT records to {output_path}")
|
| 548 |
|
| 549 |
|
| 550 |
if __name__ == "__main__":
|
scripts/publish_hf_dataset.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Upload a curated Objectverse Diary SFT JSONL file to Hugging Face Datasets."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import json
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def validate_dataset_file(dataset_file: Path) -> dict[str, object]:
|
| 12 |
+
if not dataset_file.exists() or not dataset_file.is_file():
|
| 13 |
+
raise FileNotFoundError(f"Dataset file does not exist: {dataset_file}")
|
| 14 |
+
|
| 15 |
+
record_count = 0
|
| 16 |
+
sources: set[str] = set()
|
| 17 |
+
modes: set[str] = set()
|
| 18 |
+
object_names: set[str] = set()
|
| 19 |
+
for line_number, line in enumerate(
|
| 20 |
+
dataset_file.read_text(encoding="utf-8").splitlines(),
|
| 21 |
+
start=1,
|
| 22 |
+
):
|
| 23 |
+
if not line.strip():
|
| 24 |
+
continue
|
| 25 |
+
try:
|
| 26 |
+
record = json.loads(line)
|
| 27 |
+
except json.JSONDecodeError as exc:
|
| 28 |
+
raise ValueError(f"Invalid JSON on line {line_number}: {exc.msg}") from exc
|
| 29 |
+
if not isinstance(record, dict):
|
| 30 |
+
raise ValueError(f"Line {line_number} must be a JSON object.")
|
| 31 |
+
messages = record.get("messages")
|
| 32 |
+
if not isinstance(messages, list) or not messages:
|
| 33 |
+
raise ValueError(f"Line {line_number} must include a non-empty messages list.")
|
| 34 |
+
assistant_messages = [
|
| 35 |
+
message
|
| 36 |
+
for message in messages
|
| 37 |
+
if isinstance(message, dict) and message.get("role") == "assistant"
|
| 38 |
+
]
|
| 39 |
+
if not assistant_messages:
|
| 40 |
+
raise ValueError(f"Line {line_number} must include an assistant message.")
|
| 41 |
+
assistant_content = assistant_messages[-1].get("content")
|
| 42 |
+
if not isinstance(assistant_content, str):
|
| 43 |
+
raise ValueError(f"Line {line_number} assistant content must be a string.")
|
| 44 |
+
try:
|
| 45 |
+
assistant_payload = json.loads(assistant_content)
|
| 46 |
+
except json.JSONDecodeError as exc:
|
| 47 |
+
raise ValueError(
|
| 48 |
+
f"Line {line_number} assistant content is not valid JSON: {exc.msg}"
|
| 49 |
+
) from exc
|
| 50 |
+
if not isinstance(assistant_payload, dict):
|
| 51 |
+
raise ValueError(f"Line {line_number} assistant content must be a JSON object.")
|
| 52 |
+
if "persona" not in assistant_payload or "diary" not in assistant_payload:
|
| 53 |
+
raise ValueError(
|
| 54 |
+
f"Line {line_number} assistant content must include persona and diary."
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
record_count += 1
|
| 58 |
+
if isinstance(record.get("source"), str):
|
| 59 |
+
sources.add(str(record["source"]))
|
| 60 |
+
if isinstance(record.get("mode"), str):
|
| 61 |
+
modes.add(str(record["mode"]))
|
| 62 |
+
object_understanding = record.get("object_understanding")
|
| 63 |
+
if isinstance(object_understanding, dict):
|
| 64 |
+
raw_object = object_understanding.get("object")
|
| 65 |
+
if isinstance(raw_object, dict) and isinstance(raw_object.get("name"), str):
|
| 66 |
+
object_names.add(str(raw_object["name"]))
|
| 67 |
+
|
| 68 |
+
if record_count == 0:
|
| 69 |
+
raise ValueError(f"Dataset file has no records: {dataset_file}")
|
| 70 |
+
|
| 71 |
+
return {
|
| 72 |
+
"dataset_file": str(dataset_file),
|
| 73 |
+
"record_count": record_count,
|
| 74 |
+
"sources": sorted(sources),
|
| 75 |
+
"modes": sorted(modes),
|
| 76 |
+
"unique_object_count": len(object_names),
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def upload_dataset(
|
| 81 |
+
*,
|
| 82 |
+
dataset_file: Path,
|
| 83 |
+
repo_id: str,
|
| 84 |
+
path_in_repo: str,
|
| 85 |
+
private: bool,
|
| 86 |
+
commit_message: str,
|
| 87 |
+
dry_run: bool,
|
| 88 |
+
) -> dict[str, object]:
|
| 89 |
+
summary = validate_dataset_file(dataset_file)
|
| 90 |
+
summary.update(
|
| 91 |
+
{
|
| 92 |
+
"repo_id": repo_id,
|
| 93 |
+
"path_in_repo": path_in_repo,
|
| 94 |
+
"private": private,
|
| 95 |
+
"commit_message": commit_message,
|
| 96 |
+
"dry_run": dry_run,
|
| 97 |
+
}
|
| 98 |
+
)
|
| 99 |
+
if dry_run:
|
| 100 |
+
summary["uploaded"] = False
|
| 101 |
+
return summary
|
| 102 |
+
|
| 103 |
+
from huggingface_hub import HfApi
|
| 104 |
+
|
| 105 |
+
api = HfApi()
|
| 106 |
+
api.create_repo(repo_id=repo_id, repo_type="dataset", private=private, exist_ok=True)
|
| 107 |
+
api.upload_file(
|
| 108 |
+
path_or_fileobj=str(dataset_file),
|
| 109 |
+
path_in_repo=path_in_repo,
|
| 110 |
+
repo_id=repo_id,
|
| 111 |
+
repo_type="dataset",
|
| 112 |
+
commit_message=commit_message,
|
| 113 |
+
)
|
| 114 |
+
summary["uploaded"] = True
|
| 115 |
+
summary["url"] = f"https://huggingface.co/datasets/{repo_id}"
|
| 116 |
+
return summary
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _print_json(payload: dict[str, Any]) -> None:
|
| 120 |
+
print(json.dumps(payload, indent=2, sort_keys=True), flush=True)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def _parse_args() -> argparse.Namespace:
|
| 124 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 125 |
+
parser.add_argument("--dataset-file", type=Path, required=True)
|
| 126 |
+
parser.add_argument("--repo-id", required=True)
|
| 127 |
+
parser.add_argument("--path-in-repo", required=True)
|
| 128 |
+
parser.add_argument("--private", action="store_true")
|
| 129 |
+
parser.add_argument(
|
| 130 |
+
"--commit-message",
|
| 131 |
+
default="Upload Objectverse Diary curated SFT dataset",
|
| 132 |
+
)
|
| 133 |
+
parser.add_argument("--dry-run", action="store_true")
|
| 134 |
+
return parser.parse_args()
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def main() -> None:
|
| 138 |
+
args = _parse_args()
|
| 139 |
+
_print_json(
|
| 140 |
+
upload_dataset(
|
| 141 |
+
dataset_file=args.dataset_file,
|
| 142 |
+
repo_id=args.repo_id,
|
| 143 |
+
path_in_repo=args.path_in_repo,
|
| 144 |
+
private=args.private,
|
| 145 |
+
commit_message=args.commit_message,
|
| 146 |
+
dry_run=args.dry_run,
|
| 147 |
+
)
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
if __name__ == "__main__":
|
| 152 |
+
try:
|
| 153 |
+
main()
|
| 154 |
+
except Exception as exc:
|
| 155 |
+
raise SystemExit(str(exc)) from exc
|
scripts/publish_hf_gguf.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Upload an Objectverse Diary GGUF model file to Hugging Face Hub."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import json
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def validate_gguf_file(gguf_file: Path) -> dict[str, object]:
|
| 12 |
+
if not gguf_file.exists() or not gguf_file.is_file():
|
| 13 |
+
raise FileNotFoundError(f"GGUF file does not exist: {gguf_file}")
|
| 14 |
+
if gguf_file.suffix.lower() != ".gguf":
|
| 15 |
+
raise ValueError(f"GGUF file must use .gguf suffix: {gguf_file}")
|
| 16 |
+
size_bytes = gguf_file.stat().st_size
|
| 17 |
+
if size_bytes <= 0:
|
| 18 |
+
raise ValueError(f"GGUF file is empty: {gguf_file}")
|
| 19 |
+
return {
|
| 20 |
+
"gguf_file": str(gguf_file),
|
| 21 |
+
"size_bytes": size_bytes,
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def upload_gguf(
|
| 26 |
+
*,
|
| 27 |
+
gguf_file: Path,
|
| 28 |
+
repo_id: str,
|
| 29 |
+
path_in_repo: str,
|
| 30 |
+
private: bool,
|
| 31 |
+
commit_message: str,
|
| 32 |
+
dry_run: bool,
|
| 33 |
+
) -> dict[str, object]:
|
| 34 |
+
summary = validate_gguf_file(gguf_file)
|
| 35 |
+
summary.update(
|
| 36 |
+
{
|
| 37 |
+
"repo_id": repo_id,
|
| 38 |
+
"path_in_repo": path_in_repo,
|
| 39 |
+
"private": private,
|
| 40 |
+
"commit_message": commit_message,
|
| 41 |
+
"dry_run": dry_run,
|
| 42 |
+
}
|
| 43 |
+
)
|
| 44 |
+
if dry_run:
|
| 45 |
+
summary["uploaded"] = False
|
| 46 |
+
return summary
|
| 47 |
+
|
| 48 |
+
from huggingface_hub import HfApi
|
| 49 |
+
|
| 50 |
+
api = HfApi()
|
| 51 |
+
api.create_repo(repo_id=repo_id, repo_type="model", private=private, exist_ok=True)
|
| 52 |
+
api.upload_file(
|
| 53 |
+
path_or_fileobj=str(gguf_file),
|
| 54 |
+
path_in_repo=path_in_repo,
|
| 55 |
+
repo_id=repo_id,
|
| 56 |
+
repo_type="model",
|
| 57 |
+
commit_message=commit_message,
|
| 58 |
+
)
|
| 59 |
+
summary["uploaded"] = True
|
| 60 |
+
summary["url"] = f"https://huggingface.co/{repo_id}/blob/main/{path_in_repo}"
|
| 61 |
+
return summary
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _print_json(payload: dict[str, Any]) -> None:
|
| 65 |
+
print(json.dumps(payload, indent=2, sort_keys=True), flush=True)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _parse_args() -> argparse.Namespace:
|
| 69 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 70 |
+
parser.add_argument("--gguf-file", type=Path, required=True)
|
| 71 |
+
parser.add_argument("--repo-id", required=True)
|
| 72 |
+
parser.add_argument("--path-in-repo", required=True)
|
| 73 |
+
parser.add_argument("--private", action="store_true")
|
| 74 |
+
parser.add_argument(
|
| 75 |
+
"--commit-message",
|
| 76 |
+
default="Upload Objectverse Diary GGUF model",
|
| 77 |
+
)
|
| 78 |
+
parser.add_argument("--dry-run", action="store_true")
|
| 79 |
+
return parser.parse_args()
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def main() -> None:
|
| 83 |
+
args = _parse_args()
|
| 84 |
+
_print_json(
|
| 85 |
+
upload_gguf(
|
| 86 |
+
gguf_file=args.gguf_file,
|
| 87 |
+
repo_id=args.repo_id,
|
| 88 |
+
path_in_repo=args.path_in_repo,
|
| 89 |
+
private=args.private,
|
| 90 |
+
commit_message=args.commit_message,
|
| 91 |
+
dry_run=args.dry_run,
|
| 92 |
+
)
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
if __name__ == "__main__":
|
| 97 |
+
try:
|
| 98 |
+
main()
|
| 99 |
+
except Exception as exc:
|
| 100 |
+
raise SystemExit(str(exc)) from exc
|
src/examples.py
CHANGED
|
@@ -52,8 +52,9 @@ def gradio_examples() -> list[list[str]]:
|
|
| 52 |
|
| 53 |
def example_button_label(index: int) -> str:
|
| 54 |
item = EXAMPLE_OBJECTS[index]
|
|
|
|
| 55 |
return (
|
| 56 |
f"{item['archive_id']}\n"
|
| 57 |
-
f"{
|
| 58 |
f"{item['mode']} · {item['tags']}"
|
| 59 |
)
|
|
|
|
| 52 |
|
| 53 |
def example_button_label(index: int) -> str:
|
| 54 |
item = EXAMPLE_OBJECTS[index]
|
| 55 |
+
label = item["label"].split("/")[0].strip()
|
| 56 |
return (
|
| 57 |
f"{item['archive_id']}\n"
|
| 58 |
+
f"{label}\n"
|
| 59 |
f"{item['mode']} · {item['tags']}"
|
| 60 |
)
|
src/renderer/share_card.py
CHANGED
|
@@ -15,14 +15,14 @@ def render_share_card(persona: PersonaEnvelope, diary: DiaryEntry) -> str:
|
|
| 15 |
<article class="{CARD_WRAPPER_CLASS}">
|
| 16 |
<header class="card-header">
|
| 17 |
<div>
|
| 18 |
-
<div class="card-kicker">Objectverse Diary
|
| 19 |
<h2>{escape(p.character_name)}</h2>
|
| 20 |
</div>
|
| 21 |
<span class="card-stamp">OBJECT FILE</span>
|
| 22 |
</header>
|
| 23 |
<p class="card-object">{escape(p.object_name)} · {escape(p.mood)}</p>
|
| 24 |
<p class="card-quote">{escape(diary.english)}</p>
|
| 25 |
-
<p class="card-cn">{escape(diary.chinese)}</p>
|
| 26 |
<div class="card-tags">{tags}</div>
|
| 27 |
</article>
|
| 28 |
"""
|
|
|
|
| 15 |
<article class="{CARD_WRAPPER_CLASS}">
|
| 16 |
<header class="card-header">
|
| 17 |
<div>
|
| 18 |
+
<div class="card-kicker">Objectverse Diary <span class="lang-zh">万物日记</span></div>
|
| 19 |
<h2>{escape(p.character_name)}</h2>
|
| 20 |
</div>
|
| 21 |
<span class="card-stamp">OBJECT FILE</span>
|
| 22 |
</header>
|
| 23 |
<p class="card-object">{escape(p.object_name)} · {escape(p.mood)}</p>
|
| 24 |
<p class="card-quote">{escape(diary.english)}</p>
|
| 25 |
+
<p class="card-cn lang-zh block">{escape(diary.chinese)}</p>
|
| 26 |
<div class="card-tags">{tags}</div>
|
| 27 |
</article>
|
| 28 |
"""
|
src/ui/copy.py
CHANGED
|
@@ -1,19 +1,19 @@
|
|
| 1 |
-
"""English
|
| 2 |
|
| 3 |
TITLE = "Objectverse Diary"
|
| 4 |
-
SUBTITLE = "Every object has a secret life.
|
| 5 |
-
UPLOAD_LABEL = "Upload an object photo
|
| 6 |
-
DESCRIPTION_LABEL = "Optional object description
|
| 7 |
DESCRIPTION_PLACEHOLDER = "Example: an old white coffee mug on my developer desk"
|
| 8 |
-
MODE_LABEL = "Choose a personality mode
|
| 9 |
-
GENERATE_LABEL = "Wake the object
|
| 10 |
-
EXAMPLES_LABEL = "Example objects
|
| 11 |
-
OBJECT_JSON_LABEL = "Object Understanding JSON
|
| 12 |
-
PERSONA_JSON_LABEL = "Persona JSON
|
| 13 |
-
DIARY_LABEL = "Secret Diary
|
| 14 |
-
SHARE_CARD_LABEL = "Share Card
|
| 15 |
-
TRACE_JSON_LABEL = "Trace JSON
|
| 16 |
-
TRACE_PATH_LABEL = "Saved trace path
|
| 17 |
-
CHAT_LABEL = "Chat with the object
|
| 18 |
CHAT_INPUT_PLACEHOLDER = "Ask the object something..."
|
| 19 |
-
CHAT_BUTTON_LABEL = "Ask
|
|
|
|
| 1 |
+
"""English UI copy."""
|
| 2 |
|
| 3 |
TITLE = "Objectverse Diary"
|
| 4 |
+
SUBTITLE = "Every object has a secret life."
|
| 5 |
+
UPLOAD_LABEL = "Upload an object photo"
|
| 6 |
+
DESCRIPTION_LABEL = "Optional object description"
|
| 7 |
DESCRIPTION_PLACEHOLDER = "Example: an old white coffee mug on my developer desk"
|
| 8 |
+
MODE_LABEL = "Choose a personality mode"
|
| 9 |
+
GENERATE_LABEL = "Wake the object"
|
| 10 |
+
EXAMPLES_LABEL = "Example objects"
|
| 11 |
+
OBJECT_JSON_LABEL = "Object Understanding JSON"
|
| 12 |
+
PERSONA_JSON_LABEL = "Persona JSON"
|
| 13 |
+
DIARY_LABEL = "Secret Diary"
|
| 14 |
+
SHARE_CARD_LABEL = "Share Card"
|
| 15 |
+
TRACE_JSON_LABEL = "Trace JSON"
|
| 16 |
+
TRACE_PATH_LABEL = "Saved trace path"
|
| 17 |
+
CHAT_LABEL = "Chat with the object"
|
| 18 |
CHAT_INPUT_PLACEHOLDER = "Ask the object something..."
|
| 19 |
+
CHAT_BUTTON_LABEL = "Ask"
|
src/ui/layout.py
CHANGED
|
@@ -19,37 +19,194 @@ from src.renderer.share_card import render_share_card
|
|
| 19 |
from src.ui import copy
|
| 20 |
from src.utils.zero_gpu import zero_gpu
|
| 21 |
|
| 22 |
-
CHAT_EMPTY_MESSAGE = "Wake an object first.
|
| 23 |
|
| 24 |
OBJECT_FILE_EMPTY = """
|
| 25 |
<div class="archive-empty">
|
| 26 |
-
<span class="archive-label">Object File
|
| 27 |
<h3>No object awake yet.</h3>
|
| 28 |
-
<p>Upload or describe an everyday object to open its secret archive.
|
|
|
|
| 29 |
</div>
|
| 30 |
"""
|
| 31 |
|
| 32 |
DIARY_EMPTY = """
|
| 33 |
-
### Secret Diary
|
| 34 |
|
| 35 |
-
Wake an object to open its diary.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
"""
|
| 37 |
|
| 38 |
SHARE_CARD_EMPTY = """
|
| 39 |
<div class="objectverse-placeholder">
|
| 40 |
-
<span>Share Card
|
| 41 |
<strong>Waiting for an object file.</strong>
|
| 42 |
-
<p>A screenshot-friendly archive card will appear here.
|
|
|
|
| 43 |
</div>
|
| 44 |
"""
|
| 45 |
|
| 46 |
TRACE_EMPTY = """
|
| 47 |
<div class="archive-empty compact">
|
| 48 |
-
<span class="archive-label">Trace
|
| 49 |
-
<p>No trace saved yet.
|
|
|
|
| 50 |
</div>
|
| 51 |
"""
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
GenerationUiResult = tuple[
|
| 54 |
str,
|
| 55 |
dict[str, Any],
|
|
@@ -80,210 +237,194 @@ def build_app() -> gr.Blocks:
|
|
| 80 |
background_fill_secondary_dark="rgba(30, 28, 25, 0.6)",
|
| 81 |
border_color_primary="rgba(212, 175, 55, 0.15)",
|
| 82 |
border_color_primary_dark="rgba(212, 175, 55, 0.15)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
block_background_fill="transparent",
|
| 84 |
block_background_fill_dark="transparent",
|
| 85 |
block_border_width="0px",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
panel_background_fill="transparent",
|
| 87 |
panel_background_fill_dark="transparent",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
)
|
| 89 |
|
| 90 |
-
with gr.Blocks(theme=custom_theme, head=f"<style>{css}</style>", title=APP_TITLE, fill_width=True, elem_id="objectverse-app") as demo:
|
| 91 |
-
with gr.
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
<
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
<span>No. 000827</span>
|
| 115 |
-
<small>Curate. Converse. Cherish.</small>
|
| 116 |
-
</div>
|
| 117 |
-
<div class="lang-switch">
|
| 118 |
-
<button class="active">EN</button>
|
| 119 |
-
<button>中文</button>
|
| 120 |
-
</div>
|
| 121 |
-
</div>
|
| 122 |
-
</nav>
|
| 123 |
-
""",
|
| 124 |
-
padding=False,
|
| 125 |
-
)
|
| 126 |
-
|
| 127 |
-
# === Main Content Area ===
|
| 128 |
-
with gr.Column(elem_id="main-content", scale=1):
|
| 129 |
-
gr.HTML(
|
| 130 |
-
f"""
|
| 131 |
-
<section id="objectverse-hero">
|
| 132 |
-
<div class="hero-copy">
|
| 133 |
-
<h1>{APP_TITLE}</h1>
|
| 134 |
-
<p class="hero-kicker">Every object has a secret life.<br><span>万物日记:每个物品都有秘密人生</span></p>
|
| 135 |
-
</div>
|
| 136 |
-
<div class="hero-badges" aria-label="Project constraints">
|
| 137 |
-
<span>Small Models</span>
|
| 138 |
-
<span>Local-First</span>
|
| 139 |
-
<span>No Cloud APIs</span>
|
| 140 |
-
</div>
|
| 141 |
-
</section>
|
| 142 |
-
""",
|
| 143 |
-
padding=False,
|
| 144 |
-
)
|
| 145 |
-
|
| 146 |
-
result_state = gr.State()
|
| 147 |
-
zero_gpu_probe_button = gr.Button(visible=False)
|
| 148 |
-
zero_gpu_probe_output = gr.JSON(visible=False)
|
| 149 |
-
vision_runtime_probe_button = gr.Button(visible=False)
|
| 150 |
-
vision_runtime_probe_output = gr.JSON(visible=False)
|
| 151 |
-
|
| 152 |
-
# Intake & Examples Row
|
| 153 |
-
with gr.Row(elem_id="intake", elem_classes=["content-section"]):
|
| 154 |
-
# Left: Intake
|
| 155 |
-
with gr.Column(scale=7, elem_classes=["archive-panel", "intake-panel"]):
|
| 156 |
-
image_input = gr.Image(
|
| 157 |
-
label=copy.UPLOAD_LABEL,
|
| 158 |
-
show_label=False,
|
| 159 |
-
type="filepath",
|
| 160 |
-
sources=["upload"],
|
| 161 |
-
elem_id="object-upload",
|
| 162 |
-
)
|
| 163 |
-
gr.HTML("""<div class="or-divider"><span>OR</span></div>""", padding=False)
|
| 164 |
-
description_input = gr.Textbox(
|
| 165 |
-
label=copy.DESCRIPTION_LABEL,
|
| 166 |
-
placeholder=copy.DESCRIPTION_PLACEHOLDER,
|
| 167 |
-
lines=2,
|
| 168 |
-
max_lines=5,
|
| 169 |
-
elem_id="object-description",
|
| 170 |
-
)
|
| 171 |
-
|
| 172 |
-
gr.HTML("""<div class="mode-header">Personality mode <small>人格模式</small> <span class="help-icon">?</span></div>""", padding=False)
|
| 173 |
-
mode_input = gr.Radio(
|
| 174 |
-
label=copy.MODE_LABEL,
|
| 175 |
-
show_label=False,
|
| 176 |
-
choices=PERSONALITY_MODES,
|
| 177 |
-
value=DEFAULT_MODE,
|
| 178 |
-
elem_id="personality-mode",
|
| 179 |
-
elem_classes=["mode-switch"],
|
| 180 |
-
)
|
| 181 |
-
generate_button = gr.Button("Wake the Object\n唤醒物品", variant="primary", elem_id="wake-button")
|
| 182 |
-
|
| 183 |
-
gr.HTML(
|
| 184 |
-
"""
|
| 185 |
-
<div class="how-it-works">
|
| 186 |
-
<div class="step">
|
| 187 |
-
<span class="step-num">01</span>
|
| 188 |
-
<div class="step-icon img-icon"></div>
|
| 189 |
-
<div class="step-text">
|
| 190 |
-
<strong>Upload or describe</strong>
|
| 191 |
-
<small>上传物品或描述心情</small>
|
| 192 |
-
<p>Give me a photo or words—anything that holds a story.</p>
|
| 193 |
-
</div>
|
| 194 |
-
</div>
|
| 195 |
-
<div class="step">
|
| 196 |
-
<span class="step-num">02</span>
|
| 197 |
-
<div class="step-icon pen-icon"></div>
|
| 198 |
-
<div class="step-text">
|
| 199 |
-
<strong>I imagine its life</strong>
|
| 200 |
-
<small>我为它编织人生</small>
|
| 201 |
-
<p>I'll step into its shoes and imagine its secret life.</p>
|
| 202 |
-
</div>
|
| 203 |
-
</div>
|
| 204 |
-
<div class="step">
|
| 205 |
-
<span class="step-num">03</span>
|
| 206 |
-
<div class="step-icon book-icon"></div>
|
| 207 |
-
<div class="step-text">
|
| 208 |
-
<strong>Read its diary</strong>
|
| 209 |
-
<small>阅读物品日记</small>
|
| 210 |
-
<p>Receive a diary entry written from its perspective.</p>
|
| 211 |
-
</div>
|
| 212 |
-
</div>
|
| 213 |
-
</div>
|
| 214 |
-
""",
|
| 215 |
-
padding=False,
|
| 216 |
-
)
|
| 217 |
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
)
|
| 241 |
-
gr.HTML("""<a href="#object-file" class="view-more">View more in Object File →</a>""", padding=False)
|
| 242 |
-
|
| 243 |
-
# Object File Section
|
| 244 |
-
with gr.Row(elem_id="object-file", elem_classes=["content-section"]):
|
| 245 |
-
with gr.Column(scale=1, elem_classes=["archive-panel", "file-panel"]):
|
| 246 |
-
gr.HTML(_panel_header("02", "Object File / Recognition", "物品档案", "Structured mock understanding and persona."), padding=False)
|
| 247 |
-
object_file_summary = gr.HTML(value=OBJECT_FILE_EMPTY, elem_id="object-file-summary", padding=False)
|
| 248 |
-
with gr.Accordion("Raw JSON", open=False):
|
| 249 |
-
object_json = gr.JSON(value={}, label=copy.OBJECT_JSON_LABEL)
|
| 250 |
-
persona_json = gr.JSON(value={}, label=copy.PERSONA_JSON_LABEL)
|
| 251 |
-
|
| 252 |
-
# Diary Section
|
| 253 |
-
with gr.Row(elem_id="diary", elem_classes=["content-section"]):
|
| 254 |
-
with gr.Column(scale=1, elem_classes=["archive-panel", "diary-panel"]):
|
| 255 |
-
gr.HTML(_panel_header("03", "Secret Diary", "秘密日记", "A private note written by the object."), padding=False)
|
| 256 |
-
diary_output = gr.Markdown(
|
| 257 |
-
value=DIARY_EMPTY,
|
| 258 |
-
label=copy.DIARY_LABEL,
|
| 259 |
-
elem_id="diary-output",
|
| 260 |
-
)
|
| 261 |
-
|
| 262 |
-
# Share & Chat Section
|
| 263 |
-
with gr.Row(elem_id="share", elem_classes=["content-section", "split-section"]):
|
| 264 |
-
with gr.Column(scale=5, elem_classes=["archive-panel", "share-panel", "anchored"], elem_id="share-panel"):
|
| 265 |
-
gr.HTML(_panel_header("04", "Share Card", "分享卡片", "Fixed-width card for screenshots."), padding=False)
|
| 266 |
-
share_card = gr.HTML(value=SHARE_CARD_EMPTY, label=copy.SHARE_CARD_LABEL, padding=False)
|
| 267 |
-
|
| 268 |
-
with gr.Column(scale=4, elem_classes=["archive-panel", "chat-panel", "anchored"], elem_id="chat-panel"):
|
| 269 |
-
gr.HTML(_panel_header("05", "Object Chat", "物品对话", "Ask after the object wakes up."), padding=False)
|
| 270 |
-
chatbot = gr.Chatbot(
|
| 271 |
-
value=_empty_chat_history(),
|
| 272 |
-
label=copy.CHAT_LABEL,
|
| 273 |
-
type="messages",
|
| 274 |
-
height=300,
|
| 275 |
-
allow_tags=False,
|
| 276 |
)
|
| 277 |
-
chat_input = gr.Textbox(placeholder=copy.CHAT_INPUT_PLACEHOLDER, show_label=False)
|
| 278 |
-
chat_button = gr.Button(copy.CHAT_BUTTON_LABEL, elem_classes=["quiet-button"])
|
| 279 |
|
| 280 |
-
|
| 281 |
-
with gr.
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
|
| 288 |
manual_outputs = [
|
| 289 |
object_file_summary,
|
|
@@ -337,12 +478,13 @@ def build_app() -> gr.Blocks:
|
|
| 337 |
return demo
|
| 338 |
|
| 339 |
|
| 340 |
-
def _panel_header(index: str, title: str,
|
|
|
|
| 341 |
return f"""
|
| 342 |
<header class="panel-header">
|
| 343 |
<span>{escape(index)}</span>
|
| 344 |
<div>
|
| 345 |
-
<h2>{escape(title)}
|
| 346 |
<p>{escape(note)}</p>
|
| 347 |
</div>
|
| 348 |
</header>
|
|
@@ -422,7 +564,7 @@ def _render_object_file(result: GenerationResult) -> str:
|
|
| 422 |
</div>
|
| 423 |
</dl>
|
| 424 |
<div class="feature-list">
|
| 425 |
-
<strong>Visible features
|
| 426 |
<ul>{features}</ul>
|
| 427 |
</div>
|
| 428 |
<p class="complaint">{escape(persona.complaint)}</p>
|
|
@@ -434,7 +576,7 @@ def _render_object_file(result: GenerationResult) -> str:
|
|
| 434 |
def _render_trace_summary(result: GenerationResult) -> str:
|
| 435 |
return f"""
|
| 436 |
<div class="trace-card">
|
| 437 |
-
<span class="archive-label">Trace saved
|
| 438 |
<strong>{escape(result.trace.trace_id)}</strong>
|
| 439 |
<p>{escape(result.trace.model_runtime["vision"])} · {escape(result.trace.model_runtime["text"])}</p>
|
| 440 |
</div>
|
|
@@ -451,15 +593,16 @@ def _generation_error(exc: Exception, description: str, mode: str) -> Generation
|
|
| 451 |
}
|
| 452 |
error_html = f"""
|
| 453 |
<div class="archive-error">
|
| 454 |
-
<span>Generation failed
|
| 455 |
<strong>{escape(error_type)}</strong>
|
| 456 |
<p>{escape(error_message)}</p>
|
| 457 |
</div>
|
| 458 |
"""
|
| 459 |
error_markdown = (
|
| 460 |
-
"### Generation failed
|
| 461 |
f"{error_type}: {error_message}\n\n"
|
| 462 |
-
"Please try another description or sample object.
|
|
|
|
| 463 |
)
|
| 464 |
return (
|
| 465 |
error_html,
|
|
@@ -471,7 +614,7 @@ def _generation_error(exc: Exception, description: str, mode: str) -> Generation
|
|
| 471 |
error_payload,
|
| 472 |
"",
|
| 473 |
None,
|
| 474 |
-
[{"role": "assistant", "content": f"Generation failed
|
| 475 |
)
|
| 476 |
|
| 477 |
|
|
@@ -484,7 +627,7 @@ def _awake_chat_history(result: GenerationResult) -> list[dict[str, str]]:
|
|
| 484 |
return [
|
| 485 |
{
|
| 486 |
"role": "assistant",
|
| 487 |
-
"content": f"{name} is awake. Ask what it remembers.
|
| 488 |
}
|
| 489 |
]
|
| 490 |
|
|
|
|
| 19 |
from src.ui import copy
|
| 20 |
from src.utils.zero_gpu import zero_gpu
|
| 21 |
|
| 22 |
+
CHAT_EMPTY_MESSAGE = "Wake an object first."
|
| 23 |
|
| 24 |
OBJECT_FILE_EMPTY = """
|
| 25 |
<div class="archive-empty">
|
| 26 |
+
<span class="archive-label">Object File <span class="lang-zh">物品档案</span></span>
|
| 27 |
<h3>No object awake yet.</h3>
|
| 28 |
+
<p>Upload or describe an everyday object to open its secret archive.</p>
|
| 29 |
+
<p class="lang-zh block">上传或描述一个日常物品后打开秘密档案。</p>
|
| 30 |
</div>
|
| 31 |
"""
|
| 32 |
|
| 33 |
DIARY_EMPTY = """
|
| 34 |
+
### Secret Diary
|
| 35 |
|
| 36 |
+
Wake an object to open its diary.
|
| 37 |
+
|
| 38 |
+
<div class="lang-zh block zh-helper">
|
| 39 |
+
唤醒物品后阅读它的日记。
|
| 40 |
+
</div>
|
| 41 |
"""
|
| 42 |
|
| 43 |
SHARE_CARD_EMPTY = """
|
| 44 |
<div class="objectverse-placeholder">
|
| 45 |
+
<span>Share Card <span class="lang-zh">分享卡片</span></span>
|
| 46 |
<strong>Waiting for an object file.</strong>
|
| 47 |
+
<p>A screenshot-friendly archive card will appear here.</p>
|
| 48 |
+
<p class="lang-zh block">可截图分享的档案卡片会显示在这里。</p>
|
| 49 |
</div>
|
| 50 |
"""
|
| 51 |
|
| 52 |
TRACE_EMPTY = """
|
| 53 |
<div class="archive-empty compact">
|
| 54 |
+
<span class="archive-label">Trace <span class="lang-zh">模型轨迹</span></span>
|
| 55 |
+
<p>No trace saved yet.</p>
|
| 56 |
+
<p class="lang-zh block">尚未保存 trace。</p>
|
| 57 |
</div>
|
| 58 |
"""
|
| 59 |
|
| 60 |
+
UI_CONTROL_SCRIPT = r"""
|
| 61 |
+
(() => {
|
| 62 |
+
const root = document.documentElement;
|
| 63 |
+
const INTERNAL_TEXT_REPLACEMENTS = new Map([
|
| 64 |
+
["将图像文件拖放到此处以上传", "Drop image file here to upload"],
|
| 65 |
+
["将图像拖放到此处", "Drop image here"],
|
| 66 |
+
["- 或 -", "- or -"],
|
| 67 |
+
["点击上传", "Click to upload"],
|
| 68 |
+
["清空对话", "Clear chat"],
|
| 69 |
+
["通过 API 使用", "Use via API"],
|
| 70 |
+
["使用 Gradio 构建", "Built with Gradio"],
|
| 71 |
+
["设置", "Settings"],
|
| 72 |
+
["标志", "icon"],
|
| 73 |
+
]);
|
| 74 |
+
const CJK_RE = /[\u3400-\u9fff]/;
|
| 75 |
+
const CJK_WRAP_RE = /[\u3400-\u9fff,。!?、;::“”‘’()《》【】]+/g;
|
| 76 |
+
const SKIP_TEXT_SELECTOR = "script, style, textarea, input, select, option, svg, .lang-zh, .auto-zh";
|
| 77 |
+
|
| 78 |
+
function syncLanguageButtons(value) {
|
| 79 |
+
document.querySelectorAll("[data-lang-toggle]").forEach((button) => {
|
| 80 |
+
const active = button.dataset.langToggle === value;
|
| 81 |
+
button.classList.toggle("active", active);
|
| 82 |
+
button.setAttribute("aria-pressed", String(active));
|
| 83 |
+
});
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
function applyLanguage(value) {
|
| 87 |
+
const language = value === "zh" ? "zh" : "en";
|
| 88 |
+
root.dataset.ovLang = language;
|
| 89 |
+
if (document.body) {
|
| 90 |
+
document.body.dataset.ovLang = language;
|
| 91 |
+
}
|
| 92 |
+
syncLanguageButtons(language);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
function initControls() {
|
| 96 |
+
root.lang = "en";
|
| 97 |
+
applyLanguage("en");
|
| 98 |
+
normalizeGradioChrome(document.body);
|
| 99 |
+
wrapChineseText(document.body);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
function normalizeString(value) {
|
| 103 |
+
let nextValue = value;
|
| 104 |
+
INTERNAL_TEXT_REPLACEMENTS.forEach((replacement, source) => {
|
| 105 |
+
nextValue = nextValue.split(source).join(replacement);
|
| 106 |
+
});
|
| 107 |
+
return nextValue;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
function normalizeGradioChrome(rootNode) {
|
| 111 |
+
if (!rootNode) return;
|
| 112 |
+
|
| 113 |
+
rootNode.querySelectorAll("[aria-label], [title], [alt]").forEach((element) => {
|
| 114 |
+
["aria-label", "title", "alt"].forEach((attribute) => {
|
| 115 |
+
const value = element.getAttribute(attribute);
|
| 116 |
+
if (value && CJK_RE.test(value)) {
|
| 117 |
+
const normalizedValue = normalizeString(value);
|
| 118 |
+
if (normalizedValue !== value) {
|
| 119 |
+
element.setAttribute(attribute, normalizedValue);
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
});
|
| 123 |
+
});
|
| 124 |
+
|
| 125 |
+
const walker = document.createTreeWalker(rootNode, NodeFilter.SHOW_TEXT);
|
| 126 |
+
const nodes = [];
|
| 127 |
+
let node = walker.nextNode();
|
| 128 |
+
while (node) {
|
| 129 |
+
const parent = node.parentElement;
|
| 130 |
+
const text = node.nodeValue || "";
|
| 131 |
+
if (parent && !parent.closest(SKIP_TEXT_SELECTOR) && CJK_RE.test(text)) {
|
| 132 |
+
nodes.push(node);
|
| 133 |
+
}
|
| 134 |
+
node = walker.nextNode();
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
nodes.forEach((textNode) => {
|
| 138 |
+
const text = textNode.nodeValue || "";
|
| 139 |
+
const normalizedText = normalizeString(text);
|
| 140 |
+
if (normalizedText !== text) {
|
| 141 |
+
textNode.nodeValue = normalizedText;
|
| 142 |
+
}
|
| 143 |
+
});
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
function wrapChineseText(rootNode) {
|
| 147 |
+
if (!rootNode) return;
|
| 148 |
+
|
| 149 |
+
const walker = document.createTreeWalker(rootNode, NodeFilter.SHOW_TEXT);
|
| 150 |
+
const nodes = [];
|
| 151 |
+
let node = walker.nextNode();
|
| 152 |
+
while (node) {
|
| 153 |
+
const parent = node.parentElement;
|
| 154 |
+
const text = node.nodeValue || "";
|
| 155 |
+
if (parent && !parent.closest(SKIP_TEXT_SELECTOR) && CJK_RE.test(text)) {
|
| 156 |
+
nodes.push(node);
|
| 157 |
+
}
|
| 158 |
+
node = walker.nextNode();
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
nodes.forEach((textNode) => {
|
| 162 |
+
const text = textNode.nodeValue || "";
|
| 163 |
+
const fragment = document.createDocumentFragment();
|
| 164 |
+
let lastIndex = 0;
|
| 165 |
+
text.replace(CJK_WRAP_RE, (match, index) => {
|
| 166 |
+
if (index > lastIndex) {
|
| 167 |
+
fragment.append(document.createTextNode(text.slice(lastIndex, index)));
|
| 168 |
+
}
|
| 169 |
+
const span = document.createElement("span");
|
| 170 |
+
span.className = "auto-zh";
|
| 171 |
+
span.textContent = match;
|
| 172 |
+
fragment.append(span);
|
| 173 |
+
lastIndex = index + match.length;
|
| 174 |
+
return match;
|
| 175 |
+
});
|
| 176 |
+
if (lastIndex < text.length) {
|
| 177 |
+
fragment.append(document.createTextNode(text.slice(lastIndex)));
|
| 178 |
+
}
|
| 179 |
+
textNode.replaceWith(fragment);
|
| 180 |
+
});
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
document.addEventListener("click", (event) => {
|
| 184 |
+
const langButton = event.target.closest("[data-lang-toggle]");
|
| 185 |
+
if (langButton) {
|
| 186 |
+
applyLanguage(langButton.dataset.langToggle);
|
| 187 |
+
}
|
| 188 |
+
});
|
| 189 |
+
|
| 190 |
+
if (document.readyState === "loading") {
|
| 191 |
+
document.addEventListener("DOMContentLoaded", initControls);
|
| 192 |
+
} else {
|
| 193 |
+
initControls();
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
const observer = new MutationObserver(() => {
|
| 197 |
+
normalizeGradioChrome(document.body);
|
| 198 |
+
wrapChineseText(document.body);
|
| 199 |
+
});
|
| 200 |
+
if (document.body) {
|
| 201 |
+
observer.observe(document.body, { childList: true, subtree: true });
|
| 202 |
+
} else {
|
| 203 |
+
document.addEventListener("DOMContentLoaded", () => {
|
| 204 |
+
observer.observe(document.body, { childList: true, subtree: true });
|
| 205 |
+
});
|
| 206 |
+
}
|
| 207 |
+
})();
|
| 208 |
+
"""
|
| 209 |
+
|
| 210 |
GenerationUiResult = tuple[
|
| 211 |
str,
|
| 212 |
dict[str, Any],
|
|
|
|
| 237 |
background_fill_secondary_dark="rgba(30, 28, 25, 0.6)",
|
| 238 |
border_color_primary="rgba(212, 175, 55, 0.15)",
|
| 239 |
border_color_primary_dark="rgba(212, 175, 55, 0.15)",
|
| 240 |
+
body_text_color="#E6E1D3",
|
| 241 |
+
body_text_color_dark="#E6E1D3",
|
| 242 |
+
body_text_color_subdued="#A89B84",
|
| 243 |
+
body_text_color_subdued_dark="#A89B84",
|
| 244 |
+
link_text_color="#D4AF37",
|
| 245 |
+
link_text_color_dark="#D4AF37",
|
| 246 |
+
link_text_color_hover="#F5D061",
|
| 247 |
+
link_text_color_hover_dark="#F5D061",
|
| 248 |
+
link_text_color_active="#F5D061",
|
| 249 |
+
link_text_color_active_dark="#F5D061",
|
| 250 |
+
link_text_color_visited="#D4AF37",
|
| 251 |
+
link_text_color_visited_dark="#D4AF37",
|
| 252 |
block_background_fill="transparent",
|
| 253 |
block_background_fill_dark="transparent",
|
| 254 |
block_border_width="0px",
|
| 255 |
+
block_info_text_color="#A89B84",
|
| 256 |
+
block_info_text_color_dark="#A89B84",
|
| 257 |
+
block_label_text_color="#A89B84",
|
| 258 |
+
block_label_text_color_dark="#A89B84",
|
| 259 |
+
block_title_text_color="#E6E1D3",
|
| 260 |
+
block_title_text_color_dark="#E6E1D3",
|
| 261 |
panel_background_fill="transparent",
|
| 262 |
panel_background_fill_dark="transparent",
|
| 263 |
+
accordion_text_color="#E6E1D3",
|
| 264 |
+
accordion_text_color_dark="#E6E1D3",
|
| 265 |
+
table_text_color="#E6E1D3",
|
| 266 |
+
table_text_color_dark="#E6E1D3",
|
| 267 |
+
input_background_fill="#1b1a18",
|
| 268 |
+
input_background_fill_dark="#1b1a18",
|
| 269 |
+
input_background_fill_focus="#1b1a18",
|
| 270 |
+
input_background_fill_focus_dark="#1b1a18",
|
| 271 |
+
input_background_fill_hover="#1b1a18",
|
| 272 |
+
input_background_fill_hover_dark="#1b1a18",
|
| 273 |
+
input_border_color="rgba(212, 175, 55, 0.3)",
|
| 274 |
+
input_border_color_dark="rgba(212, 175, 55, 0.3)",
|
| 275 |
+
input_border_color_focus="#D4AF37",
|
| 276 |
+
input_border_color_focus_dark="#D4AF37",
|
| 277 |
+
input_placeholder_color="#8B8678",
|
| 278 |
+
input_placeholder_color_dark="#8B8678",
|
| 279 |
+
checkbox_label_text_color="#E6E1D3",
|
| 280 |
+
checkbox_label_text_color_dark="#E6E1D3",
|
| 281 |
+
checkbox_label_text_color_selected="#F5D061",
|
| 282 |
+
checkbox_label_text_color_selected_dark="#F5D061",
|
| 283 |
+
checkbox_label_background_fill="transparent",
|
| 284 |
+
checkbox_label_background_fill_dark="transparent",
|
| 285 |
+
checkbox_label_background_fill_selected="rgba(212, 175, 55, 0.05)",
|
| 286 |
+
checkbox_label_background_fill_selected_dark="rgba(212, 175, 55, 0.05)",
|
| 287 |
+
checkbox_label_border_color="rgba(212, 175, 55, 0.3)",
|
| 288 |
+
checkbox_label_border_color_dark="rgba(212, 175, 55, 0.3)",
|
| 289 |
+
checkbox_label_border_color_selected="#D4AF37",
|
| 290 |
+
checkbox_label_border_color_selected_dark="#D4AF37",
|
| 291 |
+
button_secondary_background_fill="rgba(22, 21, 19, 0.8)",
|
| 292 |
+
button_secondary_background_fill_dark="rgba(22, 21, 19, 0.8)",
|
| 293 |
+
button_secondary_background_fill_hover="rgba(38, 35, 29, 0.9)",
|
| 294 |
+
button_secondary_background_fill_hover_dark="rgba(38, 35, 29, 0.9)",
|
| 295 |
+
button_secondary_border_color="rgba(212, 175, 55, 0.15)",
|
| 296 |
+
button_secondary_border_color_dark="rgba(212, 175, 55, 0.15)",
|
| 297 |
+
button_secondary_border_color_hover="#D4AF37",
|
| 298 |
+
button_secondary_border_color_hover_dark="#D4AF37",
|
| 299 |
+
button_secondary_text_color="#E6E1D3",
|
| 300 |
+
button_secondary_text_color_dark="#E6E1D3",
|
| 301 |
+
button_secondary_text_color_hover="#F5D061",
|
| 302 |
+
button_secondary_text_color_hover_dark="#F5D061",
|
| 303 |
+
button_primary_text_color="#2a261f",
|
| 304 |
+
button_primary_text_color_dark="#2a261f",
|
| 305 |
)
|
| 306 |
|
| 307 |
+
with gr.Blocks(theme=custom_theme, head=f"<style>{css}</style><script>{UI_CONTROL_SCRIPT}</script>", title=APP_TITLE, fill_width=True, elem_id="objectverse-app") as demo:
|
| 308 |
+
with gr.Column(elem_id="app-container"):
|
| 309 |
+
gr.HTML(
|
| 310 |
+
f"""
|
| 311 |
+
<header id="objectverse-hero">
|
| 312 |
+
<div class="hero-copy">
|
| 313 |
+
<span class="archive-label">Small Model Object Archive</span>
|
| 314 |
+
<h1>{APP_TITLE}</h1>
|
| 315 |
+
<p class="hero-kicker">Every object has a secret life.</p>
|
| 316 |
+
<p class="hero-feature">Build Small Hackathon entry: upload an object, wake its secret persona, read the diary, chat, and share the evidence. Tiny models, weird lives.</p>
|
| 317 |
+
<p class="hero-kicker lang-zh block">万物日记:每个物品都有秘密人生。</p>
|
| 318 |
+
<p class="hero-feature lang-zh block">Build Small 黑客松作品:上传物品,唤醒隐藏人格,读日记、追问它,再分享证据。小模型,怪人生。</p>
|
| 319 |
+
</div>
|
| 320 |
+
<div class="top-controls" aria-label="Display controls">
|
| 321 |
+
<span>Language</span>
|
| 322 |
+
<div class="segmented-control">
|
| 323 |
+
<button type="button" class="active" data-lang-toggle="en" aria-pressed="true">EN</button>
|
| 324 |
+
<button type="button" data-lang-toggle="zh" aria-pressed="false">ZH</button>
|
| 325 |
+
</div>
|
| 326 |
+
</div>
|
| 327 |
+
</header>
|
| 328 |
+
""",
|
| 329 |
+
padding=False,
|
| 330 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
|
| 332 |
+
result_state = gr.State()
|
| 333 |
+
zero_gpu_probe_button = gr.Button(visible=False)
|
| 334 |
+
zero_gpu_probe_output = gr.JSON(visible=False)
|
| 335 |
+
vision_runtime_probe_button = gr.Button(visible=False)
|
| 336 |
+
vision_runtime_probe_output = gr.JSON(visible=False)
|
| 337 |
+
|
| 338 |
+
with gr.Row(elem_id="intake", elem_classes=["content-section", "top-grid"]):
|
| 339 |
+
with gr.Column(scale=7, elem_classes=["archive-panel", "intake-panel"]):
|
| 340 |
+
gr.HTML(_panel_header("01", "Wake an Object", "Upload a photo or describe an everyday object.", "唤醒物品"), padding=False)
|
| 341 |
+
image_input = gr.Image(
|
| 342 |
+
label=copy.UPLOAD_LABEL,
|
| 343 |
+
show_label=False,
|
| 344 |
+
type="filepath",
|
| 345 |
+
sources=["upload"],
|
| 346 |
+
placeholder="Drop an object photo here or click to upload.",
|
| 347 |
+
elem_id="object-upload",
|
| 348 |
+
)
|
| 349 |
+
gr.HTML("""<div class="or-divider"><span>OR</span></div>""", padding=False)
|
| 350 |
+
description_input = gr.Textbox(
|
| 351 |
+
label=copy.DESCRIPTION_LABEL,
|
| 352 |
+
placeholder=copy.DESCRIPTION_PLACEHOLDER,
|
| 353 |
+
lines=2,
|
| 354 |
+
max_lines=5,
|
| 355 |
+
elem_id="object-description",
|
| 356 |
+
)
|
| 357 |
+
|
| 358 |
+
gr.HTML("""<div class="mode-header">Personality mode <span class="lang-zh">人格模式</span></div>""", padding=False)
|
| 359 |
+
mode_input = gr.Radio(
|
| 360 |
+
label=copy.MODE_LABEL,
|
| 361 |
+
show_label=False,
|
| 362 |
+
choices=PERSONALITY_MODES,
|
| 363 |
+
value=DEFAULT_MODE,
|
| 364 |
+
elem_id="personality-mode",
|
| 365 |
+
elem_classes=["mode-switch"],
|
| 366 |
+
)
|
| 367 |
+
generate_button = gr.Button("Wake the Object", variant="primary", elem_id="wake-button")
|
| 368 |
+
|
| 369 |
+
with gr.Column(scale=4, elem_classes=["archive-panel", "examples-panel"]):
|
| 370 |
+
gr.HTML(
|
| 371 |
+
"""
|
| 372 |
+
<div class="example-header">
|
| 373 |
+
<div>
|
| 374 |
+
<strong>Example Objects</strong>
|
| 375 |
+
<span class="lang-zh block">示例物品</span>
|
| 376 |
+
</div>
|
| 377 |
+
</div>
|
| 378 |
+
""",
|
| 379 |
+
padding=False,
|
| 380 |
+
)
|
| 381 |
+
example_buttons: list[gr.Button] = []
|
| 382 |
+
for index in range(len(EXAMPLE_OBJECTS)):
|
| 383 |
+
example_buttons.append(
|
| 384 |
+
gr.Button(
|
| 385 |
+
example_button_label(index),
|
| 386 |
+
elem_classes=["example-card"],
|
| 387 |
+
variant="secondary",
|
| 388 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
)
|
|
|
|
|
|
|
| 390 |
|
| 391 |
+
with gr.Row(elem_id="results", elem_classes=["content-section", "results-grid"]):
|
| 392 |
+
with gr.Column(scale=5, elem_classes=["archive-panel", "file-panel"]):
|
| 393 |
+
gr.HTML(_panel_header("02", "Object File", "Structured understanding and persona.", "物品档案"), padding=False)
|
| 394 |
+
object_file_summary = gr.HTML(value=OBJECT_FILE_EMPTY, elem_id="object-file-summary", padding=False)
|
| 395 |
+
|
| 396 |
+
with gr.Column(scale=6, elem_classes=["archive-panel", "diary-panel"]):
|
| 397 |
+
gr.HTML(_panel_header("03", "Secret Diary", "A private note written by the object.", "秘密日记"), padding=False)
|
| 398 |
+
diary_output = gr.Markdown(
|
| 399 |
+
value=DIARY_EMPTY,
|
| 400 |
+
label=copy.DIARY_LABEL,
|
| 401 |
+
elem_id="diary-output",
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
with gr.Row(elem_id="share-chat", elem_classes=["content-section", "split-section"]):
|
| 405 |
+
with gr.Column(scale=5, elem_classes=["archive-panel", "share-panel"], elem_id="share-panel"):
|
| 406 |
+
gr.HTML(_panel_header("04", "Share Card", "Fixed-width card for screenshots.", "分享卡片"), padding=False)
|
| 407 |
+
share_card = gr.HTML(value=SHARE_CARD_EMPTY, label=copy.SHARE_CARD_LABEL, padding=False)
|
| 408 |
+
|
| 409 |
+
with gr.Column(scale=4, elem_classes=["archive-panel", "chat-panel"], elem_id="chat-panel"):
|
| 410 |
+
gr.HTML(_panel_header("05", "Object Chat", "Ask after the object wakes up.", "物品对话"), padding=False)
|
| 411 |
+
chatbot = gr.Chatbot(
|
| 412 |
+
value=_empty_chat_history(),
|
| 413 |
+
label=copy.CHAT_LABEL,
|
| 414 |
+
type="messages",
|
| 415 |
+
height=300,
|
| 416 |
+
allow_tags=False,
|
| 417 |
+
)
|
| 418 |
+
chat_input = gr.Textbox(placeholder=copy.CHAT_INPUT_PLACEHOLDER, show_label=False)
|
| 419 |
+
chat_button = gr.Button(copy.CHAT_BUTTON_LABEL, elem_classes=["quiet-button"])
|
| 420 |
+
|
| 421 |
+
with gr.Accordion("Developer details", open=False, elem_classes=["developer-details"]):
|
| 422 |
+
trace_summary = gr.HTML(value=TRACE_EMPTY, elem_id="trace-summary", padding=False)
|
| 423 |
+
with gr.Row(elem_classes=["developer-json-grid"]):
|
| 424 |
+
object_json = gr.JSON(value={}, label=copy.OBJECT_JSON_LABEL)
|
| 425 |
+
persona_json = gr.JSON(value={}, label=copy.PERSONA_JSON_LABEL)
|
| 426 |
+
trace_json = gr.JSON(value={}, label=copy.TRACE_JSON_LABEL)
|
| 427 |
+
trace_path = gr.Textbox(label=copy.TRACE_PATH_LABEL, interactive=False)
|
| 428 |
|
| 429 |
manual_outputs = [
|
| 430 |
object_file_summary,
|
|
|
|
| 478 |
return demo
|
| 479 |
|
| 480 |
|
| 481 |
+
def _panel_header(index: str, title: str, note: str, chinese: str = "") -> str:
|
| 482 |
+
chinese_label = f' <small class="lang-zh">{escape(chinese)}</small>' if chinese else ""
|
| 483 |
return f"""
|
| 484 |
<header class="panel-header">
|
| 485 |
<span>{escape(index)}</span>
|
| 486 |
<div>
|
| 487 |
+
<h2>{escape(title)}{chinese_label}</h2>
|
| 488 |
<p>{escape(note)}</p>
|
| 489 |
</div>
|
| 490 |
</header>
|
|
|
|
| 564 |
</div>
|
| 565 |
</dl>
|
| 566 |
<div class="feature-list">
|
| 567 |
+
<strong>Visible features <span class="lang-zh">可见特征</span></strong>
|
| 568 |
<ul>{features}</ul>
|
| 569 |
</div>
|
| 570 |
<p class="complaint">{escape(persona.complaint)}</p>
|
|
|
|
| 576 |
def _render_trace_summary(result: GenerationResult) -> str:
|
| 577 |
return f"""
|
| 578 |
<div class="trace-card">
|
| 579 |
+
<span class="archive-label">Trace saved <span class="lang-zh">Trace 已保存</span></span>
|
| 580 |
<strong>{escape(result.trace.trace_id)}</strong>
|
| 581 |
<p>{escape(result.trace.model_runtime["vision"])} · {escape(result.trace.model_runtime["text"])}</p>
|
| 582 |
</div>
|
|
|
|
| 593 |
}
|
| 594 |
error_html = f"""
|
| 595 |
<div class="archive-error">
|
| 596 |
+
<span>Generation failed <span class="lang-zh">生成失败</span></span>
|
| 597 |
<strong>{escape(error_type)}</strong>
|
| 598 |
<p>{escape(error_message)}</p>
|
| 599 |
</div>
|
| 600 |
"""
|
| 601 |
error_markdown = (
|
| 602 |
+
"### Generation failed\n\n"
|
| 603 |
f"{error_type}: {error_message}\n\n"
|
| 604 |
+
"Please try another description or sample object.\n\n"
|
| 605 |
+
'<div class="lang-zh block zh-helper">请尝试其他描述或示例物品。</div>'
|
| 606 |
)
|
| 607 |
return (
|
| 608 |
error_html,
|
|
|
|
| 614 |
error_payload,
|
| 615 |
"",
|
| 616 |
None,
|
| 617 |
+
[{"role": "assistant", "content": f"Generation failed: {error_type}"}],
|
| 618 |
)
|
| 619 |
|
| 620 |
|
|
|
|
| 627 |
return [
|
| 628 |
{
|
| 629 |
"role": "assistant",
|
| 630 |
+
"content": f"{name} is awake. Ask what it remembers.",
|
| 631 |
}
|
| 632 |
]
|
| 633 |
|
src/ui/styles.css
CHANGED
|
@@ -1,618 +1,649 @@
|
|
| 1 |
-
/*
|
| 2 |
-
* Objectverse Diary -
|
| 3 |
-
* Updated to match reference UI.
|
| 4 |
*/
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Objectverse Diary - compact archive UI.
|
|
|
|
| 3 |
*/
|
| 4 |
|
| 5 |
+
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Courier+Prime:ital,wght@0,400;0,700;1,400&display=swap');
|
| 6 |
+
|
| 7 |
+
:root {
|
| 8 |
+
--ov-bg: #161513;
|
| 9 |
+
--ov-bg-panel: rgba(30, 28, 25, 0.72);
|
| 10 |
+
--ov-bg-input: #1b1a18;
|
| 11 |
+
--ov-border-faint: rgba(212, 175, 55, 0.15);
|
| 12 |
+
--ov-border-light: rgba(212, 175, 55, 0.34);
|
| 13 |
+
--ov-text-main: #e6e1d3;
|
| 14 |
+
--ov-text-soft: #d6d1c4;
|
| 15 |
+
--ov-text-muted: #8b8678;
|
| 16 |
+
--ov-text-dark: #2a261f;
|
| 17 |
+
--ov-gold: #d4af37;
|
| 18 |
+
--ov-gold-bright: #f5d061;
|
| 19 |
+
--font-typewriter: 'Courier Prime', 'Space Mono', 'Courier New', monospace;
|
| 20 |
+
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 21 |
+
--font-serif: Georgia, serif;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.lang-zh,
|
| 25 |
+
.auto-zh {
|
| 26 |
+
display: none !important;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
html[data-ov-lang="zh"] .lang-zh,
|
| 30 |
+
html[data-ov-lang="zh"] .auto-zh {
|
| 31 |
+
display: inline !important;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
html[data-ov-lang="zh"] .lang-zh.block {
|
| 35 |
+
display: block !important;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
html,
|
| 39 |
+
body,
|
| 40 |
+
gradio-app {
|
| 41 |
+
background: var(--ov-bg);
|
| 42 |
+
color: var(--ov-text-main);
|
| 43 |
+
min-height: 100%;
|
| 44 |
+
margin: 0;
|
| 45 |
+
overflow-x: hidden;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
body::before {
|
| 49 |
+
content: "";
|
| 50 |
+
position: fixed;
|
| 51 |
+
inset: 0;
|
| 52 |
+
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.03'/%3E%3C/svg%3E");
|
| 53 |
+
pointer-events: none;
|
| 54 |
+
z-index: 9999;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.gradio-container {
|
| 58 |
+
width: 100% !important;
|
| 59 |
+
max-width: 100% !important;
|
| 60 |
+
min-height: 100vh !important;
|
| 61 |
+
padding: 0 !important;
|
| 62 |
+
background: transparent !important;
|
| 63 |
+
color: var(--ov-text-main) !important;
|
| 64 |
+
font-family: var(--font-sans);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
footer,
|
| 68 |
+
.footer {
|
| 69 |
+
display: none !important;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
#objectverse-app,
|
| 73 |
+
#app-container,
|
| 74 |
+
#main-content,
|
| 75 |
+
.archive-panel {
|
| 76 |
+
color: var(--ov-text-main);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
#app-container {
|
| 80 |
+
max-width: 1180px;
|
| 81 |
+
margin: 0 auto !important;
|
| 82 |
+
padding: 36px 24px 56px;
|
| 83 |
+
gap: 24px !important;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
#objectverse-hero {
|
| 87 |
+
display: flex;
|
| 88 |
+
justify-content: space-between;
|
| 89 |
+
align-items: flex-start;
|
| 90 |
+
gap: 24px;
|
| 91 |
+
padding-bottom: 22px;
|
| 92 |
+
border-bottom: 1px solid var(--ov-border-faint);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.hero-copy h1 {
|
| 96 |
+
margin: 6px 0 8px;
|
| 97 |
+
color: var(--ov-text-main) !important;
|
| 98 |
+
font-family: var(--font-typewriter);
|
| 99 |
+
font-size: clamp(32px, 5vw, 48px);
|
| 100 |
+
line-height: 1.05;
|
| 101 |
+
letter-spacing: 0;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.hero-kicker {
|
| 105 |
+
margin: 0;
|
| 106 |
+
color: var(--ov-gold) !important;
|
| 107 |
+
font-family: var(--font-serif);
|
| 108 |
+
font-size: 18px;
|
| 109 |
+
font-style: italic;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.hero-feature {
|
| 113 |
+
max-width: 680px;
|
| 114 |
+
margin: 12px 0 0;
|
| 115 |
+
color: var(--ov-text-soft) !important;
|
| 116 |
+
font-size: 15px;
|
| 117 |
+
line-height: 1.65;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.archive-label {
|
| 121 |
+
color: var(--ov-gold) !important;
|
| 122 |
+
font-family: var(--font-typewriter);
|
| 123 |
+
font-size: 12px;
|
| 124 |
+
letter-spacing: 0;
|
| 125 |
+
text-transform: uppercase;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.top-controls {
|
| 129 |
+
display: flex;
|
| 130 |
+
align-items: center;
|
| 131 |
+
gap: 12px;
|
| 132 |
+
min-width: 176px;
|
| 133 |
+
color: var(--ov-text-muted);
|
| 134 |
+
font-family: var(--font-typewriter);
|
| 135 |
+
font-size: 12px;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.segmented-control {
|
| 139 |
+
display: grid;
|
| 140 |
+
grid-template-columns: 1fr 1fr;
|
| 141 |
+
min-width: 92px;
|
| 142 |
+
border: 1px solid var(--ov-border-light);
|
| 143 |
+
border-radius: 4px;
|
| 144 |
+
overflow: hidden;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.segmented-control button {
|
| 148 |
+
min-height: 34px;
|
| 149 |
+
padding: 0 12px;
|
| 150 |
+
background: transparent;
|
| 151 |
+
border: 0;
|
| 152 |
+
border-right: 1px solid var(--ov-border-faint);
|
| 153 |
+
color: var(--ov-text-muted);
|
| 154 |
+
font-family: var(--font-typewriter);
|
| 155 |
+
font-size: 12px;
|
| 156 |
+
cursor: pointer;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.segmented-control button:last-child {
|
| 160 |
+
border-right: 0;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.segmented-control button:hover,
|
| 164 |
+
.segmented-control button:focus,
|
| 165 |
+
.segmented-control button.active {
|
| 166 |
+
color: var(--ov-gold);
|
| 167 |
+
background: rgba(212, 175, 55, 0.08);
|
| 168 |
+
outline: none;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.content-section {
|
| 172 |
+
display: flex !important;
|
| 173 |
+
gap: 24px !important;
|
| 174 |
+
margin: 0 0 2px;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.archive-panel {
|
| 178 |
+
position: relative;
|
| 179 |
+
padding: 22px;
|
| 180 |
+
background: var(--ov-bg-panel) !important;
|
| 181 |
+
border: 1px solid var(--ov-border-faint) !important;
|
| 182 |
+
border-radius: 8px;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.gradio-container .block,
|
| 186 |
+
.gradio-container .form,
|
| 187 |
+
.gradio-container .box {
|
| 188 |
+
background: transparent !important;
|
| 189 |
+
border: none !important;
|
| 190 |
+
box-shadow: none !important;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.gradio-container label,
|
| 194 |
+
.gradio-container span.svelte-1gfknul {
|
| 195 |
+
color: var(--ov-text-muted) !important;
|
| 196 |
+
font-family: var(--font-typewriter);
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.gradio-container input,
|
| 200 |
+
.gradio-container textarea {
|
| 201 |
+
background: var(--ov-bg-input) !important;
|
| 202 |
+
border: 1px solid var(--ov-border-light) !important;
|
| 203 |
+
border-radius: 4px !important;
|
| 204 |
+
color: var(--ov-text-main) !important;
|
| 205 |
+
font-family: var(--font-sans) !important;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.gradio-container input:focus,
|
| 209 |
+
.gradio-container textarea:focus {
|
| 210 |
+
border-color: var(--ov-gold) !important;
|
| 211 |
+
box-shadow: none !important;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
#object-upload {
|
| 215 |
+
display: flex;
|
| 216 |
+
align-items: center;
|
| 217 |
+
justify-content: center;
|
| 218 |
+
min-height: 176px;
|
| 219 |
+
padding: 34px 20px;
|
| 220 |
+
border: 2px dashed var(--ov-border-light) !important;
|
| 221 |
+
border-radius: 8px;
|
| 222 |
+
background: transparent !important;
|
| 223 |
+
color: var(--ov-text-muted) !important;
|
| 224 |
+
text-align: center;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
#object-upload :is(label, span, p, div, button) {
|
| 228 |
+
color: var(--ov-text-muted) !important;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.or-divider {
|
| 232 |
+
position: relative;
|
| 233 |
+
margin: 18px 0;
|
| 234 |
+
text-align: center;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.or-divider::before {
|
| 238 |
+
content: "";
|
| 239 |
+
position: absolute;
|
| 240 |
+
top: 50%;
|
| 241 |
+
right: 0;
|
| 242 |
+
left: 0;
|
| 243 |
+
height: 1px;
|
| 244 |
+
background: var(--ov-border-faint);
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.or-divider span {
|
| 248 |
+
position: relative;
|
| 249 |
+
z-index: 1;
|
| 250 |
+
padding: 0 14px;
|
| 251 |
+
background: var(--ov-bg-panel);
|
| 252 |
+
color: var(--ov-text-muted);
|
| 253 |
+
font-family: var(--font-typewriter);
|
| 254 |
+
font-size: 13px;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.mode-header {
|
| 258 |
+
display: flex;
|
| 259 |
+
align-items: center;
|
| 260 |
+
gap: 8px;
|
| 261 |
+
margin: 18px 0 12px;
|
| 262 |
+
color: var(--ov-text-main) !important;
|
| 263 |
+
font-family: var(--font-typewriter);
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.mode-header .lang-zh {
|
| 267 |
+
color: var(--ov-text-muted);
|
| 268 |
+
font-family: var(--font-sans);
|
| 269 |
+
font-size: 12px;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
#personality-mode .wrap {
|
| 273 |
+
display: flex !important;
|
| 274 |
+
flex-wrap: wrap !important;
|
| 275 |
+
gap: 10px !important;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
#personality-mode label {
|
| 279 |
+
flex: 1 1 120px;
|
| 280 |
+
min-height: 48px;
|
| 281 |
+
padding: 13px 10px !important;
|
| 282 |
+
border: 1px solid var(--ov-border-light) !important;
|
| 283 |
+
border-radius: 6px !important;
|
| 284 |
+
background: transparent !important;
|
| 285 |
+
text-align: center;
|
| 286 |
+
cursor: pointer;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
#personality-mode label span {
|
| 290 |
+
display: block;
|
| 291 |
+
color: var(--ov-text-main) !important;
|
| 292 |
+
font-family: var(--font-typewriter);
|
| 293 |
+
font-size: 14px;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
#personality-mode label:has(input:checked) {
|
| 297 |
+
border-color: var(--ov-gold) !important;
|
| 298 |
+
background: rgba(212, 175, 55, 0.06) !important;
|
| 299 |
+
box-shadow: 0 0 0 1px var(--ov-gold) inset;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
#personality-mode label:has(input:checked) span {
|
| 303 |
+
color: var(--ov-gold-bright) !important;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
#wake-button {
|
| 307 |
+
width: 100%;
|
| 308 |
+
margin-top: 22px;
|
| 309 |
+
padding: 18px !important;
|
| 310 |
+
border: 0 !important;
|
| 311 |
+
border-radius: 4px !important;
|
| 312 |
+
background: linear-gradient(180deg, #d8ac54 0%, #a67c2d 100%) !important;
|
| 313 |
+
color: var(--ov-text-dark) !important;
|
| 314 |
+
font-family: var(--font-typewriter);
|
| 315 |
+
font-size: 18px !important;
|
| 316 |
+
font-weight: 700;
|
| 317 |
+
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.28), 0 4px 14px rgba(0, 0, 0, 0.34) !important;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
#wake-button:hover {
|
| 321 |
+
filter: brightness(1.08);
|
| 322 |
+
transform: translateY(-1px);
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.example-header {
|
| 326 |
+
margin-bottom: 18px;
|
| 327 |
+
padding-bottom: 14px;
|
| 328 |
+
border-bottom: 1px solid var(--ov-border-faint);
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.example-header strong {
|
| 332 |
+
display: block;
|
| 333 |
+
color: var(--ov-text-main) !important;
|
| 334 |
+
font-family: var(--font-typewriter);
|
| 335 |
+
font-size: 16px;
|
| 336 |
+
font-weight: 400;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.example-header span {
|
| 340 |
+
color: var(--ov-text-muted) !important;
|
| 341 |
+
font-size: 13px;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
button.example-card {
|
| 345 |
+
display: block;
|
| 346 |
+
width: 100%;
|
| 347 |
+
margin-bottom: 10px !important;
|
| 348 |
+
padding: 14px !important;
|
| 349 |
+
border: 1px solid var(--ov-border-faint) !important;
|
| 350 |
+
border-radius: 4px !important;
|
| 351 |
+
background: var(--ov-bg-input) !important;
|
| 352 |
+
color: var(--ov-text-main) !important;
|
| 353 |
+
font-family: var(--font-typewriter) !important;
|
| 354 |
+
text-align: left !important;
|
| 355 |
+
transition: border-color 0.2s, background 0.2s;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
button.example-card:hover,
|
| 359 |
+
button.example-card:focus {
|
| 360 |
+
border-color: var(--ov-gold) !important;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
button.example-card * {
|
| 364 |
+
color: inherit !important;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.panel-header {
|
| 368 |
+
display: grid;
|
| 369 |
+
grid-template-columns: auto 1fr;
|
| 370 |
+
gap: 14px;
|
| 371 |
+
margin-bottom: 18px;
|
| 372 |
+
padding-bottom: 14px;
|
| 373 |
+
border-bottom: 1px solid var(--ov-border-faint);
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
.panel-header > span {
|
| 377 |
+
color: var(--ov-gold) !important;
|
| 378 |
+
font-family: var(--font-typewriter);
|
| 379 |
+
font-size: 16px;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
.panel-header h2 {
|
| 383 |
+
margin: 0 0 4px;
|
| 384 |
+
color: var(--ov-text-main) !important;
|
| 385 |
+
font-family: var(--font-typewriter);
|
| 386 |
+
font-size: 22px;
|
| 387 |
+
line-height: 1.2;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
.panel-header h2 small,
|
| 391 |
+
.panel-header p {
|
| 392 |
+
color: var(--ov-text-muted) !important;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.panel-header p {
|
| 396 |
+
margin: 0;
|
| 397 |
+
font-size: 13px;
|
| 398 |
+
line-height: 1.45;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
#diary-output,
|
| 402 |
+
.diary-entry {
|
| 403 |
+
color: var(--ov-text-soft) !important;
|
| 404 |
+
font-family: var(--font-serif) !important;
|
| 405 |
+
font-size: 17px;
|
| 406 |
+
line-height: 1.75;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.diary-entry h2,
|
| 410 |
+
#diary-output h2,
|
| 411 |
+
#diary-output h3 {
|
| 412 |
+
margin: 0 0 14px;
|
| 413 |
+
color: var(--ov-gold) !important;
|
| 414 |
+
font-family: var(--font-typewriter);
|
| 415 |
+
font-size: 17px;
|
| 416 |
+
line-height: 1.3;
|
| 417 |
+
text-transform: uppercase;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
.diary-entry p {
|
| 421 |
+
margin: 0;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.zh-helper {
|
| 425 |
+
margin-top: 18px;
|
| 426 |
+
padding-top: 14px;
|
| 427 |
+
border-top: 1px dashed var(--ov-border-faint);
|
| 428 |
+
color: var(--ov-text-muted) !important;
|
| 429 |
+
font-family: var(--font-sans);
|
| 430 |
+
font-size: 14px;
|
| 431 |
+
line-height: 1.7;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
.archive-empty,
|
| 435 |
+
.objectverse-placeholder {
|
| 436 |
+
padding: 34px 24px;
|
| 437 |
+
border: 1px dashed var(--ov-border-light);
|
| 438 |
+
color: var(--ov-text-muted) !important;
|
| 439 |
+
text-align: center;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.archive-empty h3,
|
| 443 |
+
.objectverse-placeholder strong {
|
| 444 |
+
display: block;
|
| 445 |
+
margin: 8px 0;
|
| 446 |
+
color: var(--ov-text-main) !important;
|
| 447 |
+
font-family: var(--font-typewriter);
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
.archive-empty p,
|
| 451 |
+
.objectverse-placeholder p {
|
| 452 |
+
margin: 6px 0 0;
|
| 453 |
+
color: var(--ov-text-muted) !important;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
.object-file-card,
|
| 457 |
+
.trace-card,
|
| 458 |
+
.archive-error,
|
| 459 |
+
.objectverse-card {
|
| 460 |
+
color: var(--ov-text-main) !important;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
.object-file-card h3,
|
| 464 |
+
.trace-card strong,
|
| 465 |
+
.archive-error strong,
|
| 466 |
+
.objectverse-card h2 {
|
| 467 |
+
color: var(--ov-text-main) !important;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.file-meta {
|
| 471 |
+
display: flex;
|
| 472 |
+
flex-wrap: wrap;
|
| 473 |
+
gap: 8px;
|
| 474 |
+
margin-bottom: 12px;
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
.file-meta span,
|
| 478 |
+
.object-name,
|
| 479 |
+
.object-file-card dt,
|
| 480 |
+
.object-file-card p,
|
| 481 |
+
.object-file-card li,
|
| 482 |
+
.trace-card p,
|
| 483 |
+
.archive-error p,
|
| 484 |
+
.card-kicker,
|
| 485 |
+
.card-object,
|
| 486 |
+
.card-cn {
|
| 487 |
+
color: var(--ov-text-muted) !important;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.object-file-card dl {
|
| 491 |
+
display: grid;
|
| 492 |
+
gap: 12px;
|
| 493 |
+
margin: 18px 0;
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
.object-file-card dl > div {
|
| 497 |
+
padding-bottom: 10px;
|
| 498 |
+
border-bottom: 1px solid var(--ov-border-faint);
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
.object-file-card dt {
|
| 502 |
+
margin-bottom: 4px;
|
| 503 |
+
font-family: var(--font-typewriter);
|
| 504 |
+
font-size: 12px;
|
| 505 |
+
text-transform: uppercase;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.object-file-card dd {
|
| 509 |
+
margin: 0;
|
| 510 |
+
color: var(--ov-text-soft) !important;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
.feature-list strong,
|
| 514 |
+
.card-quote {
|
| 515 |
+
color: var(--ov-text-soft) !important;
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
.feature-list ul {
|
| 519 |
+
margin: 8px 0 0;
|
| 520 |
+
padding-left: 20px;
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
.complaint,
|
| 524 |
+
.card-stamp {
|
| 525 |
+
color: var(--ov-gold) !important;
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
.file-tags,
|
| 529 |
+
.card-tags {
|
| 530 |
+
display: flex;
|
| 531 |
+
flex-wrap: wrap;
|
| 532 |
+
gap: 8px;
|
| 533 |
+
margin-top: 16px;
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
.file-tags span,
|
| 537 |
+
.card-tags span {
|
| 538 |
+
padding: 4px 8px;
|
| 539 |
+
border: 1px solid var(--ov-border-light) !important;
|
| 540 |
+
border-radius: 999px;
|
| 541 |
+
color: var(--ov-gold-bright) !important;
|
| 542 |
+
font-family: var(--font-typewriter);
|
| 543 |
+
font-size: 12px;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
.objectverse-card {
|
| 547 |
+
max-width: 520px;
|
| 548 |
+
padding: 22px;
|
| 549 |
+
border: 1px solid var(--ov-border-light);
|
| 550 |
+
border-radius: 8px;
|
| 551 |
+
background: rgba(22, 21, 19, 0.7);
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
.card-header {
|
| 555 |
+
display: flex;
|
| 556 |
+
justify-content: space-between;
|
| 557 |
+
gap: 16px;
|
| 558 |
+
margin-bottom: 16px;
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
.card-header h2 {
|
| 562 |
+
margin: 4px 0 0;
|
| 563 |
+
font-family: var(--font-typewriter);
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
.card-quote {
|
| 567 |
+
font-family: var(--font-serif);
|
| 568 |
+
font-size: 17px;
|
| 569 |
+
line-height: 1.65;
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
.card-cn {
|
| 573 |
+
margin-top: 12px;
|
| 574 |
+
padding-top: 12px;
|
| 575 |
+
border-top: 1px dashed var(--ov-border-faint);
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
.quiet-button {
|
| 579 |
+
border: 1px solid var(--ov-border-light) !important;
|
| 580 |
+
border-radius: 4px !important;
|
| 581 |
+
background: transparent !important;
|
| 582 |
+
color: var(--ov-gold) !important;
|
| 583 |
+
font-family: var(--font-typewriter) !important;
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
.developer-details {
|
| 587 |
+
border: 1px solid var(--ov-border-faint) !important;
|
| 588 |
+
border-radius: 8px !important;
|
| 589 |
+
background: rgba(30, 28, 25, 0.42) !important;
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
.developer-json-grid {
|
| 593 |
+
gap: 16px !important;
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
.gradio-container :is(summary, pre, code) {
|
| 597 |
+
color: var(--ov-text-main) !important;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
.gradio-container :is(.json-holder, .json-holder *, .chatbot, .chatbot *) {
|
| 601 |
+
color: var(--ov-text-main) !important;
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
@media (max-width: 980px) {
|
| 605 |
+
#app-container {
|
| 606 |
+
padding: 28px 18px 44px;
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
#objectverse-hero,
|
| 610 |
+
.content-section,
|
| 611 |
+
.split-section,
|
| 612 |
+
.developer-json-grid {
|
| 613 |
+
flex-direction: column !important;
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
.top-controls {
|
| 617 |
+
width: 100%;
|
| 618 |
+
justify-content: space-between;
|
| 619 |
+
}
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
@media (max-width: 600px) {
|
| 623 |
+
#app-container {
|
| 624 |
+
padding: 22px 14px 36px;
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
.archive-panel {
|
| 628 |
+
padding: 18px;
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
.hero-copy h1 {
|
| 632 |
+
font-size: 30px;
|
| 633 |
+
word-break: break-word;
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
.hero-kicker {
|
| 637 |
+
font-size: 15px;
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
.hero-feature {
|
| 641 |
+
font-size: 14px;
|
| 642 |
+
line-height: 1.55;
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
#personality-mode label {
|
| 646 |
+
flex: 1 1 45% !important;
|
| 647 |
+
padding: 10px 6px !important;
|
| 648 |
+
}
|
| 649 |
+
}
|
src/utils/json_repair.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""JSON
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
@@ -14,17 +14,61 @@ def parse_json_object(raw: str) -> dict[str, Any]:
|
|
| 14 |
|
| 15 |
|
| 16 |
def _extract_json_object(raw: str) -> str:
|
| 17 |
-
clean = raw.strip()
|
| 18 |
-
if clean.startswith("```"):
|
| 19 |
-
clean = clean.strip("`").strip()
|
| 20 |
-
if clean.lower().startswith("json"):
|
| 21 |
-
clean = clean[4:].strip()
|
| 22 |
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
return clean
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
start = clean.find("{")
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Small JSON object extraction helpers for model output."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 14 |
|
| 15 |
|
| 16 |
def _extract_json_object(raw: str) -> str:
|
| 17 |
+
clean = _strip_markdown_fence(raw.strip())
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
candidate = _scan_json_object(clean)
|
| 20 |
+
if candidate is None:
|
| 21 |
+
raise ValueError("No JSON object found.")
|
| 22 |
+
return candidate
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _strip_markdown_fence(clean: str) -> str:
|
| 26 |
+
if not clean.startswith("```"):
|
| 27 |
return clean
|
| 28 |
|
| 29 |
+
lines = clean.splitlines()
|
| 30 |
+
if not lines:
|
| 31 |
+
return clean
|
| 32 |
+
|
| 33 |
+
if lines[0].strip().startswith("```"):
|
| 34 |
+
lines = lines[1:]
|
| 35 |
+
if lines and lines[-1].strip().startswith("```"):
|
| 36 |
+
lines = lines[:-1]
|
| 37 |
+
return "\n".join(lines).strip()
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _scan_json_object(clean: str) -> str | None:
|
| 41 |
start = clean.find("{")
|
| 42 |
+
if start == -1:
|
| 43 |
+
return None
|
| 44 |
+
|
| 45 |
+
stack: list[str] = []
|
| 46 |
+
in_string = False
|
| 47 |
+
escaped = False
|
| 48 |
+
|
| 49 |
+
for index, char in enumerate(clean[start:], start=start):
|
| 50 |
+
if in_string:
|
| 51 |
+
if escaped:
|
| 52 |
+
escaped = False
|
| 53 |
+
elif char == "\\":
|
| 54 |
+
escaped = True
|
| 55 |
+
elif char == '"':
|
| 56 |
+
in_string = False
|
| 57 |
+
continue
|
| 58 |
+
|
| 59 |
+
if char == '"':
|
| 60 |
+
in_string = True
|
| 61 |
+
elif char == "{":
|
| 62 |
+
stack.append("}")
|
| 63 |
+
elif char == "[":
|
| 64 |
+
stack.append("]")
|
| 65 |
+
elif char in {"}", "]"}:
|
| 66 |
+
if not stack or stack[-1] != char:
|
| 67 |
+
return None
|
| 68 |
+
stack.pop()
|
| 69 |
+
if not stack:
|
| 70 |
+
return clean[start : index + 1]
|
| 71 |
+
|
| 72 |
+
if in_string or not stack:
|
| 73 |
+
return None
|
| 74 |
+
return clean[start:] + "".join(reversed(stack))
|
tests/test_dataset_tooling.py
CHANGED
|
@@ -5,12 +5,13 @@ from __future__ import annotations
|
|
| 5 |
import json
|
| 6 |
import tempfile
|
| 7 |
import unittest
|
|
|
|
| 8 |
from pathlib import Path
|
| 9 |
|
| 10 |
from scripts.export_traces import export_trace_jsonl
|
| 11 |
from scripts.generate_dataset import build_sft_records, write_sft_jsonl
|
| 12 |
from scripts.generate_sample_traces import generate_sample_traces
|
| 13 |
-
from scripts.prepare_curated_dataset import build_curated_records, write_jsonl
|
| 14 |
from src.models.schema import TraceRecord
|
| 15 |
|
| 16 |
|
|
@@ -48,6 +49,25 @@ class DatasetToolingTest(unittest.TestCase):
|
|
| 48 |
self.assertIn("persona", assistant_payload)
|
| 49 |
self.assertIn("diary", assistant_payload)
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
def test_write_curated_jsonl(self) -> None:
|
| 52 |
with tempfile.TemporaryDirectory() as tmp_dir:
|
| 53 |
output_path = Path(tmp_dir) / "curated.jsonl"
|
|
|
|
| 5 |
import json
|
| 6 |
import tempfile
|
| 7 |
import unittest
|
| 8 |
+
from collections import Counter
|
| 9 |
from pathlib import Path
|
| 10 |
|
| 11 |
from scripts.export_traces import export_trace_jsonl
|
| 12 |
from scripts.generate_dataset import build_sft_records, write_sft_jsonl
|
| 13 |
from scripts.generate_sample_traces import generate_sample_traces
|
| 14 |
+
from scripts.prepare_curated_dataset import MODES, build_curated_records, write_jsonl
|
| 15 |
from src.models.schema import TraceRecord
|
| 16 |
|
| 17 |
|
|
|
|
| 49 |
self.assertIn("persona", assistant_payload)
|
| 50 |
self.assertIn("diary", assistant_payload)
|
| 51 |
|
| 52 |
+
def test_build_curated_v2_records_has_broader_balanced_coverage(self) -> None:
|
| 53 |
+
records = build_curated_records(200, version="v2")
|
| 54 |
+
object_names = [
|
| 55 |
+
record["object_understanding"]["object"]["name"]
|
| 56 |
+
for record in records
|
| 57 |
+
]
|
| 58 |
+
mode_counts = Counter(record["mode"] for record in records)
|
| 59 |
+
object_mode_pairs = {(name, record["mode"]) for name, record in zip(object_names, records)}
|
| 60 |
+
assistant_payload = json.loads(records[0]["messages"][2]["content"])
|
| 61 |
+
|
| 62 |
+
self.assertEqual(len(records), 200)
|
| 63 |
+
self.assertEqual(records[0]["source"], "objectverse-diary-synthetic-curated-v2")
|
| 64 |
+
self.assertGreaterEqual(len(set(object_names)), 40)
|
| 65 |
+
self.assertEqual(mode_counts, Counter({mode: 40 for mode in MODES}))
|
| 66 |
+
self.assertEqual(len(object_mode_pairs), 200)
|
| 67 |
+
self.assertIn("scene_detail", records[0])
|
| 68 |
+
self.assertIn("persona", assistant_payload)
|
| 69 |
+
self.assertIn("diary", assistant_payload)
|
| 70 |
+
|
| 71 |
def test_write_curated_jsonl(self) -> None:
|
| 72 |
with tempfile.TemporaryDirectory() as tmp_dir:
|
| 73 |
output_path = Path(tmp_dir) / "curated.jsonl"
|
tests/test_finetune_lora_tooling.py
CHANGED
|
@@ -10,6 +10,41 @@ from pathlib import Path
|
|
| 10 |
from scripts import finetune_lora
|
| 11 |
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
def _valid_record() -> dict[str, object]:
|
| 14 |
return {
|
| 15 |
"id": "sft-preview-0001",
|
|
@@ -55,9 +90,38 @@ class FinetuneLoraToolingTest(unittest.TestCase):
|
|
| 55 |
self.assertEqual(config.lora_alpha, 32)
|
| 56 |
self.assertEqual(config.lora_dropout, 0.05)
|
| 57 |
self.assertEqual(config.max_steps, 80)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
self.assertIn("q_proj", config.target_modules)
|
| 59 |
self.assertIn("down_proj", config.target_modules)
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
def test_dry_run_does_not_call_remote_runner(self) -> None:
|
| 62 |
with tempfile.TemporaryDirectory() as tmp_dir:
|
| 63 |
path = Path(tmp_dir) / "records.jsonl"
|
|
@@ -80,6 +144,44 @@ class FinetuneLoraToolingTest(unittest.TestCase):
|
|
| 80 |
self.assertEqual(summary["mode"], "dry-run")
|
| 81 |
self.assertEqual(summary["record_count"], 1)
|
| 82 |
self.assertEqual(summary["base_model"], "Qwen/Qwen2.5-1.5B-Instruct")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
|
| 85 |
if __name__ == "__main__":
|
|
|
|
| 10 |
from scripts import finetune_lora
|
| 11 |
|
| 12 |
|
| 13 |
+
class FakeTokenizer:
|
| 14 |
+
pad_token = "<pad>"
|
| 15 |
+
eos_token = "</s>"
|
| 16 |
+
|
| 17 |
+
def apply_chat_template(
|
| 18 |
+
self,
|
| 19 |
+
messages: list[dict[str, str]],
|
| 20 |
+
*,
|
| 21 |
+
tokenize: bool,
|
| 22 |
+
add_generation_prompt: bool,
|
| 23 |
+
) -> str:
|
| 24 |
+
text = "".join(
|
| 25 |
+
f"<{message['role']}>{message['content']}</{message['role']}>"
|
| 26 |
+
for message in messages
|
| 27 |
+
)
|
| 28 |
+
if add_generation_prompt:
|
| 29 |
+
text += "<assistant>"
|
| 30 |
+
return text
|
| 31 |
+
|
| 32 |
+
def __call__(
|
| 33 |
+
self,
|
| 34 |
+
text: str,
|
| 35 |
+
*,
|
| 36 |
+
truncation: bool,
|
| 37 |
+
max_length: int,
|
| 38 |
+
padding: bool,
|
| 39 |
+
add_special_tokens: bool = False,
|
| 40 |
+
) -> dict[str, list[int]]:
|
| 41 |
+
del padding, add_special_tokens
|
| 42 |
+
ids = [ord(character) % 251 + 1 for character in text]
|
| 43 |
+
if truncation:
|
| 44 |
+
ids = ids[:max_length]
|
| 45 |
+
return {"input_ids": ids, "attention_mask": [1] * len(ids)}
|
| 46 |
+
|
| 47 |
+
|
| 48 |
def _valid_record() -> dict[str, object]:
|
| 49 |
return {
|
| 50 |
"id": "sft-preview-0001",
|
|
|
|
| 90 |
self.assertEqual(config.lora_alpha, 32)
|
| 91 |
self.assertEqual(config.lora_dropout, 0.05)
|
| 92 |
self.assertEqual(config.max_steps, 80)
|
| 93 |
+
self.assertEqual(config.num_train_epochs, 3.0)
|
| 94 |
+
self.assertEqual(config.per_device_train_batch_size, 1)
|
| 95 |
+
self.assertEqual(config.gradient_accumulation_steps, 4)
|
| 96 |
+
self.assertEqual(config.eval_ratio, 0.1)
|
| 97 |
+
self.assertTrue(config.assistant_only_loss)
|
| 98 |
self.assertIn("q_proj", config.target_modules)
|
| 99 |
self.assertIn("down_proj", config.target_modules)
|
| 100 |
|
| 101 |
+
def test_training_config_serializes_v2_experiment_settings(self) -> None:
|
| 102 |
+
config = finetune_lora.TrainingConfig(
|
| 103 |
+
max_steps=0,
|
| 104 |
+
num_train_epochs=4.0,
|
| 105 |
+
per_device_train_batch_size=2,
|
| 106 |
+
gradient_accumulation_steps=8,
|
| 107 |
+
eval_ratio=0.2,
|
| 108 |
+
eval_steps=25,
|
| 109 |
+
lora_r=32,
|
| 110 |
+
lora_alpha=64,
|
| 111 |
+
assistant_only_loss=False,
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
payload = config.as_remote_dict()
|
| 115 |
+
|
| 116 |
+
self.assertEqual(payload["num_train_epochs"], 4.0)
|
| 117 |
+
self.assertEqual(payload["per_device_train_batch_size"], 2)
|
| 118 |
+
self.assertEqual(payload["gradient_accumulation_steps"], 8)
|
| 119 |
+
self.assertEqual(payload["eval_ratio"], 0.2)
|
| 120 |
+
self.assertEqual(payload["eval_steps"], 25)
|
| 121 |
+
self.assertEqual(payload["lora_r"], 32)
|
| 122 |
+
self.assertEqual(payload["lora_alpha"], 64)
|
| 123 |
+
self.assertFalse(payload["assistant_only_loss"])
|
| 124 |
+
|
| 125 |
def test_dry_run_does_not_call_remote_runner(self) -> None:
|
| 126 |
with tempfile.TemporaryDirectory() as tmp_dir:
|
| 127 |
path = Path(tmp_dir) / "records.jsonl"
|
|
|
|
| 144 |
self.assertEqual(summary["mode"], "dry-run")
|
| 145 |
self.assertEqual(summary["record_count"], 1)
|
| 146 |
self.assertEqual(summary["base_model"], "Qwen/Qwen2.5-1.5B-Instruct")
|
| 147 |
+
self.assertEqual(summary["train_record_count"], 1)
|
| 148 |
+
self.assertEqual(summary["eval_record_count"], 0)
|
| 149 |
+
|
| 150 |
+
def test_dry_run_reports_eval_split_for_larger_datasets(self) -> None:
|
| 151 |
+
records = [_valid_record() for _ in range(20)]
|
| 152 |
+
summary = finetune_lora._dry_run_summary(
|
| 153 |
+
Path("records.jsonl"),
|
| 154 |
+
records,
|
| 155 |
+
finetune_lora.TrainingConfig(eval_ratio=0.2),
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
self.assertEqual(summary["train_record_count"], 16)
|
| 159 |
+
self.assertEqual(summary["eval_record_count"], 4)
|
| 160 |
+
|
| 161 |
+
def test_assistant_only_tokenization_masks_prompt_labels(self) -> None:
|
| 162 |
+
tokenized = finetune_lora._tokenize_training_example(
|
| 163 |
+
_valid_record(),
|
| 164 |
+
FakeTokenizer(),
|
| 165 |
+
max_length=512,
|
| 166 |
+
assistant_only_loss=True,
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
labels = tokenized["labels"]
|
| 170 |
+
|
| 171 |
+
self.assertIn(-100, labels)
|
| 172 |
+
self.assertTrue(any(label != -100 for label in labels))
|
| 173 |
+
first_unmasked = next(index for index, label in enumerate(labels) if label != -100)
|
| 174 |
+
self.assertGreater(first_unmasked, 0)
|
| 175 |
+
|
| 176 |
+
def test_full_loss_tokenization_keeps_all_labels(self) -> None:
|
| 177 |
+
tokenized = finetune_lora._tokenize_training_example(
|
| 178 |
+
_valid_record(),
|
| 179 |
+
FakeTokenizer(),
|
| 180 |
+
max_length=512,
|
| 181 |
+
assistant_only_loss=False,
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
self.assertNotIn(-100, tokenized["labels"])
|
| 185 |
|
| 186 |
|
| 187 |
if __name__ == "__main__":
|
tests/test_json_repair.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for tolerant JSON object extraction from model output."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import unittest
|
| 6 |
+
|
| 7 |
+
from src.utils.json_repair import parse_json_object
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class JsonRepairTest(unittest.TestCase):
|
| 11 |
+
def test_parses_complete_object_with_surrounding_text(self) -> None:
|
| 12 |
+
payload = parse_json_object('Here is the archive:\n{"name": "mug"}\nDone.')
|
| 13 |
+
|
| 14 |
+
self.assertEqual(payload, {"name": "mug"})
|
| 15 |
+
|
| 16 |
+
def test_repairs_missing_outer_closing_brace(self) -> None:
|
| 17 |
+
payload = parse_json_object(
|
| 18 |
+
"""
|
| 19 |
+
{
|
| 20 |
+
"persona": {"object_name": "coffee mug"},
|
| 21 |
+
"diary": {"title": "Secret Diary - Day 310"}
|
| 22 |
+
"""
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
self.assertEqual(payload["persona"]["object_name"], "coffee mug")
|
| 26 |
+
self.assertEqual(payload["diary"]["title"], "Secret Diary - Day 310")
|
| 27 |
+
|
| 28 |
+
def test_does_not_repair_unterminated_string(self) -> None:
|
| 29 |
+
with self.assertRaises(ValueError):
|
| 30 |
+
parse_json_object('{"name": "coffee mug}')
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
if __name__ == "__main__":
|
| 34 |
+
unittest.main()
|
tests/test_llama_cpp_smoke.py
CHANGED
|
@@ -24,10 +24,7 @@ class LlamaCppSmokeTest(unittest.TestCase):
|
|
| 24 |
fake_llama = FakeLlamaModel(
|
| 25 |
[
|
| 26 |
"""
|
| 27 |
-
{"persona":{"object_name":"coffee mug","character_name":"Mugworth","mood":"dry and suspicious","secret_fear":"being left empty forever","core_memory":"It remembers every late-night refill.","complaint":"I am treated like a ceramic fuel tank.","tags":["desk witness","warm archive","quiet judgment"]}}
|
| 28 |
-
""",
|
| 29 |
-
"""
|
| 30 |
-
{"title":"Secret Diary - Day 418","english":"Today I held another bitter storm and called it service.","chinese":"今天我又装下一场苦涩风暴,并被称为有用。"}
|
| 31 |
""",
|
| 32 |
"""
|
| 33 |
{"reply":"Mugworth: I saw another deadline dissolve into a coffee ring."}
|
|
|
|
| 24 |
fake_llama = FakeLlamaModel(
|
| 25 |
[
|
| 26 |
"""
|
| 27 |
+
{"persona":{"object_name":"coffee mug","character_name":"Mugworth","mood":"dry and suspicious","secret_fear":"being left empty forever","core_memory":"It remembers every late-night refill.","complaint":"I am treated like a ceramic fuel tank.","tags":["desk witness","warm archive","quiet judgment"]},"diary":{"title":"Secret Diary - Day 418","english":"Today I held another bitter storm and called it service.","chinese":"今天我又装下一场苦涩风暴,并被称为有用。"}}
|
|
|
|
|
|
|
|
|
|
| 28 |
""",
|
| 29 |
"""
|
| 30 |
{"reply":"Mugworth: I saw another deadline dissolve into a coffee ring."}
|
tests/test_merge_lora_adapter.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for LoRA merge helper tooling."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import tempfile
|
| 7 |
+
import unittest
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
from scripts.merge_lora_adapter import plan_merge, validate_adapter_source
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class MergeLoraAdapterTest(unittest.TestCase):
|
| 14 |
+
def test_validate_local_adapter_requires_config_and_weights(self) -> None:
|
| 15 |
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
| 16 |
+
adapter_dir = Path(tmp_dir)
|
| 17 |
+
(adapter_dir / "adapter_config.json").write_text("{}", encoding="utf-8")
|
| 18 |
+
|
| 19 |
+
with self.assertRaises(ValueError):
|
| 20 |
+
validate_adapter_source(adapter_dir, base_model="Qwen/Qwen2.5-1.5B-Instruct")
|
| 21 |
+
|
| 22 |
+
def test_plan_merge_dry_run_returns_summary_without_loading_model(self) -> None:
|
| 23 |
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
| 24 |
+
adapter_dir = Path(tmp_dir) / "adapter"
|
| 25 |
+
adapter_dir.mkdir()
|
| 26 |
+
(adapter_dir / "adapter_config.json").write_text(
|
| 27 |
+
json.dumps({"base_model_name_or_path": "Qwen/Qwen2.5-1.5B-Instruct"}),
|
| 28 |
+
encoding="utf-8",
|
| 29 |
+
)
|
| 30 |
+
(adapter_dir / "adapter_model.safetensors").write_text("fake", encoding="utf-8")
|
| 31 |
+
|
| 32 |
+
summary = plan_merge(
|
| 33 |
+
base_model="Qwen/Qwen2.5-1.5B-Instruct",
|
| 34 |
+
adapter=adapter_dir,
|
| 35 |
+
output=Path(tmp_dir) / "merged",
|
| 36 |
+
dry_run=True,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
self.assertTrue(summary["dry_run"])
|
| 40 |
+
self.assertFalse(summary["merged"])
|
| 41 |
+
self.assertEqual(summary["base_model"], "Qwen/Qwen2.5-1.5B-Instruct")
|
| 42 |
+
self.assertEqual(summary["adapter_type"], "local")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
if __name__ == "__main__":
|
| 46 |
+
unittest.main()
|
tests/test_publish_hf_dataset.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for Hugging Face Dataset publishing helpers."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import tempfile
|
| 7 |
+
import unittest
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
from scripts.prepare_curated_dataset import build_curated_records, write_jsonl
|
| 11 |
+
from scripts.publish_hf_dataset import upload_dataset, validate_dataset_file
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class PublishHfDatasetTest(unittest.TestCase):
|
| 15 |
+
def test_validate_dataset_file_rejects_bad_assistant_json(self) -> None:
|
| 16 |
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
| 17 |
+
dataset_file = Path(tmp_dir) / "bad.jsonl"
|
| 18 |
+
dataset_file.write_text(
|
| 19 |
+
json.dumps(
|
| 20 |
+
{
|
| 21 |
+
"id": "bad",
|
| 22 |
+
"messages": [
|
| 23 |
+
{"role": "system", "content": "system"},
|
| 24 |
+
{"role": "user", "content": "user"},
|
| 25 |
+
{"role": "assistant", "content": "not json"},
|
| 26 |
+
],
|
| 27 |
+
}
|
| 28 |
+
)
|
| 29 |
+
+ "\n",
|
| 30 |
+
encoding="utf-8",
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
with self.assertRaises(ValueError):
|
| 34 |
+
validate_dataset_file(dataset_file)
|
| 35 |
+
|
| 36 |
+
def test_upload_dataset_dry_run_returns_file_summary(self) -> None:
|
| 37 |
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
| 38 |
+
dataset_file = Path(tmp_dir) / "curated_v2.jsonl"
|
| 39 |
+
write_jsonl(build_curated_records(5, version="v2"), dataset_file)
|
| 40 |
+
|
| 41 |
+
summary = upload_dataset(
|
| 42 |
+
dataset_file=dataset_file,
|
| 43 |
+
repo_id="qqyule/objectverse-diary-sft-curated",
|
| 44 |
+
path_in_repo="objectverse_sft_curated_v2.jsonl",
|
| 45 |
+
private=False,
|
| 46 |
+
commit_message="Dry run",
|
| 47 |
+
dry_run=True,
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
self.assertFalse(summary["uploaded"])
|
| 51 |
+
self.assertEqual(summary["repo_id"], "qqyule/objectverse-diary-sft-curated")
|
| 52 |
+
self.assertEqual(summary["path_in_repo"], "objectverse_sft_curated_v2.jsonl")
|
| 53 |
+
self.assertEqual(summary["record_count"], 5)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
if __name__ == "__main__":
|
| 57 |
+
unittest.main()
|
tests/test_publish_hf_gguf.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for Hugging Face GGUF publishing helpers."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import tempfile
|
| 6 |
+
import unittest
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
from scripts.publish_hf_gguf import upload_gguf, validate_gguf_file
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class PublishHfGgufTest(unittest.TestCase):
|
| 13 |
+
def test_validate_gguf_file_requires_gguf_suffix(self) -> None:
|
| 14 |
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
| 15 |
+
model_file = Path(tmp_dir) / "model.bin"
|
| 16 |
+
model_file.write_text("fake", encoding="utf-8")
|
| 17 |
+
|
| 18 |
+
with self.assertRaises(ValueError):
|
| 19 |
+
validate_gguf_file(model_file)
|
| 20 |
+
|
| 21 |
+
def test_upload_gguf_dry_run_returns_file_summary(self) -> None:
|
| 22 |
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
| 23 |
+
gguf_file = Path(tmp_dir) / "model.gguf"
|
| 24 |
+
gguf_file.write_bytes(b"fake-gguf")
|
| 25 |
+
|
| 26 |
+
summary = upload_gguf(
|
| 27 |
+
gguf_file=gguf_file,
|
| 28 |
+
repo_id="qqyule/objectverse-diary-qwen15b-lora",
|
| 29 |
+
path_in_repo="objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf",
|
| 30 |
+
private=False,
|
| 31 |
+
commit_message="Dry run",
|
| 32 |
+
dry_run=True,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
self.assertFalse(summary["uploaded"])
|
| 36 |
+
self.assertEqual(summary["repo_id"], "qqyule/objectverse-diary-qwen15b-lora")
|
| 37 |
+
self.assertEqual(
|
| 38 |
+
summary["path_in_repo"],
|
| 39 |
+
"objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf",
|
| 40 |
+
)
|
| 41 |
+
self.assertEqual(summary["size_bytes"], 9)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
if __name__ == "__main__":
|
| 45 |
+
unittest.main()
|