Spaces:
Running
Running
josesalazar2025 commited on
Commit ·
d90e9a6
1
Parent(s): 06f94a3
Add Spanish explanatory comments across JS modules and PHP proxy
Browse files- README.md +1 -33
- SKILL.md +0 -205
- USO_DE_IA.md +7 -2
- api/conexion.php +2 -4
- api/hf_proxy.php +4 -3
- js/analisis.js +6 -0
- js/auth.js +4 -1
- js/ia.js +10 -8
- js/main.js +6 -0
- js/papers.js +3 -0
- js/pdf-parser.js +12 -11
- js/ui.js +8 -1
README.md
CHANGED
|
@@ -164,38 +164,6 @@ O bien con el servidor integrado de PHP desde la raiz del proyecto:
|
|
| 164 |
php -S localhost:8000
|
| 165 |
```
|
| 166 |
|
| 167 |
-
### 5. Despliegue con Docker (HuggingFace Spaces)
|
| 168 |
-
|
| 169 |
-
El proyecto incluye un `Dockerfile` basado en `php:8.2-apache` configurado para ejecutarse en HuggingFace Spaces en el puerto `7860`.
|
| 170 |
-
|
| 171 |
-
**Construir y ejecutar localmente:**
|
| 172 |
-
|
| 173 |
-
```bash
|
| 174 |
-
docker build -t morphos .
|
| 175 |
-
docker run -p 7860:7860 morphos
|
| 176 |
-
```
|
| 177 |
-
|
| 178 |
-
Abrir en el navegador: `http://localhost:7860`
|
| 179 |
-
|
| 180 |
-
**Variables de entorno en Docker:**
|
| 181 |
-
|
| 182 |
-
La API key de HuggingFace debe estar en `api/.env` antes de construir la imagen:
|
| 183 |
-
|
| 184 |
-
```text
|
| 185 |
-
HF_API_KEY=tu_clave_de_huggingface
|
| 186 |
-
```
|
| 187 |
-
|
| 188 |
-
> En HuggingFace Spaces también puede configurarse como secreto desde *Settings → Repository secrets* para no exponerla en el repositorio.
|
| 189 |
-
|
| 190 |
-
**Notas del entorno Docker:**
|
| 191 |
-
|
| 192 |
-
* Apache escucha en el puerto `7860` (configurado vía `sed` sobre `ports.conf` y `000-default.conf`)
|
| 193 |
-
* El script `docker-entrypoint.sh` inicializa la base de datos SQLite en `/var/www/html/data/morphos.db` al arrancar el contenedor
|
| 194 |
-
* Los datos escritos en SQLite durante la ejecución **no persisten** entre reinicios del contenedor a menos que se monte un volumen externo
|
| 195 |
-
* Si el Space queda en estado de error por un proceso Apache colgado, usar **Factory reboot** desde el menú del Space en HuggingFace
|
| 196 |
-
|
| 197 |
-
---
|
| 198 |
-
|
| 199 |
## Backend de IA
|
| 200 |
|
| 201 |
El modelo de IA se configura desde la propia interfaz. La seleccion se guarda en `localStorage`.
|
|
@@ -259,7 +227,7 @@ La gravedad se calcula como la desviacion relativa al ancho del rango de referen
|
|
| 259 |
## Retos
|
| 260 |
|
| 261 |
* Por la diversidad de unidades de medición que utilizan los diferentes fabricantes de equipos de laboratorio se incorporó una detección de unidades para su conversión y normalización
|
| 262 |
-
* El modelado del output de la I.A requirió muchísimas iteraciones de formateo del prompt y harness
|
| 263 |
* Inicialmente quería usar proveedores de inferencia gratuita de medGemma (como featherless AI) pero fallaban continuamente, por eso decidí optar por hostear al modelo en Zero GPU de HF con la subscripción pro para la prueba de concepto
|
| 264 |
* Incluir las librería de parseo de pdf y las fuentes en el directorio del proyecto con la intención de reducir dependencias externas estaba generando problemas con las métricas de velocidad de lighthouse que no lograba solucionar. Claude planteó implementación de caché en htacesss y pre carga de las fuentes, lo cual llevó la puntuación de 60 a 90/100 sin mayores cambios estructurales
|
| 265 |
* Lograr una interfaz limpia y entendible requirío de muchos intentos hasta lograr un flujo de trabajo intuitivo y accsesible con la mínima friccion posible para los usuarios
|
|
|
|
| 164 |
php -S localhost:8000
|
| 165 |
```
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
## Backend de IA
|
| 168 |
|
| 169 |
El modelo de IA se configura desde la propia interfaz. La seleccion se guarda en `localStorage`.
|
|
|
|
| 227 |
## Retos
|
| 228 |
|
| 229 |
* Por la diversidad de unidades de medición que utilizan los diferentes fabricantes de equipos de laboratorio se incorporó una detección de unidades para su conversión y normalización
|
| 230 |
+
* El modelado del output de la I.A requirió muchísimas iteraciones de formateo del prompt y harness para evitar alucinaciones o que envíara su proceso de pensamiento, aún requiere de mucho trabajo extra de refinamiento
|
| 231 |
* Inicialmente quería usar proveedores de inferencia gratuita de medGemma (como featherless AI) pero fallaban continuamente, por eso decidí optar por hostear al modelo en Zero GPU de HF con la subscripción pro para la prueba de concepto
|
| 232 |
* Incluir las librería de parseo de pdf y las fuentes en el directorio del proyecto con la intención de reducir dependencias externas estaba generando problemas con las métricas de velocidad de lighthouse que no lograba solucionar. Claude planteó implementación de caché en htacesss y pre carga de las fuentes, lo cual llevó la puntuación de 60 a 90/100 sin mayores cambios estructurales
|
| 233 |
* Lograr una interfaz limpia y entendible requirío de muchos intentos hasta lograr un flujo de trabajo intuitivo y accsesible con la mínima friccion posible para los usuarios
|
SKILL.md
DELETED
|
@@ -1,205 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
name: hf-cli
|
| 3 |
-
description: "Hugging Face Hub CLI (`hf`) for downloading, uploading, and managing models, datasets, spaces, buckets, repos, papers, jobs, and more on the Hugging Face Hub. Use when: handling authentication; managing local cache; managing Hugging Face Buckets; running or scheduling jobs on Hugging Face infrastructure; managing Hugging Face repos; discussions and pull requests; browsing models, datasets and spaces; reading, searching, or browsing academic papers; managing collections; querying datasets; configuring spaces; setting up webhooks; or deploying and managing HF Inference Endpoints. Make sure to use this skill whenever the user mentions 'hf', 'huggingface', 'Hugging Face', 'huggingface-cli', or 'hugging face cli', or wants to do anything related to the Hugging Face ecosystem and to AI and ML in general. Also use for cloud storage needs like training checkpoints, data pipelines, or agent traces. Use even if the user doesn't explicitly ask for a CLI command. Replaces the deprecated `huggingface-cli`."
|
| 4 |
-
---
|
| 5 |
-
|
| 6 |
-
Install: `curl -LsSf https://hf.co/cli/install.sh | bash -s`.
|
| 7 |
-
|
| 8 |
-
The Hugging Face Hub CLI tool `hf` is available. IMPORTANT: The `hf` command replaces the deprecated `huggingface-cli` command.
|
| 9 |
-
|
| 10 |
-
Use `hf --help` to view available functions. Note that auth commands are now all under `hf auth` e.g. `hf auth whoami`.
|
| 11 |
-
|
| 12 |
-
Generated with `huggingface_hub v1.13.0`. Run `hf skills add --force` to regenerate.
|
| 13 |
-
|
| 14 |
-
## Commands
|
| 15 |
-
|
| 16 |
-
- `hf download REPO_ID` — Download files from the Hub. `[--type CHOICE --revision TEXT --include TEXT --exclude TEXT --cache-dir TEXT --local-dir TEXT --force-download --dry-run --max-workers INTEGER --format CHOICE]`
|
| 17 |
-
- `hf env` — Print information about the environment. `[--format CHOICE]`
|
| 18 |
-
- `hf sync` — Sync files between local directory and a bucket. `[--delete --ignore-times --ignore-sizes --plan TEXT --apply TEXT --dry-run --include TEXT --exclude TEXT --filter-from TEXT --existing --ignore-existing --verbose --format CHOICE]`
|
| 19 |
-
- `hf update` — Update the `hf` CLI to the latest version. `[--format CHOICE]`
|
| 20 |
-
- `hf upload REPO_ID` — Upload a file or a folder to the Hub. Recommended for single-commit uploads. `[--type CHOICE --revision TEXT --private --include TEXT --exclude TEXT --delete TEXT --commit-message TEXT --commit-description TEXT --create-pr --every FLOAT --format CHOICE]`
|
| 21 |
-
- `hf upload-large-folder REPO_ID LOCAL_PATH` — Upload a large folder to the Hub. Recommended for resumable uploads. `[--type CHOICE --revision TEXT --private --include TEXT --exclude TEXT --num-workers INTEGER --no-report --no-bars --format CHOICE]`
|
| 22 |
-
- `hf version` — Print information about the hf version. `[--format CHOICE]`
|
| 23 |
-
|
| 24 |
-
### `hf auth` — Manage authentication (login, logout, etc.).
|
| 25 |
-
|
| 26 |
-
- `hf auth list` — List all stored access tokens. `[--format CHOICE]`
|
| 27 |
-
- `hf auth login` — Login using a token from huggingface.co/settings/tokens. `[--add-to-git-credential --force --format CHOICE]`
|
| 28 |
-
- `hf auth logout` — Logout from a specific token. `[--token-name TEXT --format CHOICE]`
|
| 29 |
-
- `hf auth switch` — Switch between access tokens. `[--token-name TEXT --add-to-git-credential --format CHOICE]`
|
| 30 |
-
- `hf auth token` — Print the current access token to stdout. `[--format CHOICE]`
|
| 31 |
-
- `hf auth whoami` — Find out which huggingface.co account you are logged in as. `[--format CHOICE]`
|
| 32 |
-
|
| 33 |
-
### `hf buckets` — Commands to interact with buckets.
|
| 34 |
-
|
| 35 |
-
- `hf buckets cp SRC` — Copy files to or from buckets. `[--format CHOICE]`
|
| 36 |
-
- `hf buckets create BUCKET_ID` — Create a new bucket. `[--private --exist-ok --format CHOICE]`
|
| 37 |
-
- `hf buckets delete BUCKET_ID` — Delete a bucket. `[--yes --missing-ok --format CHOICE]`
|
| 38 |
-
- `hf buckets info BUCKET_ID` — Get info about a bucket. `[--format CHOICE]`
|
| 39 |
-
- `hf buckets list` — List buckets or files in a bucket. `[--human-readable --tree --recursive --search TEXT --format CHOICE]`
|
| 40 |
-
- `hf buckets move FROM_ID TO_ID` — Move (rename) a bucket to a new name or namespace. `[--format CHOICE]`
|
| 41 |
-
- `hf buckets remove ARGUMENT` — Remove files from a bucket. `[--recursive --yes --dry-run --include TEXT --exclude TEXT --format CHOICE]`
|
| 42 |
-
- `hf buckets sync` — Sync files between local directory and a bucket. `[--delete --ignore-times --ignore-sizes --plan TEXT --apply TEXT --dry-run --include TEXT --exclude TEXT --filter-from TEXT --existing --ignore-existing --verbose --format CHOICE]`
|
| 43 |
-
|
| 44 |
-
### `hf cache` — Manage local cache directory.
|
| 45 |
-
|
| 46 |
-
- `hf cache list` — List cached repositories or revisions. `[--cache-dir TEXT --revisions --filter TEXT --sort CHOICE --limit INTEGER --format CHOICE]`
|
| 47 |
-
- `hf cache prune` — Remove detached revisions from the cache. `[--cache-dir TEXT --yes --dry-run --format CHOICE]`
|
| 48 |
-
- `hf cache rm TARGETS` — Remove cached repositories or revisions. `[--cache-dir TEXT --yes --dry-run --format CHOICE]`
|
| 49 |
-
- `hf cache verify REPO_ID` — Verify checksums for a single repo revision from cache or a local directory. `[--type CHOICE --revision TEXT --cache-dir TEXT --local-dir TEXT --fail-on-missing-files --fail-on-extra-files --format CHOICE]`
|
| 50 |
-
|
| 51 |
-
### `hf collections` — Interact with collections on the Hub.
|
| 52 |
-
|
| 53 |
-
- `hf collections add-item COLLECTION_SLUG ITEM_ID ITEM_TYPE` — Add an item to a collection. `[--note TEXT --exists-ok --format CHOICE]`
|
| 54 |
-
- `hf collections create TITLE` — Create a new collection on the Hub. `[--namespace TEXT --description TEXT --private --exists-ok --format CHOICE]`
|
| 55 |
-
- `hf collections delete COLLECTION_SLUG` — Delete a collection from the Hub. `[--missing-ok --format CHOICE]`
|
| 56 |
-
- `hf collections delete-item COLLECTION_SLUG ITEM_OBJECT_ID` — Delete an item from a collection. `[--missing-ok --format CHOICE]`
|
| 57 |
-
- `hf collections info COLLECTION_SLUG` — Get info about a collection on the Hub. `[--format CHOICE]`
|
| 58 |
-
- `hf collections list` — List collections on the Hub. `[--owner TEXT --item TEXT --sort CHOICE --limit INTEGER --format CHOICE]`
|
| 59 |
-
- `hf collections update COLLECTION_SLUG` — Update a collection's metadata on the Hub. `[--title TEXT --description TEXT --position INTEGER --private --theme TEXT --format CHOICE]`
|
| 60 |
-
- `hf collections update-item COLLECTION_SLUG ITEM_OBJECT_ID` — Update an item in a collection. `[--note TEXT --position INTEGER --format CHOICE]`
|
| 61 |
-
|
| 62 |
-
### `hf datasets` — Interact with datasets on the Hub.
|
| 63 |
-
|
| 64 |
-
- `hf datasets card DATASET_ID` — Get the dataset card (README) for a dataset on the Hub. `[--metadata --text --format CHOICE]`
|
| 65 |
-
- `hf datasets info DATASET_ID` — Get info about a dataset on the Hub. `[--revision TEXT --expand TEXT --format CHOICE]`
|
| 66 |
-
- `hf datasets leaderboard DATASET_ID` — List model scores from a dataset leaderboard. This command helps find the best models for a task or compare models by benchmark scores. `[--limit INTEGER --format CHOICE]`
|
| 67 |
-
- `hf datasets list` — List datasets on the Hub, or files in a dataset repo. `[--search TEXT --author TEXT --filter TEXT --sort CHOICE --limit INTEGER --expand TEXT --human-readable --tree --recursive --revision TEXT --format CHOICE]`
|
| 68 |
-
- `hf datasets parquet DATASET_ID` — List parquet file URLs available for a dataset. `[--subset TEXT --split TEXT --format CHOICE]`
|
| 69 |
-
- `hf datasets sql SQL` — Execute a raw SQL query with DuckDB against dataset parquet URLs. `[--format CHOICE]`
|
| 70 |
-
|
| 71 |
-
### `hf discussions` — Manage discussions and pull requests on the Hub.
|
| 72 |
-
|
| 73 |
-
- `hf discussions close REPO_ID NUM` — Close a discussion or pull request. `[--comment TEXT --yes --type CHOICE --format CHOICE]`
|
| 74 |
-
- `hf discussions comment REPO_ID NUM` — Comment on a discussion or pull request. `[--body TEXT --body-file PATH --type CHOICE --format CHOICE]`
|
| 75 |
-
- `hf discussions create REPO_ID --title TEXT` — Create a new discussion or pull request on a repo. `[--body TEXT --body-file PATH --pull-request --type CHOICE --format CHOICE]`
|
| 76 |
-
- `hf discussions diff REPO_ID NUM` — Show the diff of a pull request. `[--type CHOICE --format CHOICE]`
|
| 77 |
-
- `hf discussions info REPO_ID NUM` — Get info about a discussion or pull request. `[--type CHOICE --format CHOICE]`
|
| 78 |
-
- `hf discussions list REPO_ID` — List discussions and pull requests on a repo. `[--status CHOICE --kind CHOICE --author TEXT --limit INTEGER --type CHOICE --format CHOICE]`
|
| 79 |
-
- `hf discussions merge REPO_ID NUM` — Merge a pull request. `[--comment TEXT --yes --type CHOICE --format CHOICE]`
|
| 80 |
-
- `hf discussions rename REPO_ID NUM NEW_TITLE` — Rename a discussion or pull request. `[--type CHOICE --format CHOICE]`
|
| 81 |
-
- `hf discussions reopen REPO_ID NUM` — Reopen a closed discussion or pull request. `[--comment TEXT --yes --type CHOICE --format CHOICE]`
|
| 82 |
-
|
| 83 |
-
### `hf endpoints` — Manage Hugging Face Inference Endpoints.
|
| 84 |
-
|
| 85 |
-
- `hf endpoints catalog deploy --repo TEXT` — Deploy an Inference Endpoint from the Model Catalog. `[--name TEXT --accelerator TEXT --namespace TEXT --format CHOICE]`
|
| 86 |
-
- `hf endpoints catalog list` — List available Catalog models. `[--format CHOICE]`
|
| 87 |
-
- `hf endpoints delete NAME` — Delete an Inference Endpoint permanently. `[--namespace TEXT --yes --format CHOICE]`
|
| 88 |
-
- `hf endpoints deploy NAME --repo TEXT --framework TEXT --accelerator TEXT --instance-size TEXT --instance-type TEXT --region TEXT --vendor TEXT` — Deploy an Inference Endpoint from a Hub repository. `[--namespace TEXT --task TEXT --min-replica INTEGER --max-replica INTEGER --scale-to-zero-timeout INTEGER --scaling-metric CHOICE --scaling-threshold FLOAT --format CHOICE]`
|
| 89 |
-
- `hf endpoints describe NAME` — Get information about an existing endpoint. `[--namespace TEXT --format CHOICE]`
|
| 90 |
-
- `hf endpoints list` — Lists all Inference Endpoints for the given namespace. `[--namespace TEXT --format CHOICE]`
|
| 91 |
-
- `hf endpoints pause NAME` — Pause an Inference Endpoint. `[--namespace TEXT --format CHOICE]`
|
| 92 |
-
- `hf endpoints resume NAME` — Resume an Inference Endpoint. `[--namespace TEXT --fail-if-already-running --format CHOICE]`
|
| 93 |
-
- `hf endpoints scale-to-zero NAME` — Scale an Inference Endpoint to zero. `[--namespace TEXT --format CHOICE]`
|
| 94 |
-
- `hf endpoints update NAME` — Update an existing endpoint. `[--namespace TEXT --repo TEXT --accelerator TEXT --instance-size TEXT --instance-type TEXT --framework TEXT --revision TEXT --task TEXT --min-replica INTEGER --max-replica INTEGER --scale-to-zero-timeout INTEGER --scaling-metric CHOICE --scaling-threshold FLOAT --format CHOICE]`
|
| 95 |
-
|
| 96 |
-
### `hf extensions` — Manage hf CLI extensions.
|
| 97 |
-
|
| 98 |
-
- `hf extensions exec NAME` — Execute an installed extension.
|
| 99 |
-
- `hf extensions install REPO_ID` — Install an extension from a public GitHub repository. `[--force --format CHOICE]`
|
| 100 |
-
- `hf extensions list` — List installed extension commands. `[--format CHOICE]`
|
| 101 |
-
- `hf extensions remove NAME` — Remove an installed extension. `[--format CHOICE]`
|
| 102 |
-
- `hf extensions search` — Search extensions available on GitHub (tagged with 'hf-extension' topic). `[--format CHOICE]`
|
| 103 |
-
|
| 104 |
-
### `hf jobs` — Run and manage Jobs on the Hub.
|
| 105 |
-
|
| 106 |
-
- `hf jobs cancel JOB_ID` — Cancel a Job `[--namespace TEXT --format CHOICE]`
|
| 107 |
-
- `hf jobs hardware` — List available hardware options for Jobs `[--format CHOICE]`
|
| 108 |
-
- `hf jobs inspect JOB_IDS` — Display detailed information on one or more Jobs `[--namespace TEXT --format CHOICE]`
|
| 109 |
-
- `hf jobs logs JOB_ID` — Fetch the logs of a Job. `[--follow --tail INTEGER --namespace TEXT --format CHOICE]`
|
| 110 |
-
- `hf jobs ps` — List Jobs. `[--all --namespace TEXT --filter TEXT --format TEXT --quiet]`
|
| 111 |
-
- `hf jobs run IMAGE COMMAND` — Run a Job. `[--env TEXT --secrets TEXT --label TEXT --volume TEXT --env-file TEXT --secrets-file TEXT --flavor CHOICE --timeout TEXT --detach --namespace TEXT]`
|
| 112 |
-
- `hf jobs scheduled delete SCHEDULED_JOB_ID` — Delete a scheduled Job. `[--namespace TEXT --format CHOICE]`
|
| 113 |
-
- `hf jobs scheduled inspect SCHEDULED_JOB_IDS` — Display detailed information on one or more scheduled Jobs `[--namespace TEXT --format CHOICE]`
|
| 114 |
-
- `hf jobs scheduled ps` — List scheduled Jobs `[--all --namespace TEXT --filter TEXT --format TEXT --quiet]`
|
| 115 |
-
- `hf jobs scheduled resume SCHEDULED_JOB_ID` — Resume (unpause) a scheduled Job. `[--namespace TEXT --format CHOICE]`
|
| 116 |
-
- `hf jobs scheduled run SCHEDULE IMAGE COMMAND` — Schedule a Job. `[--suspend --concurrency --env TEXT --secrets TEXT --label TEXT --volume TEXT --env-file TEXT --secrets-file TEXT --flavor CHOICE --timeout TEXT --namespace TEXT]`
|
| 117 |
-
- `hf jobs scheduled suspend SCHEDULED_JOB_ID` — Suspend (pause) a scheduled Job. `[--namespace TEXT --format CHOICE]`
|
| 118 |
-
- `hf jobs scheduled uv run SCHEDULE SCRIPT` — Run a UV script (local file or URL) on HF infrastructure `[--suspend --concurrency --image TEXT --flavor CHOICE --env TEXT --secrets TEXT --label TEXT --volume TEXT --env-file TEXT --secrets-file TEXT --timeout TEXT --namespace TEXT --with TEXT --python TEXT]`
|
| 119 |
-
- `hf jobs stats` — Fetch the resource usage statistics and metrics of Jobs `[--namespace TEXT --format CHOICE]`
|
| 120 |
-
- `hf jobs uv run SCRIPT` — Run a UV script (local file or URL) on HF infrastructure `[--image TEXT --flavor CHOICE --env TEXT --secrets TEXT --label TEXT --volume TEXT --env-file TEXT --secrets-file TEXT --timeout TEXT --detach --namespace TEXT --with TEXT --python TEXT]`
|
| 121 |
-
|
| 122 |
-
### `hf models` — Interact with models on the Hub.
|
| 123 |
-
|
| 124 |
-
- `hf models card MODEL_ID` — Get the model card (README) for a model on the Hub. `[--metadata --text --format CHOICE]`
|
| 125 |
-
- `hf models info MODEL_ID` — Get info about a model on the Hub. `[--revision TEXT --expand TEXT --format CHOICE]`
|
| 126 |
-
- `hf models list` — List models on the Hub, or files in a model repo. `[--search TEXT --author TEXT --filter TEXT --num-parameters TEXT --sort CHOICE --limit INTEGER --expand TEXT --human-readable --tree --recursive --revision TEXT --format CHOICE]`
|
| 127 |
-
|
| 128 |
-
### `hf papers` — Interact with papers on the Hub.
|
| 129 |
-
|
| 130 |
-
- `hf papers info PAPER_ID` — Get info about a paper on the Hub. `[--format CHOICE]`
|
| 131 |
-
- `hf papers list` — List daily papers on the Hub. `[--date TEXT --week TEXT --month TEXT --submitter TEXT --sort CHOICE --limit INTEGER --format CHOICE]`
|
| 132 |
-
- `hf papers read PAPER_ID` — Read a paper as markdown. `[--format CHOICE]`
|
| 133 |
-
- `hf papers search QUERY` — Search papers on the Hub. `[--limit INTEGER --format CHOICE]`
|
| 134 |
-
|
| 135 |
-
### `hf repos` — Manage repos on the Hub.
|
| 136 |
-
|
| 137 |
-
- `hf repos branch create REPO_ID BRANCH` — Create a new branch for a repo on the Hub. `[--revision TEXT --type CHOICE --exist-ok --format CHOICE]`
|
| 138 |
-
- `hf repos branch delete REPO_ID BRANCH` — Delete a branch from a repo on the Hub. `[--type CHOICE --format CHOICE]`
|
| 139 |
-
- `hf repos create REPO_ID` — Create a new repo on the Hub. `[--type CHOICE --space-sdk TEXT --private --public --protected --exist-ok --resource-group-id TEXT --flavor CHOICE --storage CHOICE --sleep-time INTEGER --secrets TEXT --secrets-file TEXT --env TEXT --env-file TEXT --volume TEXT --format CHOICE]`
|
| 140 |
-
- `hf repos delete REPO_ID` — Delete a repo from the Hub. This is an irreversible operation. `[--type CHOICE --missing-ok --yes --format CHOICE]`
|
| 141 |
-
- `hf repos delete-files REPO_ID PATTERNS` — Delete files from a repo on the Hub. `[--type CHOICE --revision TEXT --commit-message TEXT --commit-description TEXT --create-pr --format CHOICE]`
|
| 142 |
-
- `hf repos duplicate FROM_ID` — Duplicate a repo on the Hub (model, dataset, or Space). `[--type CHOICE --private --public --protected --exist-ok --flavor CHOICE --storage CHOICE --sleep-time INTEGER --secrets TEXT --secrets-file TEXT --env TEXT --env-file TEXT --volume TEXT --format CHOICE]`
|
| 143 |
-
- `hf repos move FROM_ID TO_ID` — Move a repository from a namespace to another namespace. `[--type CHOICE --format CHOICE]`
|
| 144 |
-
- `hf repos settings REPO_ID` — Update the settings of a repository. `[--gated CHOICE --private --public --protected --type CHOICE --format CHOICE]`
|
| 145 |
-
- `hf repos tag create REPO_ID TAG` — Create a tag for a repo. `[--message TEXT --revision TEXT --type CHOICE --format CHOICE]`
|
| 146 |
-
- `hf repos tag delete REPO_ID TAG` — Delete a tag for a repo. `[--yes --type CHOICE --format CHOICE]`
|
| 147 |
-
- `hf repos tag list REPO_ID` — List tags for a repo. `[--type CHOICE --format CHOICE]`
|
| 148 |
-
|
| 149 |
-
### `hf skills` — Manage skills for AI assistants.
|
| 150 |
-
|
| 151 |
-
- `hf skills add` — Download a Hugging Face skill and install it for an AI assistant. `[--claude --global --dest PATH --force --format CHOICE]`
|
| 152 |
-
- `hf skills preview` — Print the generated `hf-cli` SKILL.md to stdout. `[--format CHOICE]`
|
| 153 |
-
- `hf skills upgrade` — Upgrade installed Hugging Face marketplace skills. `[--claude --global --dest PATH --format CHOICE]`
|
| 154 |
-
|
| 155 |
-
### `hf spaces` — Interact with spaces on the Hub.
|
| 156 |
-
|
| 157 |
-
- `hf spaces card SPACE_ID` — Get the Space card (README) for a Space on the Hub. `[--metadata --text --format CHOICE]`
|
| 158 |
-
- `hf spaces dev-mode SPACE_ID` — Enable or disable dev mode on a Space. `[--stop --format CHOICE]`
|
| 159 |
-
- `hf spaces hardware` — List available hardware options for Spaces. `[--format CHOICE]`
|
| 160 |
-
- `hf spaces hot-reload SPACE_ID` — Hot-reload any Python file of a Space without a full rebuild + restart. `[--local-file PATH --skip-checks --skip-summary --format CHOICE]`
|
| 161 |
-
- `hf spaces info SPACE_ID` — Get info about a space on the Hub. `[--revision TEXT --expand TEXT --format CHOICE]`
|
| 162 |
-
- `hf spaces list` — List spaces on the Hub, or files in a space repo. `[--search TEXT --author TEXT --filter TEXT --sort CHOICE --limit INTEGER --expand TEXT --human-readable --tree --recursive --revision TEXT --format CHOICE]`
|
| 163 |
-
- `hf spaces logs SPACE_ID` — Fetch the run or build logs of a Space. `[--build --follow --tail INTEGER --format CHOICE]`
|
| 164 |
-
- `hf spaces pause SPACE_ID` — Pause a Space. `[--format CHOICE]`
|
| 165 |
-
- `hf spaces restart SPACE_ID` — Restart a Space. `[--factory-reboot --format CHOICE]`
|
| 166 |
-
- `hf spaces search QUERY` — Search spaces on the Hub using semantic search. `[--filter TEXT --sdk TEXT --include-non-running --description --limit INTEGER --format CHOICE]`
|
| 167 |
-
- `hf spaces settings SPACE_ID` — Update the settings of a Space. `[--sleep-time INTEGER --hardware CHOICE --format CHOICE]`
|
| 168 |
-
- `hf spaces volumes delete SPACE_ID` — Remove all volumes from a Space. `[--yes --format CHOICE]`
|
| 169 |
-
- `hf spaces volumes list SPACE_ID` — List volumes mounted in a Space. `[--format CHOICE]`
|
| 170 |
-
- `hf spaces volumes set SPACE_ID` — Set (replace) volumes for a Space. `[--volume TEXT --format CHOICE]`
|
| 171 |
-
|
| 172 |
-
### `hf webhooks` — Manage webhooks on the Hub.
|
| 173 |
-
|
| 174 |
-
- `hf webhooks create --watch TEXT` — Create a new webhook. `[--url TEXT --job-id TEXT --domain CHOICE --secret TEXT --format CHOICE]`
|
| 175 |
-
- `hf webhooks delete WEBHOOK_ID` — Delete a webhook permanently. `[--yes --format CHOICE]`
|
| 176 |
-
- `hf webhooks disable WEBHOOK_ID` — Disable an active webhook. `[--format CHOICE]`
|
| 177 |
-
- `hf webhooks enable WEBHOOK_ID` — Enable a disabled webhook. `[--format CHOICE]`
|
| 178 |
-
- `hf webhooks info WEBHOOK_ID` — Show full details for a single webhook. `[--format CHOICE]`
|
| 179 |
-
- `hf webhooks list` — List all webhooks for the current user. `[--format CHOICE]`
|
| 180 |
-
- `hf webhooks update WEBHOOK_ID` — Update an existing webhook. Only provided options are changed. `[--url TEXT --watch TEXT --domain CHOICE --secret TEXT --format CHOICE]`
|
| 181 |
-
|
| 182 |
-
## Common options
|
| 183 |
-
|
| 184 |
-
- `--format` — Output format: `--format json` (or `--json`) or `--format table` (default).
|
| 185 |
-
- `-q / --quiet` — Print only IDs (one per line).
|
| 186 |
-
- `--revision` — Git revision id which can be a branch name, a tag, or a commit hash.
|
| 187 |
-
- `--token` — Use a User Access Token. Prefer setting `HF_TOKEN` env var instead of passing `--token`.
|
| 188 |
-
- `--type` — The type of repository (model, dataset, or space).
|
| 189 |
-
|
| 190 |
-
## Mounting repos as local filesystems
|
| 191 |
-
|
| 192 |
-
To mount Hub repositories or buckets as local filesystems — no download, no copy, no waiting — use `hf-mount`. Files are fetched on demand. GitHub: https://github.com/huggingface/hf-mount
|
| 193 |
-
|
| 194 |
-
Install: `curl -fsSL https://raw.githubusercontent.com/huggingface/hf-mount/main/install.sh | sh`
|
| 195 |
-
|
| 196 |
-
Some command examples:
|
| 197 |
-
- `hf-mount start repo openai-community/gpt2 /tmp/gpt2` — mount a repo (read-only)
|
| 198 |
-
- `hf-mount start --hf-token $HF_TOKEN bucket myuser/my-bucket /tmp/data` — mount a bucket (read-write)
|
| 199 |
-
- `hf-mount status` / `hf-mount stop /tmp/data` — list or unmount
|
| 200 |
-
|
| 201 |
-
## Tips
|
| 202 |
-
|
| 203 |
-
- Use `hf <command> --help` for full options, descriptions, usage, and real-world examples
|
| 204 |
-
- Authenticate with `HF_TOKEN` env var (recommended) or with `--token`
|
| 205 |
-
- Update the CLI with `hf update` (uses the correct command for the detected install method)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
USO_DE_IA.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
| 1 |
# Uso de inteligencia artificial en el desarrollo de Morphos
|
|
|
|
| 2 |
## Proyecto final — Curso de Desarrollo Web 2026
|
| 3 |
|
| 4 |
---
|
| 5 |
|
| 6 |
## Modelo utilizado
|
| 7 |
|
| 8 |
-
Se utilizó
|
| 9 |
|
| 10 |
---
|
| 11 |
|
|
@@ -21,11 +22,13 @@ El modelo actuó como ejecutor de decisiones ya tomadas, no como tomador de deci
|
|
| 21 |
|
| 22 |
Para el motor de detección de patrones (`analisis.js`) y los datos de referencia (`valores_referencia.json`, `alteraciones.json`), se proporcionó al modelo literatura especializada en patología clínica veterinaria como contexto de generación.
|
| 23 |
|
| 24 |
-
Los rangos de referencia por especie, los ajustes por edad, raza y sexo, las descripciones clínicas de cada alteración, y la lógica de clasificación de gravedad fueron **validados gracias a mi formación y experiencia profesional en Medicina Veterinaria**, antes de ser incorporados al código. El modelo fue un medio para estructurar y codificar ese conocimiento, no la fuente del mismo.
|
| 25 |
|
| 26 |
Textos de referencia utilizados:
|
|
|
|
| 27 |
- Thrall, *Veterinary Hematology and Clinical Chemistry*, 3.ª ed. 2022
|
| 28 |
- Weiss, — *Schalm's Veterinary Hematology*, 7.ª ed. 2022
|
|
|
|
| 29 |
---
|
| 30 |
|
| 31 |
## Afinamiento del prompt y control de salidas del modelo de IA
|
|
@@ -58,9 +61,11 @@ Durante las auditorías de rendimiento con Lighthouse se identificó que la carg
|
|
| 58 |
Al concluir el desarrollo se realizó una auditoría asistida por IA con los siguientes objetivos:
|
| 59 |
|
| 60 |
**Código muerto**
|
|
|
|
| 61 |
- Identificación y eliminación de exportaciones sin consumidores y código que ya no era necesario o era experimental
|
| 62 |
|
| 63 |
**Seguridad**
|
|
|
|
| 64 |
- Protección de archivos sensibles (`.env`, `setup.php`) mediante `.htaccess`
|
| 65 |
- Sanitización de datos externos de APIs con `textContent` en lugar de `innerHTML`, eliminando el riesgo de XSS
|
| 66 |
- Validación de URLs externas antes de usarlas como atributos `href`
|
|
|
|
| 1 |
# Uso de inteligencia artificial en el desarrollo de Morphos
|
| 2 |
+
|
| 3 |
## Proyecto final — Curso de Desarrollo Web 2026
|
| 4 |
|
| 5 |
---
|
| 6 |
|
| 7 |
## Modelo utilizado
|
| 8 |
|
| 9 |
+
Se utilizó Claude Code (Sonnet 4.6) de Anthropic como asistente de desarrollo a lo largo de todo el proyecto y para el despliegue, docker y debug del comportamiento del modelo de IA se utilizó Kimi 2.6 con Open Code CLI.
|
| 10 |
|
| 11 |
---
|
| 12 |
|
|
|
|
| 22 |
|
| 23 |
Para el motor de detección de patrones (`analisis.js`) y los datos de referencia (`valores_referencia.json`, `alteraciones.json`), se proporcionó al modelo literatura especializada en patología clínica veterinaria como contexto de generación.
|
| 24 |
|
| 25 |
+
Los rangos de referencia por especie, los ajustes por edad, raza y sexo, las descripciones clínicas de cada alteración, y la lógica de clasificación de gravedad fueron **validados gracias a mi formación y experiencia profesional en Medicina Veterinaria**, antes de ser incorporados al código. El modelo fue un medio para estructurar y codificar ese conocimiento, no la fuente del mismo. Se han realizado múltiples pruebas con resultados de laboratorio reales y citologías de pacientes facilitados por otros veterinarios ejercientes.
|
| 26 |
|
| 27 |
Textos de referencia utilizados:
|
| 28 |
+
|
| 29 |
- Thrall, *Veterinary Hematology and Clinical Chemistry*, 3.ª ed. 2022
|
| 30 |
- Weiss, — *Schalm's Veterinary Hematology*, 7.ª ed. 2022
|
| 31 |
+
|
| 32 |
---
|
| 33 |
|
| 34 |
## Afinamiento del prompt y control de salidas del modelo de IA
|
|
|
|
| 61 |
Al concluir el desarrollo se realizó una auditoría asistida por IA con los siguientes objetivos:
|
| 62 |
|
| 63 |
**Código muerto**
|
| 64 |
+
|
| 65 |
- Identificación y eliminación de exportaciones sin consumidores y código que ya no era necesario o era experimental
|
| 66 |
|
| 67 |
**Seguridad**
|
| 68 |
+
|
| 69 |
- Protección de archivos sensibles (`.env`, `setup.php`) mediante `.htaccess`
|
| 70 |
- Sanitización de datos externos de APIs con `textContent` en lugar de `innerHTML`, eliminando el riesgo de XSS
|
| 71 |
- Validación de URLs externas antes de usarlas como atributos `href`
|
api/conexion.php
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
<?php
|
| 2 |
|
| 3 |
-
//
|
| 4 |
$dbHost = '127.0.0.1';
|
| 5 |
$dbUsuario = 'root';
|
| 6 |
$dbClave = '';
|
|
@@ -16,7 +16,6 @@ if (file_exists(__DIR__ . '/.env')) {
|
|
| 16 |
|
| 17 |
$conexion = null;
|
| 18 |
|
| 19 |
-
// Attempt MySQL only if not explicitly forced to SQLite
|
| 20 |
$useSqlite = getenv('DB_FORCE_SQLITE') === '1';
|
| 21 |
|
| 22 |
if (!$useSqlite) {
|
|
@@ -28,13 +27,12 @@ if (!$useSqlite) {
|
|
| 28 |
}
|
| 29 |
}
|
| 30 |
|
| 31 |
-
// Fallback
|
| 32 |
if (!$conexion) {
|
| 33 |
try {
|
| 34 |
$conexion = new PDO("sqlite:$dbPath");
|
| 35 |
$conexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
| 36 |
|
| 37 |
-
// Ensure users table exists (idempotent)
|
| 38 |
$conexion->exec("CREATE TABLE IF NOT EXISTS usuarios (
|
| 39 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 40 |
nombre TEXT NOT NULL,
|
|
|
|
| 1 |
<?php
|
| 2 |
|
| 3 |
+
// Primero intenta conectar con MySQL (XAMPP), fallback a SQLite (Docker / HF Spaces)
|
| 4 |
$dbHost = '127.0.0.1';
|
| 5 |
$dbUsuario = 'root';
|
| 6 |
$dbClave = '';
|
|
|
|
| 16 |
|
| 17 |
$conexion = null;
|
| 18 |
|
|
|
|
| 19 |
$useSqlite = getenv('DB_FORCE_SQLITE') === '1';
|
| 20 |
|
| 21 |
if (!$useSqlite) {
|
|
|
|
| 27 |
}
|
| 28 |
}
|
| 29 |
|
| 30 |
+
// Fallback de SQLite
|
| 31 |
if (!$conexion) {
|
| 32 |
try {
|
| 33 |
$conexion = new PDO("sqlite:$dbPath");
|
| 34 |
$conexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
| 35 |
|
|
|
|
| 36 |
$conexion->exec("CREATE TABLE IF NOT EXISTS usuarios (
|
| 37 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 38 |
nombre TEXT NOT NULL,
|
api/hf_proxy.php
CHANGED
|
@@ -9,6 +9,7 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); exit; }
|
|
| 9 |
|
| 10 |
$hfKey = $_ENV['HF_API_KEY'] ?? $_SERVER['HF_API_KEY'] ?? getenv('HF_API_KEY') ?? '';
|
| 11 |
|
|
|
|
| 12 |
if (!$hfKey && file_exists(__DIR__ . '/.env')) {
|
| 13 |
foreach (file(__DIR__ . '/.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
| 14 |
if (str_starts_with($line, 'HF_API_KEY=')) { $hfKey = trim(substr($line, 11)); break; }
|
|
@@ -30,8 +31,6 @@ function hf_get(string $url, array $headers, ?string $post = null): array {
|
|
| 30 |
return [curl_exec($ch), curl_getinfo($ch, CURLINFO_HTTP_CODE)];
|
| 31 |
}
|
| 32 |
|
| 33 |
-
// Upload a data URL to the Gradio /upload endpoint and return a FileData object,
|
| 34 |
-
// or fall back to the raw data URL format if the upload fails.
|
| 35 |
function uploadImagen(string $space, string $hfKey, string $dataUrl): ?array {
|
| 36 |
if (!preg_match('/^data:(image\/[\w+]+);base64,(.+)$/s', $dataUrl, $m)) return null;
|
| 37 |
$mimeType = $m[1];
|
|
@@ -39,6 +38,7 @@ function uploadImagen(string $space, string $hfKey, string $dataUrl): ?array {
|
|
| 39 |
$binary = base64_decode($m[2]);
|
| 40 |
if ($binary === false) return null;
|
| 41 |
|
|
|
|
| 42 |
$boundary = bin2hex(random_bytes(16));
|
| 43 |
$body = "--$boundary\r\n"
|
| 44 |
. "Content-Disposition: form-data; name=\"files\"; filename=\"image.$ext\"\r\n"
|
|
@@ -61,8 +61,8 @@ function uploadImagen(string $space, string $hfKey, string $dataUrl): ?array {
|
|
| 61 |
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
| 62 |
curl_close($ch);
|
| 63 |
|
|
|
|
| 64 |
if ($code >= 400 || !$result) {
|
| 65 |
-
// Fall back: pass as data URL directly (older Gradio versions accept this)
|
| 66 |
return ['url' => $dataUrl, 'orig_name' => "image.$ext", 'mime_type' => $mimeType];
|
| 67 |
}
|
| 68 |
|
|
@@ -98,6 +98,7 @@ if (!$eventId) { http_response_code(502); echo json_encode(['error' => 'No se ob
|
|
| 98 |
[$stream] = hf_get("$SPACE/call/analyze/$eventId", ["Authorization: Bearer $hfKey"]);
|
| 99 |
|
| 100 |
$result = $error = null; $lastEvent = '';
|
|
|
|
| 101 |
foreach (explode("\n", $stream) as $raw) {
|
| 102 |
$line = rtrim($raw, "\r");
|
| 103 |
if (str_starts_with($line, 'event:')) $lastEvent = trim(substr($line, 6));
|
|
|
|
| 9 |
|
| 10 |
$hfKey = $_ENV['HF_API_KEY'] ?? $_SERVER['HF_API_KEY'] ?? getenv('HF_API_KEY') ?? '';
|
| 11 |
|
| 12 |
+
// Fallback a archivo .env si la variable de entorno no esta definida
|
| 13 |
if (!$hfKey && file_exists(__DIR__ . '/.env')) {
|
| 14 |
foreach (file(__DIR__ . '/.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
| 15 |
if (str_starts_with($line, 'HF_API_KEY=')) { $hfKey = trim(substr($line, 11)); break; }
|
|
|
|
| 31 |
return [curl_exec($ch), curl_getinfo($ch, CURLINFO_HTTP_CODE)];
|
| 32 |
}
|
| 33 |
|
|
|
|
|
|
|
| 34 |
function uploadImagen(string $space, string $hfKey, string $dataUrl): ?array {
|
| 35 |
if (!preg_match('/^data:(image\/[\w+]+);base64,(.+)$/s', $dataUrl, $m)) return null;
|
| 36 |
$mimeType = $m[1];
|
|
|
|
| 38 |
$binary = base64_decode($m[2]);
|
| 39 |
if ($binary === false) return null;
|
| 40 |
|
| 41 |
+
// Construye manualmente el cuerpo multipart para enviar la imagen al upload endpoint de Gradio
|
| 42 |
$boundary = bin2hex(random_bytes(16));
|
| 43 |
$body = "--$boundary\r\n"
|
| 44 |
. "Content-Disposition: form-data; name=\"files\"; filename=\"image.$ext\"\r\n"
|
|
|
|
| 61 |
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
| 62 |
curl_close($ch);
|
| 63 |
|
| 64 |
+
// Si el upload falla, devuelve la imagen inline para que el modelo la procese igualmente
|
| 65 |
if ($code >= 400 || !$result) {
|
|
|
|
| 66 |
return ['url' => $dataUrl, 'orig_name' => "image.$ext", 'mime_type' => $mimeType];
|
| 67 |
}
|
| 68 |
|
|
|
|
| 98 |
[$stream] = hf_get("$SPACE/call/analyze/$eventId", ["Authorization: Bearer $hfKey"]);
|
| 99 |
|
| 100 |
$result = $error = null; $lastEvent = '';
|
| 101 |
+
// Parsea el stream SSE de Gradio buscando el evento 'complete' o 'process_completed'
|
| 102 |
foreach (explode("\n", $stream) as $raw) {
|
| 103 |
$line = rtrim($raw, "\r");
|
| 104 |
if (str_starts_with($line, 'event:')) $lastEvent = trim(substr($line, 6));
|
js/analisis.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
| 6 |
const UMBRALES_GRAVEDAD = { leve: 0.5, moderado: 1.5 };
|
| 7 |
|
| 8 |
const clasificarGravedad = (valor, ref) => {
|
|
|
|
| 9 |
const rango = ref.superior - ref.inferior;
|
| 10 |
const desviacion = valor > ref.superior
|
| 11 |
? (valor - ref.superior) / rango
|
|
@@ -98,6 +99,7 @@ const ajustarReferencias = (refsEspecie, paciente) => {
|
|
| 98 |
const ajRaza = obtenerAjustesRaza(paciente.raza, paciente.especie);
|
| 99 |
const ajSexo = AJUSTES_SEXO[paciente.especie]?.[paciente.sexo] ?? {};
|
| 100 |
|
|
|
|
| 101 |
return Object.entries(refsEspecie).reduce((acc, [clave, ref]) => {
|
| 102 |
const factorEdad = ajEdad[clave] ?? {};
|
| 103 |
const factorRaza = ajRaza[clave] ?? {};
|
|
@@ -133,6 +135,7 @@ const detectarPatrones = (hallazgos, especie, alt) => {
|
|
| 133 |
// Serie roja
|
| 134 |
|
| 135 |
if (esBajo('hct') || esBajo('hgb') || esBajo('rbc')) {
|
|
|
|
| 136 |
const tipoPorVcm = !presente('vcm') ? '' :
|
| 137 |
esBajo('vcm') ? 'microcítica' :
|
| 138 |
esAlto('vcm') ? 'macrocítica' : 'normocítica';
|
|
@@ -160,6 +163,7 @@ const detectarPatrones = (hallazgos, especie, alt) => {
|
|
| 160 |
// Serie blanca
|
| 161 |
|
| 162 |
if (esAlto('wbc')) {
|
|
|
|
| 163 |
const neutrofilia = esAlto('neutro');
|
| 164 |
const linfocitosis = esAlto('linfo');
|
| 165 |
|
|
@@ -329,6 +333,7 @@ const detectarPatrones = (hallazgos, especie, alt) => {
|
|
| 329 |
const valSodio = valor('sodio');
|
| 330 |
const valPotasio = valor('potasio');
|
| 331 |
|
|
|
|
| 332 |
if (valSodio !== null && valPotasio !== null && valPotasio > 0) {
|
| 333 |
const ratioNaK = valSodio / valPotasio;
|
| 334 |
if (ratioNaK < 27) agregar({
|
|
@@ -461,6 +466,7 @@ export const analizarResultados = (valoresInput, paciente, referencias, alteraci
|
|
| 461 |
const refsEspecie = referencias[paciente.especie];
|
| 462 |
if (!refsEspecie) return { hallazgos: [], patrones: [] };
|
| 463 |
|
|
|
|
| 464 |
const refsAjustadas = ajustarReferencias(refsEspecie, paciente);
|
| 465 |
const hallazgos = [];
|
| 466 |
|
|
|
|
| 6 |
const UMBRALES_GRAVEDAD = { leve: 0.5, moderado: 1.5 };
|
| 7 |
|
| 8 |
const clasificarGravedad = (valor, ref) => {
|
| 9 |
+
// Mide cuantos anchos de rango de referencia se desvia el valor
|
| 10 |
const rango = ref.superior - ref.inferior;
|
| 11 |
const desviacion = valor > ref.superior
|
| 12 |
? (valor - ref.superior) / rango
|
|
|
|
| 99 |
const ajRaza = obtenerAjustesRaza(paciente.raza, paciente.especie);
|
| 100 |
const ajSexo = AJUSTES_SEXO[paciente.especie]?.[paciente.sexo] ?? {};
|
| 101 |
|
| 102 |
+
// Multiplica los limites inferiores y superiores por los factores de edad, raza y sexo
|
| 103 |
return Object.entries(refsEspecie).reduce((acc, [clave, ref]) => {
|
| 104 |
const factorEdad = ajEdad[clave] ?? {};
|
| 105 |
const factorRaza = ajRaza[clave] ?? {};
|
|
|
|
| 135 |
// Serie roja
|
| 136 |
|
| 137 |
if (esBajo('hct') || esBajo('hgb') || esBajo('rbc')) {
|
| 138 |
+
// Clasifica el tipo de anemia segun el VCM para sugerir la etiologia mas probable
|
| 139 |
const tipoPorVcm = !presente('vcm') ? '' :
|
| 140 |
esBajo('vcm') ? 'microcítica' :
|
| 141 |
esAlto('vcm') ? 'macrocítica' : 'normocítica';
|
|
|
|
| 163 |
// Serie blanca
|
| 164 |
|
| 165 |
if (esAlto('wbc')) {
|
| 166 |
+
// Diferencia leucocitosis neutrofilica de linfocitica; si no hay diferencial, informa generico
|
| 167 |
const neutrofilia = esAlto('neutro');
|
| 168 |
const linfocitosis = esAlto('linfo');
|
| 169 |
|
|
|
|
| 333 |
const valSodio = valor('sodio');
|
| 334 |
const valPotasio = valor('potasio');
|
| 335 |
|
| 336 |
+
// Ratio Na/K < 27 es sugestivo de hipoadrenocorticismo; la gravedad aumenta a menor ratio
|
| 337 |
if (valSodio !== null && valPotasio !== null && valPotasio > 0) {
|
| 338 |
const ratioNaK = valSodio / valPotasio;
|
| 339 |
if (ratioNaK < 27) agregar({
|
|
|
|
| 466 |
const refsEspecie = referencias[paciente.especie];
|
| 467 |
if (!refsEspecie) return { hallazgos: [], patrones: [] };
|
| 468 |
|
| 469 |
+
// Ajusta los rangos segun edad, raza y sexo antes de comparar
|
| 470 |
const refsAjustadas = ajustarReferencias(refsEspecie, paciente);
|
| 471 |
const hallazgos = [];
|
| 472 |
|
js/auth.js
CHANGED
|
@@ -103,6 +103,7 @@ function limpiarCampo(input) {
|
|
| 103 |
}
|
| 104 |
|
| 105 |
function activarValidacionCampo(input, reglaDeFalso) {
|
|
|
|
| 106 |
let tocado = false;
|
| 107 |
input.addEventListener('blur', () => { tocado = true; marcarCampo(input, !reglaDeFalso()); });
|
| 108 |
input.addEventListener('input', () => { if (tocado) marcarCampo(input, !reglaDeFalso()); });
|
|
@@ -127,7 +128,7 @@ function inicializarValidacionRegistro() {
|
|
| 127 |
activarValidacionCampo(email, () => !esEmailValido(email.value));
|
| 128 |
activarValidacionCampo(password, () => password.value.length < 6);
|
| 129 |
|
| 130 |
-
// password2 depende del valor de password, se re-
|
| 131 |
let tocadoP2 = false;
|
| 132 |
const validarP2 = () => password.value === password2.value && password2.value.length > 0;
|
| 133 |
password2.addEventListener('blur', () => { tocadoP2 = true; marcarCampo(password2, validarP2()); });
|
|
@@ -173,6 +174,7 @@ formLogin.addEventListener('submit', async e => {
|
|
| 173 |
estadoAuth = true;
|
| 174 |
actualizarBtnUsuario(datos.nombre);
|
| 175 |
cerrarModal();
|
|
|
|
| 176 |
accionPendiente?.();
|
| 177 |
accionPendiente = null;
|
| 178 |
});
|
|
@@ -216,6 +218,7 @@ formRegistro.addEventListener('submit', async e => {
|
|
| 216 |
estadoAuth = true;
|
| 217 |
actualizarBtnUsuario(datos.nombre);
|
| 218 |
cerrarModal();
|
|
|
|
| 219 |
accionPendiente?.();
|
| 220 |
accionPendiente = null;
|
| 221 |
});
|
|
|
|
| 103 |
}
|
| 104 |
|
| 105 |
function activarValidacionCampo(input, reglaDeFalso) {
|
| 106 |
+
// Solo marca despues del primer blur para no agobiar al usuario mientras escribe
|
| 107 |
let tocado = false;
|
| 108 |
input.addEventListener('blur', () => { tocado = true; marcarCampo(input, !reglaDeFalso()); });
|
| 109 |
input.addEventListener('input', () => { if (tocado) marcarCampo(input, !reglaDeFalso()); });
|
|
|
|
| 128 |
activarValidacionCampo(email, () => !esEmailValido(email.value));
|
| 129 |
activarValidacionCampo(password, () => password.value.length < 6);
|
| 130 |
|
| 131 |
+
// password2 depende del valor de password, se re-evalua en ambos campos para dar feedback inmediato
|
| 132 |
let tocadoP2 = false;
|
| 133 |
const validarP2 = () => password.value === password2.value && password2.value.length > 0;
|
| 134 |
password2.addEventListener('blur', () => { tocadoP2 = true; marcarCampo(password2, validarP2()); });
|
|
|
|
| 174 |
estadoAuth = true;
|
| 175 |
actualizarBtnUsuario(datos.nombre);
|
| 176 |
cerrarModal();
|
| 177 |
+
// Ejecuta la accion que el usuario intento hacer antes de loguearse (ej. analisis IA)
|
| 178 |
accionPendiente?.();
|
| 179 |
accionPendiente = null;
|
| 180 |
});
|
|
|
|
| 218 |
estadoAuth = true;
|
| 219 |
actualizarBtnUsuario(datos.nombre);
|
| 220 |
cerrarModal();
|
| 221 |
+
// Ejecuta la accion que el usuario intento hacer antes de registrarse (ej. analisis IA)
|
| 222 |
accionPendiente?.();
|
| 223 |
accionPendiente = null;
|
| 224 |
});
|
js/ia.js
CHANGED
|
@@ -74,6 +74,7 @@ function construirPrompt(obtenerDatosPaciente, obtenerValoresFormulario, getUlti
|
|
| 74 |
? hallazgos.map(h => ` ${h.nombre}: ${h.valor} ${h.unidad || ''} (${h.direccion} · ${h.gravedad})`).join('\n')
|
| 75 |
: ' Todos los valores normales';
|
| 76 |
|
|
|
|
| 77 |
return `Eres médico veterinario especialista en patología clínica.
|
| 78 |
|
| 79 |
Paciente: ${paciente.especie || 'desconocido'}, ${paciente.raza || 'raza desconocida'}, ${edadTexto}, ${paciente.sexo || 'sexo desconocido'}
|
|
@@ -105,6 +106,7 @@ Proporciona una interpretación clínica breve (6-8 oraciones) destacando los ha
|
|
| 105 |
}
|
| 106 |
|
| 107 |
function limpiarRespuesta(text) {
|
|
|
|
| 108 |
if (text.includes('<start_of_turn>model')) {
|
| 109 |
text = text.split('<start_of_turn>model').pop();
|
| 110 |
}
|
|
@@ -115,7 +117,7 @@ function limpiarRespuesta(text) {
|
|
| 115 |
// Formato normal: <unused94>pensamiento<unused95>respuesta
|
| 116 |
text = text.split('<unused95>').pop();
|
| 117 |
} else if (text.includes('<unused94>')) {
|
| 118 |
-
// El modelo
|
| 119 |
text = text.split('<unused94>').slice(1).join('').trim();
|
| 120 |
}
|
| 121 |
text = text.replace(/<unused\d+>/g, '');
|
|
@@ -127,7 +129,7 @@ function limpiarRespuesta(text) {
|
|
| 127 |
// Quitar bloques de razonamiento / thinking process
|
| 128 |
text = text.replace(/^thought\s*\n?/i, '');
|
| 129 |
|
| 130 |
-
// Detectar si el modelo solo
|
| 131 |
const tieneRazonamiento = /Here'?s a thinking process|Understand the Role|Analyze the Request|Review the Lab Results|Synthesize Findings|Formulate Clinical Interpretation/i.test(text);
|
| 132 |
const tieneEspanol = /[áéíóúñÁÉÍÓÚÑ]{2,}/.test(text) || /\b(paciente|hallazgos|interpretación|recomendaciones|análisis|resultados|clínica|diagnóstico|evaluación|hepatopatía|nefropatía|anemia|leucocitosis|neutrofilia|linfopenia|hiperglucemia|hipoglucemia|pancreatitis|hepatitis|cirrosis|insuficiencia)\b/i.test(text);
|
| 133 |
if (tieneRazonamiento && !tieneEspanol) {
|
|
@@ -136,7 +138,7 @@ function limpiarRespuesta(text) {
|
|
| 136 |
|
| 137 |
// Si hay razonamiento mezclado con español, intentar extraer solo la respuesta
|
| 138 |
if (tieneRazonamiento) {
|
| 139 |
-
// Buscar la primera
|
| 140 |
const lineas = text.split('\n');
|
| 141 |
let inicioRespuesta = -1;
|
| 142 |
for (let i = 0; i < lineas.length; i++) {
|
|
@@ -157,10 +159,10 @@ function limpiarRespuesta(text) {
|
|
| 157 |
text = text.replace(/\\[a-zA-Z]+(\{[^}]*\})?/g, '');
|
| 158 |
text = text.replace(/\$[^$]*\$/g, '');
|
| 159 |
|
| 160 |
-
// Colapsar
|
| 161 |
text = text.replace(/\n{3,}/g, '\n\n').trim();
|
| 162 |
|
| 163 |
-
// Cortar al primer
|
| 164 |
const parrafos = text.split(/\n\n+/);
|
| 165 |
const vistos = new Set();
|
| 166 |
const sinRepetidos = [];
|
|
@@ -203,6 +205,7 @@ async function _llamarOllama(salidaEl, obtenerDatosPaciente, obtenerValoresFormu
|
|
| 203 |
const prompt = construirPrompt(obtenerDatosPaciente, obtenerValoresFormulario, getUltimoAnalisis, getReferencias);
|
| 204 |
const imagenes = [...imagenesDataUrl.filter(Boolean), ...capturasMicroscopio];
|
| 205 |
|
|
|
|
| 206 |
const contenido = [];
|
| 207 |
for (const img of imagenes) {
|
| 208 |
if (typeof img === 'string' && img.startsWith('data:image/'))
|
|
@@ -244,16 +247,16 @@ async function _llamarOllama(salidaEl, obtenerDatosPaciente, obtenerValoresFormu
|
|
| 244 |
async function _llamarSpace(salidaEl, obtenerDatosPaciente, obtenerValoresFormulario, getUltimoAnalisis, getReferencias) {
|
| 245 |
let prompt = construirPrompt(obtenerDatosPaciente, obtenerValoresFormulario, getUltimoAnalisis, getReferencias);
|
| 246 |
|
| 247 |
-
//
|
| 248 |
if (!prompt.includes('<unused95>')) {
|
| 249 |
prompt = '<unused95>' + prompt;
|
| 250 |
}
|
| 251 |
|
|
|
|
| 252 |
const imagenes = [...imagenesDataUrl.filter(Boolean), ...capturasMicroscopio]
|
| 253 |
.filter(img => typeof img === 'string' && /^data:image\/(jpeg|png|gif|webp);base64,/.test(img))
|
| 254 |
.slice(0, 4);
|
| 255 |
|
| 256 |
-
// DEBUG: Log exact payload for desktop vs mobile comparison
|
| 257 |
console.log('=== MORPHOS AI REQUEST ===');
|
| 258 |
console.log('Images count:', imagenes.length);
|
| 259 |
console.log('Prompt length:', prompt.length);
|
|
@@ -269,7 +272,6 @@ async function _llamarSpace(salidaEl, obtenerDatosPaciente, obtenerValoresFormul
|
|
| 269 |
|
| 270 |
const data = await res.json();
|
| 271 |
|
| 272 |
-
// DEBUG: Log raw response
|
| 273 |
console.log('=== MORPHOS AI RESPONSE ===');
|
| 274 |
console.log('Status:', res.status);
|
| 275 |
console.log('Raw text preview:', (data.text ?? 'NO TEXT').substring(0, 300));
|
|
|
|
| 74 |
? hallazgos.map(h => ` ${h.nombre}: ${h.valor} ${h.unidad || ''} (${h.direccion} · ${h.gravedad})`).join('\n')
|
| 75 |
: ' Todos los valores normales';
|
| 76 |
|
| 77 |
+
// Prompt enfocado en citologia cuando hay imagenes adjuntas
|
| 78 |
return `Eres médico veterinario especialista en patología clínica.
|
| 79 |
|
| 80 |
Paciente: ${paciente.especie || 'desconocido'}, ${paciente.raza || 'raza desconocida'}, ${edadTexto}, ${paciente.sexo || 'sexo desconocido'}
|
|
|
|
| 106 |
}
|
| 107 |
|
| 108 |
function limpiarRespuesta(text) {
|
| 109 |
+
// Elimina tokens especiales del modelo medGemma y otros artefactos de generacion
|
| 110 |
if (text.includes('<start_of_turn>model')) {
|
| 111 |
text = text.split('<start_of_turn>model').pop();
|
| 112 |
}
|
|
|
|
| 117 |
// Formato normal: <unused94>pensamiento<unused95>respuesta
|
| 118 |
text = text.split('<unused95>').pop();
|
| 119 |
} else if (text.includes('<unused94>')) {
|
| 120 |
+
// El modelo agoto tokens en el razonamiento; muestra el pensamiento como respuesta
|
| 121 |
text = text.split('<unused94>').slice(1).join('').trim();
|
| 122 |
}
|
| 123 |
text = text.replace(/<unused\d+>/g, '');
|
|
|
|
| 129 |
// Quitar bloques de razonamiento / thinking process
|
| 130 |
text = text.replace(/^thought\s*\n?/i, '');
|
| 131 |
|
| 132 |
+
// Detectar si el modelo solo genero razonamiento en ingles sin respuesta clinica
|
| 133 |
const tieneRazonamiento = /Here'?s a thinking process|Understand the Role|Analyze the Request|Review the Lab Results|Synthesize Findings|Formulate Clinical Interpretation/i.test(text);
|
| 134 |
const tieneEspanol = /[áéíóúñÁÉÍÓÚÑ]{2,}/.test(text) || /\b(paciente|hallazgos|interpretación|recomendaciones|análisis|resultados|clínica|diagnóstico|evaluación|hepatopatía|nefropatía|anemia|leucocitosis|neutrofilia|linfopenia|hiperglucemia|hipoglucemia|pancreatitis|hepatitis|cirrosis|insuficiencia)\b/i.test(text);
|
| 135 |
if (tieneRazonamiento && !tieneEspanol) {
|
|
|
|
| 138 |
|
| 139 |
// Si hay razonamiento mezclado con español, intentar extraer solo la respuesta
|
| 140 |
if (tieneRazonamiento) {
|
| 141 |
+
// Buscar la primera linea que parezca español clinico
|
| 142 |
const lineas = text.split('\n');
|
| 143 |
let inicioRespuesta = -1;
|
| 144 |
for (let i = 0; i < lineas.length; i++) {
|
|
|
|
| 159 |
text = text.replace(/\\[a-zA-Z]+(\{[^}]*\})?/g, '');
|
| 160 |
text = text.replace(/\$[^$]*\$/g, '');
|
| 161 |
|
| 162 |
+
// Colapsar lineas vacias multiples
|
| 163 |
text = text.replace(/\n{3,}/g, '\n\n').trim();
|
| 164 |
|
| 165 |
+
// Cortar al primer parrafo que se repite (loop del modelo)
|
| 166 |
const parrafos = text.split(/\n\n+/);
|
| 167 |
const vistos = new Set();
|
| 168 |
const sinRepetidos = [];
|
|
|
|
| 205 |
const prompt = construirPrompt(obtenerDatosPaciente, obtenerValoresFormulario, getUltimoAnalisis, getReferencias);
|
| 206 |
const imagenes = [...imagenesDataUrl.filter(Boolean), ...capturasMicroscopio];
|
| 207 |
|
| 208 |
+
// Construye el payload compatible con OpenAI vision: imagenes primero, luego el texto
|
| 209 |
const contenido = [];
|
| 210 |
for (const img of imagenes) {
|
| 211 |
if (typeof img === 'string' && img.startsWith('data:image/'))
|
|
|
|
| 247 |
async function _llamarSpace(salidaEl, obtenerDatosPaciente, obtenerValoresFormulario, getUltimoAnalisis, getReferencias) {
|
| 248 |
let prompt = construirPrompt(obtenerDatosPaciente, obtenerValoresFormulario, getUltimoAnalisis, getReferencias);
|
| 249 |
|
| 250 |
+
// El modelo medGemma espera el token <unused95> al inicio del prompt para modo respuesta directa
|
| 251 |
if (!prompt.includes('<unused95>')) {
|
| 252 |
prompt = '<unused95>' + prompt;
|
| 253 |
}
|
| 254 |
|
| 255 |
+
// Filtra y limita a 4 imagenes por restriccion del backend de HuggingFace
|
| 256 |
const imagenes = [...imagenesDataUrl.filter(Boolean), ...capturasMicroscopio]
|
| 257 |
.filter(img => typeof img === 'string' && /^data:image\/(jpeg|png|gif|webp);base64,/.test(img))
|
| 258 |
.slice(0, 4);
|
| 259 |
|
|
|
|
| 260 |
console.log('=== MORPHOS AI REQUEST ===');
|
| 261 |
console.log('Images count:', imagenes.length);
|
| 262 |
console.log('Prompt length:', prompt.length);
|
|
|
|
| 272 |
|
| 273 |
const data = await res.json();
|
| 274 |
|
|
|
|
| 275 |
console.log('=== MORPHOS AI RESPONSE ===');
|
| 276 |
console.log('Status:', res.status);
|
| 277 |
console.log('Raw text preview:', (data.text ?? 'NO TEXT').substring(0, 300));
|
js/main.js
CHANGED
|
@@ -56,6 +56,7 @@ const obtenerDatosPaciente = () => {
|
|
| 56 |
const especieCruda = document.getElementById('pt-especie').value;
|
| 57 |
const valorEdad = document.getElementById('pt-edad').value;
|
| 58 |
const edadUnidad = document.getElementById('pt-edad-unidad').value;
|
|
|
|
| 59 |
const edadMeses = valorEdad === '' ? null
|
| 60 |
: edadUnidad === 'meses' ? parseFloat(valorEdad)
|
| 61 |
: parseFloat(valorEdad) * 12;
|
|
@@ -124,6 +125,7 @@ const renderizarPatrones = (patrones) => {
|
|
| 124 |
const evaluar = () => {
|
| 125 |
const paciente = obtenerDatosPaciente();
|
| 126 |
|
|
|
|
| 127 |
if (!paciente.especie || !referencias[paciente.especie]) {
|
| 128 |
actualizarClasesInputs([]);
|
| 129 |
renderizarPatrones([]);
|
|
@@ -131,6 +133,7 @@ const evaluar = () => {
|
|
| 131 |
}
|
| 132 |
|
| 133 |
const valores = obtenerValoresFormulario();
|
|
|
|
| 134 |
const { hallazgos, patrones } = analizarResultados(valores, paciente, referencias, alteraciones);
|
| 135 |
ultimoAnalisis = { hallazgos, patrones };
|
| 136 |
|
|
@@ -142,6 +145,7 @@ const evaluar = () => {
|
|
| 142 |
|
| 143 |
document.addEventListener('input', e => {
|
| 144 |
if (e.target.type !== 'number') return;
|
|
|
|
| 145 |
if (e.target.value < 0) e.target.value = 0;
|
| 146 |
if (e.target.value.replace('.', '').length > 4) e.target.value = e.target.value.slice(0, 4);
|
| 147 |
e.target.classList.toggle('max-chars', e.target.value.replace('.', '').length >= 4);
|
|
@@ -164,6 +168,7 @@ document.addEventListener('click', e => {
|
|
| 164 |
if (!btn) return;
|
| 165 |
const panel = document.getElementById(`panel-${btn.dataset.panel}`);
|
| 166 |
if (!panel) return;
|
|
|
|
| 167 |
panel.querySelectorAll('input[type="number"], input[type="text"], input[type="url"], select').forEach(el => {
|
| 168 |
if (el.tagName === 'SELECT') {
|
| 169 |
el.selectedIndex = 0;
|
|
@@ -181,6 +186,7 @@ document.addEventListener('click', e => {
|
|
| 181 |
});
|
| 182 |
|
| 183 |
document.querySelector('.boton-analizar').addEventListener('click', async () => {
|
|
|
|
| 184 |
const autenticado = await verificarAuth();
|
| 185 |
if (!autenticado) {
|
| 186 |
abrirModalAuth(() => {
|
|
|
|
| 56 |
const especieCruda = document.getElementById('pt-especie').value;
|
| 57 |
const valorEdad = document.getElementById('pt-edad').value;
|
| 58 |
const edadUnidad = document.getElementById('pt-edad-unidad').value;
|
| 59 |
+
// Normaliza la edad siempre a meses para que analisis.js pueda aplicar ajustes por cachorro/adulto/senior
|
| 60 |
const edadMeses = valorEdad === '' ? null
|
| 61 |
: edadUnidad === 'meses' ? parseFloat(valorEdad)
|
| 62 |
: parseFloat(valorEdad) * 12;
|
|
|
|
| 125 |
const evaluar = () => {
|
| 126 |
const paciente = obtenerDatosPaciente();
|
| 127 |
|
| 128 |
+
// Si no hay especie o aun no cargaron las referencias, limpia la UI para evitar falsos positivos
|
| 129 |
if (!paciente.especie || !referencias[paciente.especie]) {
|
| 130 |
actualizarClasesInputs([]);
|
| 131 |
renderizarPatrones([]);
|
|
|
|
| 133 |
}
|
| 134 |
|
| 135 |
const valores = obtenerValoresFormulario();
|
| 136 |
+
// Delega en analisis.js la comparacion contra rangos de referencia y la deteccion de patrones clinicos
|
| 137 |
const { hallazgos, patrones } = analizarResultados(valores, paciente, referencias, alteraciones);
|
| 138 |
ultimoAnalisis = { hallazgos, patrones };
|
| 139 |
|
|
|
|
| 145 |
|
| 146 |
document.addEventListener('input', e => {
|
| 147 |
if (e.target.type !== 'number') return;
|
| 148 |
+
// Impide valores negativos y limita a 4 digitos significativos para mantener la UI limpia
|
| 149 |
if (e.target.value < 0) e.target.value = 0;
|
| 150 |
if (e.target.value.replace('.', '').length > 4) e.target.value = e.target.value.slice(0, 4);
|
| 151 |
e.target.classList.toggle('max-chars', e.target.value.replace('.', '').length >= 4);
|
|
|
|
| 168 |
if (!btn) return;
|
| 169 |
const panel = document.getElementById(`panel-${btn.dataset.panel}`);
|
| 170 |
if (!panel) return;
|
| 171 |
+
// Recorre todos los campos editables del panel y los resetea, incluyendo indicadores visuales de estado
|
| 172 |
panel.querySelectorAll('input[type="number"], input[type="text"], input[type="url"], select').forEach(el => {
|
| 173 |
if (el.tagName === 'SELECT') {
|
| 174 |
el.selectedIndex = 0;
|
|
|
|
| 186 |
});
|
| 187 |
|
| 188 |
document.querySelector('.boton-analizar').addEventListener('click', async () => {
|
| 189 |
+
// Si el usuario no esta logueado, abre el modal de auth y encola la llamada a IA como callback
|
| 190 |
const autenticado = await verificarAuth();
|
| 191 |
if (!autenticado) {
|
| 192 |
abrirModalAuth(() => {
|
js/papers.js
CHANGED
|
@@ -60,6 +60,7 @@ const traducirPatron = (nombre) => {
|
|
| 60 |
|
| 61 |
const construirQuery = (patrones) => {
|
| 62 |
if (!patrones || patrones.length === 0) return 'veterinary clinical laboratory diagnosis canine feline';
|
|
|
|
| 63 |
const terminos = [...new Set(patrones.map(p => traducirPatron(p.nombre)))].slice(0, 3);
|
| 64 |
return `${terminos.join(' ')} canine OR feline veterinary`;
|
| 65 |
};
|
|
@@ -139,6 +140,7 @@ const renderizarPaginacion = () => {
|
|
| 139 |
return;
|
| 140 |
}
|
| 141 |
|
|
|
|
| 142 |
const inicio = Math.max(0, paginaActual - 2);
|
| 143 |
const fin = Math.min(totalPaginas, inicio + 5);
|
| 144 |
|
|
@@ -211,6 +213,7 @@ export const abrirModalPapers = async (patrones) => {
|
|
| 211 |
const etiquetaConsulta = document.getElementById('papers-consulta');
|
| 212 |
if (etiquetaConsulta) etiquetaConsulta.textContent = `"${nuevaConsulta}"`;
|
| 213 |
|
|
|
|
| 214 |
if (nuevaConsulta === consultaActual && todosLosPapers.length > 0) {
|
| 215 |
renderizarPaginaActual();
|
| 216 |
return;
|
|
|
|
| 60 |
|
| 61 |
const construirQuery = (patrones) => {
|
| 62 |
if (!patrones || patrones.length === 0) return 'veterinary clinical laboratory diagnosis canine feline';
|
| 63 |
+
// Limita a 3 terminos para mantener la query enfocada y evitar resultados irrelevantes
|
| 64 |
const terminos = [...new Set(patrones.map(p => traducirPatron(p.nombre)))].slice(0, 3);
|
| 65 |
return `${terminos.join(' ')} canine OR feline veterinary`;
|
| 66 |
};
|
|
|
|
| 140 |
return;
|
| 141 |
}
|
| 142 |
|
| 143 |
+
// Ventana deslizante de maximo 5 botones centrada en la pagina actual
|
| 144 |
const inicio = Math.max(0, paginaActual - 2);
|
| 145 |
const fin = Math.min(totalPaginas, inicio + 5);
|
| 146 |
|
|
|
|
| 213 |
const etiquetaConsulta = document.getElementById('papers-consulta');
|
| 214 |
if (etiquetaConsulta) etiquetaConsulta.textContent = `"${nuevaConsulta}"`;
|
| 215 |
|
| 216 |
+
// Reutiliza resultados si la consulta no cambio desde la ultima vez
|
| 217 |
if (nuevaConsulta === consultaActual && todosLosPapers.length > 0) {
|
| 218 |
renderizarPaginaActual();
|
| 219 |
return;
|
js/pdf-parser.js
CHANGED
|
@@ -10,7 +10,7 @@ const DEFS_ANALITOS = [
|
|
| 10 |
{ campo: 'hgb', re: /\b(?:hemoglobin[ao]?\w*|hgb|hb)\b(?!a\d)/i },
|
| 11 |
{ campo: 'hct', re: /\b(?:hematocrit[oo]?\w*|hct|pcv)\b/i },
|
| 12 |
{ campo: 'vcm', re: /\b(?:v\.?c\.?m\.?|m\.?c\.?v\.?|vol(?:umen)?\s+corp\w*)\b/i },
|
| 13 |
-
// CHCM debe ir antes que HCM para evitar que MCH coincida con MCHC
|
| 14 |
{ campo: 'chcm', re: /\b(?:c\.?h\.?c\.?m\.?|m\.?c\.?h\.?c\.?|concentr\w+\s+hem\w+\s+corp\w*)\b/i },
|
| 15 |
{ campo: 'hcm', re: /\b(?:h\.?c\.?m\.?|m\.?c\.?h\.?)(?![cC]\.?)\b/i },
|
| 16 |
{ campo: 'rdw', re: /\b(?:r\.?d\.?w\.?(?:-cv)?|anch\w+\s+distrib\w+)\b/i },
|
|
@@ -201,6 +201,7 @@ function aplicarConversion(campo, claveConv, value, cadenaUnidad) {
|
|
| 201 |
const key = claveConv || campo;
|
| 202 |
const reglas = CONVERSIONES_UNIDADES[key];
|
| 203 |
if (!reglas) return value;
|
|
|
|
| 204 |
for (const regla of reglas) {
|
| 205 |
if (regla.re.test(cadenaUnidad)) {
|
| 206 |
const f = regla.factor;
|
|
@@ -211,8 +212,8 @@ function aplicarConversion(campo, claveConv, value, cadenaUnidad) {
|
|
| 211 |
return value;
|
| 212 |
}
|
| 213 |
|
| 214 |
-
// Retorna { num, unit } donde unit es la cadena de ~50 caracteres tras el valor
|
| 215 |
-
// Las reglas de
|
| 216 |
function extraerValorYUnidad(contexto) {
|
| 217 |
const m = contexto.match(/[<>≤≥]?\s*(\d+(?:[.,]\d+)?)([\s\S]*)/);
|
| 218 |
if (!m) return { num: null, unit: '' };
|
|
@@ -244,7 +245,7 @@ function parsearTextoLab(textoCrudo) {
|
|
| 244 |
resultados[def.campo] = aplicarConversion(def.campo, def.claveConv, num, unit);
|
| 245 |
}
|
| 246 |
|
| 247 |
-
// Derivar % desde conteos absolutos si el % no se
|
| 248 |
if (resultados.wbc && resultados.wbc > 0) {
|
| 249 |
for (const f of ['neutro', 'linfo', 'mono', 'eosino', 'baso']) {
|
| 250 |
if (resultados[f] === undefined && resultados[`${f}_abs`] !== undefined) {
|
|
@@ -254,8 +255,8 @@ function parsearTextoLab(textoCrudo) {
|
|
| 254 |
}
|
| 255 |
}
|
| 256 |
|
| 257 |
-
// Derivar % de reticulocitos desde el conteo absoluto y RBC si no se
|
| 258 |
-
// reti_abs (x10³/
|
| 259 |
if (resultados.rbc && resultados.rbc > 0 && resultados.reti === undefined && resultados.reti_abs !== undefined) {
|
| 260 |
const pct = resultados.reti_abs / (resultados.rbc * 10);
|
| 261 |
if (pct >= 0 && pct <= 20) resultados.reti = Math.round(pct * 100) / 100;
|
|
@@ -311,7 +312,7 @@ function inferEspecie(raza) {
|
|
| 311 |
function parsearTextoPaciente(textoCrudo) {
|
| 312 |
const p = {};
|
| 313 |
|
| 314 |
-
// Especie
|
| 315 |
const coincEsp = textoCrudo.match(/\b(?:especies?|species|tipo(?:\s+de)?\s+animal)\s*:?\s{0,4}([A-Za-záéíóúÁÉÍÓÚñÑ]{3,20})/i);
|
| 316 |
if (coincEsp) {
|
| 317 |
const v = coincEsp[1].toLowerCase();
|
|
@@ -319,11 +320,10 @@ function parsearTextoPaciente(textoCrudo) {
|
|
| 319 |
else if (/fel[io]|gat[ao]|cat/.test(v)) p.especie = 'Felino';
|
| 320 |
}
|
| 321 |
|
| 322 |
-
// Raza
|
| 323 |
const coincRaza = textoCrudo.match(/\b(?:raza|breed|race|cruce)\s*:?\s{0,4}([^\n\r;:]{2,60})/i);
|
| 324 |
if (coincRaza) {
|
| 325 |
const crudo = coincRaza[1];
|
| 326 |
-
// Detener en la siguiente palabra clave de etiqueta o 2+ espacios consecutivos (diseño tabular)
|
| 327 |
const indiceParo = crudo.search(SIGUIENTE_ETIQUETA);
|
| 328 |
const limpiado = (indiceParo > 0 ? crudo.slice(0, indiceParo) : crudo)
|
| 329 |
.split(/\s{2,}/)[0]
|
|
@@ -331,9 +331,10 @@ function parsearTextoPaciente(textoCrudo) {
|
|
| 331 |
if (limpiado.length >= 2) p.raza = limpiado.length > 40 ? limpiado.slice(0, 40).trim() : limpiado;
|
| 332 |
}
|
| 333 |
|
|
|
|
| 334 |
if (!p.especie && p.raza) p.especie = inferEspecie(p.raza);
|
| 335 |
|
| 336 |
-
// Sexo
|
| 337 |
const coincSex = textoCrudo.match(/\b(?:sexo|sex[ou]?|g[eé]nero|gender)\s*:?\s{0,4}([^\n\r;:]{1,30})/i);
|
| 338 |
if (coincSex) {
|
| 339 |
const v = coincSex[1].trim();
|
|
@@ -341,7 +342,7 @@ function parsearTextoPaciente(textoCrudo) {
|
|
| 341 |
else if (/\b(?:hembra|female|esterilizada?|spayed)\b/i.test(v) || /^[fh]\.?\s*$/i.test(v)) p.sexo = 'Hembra';
|
| 342 |
}
|
| 343 |
|
| 344 |
-
// Edad
|
| 345 |
const coincEdad = textoCrudo.match(/\b(?:edad|age)\s*:?\s{0,4}(\d+(?:[.,]\d+)?)\s*(a[ñn]os?|years?|yr?s?|meses?|months?)\b/i);
|
| 346 |
if (coincEdad) {
|
| 347 |
p.edad = parseFloat(coincEdad[1].replace(',', '.'));
|
|
|
|
| 10 |
{ campo: 'hgb', re: /\b(?:hemoglobin[ao]?\w*|hgb|hb)\b(?!a\d)/i },
|
| 11 |
{ campo: 'hct', re: /\b(?:hematocrit[oo]?\w*|hct|pcv)\b/i },
|
| 12 |
{ campo: 'vcm', re: /\b(?:v\.?c\.?m\.?|m\.?c\.?v\.?|vol(?:umen)?\s+corp\w*)\b/i },
|
| 13 |
+
// CHCM debe ir antes que HCM para evitar que MCH coincida con MCHC al usar lookahead negativo
|
| 14 |
{ campo: 'chcm', re: /\b(?:c\.?h\.?c\.?m\.?|m\.?c\.?h\.?c\.?|concentr\w+\s+hem\w+\s+corp\w*)\b/i },
|
| 15 |
{ campo: 'hcm', re: /\b(?:h\.?c\.?m\.?|m\.?c\.?h\.?)(?![cC]\.?)\b/i },
|
| 16 |
{ campo: 'rdw', re: /\b(?:r\.?d\.?w\.?(?:-cv)?|anch\w+\s+distrib\w+)\b/i },
|
|
|
|
| 201 |
const key = claveConv || campo;
|
| 202 |
const reglas = CONVERSIONES_UNIDADES[key];
|
| 203 |
if (!reglas) return value;
|
| 204 |
+
// Aplica la primera regla cuya regex coincida con la cadena de unidad detectada
|
| 205 |
for (const regla of reglas) {
|
| 206 |
if (regla.re.test(cadenaUnidad)) {
|
| 207 |
const f = regla.factor;
|
|
|
|
| 212 |
return value;
|
| 213 |
}
|
| 214 |
|
| 215 |
+
// Retorna { num, unit } donde unit es la cadena de ~50 caracteres tras el valor numerico.
|
| 216 |
+
// Las reglas de conversion evaluan su regex contra esta cadena para decidir el factor.
|
| 217 |
function extraerValorYUnidad(contexto) {
|
| 218 |
const m = contexto.match(/[<>≤≥]?\s*(\d+(?:[.,]\d+)?)([\s\S]*)/);
|
| 219 |
if (!m) return { num: null, unit: '' };
|
|
|
|
| 245 |
resultados[def.campo] = aplicarConversion(def.campo, def.claveConv, num, unit);
|
| 246 |
}
|
| 247 |
|
| 248 |
+
// Derivar % desde conteos absolutos si el % no se encontro directamente y se conoce el WBC
|
| 249 |
if (resultados.wbc && resultados.wbc > 0) {
|
| 250 |
for (const f of ['neutro', 'linfo', 'mono', 'eosino', 'baso']) {
|
| 251 |
if (resultados[f] === undefined && resultados[`${f}_abs`] !== undefined) {
|
|
|
|
| 255 |
}
|
| 256 |
}
|
| 257 |
|
| 258 |
+
// Derivar % de reticulocitos desde el conteo absoluto y RBC si no se encontro directamente
|
| 259 |
+
// reti_abs (x10³/uL) / (rbc (x10⁶/uL) * 10) = reti%
|
| 260 |
if (resultados.rbc && resultados.rbc > 0 && resultados.reti === undefined && resultados.reti_abs !== undefined) {
|
| 261 |
const pct = resultados.reti_abs / (resultados.rbc * 10);
|
| 262 |
if (pct >= 0 && pct <= 20) resultados.reti = Math.round(pct * 100) / 100;
|
|
|
|
| 312 |
function parsearTextoPaciente(textoCrudo) {
|
| 313 |
const p = {};
|
| 314 |
|
| 315 |
+
// Especie: tolera variaciones como "Canino", "Dog", "Felino", "Cat"
|
| 316 |
const coincEsp = textoCrudo.match(/\b(?:especies?|species|tipo(?:\s+de)?\s+animal)\s*:?\s{0,4}([A-Za-záéíóúÁÉÍÓÚñÑ]{3,20})/i);
|
| 317 |
if (coincEsp) {
|
| 318 |
const v = coincEsp[1].toLowerCase();
|
|
|
|
| 320 |
else if (/fel[io]|gat[ao]|cat/.test(v)) p.especie = 'Felino';
|
| 321 |
}
|
| 322 |
|
| 323 |
+
// Raza: corta en la siguiente etiqueta o doble espacio para evitar absorber campos adyacentes en tablas
|
| 324 |
const coincRaza = textoCrudo.match(/\b(?:raza|breed|race|cruce)\s*:?\s{0,4}([^\n\r;:]{2,60})/i);
|
| 325 |
if (coincRaza) {
|
| 326 |
const crudo = coincRaza[1];
|
|
|
|
| 327 |
const indiceParo = crudo.search(SIGUIENTE_ETIQUETA);
|
| 328 |
const limpiado = (indiceParo > 0 ? crudo.slice(0, indiceParo) : crudo)
|
| 329 |
.split(/\s{2,}/)[0]
|
|
|
|
| 331 |
if (limpiado.length >= 2) p.raza = limpiado.length > 40 ? limpiado.slice(0, 40).trim() : limpiado;
|
| 332 |
}
|
| 333 |
|
| 334 |
+
// Si no encontro especie pero si raza, infiere la especie a partir de listas de razas conocidas
|
| 335 |
if (!p.especie && p.raza) p.especie = inferEspecie(p.raza);
|
| 336 |
|
| 337 |
+
// Sexo: soporta abreviaturas (M, F, H) y variantes como "Esterilizada"
|
| 338 |
const coincSex = textoCrudo.match(/\b(?:sexo|sex[ou]?|g[eé]nero|gender)\s*:?\s{0,4}([^\n\r;:]{1,30})/i);
|
| 339 |
if (coincSex) {
|
| 340 |
const v = coincSex[1].trim();
|
|
|
|
| 342 |
else if (/\b(?:hembra|female|esterilizada?|spayed)\b/i.test(v) || /^[fh]\.?\s*$/i.test(v)) p.sexo = 'Hembra';
|
| 343 |
}
|
| 344 |
|
| 345 |
+
// Edad: extrae numero y unidad, normalizando comas decimales
|
| 346 |
const coincEdad = textoCrudo.match(/\b(?:edad|age)\s*:?\s{0,4}(\d+(?:[.,]\d+)?)\s*(a[ñn]os?|years?|yr?s?|meses?|months?)\b/i);
|
| 347 |
if (coincEdad) {
|
| 348 |
p.edad = parseFloat(coincEdad[1].replace(',', '.'));
|
js/ui.js
CHANGED
|
@@ -15,6 +15,7 @@ export function activarTab(targetId) {
|
|
| 15 |
const esTabExamenes = targetId === 'examenes';
|
| 16 |
const mostrarExamenes = esTabExamenes || esSubpanelExamenes;
|
| 17 |
|
|
|
|
| 18 |
let idPanelActual;
|
| 19 |
if (esTabExamenes) {
|
| 20 |
idPanelActual = panelExamenActivo;
|
|
@@ -70,6 +71,7 @@ document.querySelector('main').addEventListener('touchstart', e => {
|
|
| 70 |
document.querySelector('main').addEventListener('touchend', e => {
|
| 71 |
const dx = e.changedTouches[0].clientX - inicioSwipeX;
|
| 72 |
const dy = e.changedTouches[0].clientY - inicioSwipeY;
|
|
|
|
| 73 |
if (Math.abs(dx) < 50 || Math.abs(dx) < Math.abs(dy)) return;
|
| 74 |
const indice = SWIPE_ORDER.indexOf(panelActivo);
|
| 75 |
const siguiente = dx < 0 ? SWIPE_ORDER[indice + 1] : SWIPE_ORDER[indice - 1];
|
|
@@ -122,6 +124,7 @@ function inicializarFilasGrid() {
|
|
| 122 |
if (!esGridEscritorio()) return;
|
| 123 |
mainEl.style.gridTemplateRows = '1fr auto auto';
|
| 124 |
|
|
|
|
| 125 |
const alturaPanel = panelFlujo.getBoundingClientRect().height;
|
| 126 |
const alturaEncabezado = panelFlujo.querySelector('.panel-cabecera').getBoundingClientRect().height;
|
| 127 |
if (alturaPanel > 0) filaExpandida = `${alturaPanel}px`;
|
|
@@ -188,6 +191,7 @@ function establecerSubpanelColapsado(subpanel, debeColapsar) {
|
|
| 188 |
if (debeColapsar === subpanel.classList.contains('collapsed')) return;
|
| 189 |
subpanel.classList.toggle('collapsed', debeColapsar);
|
| 190 |
if (btn) btn.setAttribute('aria-expanded', String(!debeColapsar));
|
|
|
|
| 191 |
if (debeColapsar) {
|
| 192 |
animEl.style.height = `${animEl.offsetHeight}px`;
|
| 193 |
animEl.offsetHeight;
|
|
@@ -324,6 +328,7 @@ document.querySelectorAll('.zona-imagen').forEach(zona => {
|
|
| 324 |
reader.onload = ev => {
|
| 325 |
const img = new Image();
|
| 326 |
img.onload = () => {
|
|
|
|
| 327 |
const MAX_PIXELES = 1024;
|
| 328 |
const scale = Math.min(MAX_PIXELES / img.width, MAX_PIXELES / img.height, 1);
|
| 329 |
const canvas = document.createElement('canvas');
|
|
@@ -420,6 +425,7 @@ document.querySelectorAll('.zona-imagen').forEach(zona => {
|
|
| 420 |
|
| 421 |
async function abrirCamara() {
|
| 422 |
try {
|
|
|
|
| 423 |
stream = await navigator.mediaDevices.getUserMedia({
|
| 424 |
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1920 } }
|
| 425 |
});
|
|
@@ -429,7 +435,7 @@ document.querySelectorAll('.zona-imagen').forEach(zona => {
|
|
| 429 |
controles.hidden = false;
|
| 430 |
actualizarInsignia();
|
| 431 |
} catch {
|
| 432 |
-
// Permiso denegado o
|
| 433 |
}
|
| 434 |
}
|
| 435 |
|
|
@@ -447,6 +453,7 @@ document.querySelectorAll('.zona-imagen').forEach(zona => {
|
|
| 447 |
e.stopPropagation();
|
| 448 |
if (capturasMicroscopio.length >= MAX_CAPTURAS_MICRO) return;
|
| 449 |
const canvas = document.createElement('canvas');
|
|
|
|
| 450 |
const MAX_PIXELES = 1024;
|
| 451 |
const scale = Math.min(MAX_PIXELES / video.videoWidth, MAX_PIXELES / video.videoHeight, 1);
|
| 452 |
canvas.width = Math.round(video.videoWidth * scale);
|
|
|
|
| 15 |
const esTabExamenes = targetId === 'examenes';
|
| 16 |
const mostrarExamenes = esTabExamenes || esSubpanelExamenes;
|
| 17 |
|
| 18 |
+
// Si se clickea la tab generica "Examenes", muestra el ultimo subtab activo; si es un subtab, lo guarda
|
| 19 |
let idPanelActual;
|
| 20 |
if (esTabExamenes) {
|
| 21 |
idPanelActual = panelExamenActivo;
|
|
|
|
| 71 |
document.querySelector('main').addEventListener('touchend', e => {
|
| 72 |
const dx = e.changedTouches[0].clientX - inicioSwipeX;
|
| 73 |
const dy = e.changedTouches[0].clientY - inicioSwipeY;
|
| 74 |
+
// Ignora gestos cortos o verticales para no interferir con scroll
|
| 75 |
if (Math.abs(dx) < 50 || Math.abs(dx) < Math.abs(dy)) return;
|
| 76 |
const indice = SWIPE_ORDER.indexOf(panelActivo);
|
| 77 |
const siguiente = dx < 0 ? SWIPE_ORDER[indice + 1] : SWIPE_ORDER[indice - 1];
|
|
|
|
| 124 |
if (!esGridEscritorio()) return;
|
| 125 |
mainEl.style.gridTemplateRows = '1fr auto auto';
|
| 126 |
|
| 127 |
+
// Mide el panel de flujo expandido y colapsado para animar grid-template-rows con precision
|
| 128 |
const alturaPanel = panelFlujo.getBoundingClientRect().height;
|
| 129 |
const alturaEncabezado = panelFlujo.querySelector('.panel-cabecera').getBoundingClientRect().height;
|
| 130 |
if (alturaPanel > 0) filaExpandida = `${alturaPanel}px`;
|
|
|
|
| 191 |
if (debeColapsar === subpanel.classList.contains('collapsed')) return;
|
| 192 |
subpanel.classList.toggle('collapsed', debeColapsar);
|
| 193 |
if (btn) btn.setAttribute('aria-expanded', String(!debeColapsar));
|
| 194 |
+
// Forzar reflujo antes de cambiar height permite que CSS transition anime correctamente
|
| 195 |
if (debeColapsar) {
|
| 196 |
animEl.style.height = `${animEl.offsetHeight}px`;
|
| 197 |
animEl.offsetHeight;
|
|
|
|
| 328 |
reader.onload = ev => {
|
| 329 |
const img = new Image();
|
| 330 |
img.onload = () => {
|
| 331 |
+
// Reduce la imagen a max 1024px en su lado mayor para no saturar la memoria ni la API
|
| 332 |
const MAX_PIXELES = 1024;
|
| 333 |
const scale = Math.min(MAX_PIXELES / img.width, MAX_PIXELES / img.height, 1);
|
| 334 |
const canvas = document.createElement('canvas');
|
|
|
|
| 425 |
|
| 426 |
async function abrirCamara() {
|
| 427 |
try {
|
| 428 |
+
// Preferencia por camara trasera (microscopio o movil apuntando a la muestra)
|
| 429 |
stream = await navigator.mediaDevices.getUserMedia({
|
| 430 |
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1920 } }
|
| 431 |
});
|
|
|
|
| 435 |
controles.hidden = false;
|
| 436 |
actualizarInsignia();
|
| 437 |
} catch {
|
| 438 |
+
// Permiso denegado o camara no disponible; no se requiere fallback
|
| 439 |
}
|
| 440 |
}
|
| 441 |
|
|
|
|
| 453 |
e.stopPropagation();
|
| 454 |
if (capturasMicroscopio.length >= MAX_CAPTURAS_MICRO) return;
|
| 455 |
const canvas = document.createElement('canvas');
|
| 456 |
+
// Escala el fotograma de video para mantener un tamano razonable antes de enviarlo al modelo
|
| 457 |
const MAX_PIXELES = 1024;
|
| 458 |
const scale = Math.min(MAX_PIXELES / video.videoWidth, MAX_PIXELES / video.videoHeight, 1);
|
| 459 |
canvas.width = Math.round(video.videoWidth * scale);
|