Spaces:
Sleeping
Sleeping
GitHub Actions commited on
Commit ·
f6e3d73
1
Parent(s): e27b514
🚀 Auto-deploy from GitHub
Browse files- .gitignore +188 -10
- Dockerfile +1 -0
- app/api/v1/endpoints/download.py +1 -3
- app/api/v1/endpoints/generate.py +87 -163
- app/api/v1/schemas/{horoscope_schemas.py → card_schemas.py} +44 -22
- app/core/card_renderer.py +8 -8
- app/core/config.py +17 -0
- app/core/constraints.py +42 -5
- app/core/generator.py +24 -16
- app/core/hf_api.py +367 -0
- app/core/lora_config.py +85 -1
- app/core/model_loader.py +140 -9
- app/hf_api.py +0 -2
- app/main.py +1 -1
- app/services/database.py +5 -5
- requirements.txt +26 -13
- scripts/cleanup_large_files.sh +0 -0
- {tests → scripts}/quick_test.sh +0 -0
- scripts/setup_training.sh +37 -0
- scripts/start.sh +24 -0
- tests/MANUAL_VERIFICATION.md +0 -149
- training/data/cards_training_data.json +38 -0
- training/deploy_lora.py +221 -0
- training/test_setup.py +27 -0
- training/train_lora.py +175 -0
- training/training_requirements.txt +25 -0
.gitignore
CHANGED
|
@@ -1,21 +1,199 @@
|
|
| 1 |
.git
|
| 2 |
__pycache__
|
| 3 |
__pycache__/
|
| 4 |
-
*.
|
| 5 |
-
*.
|
| 6 |
-
*.pyd
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
.Python
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
env/
|
| 10 |
venv/
|
| 11 |
-
|
| 12 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
.idea/
|
| 14 |
*.swp
|
| 15 |
*.swo
|
|
|
|
|
|
|
|
|
|
| 16 |
.DS_Store
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
Thumbs.db
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
.git
|
| 2 |
__pycache__
|
| 3 |
__pycache__/
|
| 4 |
+
*.py[cod]
|
| 5 |
+
*$py.class
|
| 6 |
+
*.pyd # Added from user prompt
|
| 7 |
+
|
| 8 |
+
# C extensions
|
| 9 |
+
*.so
|
| 10 |
+
|
| 11 |
+
# Distribution / packaging
|
| 12 |
.Python
|
| 13 |
+
build/
|
| 14 |
+
develop-eggs/
|
| 15 |
+
dist/
|
| 16 |
+
downloads/
|
| 17 |
+
eggs/
|
| 18 |
+
.eggs/
|
| 19 |
+
lib/
|
| 20 |
+
lib64/
|
| 21 |
+
parts/
|
| 22 |
+
sdist/
|
| 23 |
+
var/
|
| 24 |
+
wheels/
|
| 25 |
+
pip-wheel-metadata/
|
| 26 |
+
share/python-wheels/
|
| 27 |
+
*.egg-info/
|
| 28 |
+
.installed.cfg
|
| 29 |
+
*.egg
|
| 30 |
+
MANIFEST
|
| 31 |
+
node_modules/ # Added from existing, good to keep
|
| 32 |
+
*.manifest
|
| 33 |
+
*.spec
|
| 34 |
+
|
| 35 |
+
# Installer logs
|
| 36 |
+
pip-log.txt
|
| 37 |
+
pip-delete-this-directory.txt
|
| 38 |
+
|
| 39 |
+
# Unit test / coverage reports
|
| 40 |
+
htmlcov/
|
| 41 |
+
.tox/
|
| 42 |
+
.nox/
|
| 43 |
+
.coverage
|
| 44 |
+
.coverage.*
|
| 45 |
+
.cache # Moved from ML section, as it's general
|
| 46 |
+
nosetests.xml
|
| 47 |
+
coverage.xml
|
| 48 |
+
*.cover
|
| 49 |
+
*.py,cover # Corrected from *.py,cover
|
| 50 |
+
.hypothesis/
|
| 51 |
+
.pytest_cache/
|
| 52 |
+
|
| 53 |
+
# Translations
|
| 54 |
+
*.mo
|
| 55 |
+
*.pot
|
| 56 |
+
|
| 57 |
+
# Django stuff:
|
| 58 |
+
# *.log # Moved to general logs
|
| 59 |
+
local_settings.py
|
| 60 |
+
db.sqlite3
|
| 61 |
+
db.sqlite3-journal
|
| 62 |
+
|
| 63 |
+
# Flask stuff:
|
| 64 |
+
instance/
|
| 65 |
+
.webassets-cache
|
| 66 |
+
|
| 67 |
+
# Scrapy stuff:
|
| 68 |
+
.scrapy
|
| 69 |
+
|
| 70 |
+
# Sphinx documentation
|
| 71 |
+
docs/_build/
|
| 72 |
+
|
| 73 |
+
# PyBuilder
|
| 74 |
+
target/
|
| 75 |
+
|
| 76 |
+
# Jupyter Notebook
|
| 77 |
+
.ipynb_checkpoints/
|
| 78 |
+
*.ipynb # Moved from ML section
|
| 79 |
+
|
| 80 |
+
# IPython
|
| 81 |
+
profile_default/
|
| 82 |
+
ipython_config.py
|
| 83 |
+
|
| 84 |
+
# pyenv
|
| 85 |
+
.python-version
|
| 86 |
+
|
| 87 |
+
# pipenv
|
| 88 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 89 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 90 |
+
# having no cross-platform support, pipenv may install dependencies that don\'t work, or not
|
| 91 |
+
# install all needed dependencies.
|
| 92 |
+
Pipfile.lock
|
| 93 |
+
|
| 94 |
+
# poetry
|
| 95 |
+
#poetry.lock
|
| 96 |
+
|
| 97 |
+
# pdm
|
| 98 |
+
#pdm.lock
|
| 99 |
+
#pd.toml
|
| 100 |
+
|
| 101 |
+
# PEP 582; __pypackages__ directory
|
| 102 |
+
__pypackages__/
|
| 103 |
+
|
| 104 |
+
# Celery stuff
|
| 105 |
+
celerybeat-schedule
|
| 106 |
+
celerybeat.pid
|
| 107 |
+
|
| 108 |
+
# SageMath parsed files
|
| 109 |
+
*.sage.py
|
| 110 |
+
|
| 111 |
+
# Environments
|
| 112 |
+
.env
|
| 113 |
+
.venv
|
| 114 |
env/
|
| 115 |
venv/
|
| 116 |
+
ENV/
|
| 117 |
+
env.bak/
|
| 118 |
+
venv.bak/
|
| 119 |
+
# Specific training venvs from existing .gitignore
|
| 120 |
+
training/env/
|
| 121 |
+
training/.venv/
|
| 122 |
+
**/venv_training/ # This covers training/venv_training/ from user prompt as well
|
| 123 |
+
|
| 124 |
+
# IDE specific
|
| 125 |
+
.vscode/ # Covers .vscode/settings.json
|
| 126 |
.idea/
|
| 127 |
*.swp
|
| 128 |
*.swo
|
| 129 |
+
*~
|
| 130 |
+
|
| 131 |
+
# OS Specific
|
| 132 |
.DS_Store
|
| 133 |
+
.DS_Store? # From existing
|
| 134 |
+
._* # From existing
|
| 135 |
+
.Spotlight-V100 # From existing
|
| 136 |
+
.Trashes # From existing
|
| 137 |
Thumbs.db
|
| 138 |
+
ehthumbs.db # From existing
|
| 139 |
+
|
| 140 |
+
# Secrets / Sensitive Config
|
| 141 |
+
secrets.txt # From existing
|
| 142 |
+
*.env # From existing, consolidated here
|
| 143 |
+
|
| 144 |
+
# Log files
|
| 145 |
+
*.log # Consolidated from Django and general
|
| 146 |
+
|
| 147 |
+
# Temporary files
|
| 148 |
+
*.tmp
|
| 149 |
+
temp/
|
| 150 |
+
tmp/
|
| 151 |
+
|
| 152 |
+
# ================================
|
| 153 |
+
# ML & Training specific ignores
|
| 154 |
+
# ================================
|
| 155 |
+
|
| 156 |
+
# Virtual environments (training) - Covered by general Environments and **/venv_training/
|
| 157 |
+
# training/venv_training/ # Redundant
|
| 158 |
+
|
| 159 |
+
# Model files & checkpoints (sehr groß!)
|
| 160 |
+
models/*/
|
| 161 |
+
training/models/
|
| 162 |
+
training/checkpoints/
|
| 163 |
+
# training/outputs/ # Moved to logs/outputs as it's often mixed
|
| 164 |
+
cardserver/models/lora-checkpoint/ # From user prompt
|
| 165 |
+
# Common model file extensions
|
| 166 |
+
*.bin
|
| 167 |
+
*.safetensors
|
| 168 |
+
*.ckpt
|
| 169 |
+
*.pt
|
| 170 |
+
*.pth
|
| 171 |
+
*.h5
|
| 172 |
+
*.hdf5
|
| 173 |
+
|
| 174 |
+
# PyTorch & CUDA cache
|
| 175 |
+
.torch/
|
| 176 |
+
.torchvision/
|
| 177 |
+
.triton/
|
| 178 |
+
# .cache/ # Moved to unit test/coverage reports as it's more general
|
| 179 |
+
|
| 180 |
+
# Training logs & outputs
|
| 181 |
+
training/logs/
|
| 182 |
+
training/runs/
|
| 183 |
+
training/wandb/ # Covers wandb/ from existing
|
| 184 |
+
training/outputs/ # Consolidated from existing
|
| 185 |
+
tensorboard_logs/
|
| 186 |
+
mlruns/
|
| 187 |
+
|
| 188 |
+
# Datasets (können groß sein)
|
| 189 |
+
training/data/large_datasets/
|
| 190 |
+
training/data/*.parquet
|
| 191 |
+
training/data/*.arrow
|
| 192 |
+
training/data/*.csv
|
| 193 |
+
training/data/*.json.gz
|
| 194 |
+
training/data/*.jsonl
|
| 195 |
+
|
| 196 |
+
# Dependencies that should be reinstalled (already covered by build/, dist/, etc.)
|
| 197 |
+
# node_modules/ # Covered
|
| 198 |
+
# dist/ # Covered
|
| 199 |
+
# build/ # Covered
|
Dockerfile
CHANGED
|
@@ -19,3 +19,4 @@ RUN chmod +x start.sh
|
|
| 19 |
|
| 20 |
# Start FastAPI with debug startup script
|
| 21 |
CMD ["./start.sh"]
|
|
|
|
|
|
| 19 |
|
| 20 |
# Start FastAPI with debug startup script
|
| 21 |
CMD ["./start.sh"]
|
| 22 |
+
|
app/api/v1/endpoints/download.py
CHANGED
|
@@ -16,9 +16,7 @@ async def download_generated_image(
|
|
| 16 |
Download a custom generated image using the card_id to find the image path from Supabase.
|
| 17 |
"""
|
| 18 |
try:
|
| 19 |
-
|
| 20 |
-
# Assuming your table is named 'horoscope_cards' and has 'id' and 'image_path'
|
| 21 |
-
response = supabase.table("horoscope_cards").select("image_path").eq("id", card_id).execute()
|
| 22 |
|
| 23 |
if not response.data:
|
| 24 |
raise HTTPException(status_code=404, detail="Card not found in database")
|
|
|
|
| 16 |
Download a custom generated image using the card_id to find the image path from Supabase.
|
| 17 |
"""
|
| 18 |
try:
|
| 19 |
+
response = supabase.table("cards").select("image_path").eq("id", card_id).execute()
|
|
|
|
|
|
|
| 20 |
|
| 21 |
if not response.data:
|
| 22 |
raise HTTPException(status_code=404, detail="Card not found in database")
|
app/api/v1/endpoints/generate.py
CHANGED
|
@@ -1,200 +1,124 @@
|
|
| 1 |
from fastapi import APIRouter, HTTPException, Depends
|
| 2 |
-
import os
|
| 3 |
from dotenv import load_dotenv
|
| 4 |
-
|
| 5 |
-
# Supabase Client importieren
|
| 6 |
from supabase import Client
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
from ..
|
| 10 |
-
from ....core.
|
| 11 |
-
from ....core.card_renderer import generate_card as render_horoscope_card # Umbenannt für Klarheit
|
| 12 |
from ....utils.qr_utils import generate_qr_code
|
| 13 |
-
from ....services.database import get_supabase_client,
|
| 14 |
-
from ....core.config import settings
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
|
| 18 |
-
from peft import PeftModel
|
| 19 |
|
| 20 |
load_dotenv()
|
| 21 |
router = APIRouter()
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
text_generator_pipeline = None
|
| 27 |
-
|
| 28 |
-
def load_language_model():
|
| 29 |
-
global text_generator_pipeline
|
| 30 |
-
if text_generator_pipeline is not None:
|
| 31 |
-
return text_generator_pipeline
|
| 32 |
-
|
| 33 |
-
try:
|
| 34 |
-
tokenizer = AutoTokenizer.from_pretrained(DEFAULT_MODEL_ID)
|
| 35 |
-
model = AutoModelForCausalLM.from_pretrained(
|
| 36 |
-
DEFAULT_MODEL_ID,
|
| 37 |
-
load_in_4bit=True,
|
| 38 |
-
device_map="auto" # "auto" für GPU-Nutzung falls verfügbar
|
| 39 |
-
)
|
| 40 |
-
# Versuche, LoRA-Gewichte zu laden
|
| 41 |
-
if settings.resolved_model_path.exists(): # Prüfen ob der Pfad existiert
|
| 42 |
-
try:
|
| 43 |
-
model = PeftModel.from_pretrained(model, str(settings.resolved_model_path))
|
| 44 |
-
except Exception as e:
|
| 45 |
-
print(f"Konnte LoRA-Modell nicht laden von {settings.resolved_model_path}: {e}. Fallback auf Basismodell.")
|
| 46 |
-
# Hier könnte man entscheiden, ob man einen Fehler wirft oder mit dem Basismodell weitermacht
|
| 47 |
-
else:
|
| 48 |
-
print(f"LoRA-Modellpfad {settings.resolved_model_path} nicht gefunden. Verwende Basismodell.")
|
| 49 |
-
|
| 50 |
-
text_generator_pipeline = pipeline(
|
| 51 |
-
"text-generation",
|
| 52 |
-
model=model,
|
| 53 |
-
tokenizer=tokenizer,
|
| 54 |
-
# Ggf. weitere Pipeline-Parameter hier
|
| 55 |
-
)
|
| 56 |
-
return text_generator_pipeline
|
| 57 |
-
except Exception as e:
|
| 58 |
-
# Loggen des Fehlers wäre hier gut
|
| 59 |
-
raise HTTPException(status_code=500, detail=f"Fehler beim Laden des Sprachmodells: {str(e)}")
|
| 60 |
-
|
| 61 |
-
# Sicherstellen, dass das Modell beim Start geladen wird (optional, kann auch lazy loading sein)
|
| 62 |
-
# load_language_model()
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
@router.post("/generate-horoscope", response_model=HoroscopeGenerateResponse)
|
| 66 |
-
async def generate_horoscope_endpoint(
|
| 67 |
-
request: HoroscopeGenerateRequest,
|
| 68 |
supabase: Client = Depends(get_supabase_client)
|
| 69 |
):
|
| 70 |
try:
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
lang = "de"
|
| 74 |
-
birthdate_str = request.date_of_birth.isoformat()
|
| 75 |
|
| 76 |
-
|
| 77 |
lang=lang,
|
| 78 |
-
|
| 79 |
terms=request.terms
|
| 80 |
)
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
# Parameter für die Textgenerierung (könnten auch aus request oder config kommen)
|
| 85 |
generation_params = {
|
| 86 |
-
"
|
| 87 |
-
"temperature":
|
| 88 |
-
"do_sample":
|
| 89 |
-
"
|
|
|
|
|
|
|
| 90 |
}
|
| 91 |
|
| 92 |
-
#
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
horoscope_text = generated_outputs[0]["generated_text"].strip()
|
| 102 |
-
|
| 103 |
-
# (Optional) Constraints prüfen, falls implementiert in app.core.constraints
|
| 104 |
-
# from app.core.constraints import check_constraints, generate_with_retry
|
| 105 |
-
# if not check_constraints(horoscope_text, request.terms):
|
| 106 |
-
# # Hier könnte eine Retry-Logik oder Fehlerbehandlung erfolgen
|
| 107 |
-
# pass
|
| 108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
-
#
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
symbol_ids = [1, 2, 3, 4] # Beispiel
|
| 114 |
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
#
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
text=horoscope_text,
|
| 131 |
-
# Übergabe der Pfade an card_renderer, falls es diese nicht selbst aus settings holt
|
| 132 |
base_images_path=settings.resolved_base_path,
|
| 133 |
symbols_images_path=settings.resolved_symbols_path,
|
| 134 |
font_path=settings.resolved_default_font_path,
|
| 135 |
output_path=settings.resolved_generated_path
|
| 136 |
)
|
| 137 |
-
card_image_filename = f"{card_file_id}.png" # Annahme, dass generate_card die ID ohne Extension zurückgibt
|
| 138 |
-
|
| 139 |
-
# 4. QR-Code erstellen
|
| 140 |
-
# Der Link im QR-Code sollte auf den Download-Endpunkt für das Bild zeigen
|
| 141 |
-
# z.B. HOST_URL/api/v1/download/image?image_id=<card_file_id>
|
| 142 |
-
# Die HOST_URL sollte aus den Settings kommen (ggf. anpassen in config.py)
|
| 143 |
-
host_url = os.getenv("HOST_URL", "http://localhost:8000") # Fallback, besser aus settings
|
| 144 |
-
|
| 145 |
-
# Der image_id Parameter für den Download-Endpunkt ist die card_file_id (UUID)
|
| 146 |
-
qr_code_content_link = f"{host_url}{settings.API_PREFIX}/download/image/{card_file_id}"
|
| 147 |
-
|
| 148 |
-
qr_code_filename_base = f"qr_{card_file_id}.png"
|
| 149 |
-
qr_code_save_path = settings.resolved_qr_code_path / qr_code_filename_base
|
| 150 |
-
|
| 151 |
-
generate_qr_code(link=qr_code_content_link, output_path=qr_code_save_path)
|
| 152 |
-
|
| 153 |
-
# 5. Daten in Supabase speichern
|
| 154 |
-
# Die Tabelle sollte 'horoscope_cards' heißen und Spalten gemäß HoroscopeCardData haben
|
| 155 |
-
# (id, terms, date_of_birth, horoscope_text, image_filename, qr_code_filename, qr_code_link)
|
| 156 |
-
save_horoscope_card(
|
| 157 |
-
supabase_client=supabase, # Übergeben des Supabase Clients
|
| 158 |
-
card_id=card_file_id, # UUID als String
|
| 159 |
-
terms=request.terms,
|
| 160 |
-
date_of_birth=birthdate_str,
|
| 161 |
-
horoscope_text=horoscope_text,
|
| 162 |
-
image_filename=card_image_filename,
|
| 163 |
-
qr_code_filename=qr_code_filename_base,
|
| 164 |
-
qr_code_link=qr_code_content_link
|
| 165 |
-
)
|
| 166 |
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
)
|
| 178 |
|
| 179 |
except FileNotFoundError as e:
|
| 180 |
-
|
| 181 |
-
raise HTTPException(status_code=500, detail=f"
|
| 182 |
except HTTPException as e:
|
| 183 |
-
# Weiterleiten von HTTPExceptions (z.B. vom Modell-Laden)
|
| 184 |
raise e
|
| 185 |
except Exception as e:
|
| 186 |
-
# Allgemeiner Fehler, hier wäre besseres Logging wichtig
|
| 187 |
-
print(f"Unerwarteter Fehler im generate_horoscope_endpoint: {type(e).__name__} - {str(e)}")
|
| 188 |
-
# Stacktrace loggen für Debugging:
|
| 189 |
import traceback
|
|
|
|
| 190 |
traceback.print_exc()
|
| 191 |
-
raise HTTPException(status_code=500, detail=f"
|
| 192 |
-
|
| 193 |
-
# Alte Endpunkt-Definition entfernen oder anpassen, falls der Pfad gleich bleiben soll
|
| 194 |
-
# Der Pfad wurde zu "/generate-horoscope" geändert, um Konflikte zu vermeiden,
|
| 195 |
-
# und das Request/Response-Modell wurde angepasst.
|
| 196 |
-
|
| 197 |
-
# @router.post("/generate", response_model=GenerateResponse)
|
| 198 |
-
# async def generate_text(request: GenerateRequest):
|
| 199 |
-
# # ... alter Code ...
|
| 200 |
-
# pass
|
|
|
|
| 1 |
from fastapi import APIRouter, HTTPException, Depends
|
|
|
|
| 2 |
from dotenv import load_dotenv
|
|
|
|
|
|
|
| 3 |
from supabase import Client
|
| 4 |
+
import uuid
|
| 5 |
+
from ..schemas.card_schemas import CardGenerateRequest, CardGenerateResponse
|
| 6 |
+
from ....core.generator import build_prompt, get_constellation
|
| 7 |
+
from ....core.card_renderer import generate_card as render_card
|
|
|
|
| 8 |
from ....utils.qr_utils import generate_qr_code
|
| 9 |
+
from ....services.database import get_supabase_client, save_card
|
| 10 |
+
from ....core.config import settings
|
| 11 |
+
from ....core.model_loader import get_generator
|
| 12 |
+
from ....core.constraints import generate_with_retry, check_constraints
|
|
|
|
|
|
|
| 13 |
|
| 14 |
load_dotenv()
|
| 15 |
router = APIRouter()
|
| 16 |
|
| 17 |
+
@router.post("/generate", response_model=CardGenerateResponse)
|
| 18 |
+
async def generate_endpoint(
|
| 19 |
+
request: CardGenerateRequest,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
supabase: Client = Depends(get_supabase_client)
|
| 21 |
):
|
| 22 |
try:
|
| 23 |
+
lang = request.lang or "de"
|
| 24 |
+
input_date_str = request.card_date.isoformat()
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
card_prompt = build_prompt(
|
| 27 |
lang=lang,
|
| 28 |
+
card_date=input_date_str,
|
| 29 |
terms=request.terms
|
| 30 |
)
|
| 31 |
|
| 32 |
+
llm_pipeline = get_generator()
|
| 33 |
+
|
|
|
|
| 34 |
generation_params = {
|
| 35 |
+
"max_new_tokens": settings.GENERATION_MAX_NEW_TOKENS,
|
| 36 |
+
"temperature": settings.GENERATION_TEMPERATURE,
|
| 37 |
+
"do_sample": settings.GENERATION_DO_SAMPLE,
|
| 38 |
+
"top_k": settings.GENERATION_TOP_K,
|
| 39 |
+
"top_p": settings.GENERATION_TOP_P,
|
| 40 |
+
"return_full_text": False
|
| 41 |
}
|
| 42 |
|
| 43 |
+
# Verwende generate_with_retry aus constraints.py
|
| 44 |
+
card_text = generate_with_retry(
|
| 45 |
+
prompt=card_prompt,
|
| 46 |
+
generator=llm_pipeline,
|
| 47 |
+
terms=request.terms,
|
| 48 |
+
max_retries=settings.GENERATION_MAX_RETRIES,
|
| 49 |
+
generation_params=generation_params
|
| 50 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
+
if card_text == "Leider konnte kein gültiger Text erzeugt werden." or not check_constraints(card_text, request.terms):
|
| 53 |
+
raise HTTPException(
|
| 54 |
+
status_code=500,
|
| 55 |
+
detail="Kartentext konnte nicht generiert werden oder erfüllt nicht die Bedingungen."
|
| 56 |
+
)
|
| 57 |
|
| 58 |
+
# 4. QR-Code generieren
|
| 59 |
+
card_id_for_url = str(uuid.uuid4())
|
| 60 |
+
qr_content_url = f"{settings.FRONTEND_BASE_URL}/card/{card_id_for_url}"
|
|
|
|
| 61 |
|
| 62 |
+
qr_code_file_id = generate_qr_code(
|
| 63 |
+
data=qr_content_url,
|
| 64 |
+
output_path=settings.resolved_qr_code_path,
|
| 65 |
+
size=settings.QR_CODE_SIZE
|
| 66 |
+
)
|
| 67 |
+
qr_code_url = f"{settings.API_PREFIX}/static/images/qr/{qr_code_file_id}.png"
|
| 68 |
+
|
| 69 |
+
# 5. Karte rendern
|
| 70 |
+
card_design_id_to_render = request.card_design_id_override or 1
|
| 71 |
+
symbol_ids_to_render = request.symbol_ids_override or [1, 2]
|
| 72 |
+
|
| 73 |
+
card_file_id = render_card(
|
| 74 |
+
card_design_id=card_design_id_to_render,
|
| 75 |
+
symbol_ids=symbol_ids_to_render,
|
| 76 |
+
text=card_text,
|
|
|
|
|
|
|
| 77 |
base_images_path=settings.resolved_base_path,
|
| 78 |
symbols_images_path=settings.resolved_symbols_path,
|
| 79 |
font_path=settings.resolved_default_font_path,
|
| 80 |
output_path=settings.resolved_generated_path
|
| 81 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
+
card_data_for_db = {
|
| 84 |
+
"terms": request.terms,
|
| 85 |
+
"card_date": input_date_str,
|
| 86 |
+
"card_text": card_text,
|
| 87 |
+
"image_filename": f"{card_file_id}.png",
|
| 88 |
+
"qr_code_filename": f"{qr_code_file_id}.png",
|
| 89 |
+
"qr_code_link": qr_content_url,
|
| 90 |
+
"session_id": uuid.UUID(card_id_for_url),
|
| 91 |
+
"lang": lang,
|
| 92 |
+
"prompt_text": card_prompt,
|
| 93 |
+
"model_info": llm_pipeline.model.config.to_dict() if hasattr(llm_pipeline, 'model') and hasattr(llm_pipeline.model, 'config') else {"name": str(type(llm_pipeline.model).__name__)},
|
| 94 |
+
"generation_params": generation_params
|
| 95 |
+
}
|
| 96 |
|
| 97 |
+
try:
|
| 98 |
+
db_response = await save_card(supabase, card_data_for_db)
|
| 99 |
+
db_id = None
|
| 100 |
+
if db_response and hasattr(db_response, 'data') and db_response.data and len(db_response.data) > 0:
|
| 101 |
+
db_id = str(db_response.data[0].get('id'))
|
| 102 |
+
elif isinstance(db_response, list) and db_response and isinstance(db_response[0], dict):
|
| 103 |
+
db_id = str(db_response[0].get('id'))
|
| 104 |
+
|
| 105 |
+
except Exception as e:
|
| 106 |
+
print(f"Fehler beim Speichern der Karte in Supabase: {e}")
|
| 107 |
+
|
| 108 |
+
return CardGenerateResponse(
|
| 109 |
+
message="Horoskopkarte erfolgreich generiert.",
|
| 110 |
+
# Der card_id in der Response sollte nun auch die neue UUID sein, wenn db_id nicht verfügbar ist
|
| 111 |
+
card_id=db_id if db_id else card_id_for_url,
|
| 112 |
+
qr_code_image_url=qr_code_url
|
| 113 |
)
|
| 114 |
|
| 115 |
except FileNotFoundError as e:
|
| 116 |
+
print(f"FileNotFoundError in generate_endpoint: {e}")
|
| 117 |
+
raise HTTPException(status_code=500, detail=f"Ein benötigtes Template oder eine Datei wurde nicht gefunden: {e.filename}")
|
| 118 |
except HTTPException as e:
|
|
|
|
| 119 |
raise e
|
| 120 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
| 121 |
import traceback
|
| 122 |
+
print(f"Unerwarteter Fehler in generate_endpoint: {type(e).__name__} - {str(e)}")
|
| 123 |
traceback.print_exc()
|
| 124 |
+
raise HTTPException(status_code=500, detail=f"Ein interner Fehler ist aufgetreten: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/v1/schemas/{horoscope_schemas.py → card_schemas.py}
RENAMED
|
@@ -1,5 +1,3 @@
|
|
| 1 |
-
# app/schemas/horoscope_schemas.py
|
| 2 |
-
|
| 3 |
from pydantic import BaseModel, Field
|
| 4 |
from datetime import date
|
| 5 |
from typing import List, Optional
|
|
@@ -7,38 +5,57 @@ import uuid
|
|
| 7 |
|
| 8 |
# --- Request Models ---
|
| 9 |
|
| 10 |
-
class
|
| 11 |
"""
|
| 12 |
-
Schema für die Anfrage zur Generierung einer personalisierten
|
| 13 |
"""
|
| 14 |
terms: List[str] = Field(
|
| 15 |
...,
|
| 16 |
min_length=5,
|
| 17 |
max_length=5,
|
| 18 |
-
description="Fünf Schlüsselbegriffe oder Konzepte für
|
| 19 |
example=["Liebe", "Erfolg", "Glück", "Herausforderung", "Wachstum"]
|
| 20 |
)
|
| 21 |
-
|
| 22 |
...,
|
| 23 |
-
description="Das
|
| 24 |
example="1990-05-15"
|
| 25 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
# --- Response Models ---
|
| 28 |
|
| 29 |
-
class
|
| 30 |
"""
|
| 31 |
-
Schema für die Antwort nach erfolgreicher Anfrage zur
|
| 32 |
Enthält den Link zum QR-Code und die ID der generierten Karte.
|
| 33 |
"""
|
| 34 |
card_id: str = Field(
|
| 35 |
...,
|
| 36 |
-
description="Die eindeutige ID der generierten
|
| 37 |
example="a1b2c3d4-e5f6-7890-1234-567890abcdef"
|
| 38 |
)
|
| 39 |
qr_code_image_url: str = Field(
|
| 40 |
...,
|
| 41 |
-
description="Die URL, unter der das QR-Code-Bild (das den Download-Link zur
|
| 42 |
example="http://localhost:8000/static/qr_codes/qr_code_a1b2c3d4-e5f6-7890-1234-567890abcdef.png"
|
| 43 |
)
|
| 44 |
message: str = Field(
|
|
@@ -46,16 +63,21 @@ class HoroscopeGenerateResponse(BaseModel):
|
|
| 46 |
description="Eine Bestätigungsnachricht."
|
| 47 |
)
|
| 48 |
|
| 49 |
-
class
|
| 50 |
"""
|
| 51 |
-
Schema für die internen Daten einer gespeicherten
|
| 52 |
-
|
| 53 |
"""
|
| 54 |
-
id:
|
| 55 |
-
terms: List[str] = Field(..., description="Die ursprünglichen fünf Schlüsselbegriffe.")
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
qr_code_link: str = Field(..., description="Der URL, der im QR-Code kodiert ist.")
|
| 61 |
-
created_at:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from pydantic import BaseModel, Field
|
| 2 |
from datetime import date
|
| 3 |
from typing import List, Optional
|
|
|
|
| 5 |
|
| 6 |
# --- Request Models ---
|
| 7 |
|
| 8 |
+
class CardGenerateRequest(BaseModel):
|
| 9 |
"""
|
| 10 |
+
Schema für die Anfrage zur Generierung einer personalisierten Karte.
|
| 11 |
"""
|
| 12 |
terms: List[str] = Field(
|
| 13 |
...,
|
| 14 |
min_length=5,
|
| 15 |
max_length=5,
|
| 16 |
+
description="Fünf Schlüsselbegriffe oder Konzepte für die Karte.",
|
| 17 |
example=["Liebe", "Erfolg", "Glück", "Herausforderung", "Wachstum"]
|
| 18 |
)
|
| 19 |
+
card_date: date = Field(
|
| 20 |
...,
|
| 21 |
+
description="Das Datum für die Karte im Format JJJJ-MM-TT (z.B. Geburtsdatum oder ein anderes relevantes Datum).",
|
| 22 |
example="1990-05-15"
|
| 23 |
)
|
| 24 |
+
lang: Optional[str] = Field(
|
| 25 |
+
"de",
|
| 26 |
+
description="Sprache für den Kartentext, Standardwert ist 'de'.",
|
| 27 |
+
example="en"
|
| 28 |
+
)
|
| 29 |
+
session_id: uuid.UUID = Field(
|
| 30 |
+
default_factory=uuid.uuid4,
|
| 31 |
+
description="Session-ID zur Verfolgung der Anfrage, Standardwert ist eine neue UUID."
|
| 32 |
+
)
|
| 33 |
+
card_design_id_override: Optional[int] = Field(
|
| 34 |
+
None,
|
| 35 |
+
description="Überschreibung der Karten-Design-ID. Beispiel: 1",
|
| 36 |
+
example=1
|
| 37 |
+
)
|
| 38 |
+
symbol_ids_override: Optional[List[int]] = Field(
|
| 39 |
+
None,
|
| 40 |
+
description="Überschreibung der Symbol-IDs. Beispiel: [1, 2, 3]",
|
| 41 |
+
example=[1, 2]
|
| 42 |
+
)
|
| 43 |
|
| 44 |
# --- Response Models ---
|
| 45 |
|
| 46 |
+
class CardGenerateResponse(BaseModel):
|
| 47 |
"""
|
| 48 |
+
Schema für die Antwort nach erfolgreicher Anfrage zur Kartengenerierung.
|
| 49 |
Enthält den Link zum QR-Code und die ID der generierten Karte.
|
| 50 |
"""
|
| 51 |
card_id: str = Field(
|
| 52 |
...,
|
| 53 |
+
description="Die eindeutige ID der generierten Karte. Wird für den Download benötigt.",
|
| 54 |
example="a1b2c3d4-e5f6-7890-1234-567890abcdef"
|
| 55 |
)
|
| 56 |
qr_code_image_url: str = Field(
|
| 57 |
...,
|
| 58 |
+
description="Die URL, unter der das QR-Code-Bild (das den Download-Link zur Karte enthält) direkt abrufbar ist.",
|
| 59 |
example="http://localhost:8000/static/qr_codes/qr_code_a1b2c3d4-e5f6-7890-1234-567890abcdef.png"
|
| 60 |
)
|
| 61 |
message: str = Field(
|
|
|
|
| 63 |
description="Eine Bestätigungsnachricht."
|
| 64 |
)
|
| 65 |
|
| 66 |
+
class CardData(BaseModel):
|
| 67 |
"""
|
| 68 |
+
Schema für die internen Daten einer gespeicherten Karte (z.B. aus Supabase),
|
| 69 |
+
basierend auf der Tabelle 'cards'.
|
| 70 |
"""
|
| 71 |
+
id: int = Field(..., description="Eindeutige ID der Karte (bigint).")
|
| 72 |
+
terms: List[str] = Field(..., description="Die ursprünglichen fünf Schlüsselbegriffe (text[]).")
|
| 73 |
+
card_date: date = Field(..., description="Das Geburtsdatum des Nutzers (timestamp with time zone, Pydantic uses date for simplicity here, conversion might be needed).")
|
| 74 |
+
card_text: str = Field(..., description="Der generierte Kartentext (text).")
|
| 75 |
+
image_filename: str = Field(..., description="Dateiname des generierten Kartenbildes (text).")
|
| 76 |
+
qr_code_filename: str = Field(..., description="Dateiname des generierten QR-Code-Bildes (text).")
|
| 77 |
+
qr_code_link: str = Field(..., description="Der URL, der im QR-Code kodiert ist (text).")
|
| 78 |
+
created_at: date = Field(..., description="Zeitpunkt der Erstellung des Eintrags (timestamp with time zone, Pydantic uses date).")
|
| 79 |
+
session_id: Optional[uuid.UUID] = Field(None, description="Session ID used for the request.")
|
| 80 |
+
lang: Optional[str] = Field(None, description="Language used for generation.")
|
| 81 |
+
prompt_text: Optional[str] = Field(None, description="The prompt text used for generation.")
|
| 82 |
+
model_info: Optional[dict] = Field(None, description="Information about the model used.")
|
| 83 |
+
generation_params: Optional[dict] = Field(None, description="Parameters used for text generation.")
|
app/core/card_renderer.py
CHANGED
|
@@ -3,7 +3,7 @@ import uuid
|
|
| 3 |
from pathlib import Path
|
| 4 |
|
| 5 |
def generate_card(
|
| 6 |
-
|
| 7 |
symbol_ids: list[int],
|
| 8 |
text: str,
|
| 9 |
base_images_path: Path,
|
|
@@ -18,10 +18,10 @@ def generate_card(
|
|
| 18 |
"""
|
| 19 |
try:
|
| 20 |
# Basiskarte laden
|
| 21 |
-
base_image_file = base_images_path / f"{
|
| 22 |
if not base_image_file.exists():
|
| 23 |
raise FileNotFoundError(f"Basiskartenbild nicht gefunden: {base_image_file}")
|
| 24 |
-
|
| 25 |
|
| 26 |
# Symbole hinzufügen
|
| 27 |
# Die Positionierung hier ist ein Beispiel und muss ggf. angepasst werden
|
|
@@ -40,14 +40,14 @@ def generate_card(
|
|
| 40 |
# Beispielhafte Positionierung - muss ggf. verfeinert werden
|
| 41 |
# Annahme: Symbole werden nebeneinander platziert
|
| 42 |
position = (current_x, symbol_y_start)
|
| 43 |
-
|
| 44 |
current_x += symbol_img.width + symbol_spacing
|
| 45 |
|
| 46 |
# Text hinzufügen
|
| 47 |
if not font_path.exists():
|
| 48 |
raise FileNotFoundError(f"Schriftart nicht gefunden: {font_path}")
|
| 49 |
|
| 50 |
-
draw = ImageDraw.Draw(
|
| 51 |
try:
|
| 52 |
font_size = 20 # Beispiel
|
| 53 |
font = ImageFont.truetype(str(font_path), font_size)
|
|
@@ -58,11 +58,11 @@ def generate_card(
|
|
| 58 |
# Textpositionierung und -umbruch (vereinfacht)
|
| 59 |
# Für besseren Textumbruch wären komplexere Logiken nötig (z.B. textwrap Modul)
|
| 60 |
text_x = 50
|
| 61 |
-
text_y =
|
| 62 |
# draw.text((text_x, text_y), text, font=font, fill=(0, 0, 0, 255)) # Schwarz, voll opak
|
| 63 |
|
| 64 |
# Einfacher Textumbruch
|
| 65 |
-
max_width =
|
| 66 |
lines = []
|
| 67 |
words = text.split()
|
| 68 |
current_line = ""
|
|
@@ -92,7 +92,7 @@ def generate_card(
|
|
| 92 |
file_id = str(uuid.uuid4()) # Eindeutige ID für die Datei
|
| 93 |
output_file_path = output_path / f"{file_id}.png"
|
| 94 |
|
| 95 |
-
|
| 96 |
|
| 97 |
return file_id # Gibt die UUID (ohne .png) zurück
|
| 98 |
|
|
|
|
| 3 |
from pathlib import Path
|
| 4 |
|
| 5 |
def generate_card(
|
| 6 |
+
card_design_id: int,
|
| 7 |
symbol_ids: list[int],
|
| 8 |
text: str,
|
| 9 |
base_images_path: Path,
|
|
|
|
| 18 |
"""
|
| 19 |
try:
|
| 20 |
# Basiskarte laden
|
| 21 |
+
base_image_file = base_images_path / f"{card_design_id}.png"
|
| 22 |
if not base_image_file.exists():
|
| 23 |
raise FileNotFoundError(f"Basiskartenbild nicht gefunden: {base_image_file}")
|
| 24 |
+
card_design_img = Image.open(base_image_file).convert("RGBA")
|
| 25 |
|
| 26 |
# Symbole hinzufügen
|
| 27 |
# Die Positionierung hier ist ein Beispiel und muss ggf. angepasst werden
|
|
|
|
| 40 |
# Beispielhafte Positionierung - muss ggf. verfeinert werden
|
| 41 |
# Annahme: Symbole werden nebeneinander platziert
|
| 42 |
position = (current_x, symbol_y_start)
|
| 43 |
+
card_design_img.paste(symbol_img, position, symbol_img)
|
| 44 |
current_x += symbol_img.width + symbol_spacing
|
| 45 |
|
| 46 |
# Text hinzufügen
|
| 47 |
if not font_path.exists():
|
| 48 |
raise FileNotFoundError(f"Schriftart nicht gefunden: {font_path}")
|
| 49 |
|
| 50 |
+
draw = ImageDraw.Draw(card_design_img)
|
| 51 |
try:
|
| 52 |
font_size = 20 # Beispiel
|
| 53 |
font = ImageFont.truetype(str(font_path), font_size)
|
|
|
|
| 58 |
# Textpositionierung und -umbruch (vereinfacht)
|
| 59 |
# Für besseren Textumbruch wären komplexere Logiken nötig (z.B. textwrap Modul)
|
| 60 |
text_x = 50
|
| 61 |
+
text_y = card_design_img.height - 150 # Beispiel Y-Position von unten
|
| 62 |
# draw.text((text_x, text_y), text, font=font, fill=(0, 0, 0, 255)) # Schwarz, voll opak
|
| 63 |
|
| 64 |
# Einfacher Textumbruch
|
| 65 |
+
max_width = card_design_img.width - (2 * text_x) # Maximale Breite für Text
|
| 66 |
lines = []
|
| 67 |
words = text.split()
|
| 68 |
current_line = ""
|
|
|
|
| 92 |
file_id = str(uuid.uuid4()) # Eindeutige ID für die Datei
|
| 93 |
output_file_path = output_path / f"{file_id}.png"
|
| 94 |
|
| 95 |
+
card_design_img.save(output_file_path)
|
| 96 |
|
| 97 |
return file_id # Gibt die UUID (ohne .png) zurück
|
| 98 |
|
app/core/config.py
CHANGED
|
@@ -33,6 +33,23 @@ class Settings(BaseSettings):
|
|
| 33 |
PROJECT_NAME: str = "Horoskopkarten-Generator"
|
| 34 |
APP_VERSION: str = "1.0.0"
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
@property
|
| 37 |
def resolved_model_path(self) -> Path:
|
| 38 |
return Path(self.MODEL_PATH)
|
|
|
|
| 33 |
PROJECT_NAME: str = "Horoskopkarten-Generator"
|
| 34 |
APP_VERSION: str = "1.0.0"
|
| 35 |
|
| 36 |
+
# Generation parameters
|
| 37 |
+
GENERATION_MAX_NEW_TOKENS: int = 250
|
| 38 |
+
GENERATION_TEMPERATURE: float = 0.75
|
| 39 |
+
GENERATION_DO_SAMPLE: bool = True
|
| 40 |
+
GENERATION_TOP_K: int = 50
|
| 41 |
+
GENERATION_TOP_P: float = 0.95
|
| 42 |
+
GENERATION_MAX_RETRIES: int = 3
|
| 43 |
+
|
| 44 |
+
# QR Code settings
|
| 45 |
+
FRONTEND_BASE_URL: str = "http://localhost:3000" # Beispiel, anpassen!
|
| 46 |
+
QR_CODE_SIZE: int = 200 # Größe des QR-Codes in Pixeln
|
| 47 |
+
|
| 48 |
+
# Model and Tokenizer settings
|
| 49 |
+
# HUGGING_FACE_HUB_TOKEN: str | None = os.getenv("HUGGING_FACE_HUB_TOKEN") # Handled by HF libs
|
| 50 |
+
LORA_MODEL_REPO_ID: str | None = None # Beispiel: "your-username/your-lora-model-repo"
|
| 51 |
+
# LORA_MODEL_REVISION: str = "main" # Beispiel: "main" oder ein spezifischer Commit/Tag
|
| 52 |
+
|
| 53 |
@property
|
| 54 |
def resolved_model_path(self) -> Path:
|
| 55 |
return Path(self.MODEL_PATH)
|
app/core/constraints.py
CHANGED
|
@@ -2,11 +2,48 @@
|
|
| 2 |
# Constraints: Begriffe prüfen, Filter
|
| 3 |
|
| 4 |
def check_constraints(output: str, terms: list[str]) -> bool:
|
|
|
|
|
|
|
| 5 |
return all(term.lower() in output.lower() for term in terms)
|
| 6 |
|
| 7 |
-
def generate_with_retry(prompt, generator, terms, max_retries=3):
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
return "Leider konnte kein gültiger Text erzeugt werden."
|
|
|
|
| 2 |
# Constraints: Begriffe prüfen, Filter
|
| 3 |
|
| 4 |
def check_constraints(output: str, terms: list[str]) -> bool:
|
| 5 |
+
if not terms: # Wenn keine Begriffe vorgegeben sind, ist die Bedingung immer erfüllt
|
| 6 |
+
return True
|
| 7 |
return all(term.lower() in output.lower() for term in terms)
|
| 8 |
|
| 9 |
+
def generate_with_retry(prompt: str, generator, terms: list[str], max_retries: int = 3, generation_params: dict | None = None):
|
| 10 |
+
"""
|
| 11 |
+
Generiert Text mit Wiederholungsversuchen, bis die Bedingungen (Constraints) erfüllt sind.
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
prompt (str): Der Eingabe-Prompt für den Generator.
|
| 15 |
+
generator: Die Text-Generierungs-Pipeline oder -Funktion.
|
| 16 |
+
terms (list[str]): Eine Liste von Begriffen, die im generierten Text enthalten sein müssen.
|
| 17 |
+
max_retries (int): Maximale Anzahl von Wiederholungsversuchen.
|
| 18 |
+
generation_params (dict | None): Zusätzliche Parameter für den Generator.
|
| 19 |
+
|
| 20 |
+
Returns:
|
| 21 |
+
str: Der generierte Text, der die Bedingungen erfüllt, oder eine Fehlermeldung.
|
| 22 |
+
"""
|
| 23 |
+
if generation_params is None:
|
| 24 |
+
generation_params = {}
|
| 25 |
+
|
| 26 |
+
for attempt in range(max_retries):
|
| 27 |
+
try:
|
| 28 |
+
# Stelle sicher, dass der Prompt als erster Parameter übergeben wird,
|
| 29 |
+
# und generation_params als Keyword-Argumente.
|
| 30 |
+
# Die meisten Hugging Face Pipelines erwarten den Prompt als positional argument.
|
| 31 |
+
responses = generator(prompt, **generation_params)
|
| 32 |
+
|
| 33 |
+
# Die Struktur der Antwort kann variieren. Üblich ist eine Liste von Diktionären.
|
| 34 |
+
if responses and isinstance(responses, list) and responses[0].get("generated_text"):
|
| 35 |
+
generated_text = responses[0]["generated_text"].strip()
|
| 36 |
+
if check_constraints(generated_text, terms):
|
| 37 |
+
return generated_text
|
| 38 |
+
else:
|
| 39 |
+
# Fallback, falls die Antwortstruktur unerwartet ist oder kein Text generiert wurde
|
| 40 |
+
print(f"Unerwartete Antwortstruktur vom Generator bei Versuch {attempt + 1}: {responses}")
|
| 41 |
+
generated_text = "" # oder None, je nachdem wie der Rest des Codes damit umgeht
|
| 42 |
+
|
| 43 |
+
except Exception as e:
|
| 44 |
+
print(f"Fehler bei der Textgenerierung (Versuch {attempt + 1}/{max_retries}): {e}")
|
| 45 |
+
# Optional: Kurze Pause vor dem nächsten Versuch
|
| 46 |
+
# import time
|
| 47 |
+
# time.sleep(0.5)
|
| 48 |
+
|
| 49 |
return "Leider konnte kein gültiger Text erzeugt werden."
|
app/core/generator.py
CHANGED
|
@@ -7,31 +7,39 @@ import datetime
|
|
| 7 |
218 = 02.18. → Ende Wassermann
|
| 8 |
"""
|
| 9 |
|
| 10 |
-
def
|
| 11 |
"""Leitet das Sternzeichen aus dem Geburtsdatum ab."""
|
| 12 |
date = datetime.datetime.strptime(birthdate, "%Y-%m-%d").date()
|
| 13 |
-
|
| 14 |
(120, "Steinbock"), (218, "Wassermann"), (320, "Fische"), (420, "Widder"),
|
| 15 |
(521, "Stier"), (621, "Zwillinge"), (722, "Krebs"), (823, "Löwe"),
|
| 16 |
(923, "Jungfrau"), (1023, "Waage"), (1122, "Skorpion"), (1222, "Schütze"), (1231, "Steinbock")
|
| 17 |
]
|
| 18 |
date_as_number = int(f"{date.month:02d}{date.day:02d}")
|
| 19 |
-
for boundary, sign in
|
| 20 |
if date_as_number <= boundary:
|
| 21 |
return sign
|
| 22 |
return "Steinbock"
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
if not template_path.exists():
|
| 27 |
-
raise FileNotFoundError(f"Template nicht gefunden: {template_path}")
|
| 28 |
-
|
| 29 |
-
template = Template(template_path.read_text(encoding="utf-8"))
|
| 30 |
-
sternzeichen = get_zodiac(birthdate)
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
218 = 02.18. → Ende Wassermann
|
| 8 |
"""
|
| 9 |
|
| 10 |
+
def get_constellation(birthdate: str) -> str:
|
| 11 |
"""Leitet das Sternzeichen aus dem Geburtsdatum ab."""
|
| 12 |
date = datetime.datetime.strptime(birthdate, "%Y-%m-%d").date()
|
| 13 |
+
constellations = [
|
| 14 |
(120, "Steinbock"), (218, "Wassermann"), (320, "Fische"), (420, "Widder"),
|
| 15 |
(521, "Stier"), (621, "Zwillinge"), (722, "Krebs"), (823, "Löwe"),
|
| 16 |
(923, "Jungfrau"), (1023, "Waage"), (1122, "Skorpion"), (1222, "Schütze"), (1231, "Steinbock")
|
| 17 |
]
|
| 18 |
date_as_number = int(f"{date.month:02d}{date.day:02d}")
|
| 19 |
+
for boundary, sign in constellations:
|
| 20 |
if date_as_number <= boundary:
|
| 21 |
return sign
|
| 22 |
return "Steinbock"
|
| 23 |
|
| 24 |
+
PROMPT_TEMPLATE = Template("""
|
| 25 |
+
Du bist ein Sexoskop-Orakel. Erstelle einen kurzen, prägnanten Horoskoptext (maximal 1-2 Sätze) für eine Person, basierend auf folgenden Informationen:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
+
Sprache: {{ lang }}
|
| 28 |
+
Datum: {{ card_date }} {# Geändert von birthdate #}
|
| 29 |
+
Sternzeichen: {{ constellation }}
|
| 30 |
+
Schlüsselbegriffe: {{ terms_str }}
|
| 31 |
+
|
| 32 |
+
Der Text soll die Schlüsselbegriffe kreativ und subtil einbinden und zum angegebenen Datum passen.
|
| 33 |
+
Antworte nur mit dem Horoskoptext.
|
| 34 |
+
""")
|
| 35 |
+
|
| 36 |
+
def build_prompt(lang: str, card_date: str, terms: list[str]) -> str:
|
| 37 |
+
"""Baut den Prompt für das LLM basierend auf Geburtsdatum und Begriffen."""
|
| 38 |
+
constellation = get_constellation(card_date)
|
| 39 |
+
terms_str = ", ".join(terms)
|
| 40 |
+
return PROMPT_TEMPLATE.render(
|
| 41 |
+
lang=lang,
|
| 42 |
+
card_date=card_date,
|
| 43 |
+
constellation=constellation,
|
| 44 |
+
terms_str=terms_str
|
| 45 |
+
)
|
app/core/hf_api.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from huggingface_hub import HfApi, HfFolder, Repository, create_repo, get_full_repo_name
|
| 3 |
+
from huggingface_hub.utils import HfHubHTTPError
|
| 4 |
+
import logging
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import json # Added for example usage
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
class HuggingFaceWrapper:
|
| 11 |
+
"""
|
| 12 |
+
A wrapper for interacting with the Hugging Face Hub API.
|
| 13 |
+
Handles authentication, model and dataset uploads/downloads,
|
| 14 |
+
and repository creation.
|
| 15 |
+
"""
|
| 16 |
+
def __init__(self, token: str | None = None, default_repo_prefix: str = "museum-sexoskop"):
|
| 17 |
+
"""
|
| 18 |
+
Initializes the HuggingFaceWrapper.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
token: Your Hugging Face API token. If None, it will try to use
|
| 22 |
+
a token saved locally via `huggingface-cli login`.
|
| 23 |
+
default_repo_prefix: A default prefix for repository names.
|
| 24 |
+
"""
|
| 25 |
+
self.api = HfApi()
|
| 26 |
+
if token:
|
| 27 |
+
self.token = token
|
| 28 |
+
# Note: HfApi uses the token from HfFolder by default if logged in.
|
| 29 |
+
# To explicitly use a provided token for all operations,
|
| 30 |
+
# some HfApi methods accept it directly.
|
| 31 |
+
# For operations like Repository, ensure the environment or HfFolder is set.
|
| 32 |
+
HfFolder.save_token(token)
|
| 33 |
+
logger.info("Hugging Face token saved for the session.")
|
| 34 |
+
else:
|
| 35 |
+
self.token = HfFolder.get_token()
|
| 36 |
+
if not self.token:
|
| 37 |
+
logger.warning("No Hugging Face token provided or found locally. "
|
| 38 |
+
"Please login using `huggingface-cli login` or provide a token.")
|
| 39 |
+
else:
|
| 40 |
+
logger.info("Using locally saved Hugging Face token.")
|
| 41 |
+
|
| 42 |
+
self.default_repo_prefix = default_repo_prefix
|
| 43 |
+
|
| 44 |
+
def _get_full_repo_id(self, repo_name: str, repo_type: str | None = None) -> str:
|
| 45 |
+
"""Helper to construct full repo ID, ensuring it includes the username/org."""
|
| 46 |
+
# If repo_name already contains a slash, it's likely a full ID (user/repo or org/repo)
|
| 47 |
+
if "/" in repo_name:
|
| 48 |
+
# Further check: if it doesn't have the prefix and prefix is defined,
|
| 49 |
+
# this might be an attempt to use a non-prefixed name directly.
|
| 50 |
+
# For simplicity, we assume if '/' is present, it's a deliberate full ID.
|
| 51 |
+
return repo_name
|
| 52 |
+
|
| 53 |
+
user_or_org = self.api.whoami(token=self.token).get("name") if self.token else None
|
| 54 |
+
if not user_or_org:
|
| 55 |
+
raise ValueError("Could not determine Hugging Face username/org. Ensure you are logged in or token is valid.")
|
| 56 |
+
|
| 57 |
+
effective_repo_name = repo_name
|
| 58 |
+
if self.default_repo_prefix and not repo_name.startswith(self.default_repo_prefix):
|
| 59 |
+
effective_repo_name = f"{self.default_repo_prefix}-{repo_name}"
|
| 60 |
+
|
| 61 |
+
return f"{user_or_org}/{effective_repo_name}"
|
| 62 |
+
|
| 63 |
+
def create_repository(self, repo_name: str, repo_type: str | None = None, private: bool = True, organization: str | None = None) -> str:
|
| 64 |
+
"""
|
| 65 |
+
Creates a new repository on the Hugging Face Hub.
|
| 66 |
+
If organization is provided, repo_name should be the base name.
|
| 67 |
+
If organization is None, repo_name can be a base name (username will be prepended)
|
| 68 |
+
or a full name like 'username/repo_name'.
|
| 69 |
+
|
| 70 |
+
Args:
|
| 71 |
+
repo_name: The name of the repository. Can be 'my-repo' or 'username/my-repo'.
|
| 72 |
+
repo_type: Type of the repository ('model', 'dataset', 'space').
|
| 73 |
+
private: Whether the repository should be private.
|
| 74 |
+
organization: Optional organization name to create the repo under. If provided,
|
| 75 |
+
repo_name should be the base name for the repo within that org.
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
The full repository ID (e.g., "username/repo_name" or "orgname/repo_name").
|
| 79 |
+
"""
|
| 80 |
+
if organization:
|
| 81 |
+
# If org is specified, repo_name must be the base name for that org
|
| 82 |
+
if "/" in repo_name:
|
| 83 |
+
raise ValueError("When organization is specified, repo_name should be a base name, not 'org/repo'.")
|
| 84 |
+
full_repo_id = f"{organization}/{repo_name}"
|
| 85 |
+
elif "/" in repo_name:
|
| 86 |
+
# User provided a full name like "username/repo_name"
|
| 87 |
+
full_repo_id = repo_name
|
| 88 |
+
else:
|
| 89 |
+
# User provided a base name, prepend current user
|
| 90 |
+
user = self.api.whoami(token=self.token).get("name")
|
| 91 |
+
if not user:
|
| 92 |
+
raise ConnectionError("Could not determine Hugging Face username. Ensure token is valid and you are logged in.")
|
| 93 |
+
full_repo_id = f"{user}/{repo_name}"
|
| 94 |
+
|
| 95 |
+
try:
|
| 96 |
+
url = create_repo(repo_id=full_repo_id, token=self.token, private=private, repo_type=repo_type, exist_ok=True)
|
| 97 |
+
logger.info(f"Repository '{full_repo_id}' ensured to exist. URL: {url}")
|
| 98 |
+
return full_repo_id
|
| 99 |
+
except HfHubHTTPError as e:
|
| 100 |
+
logger.error(f"Error creating repository '{full_repo_id}': {e}")
|
| 101 |
+
# If error indicates it's because it's a user repo and trying to use org logic or vice-versa
|
| 102 |
+
# it might be complex to auto-fix, so better to raise.
|
| 103 |
+
raise
|
| 104 |
+
|
| 105 |
+
def upload_file_or_folder(self, local_path: str | Path, repo_id: str, path_in_repo: str | None = None, repo_type: str | None = None, commit_message: str = "Upload content"):
|
| 106 |
+
"""Helper to upload a single file or an entire folder."""
|
| 107 |
+
local_path_obj = Path(local_path)
|
| 108 |
+
|
| 109 |
+
if not path_in_repo and local_path_obj.is_file():
|
| 110 |
+
path_in_repo = local_path_obj.name
|
| 111 |
+
elif not path_in_repo and local_path_obj.is_dir():
|
| 112 |
+
# For folders, path_in_repo is relative to the repo root.
|
| 113 |
+
# If None, files will be uploaded to the root.
|
| 114 |
+
# If you want to upload contents of 'my_folder' into 'target_folder_in_repo/',
|
| 115 |
+
# then path_in_repo should be 'target_folder_in_repo'
|
| 116 |
+
# For simplicity here, if path_in_repo is None for a folder, we upload its contents to the root.
|
| 117 |
+
pass
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
if local_path_obj.is_file():
|
| 121 |
+
self.api.upload_file(
|
| 122 |
+
path_or_fileobj=str(local_path_obj),
|
| 123 |
+
path_in_repo=path_in_repo if path_in_repo else local_path_obj.name,
|
| 124 |
+
repo_id=repo_id,
|
| 125 |
+
repo_type=repo_type,
|
| 126 |
+
token=self.token,
|
| 127 |
+
commit_message=commit_message,
|
| 128 |
+
)
|
| 129 |
+
logger.info(f"File '{local_path_obj}' uploaded to '{repo_id}/{path_in_repo if path_in_repo else local_path_obj.name}'.")
|
| 130 |
+
elif local_path_obj.is_dir():
|
| 131 |
+
# upload_folder uploads the *contents* of folder_path into the repo_id,
|
| 132 |
+
# optionally under a path_in_repo.
|
| 133 |
+
self.api.upload_folder(
|
| 134 |
+
folder_path=str(local_path_obj),
|
| 135 |
+
path_in_repo=path_in_repo if path_in_repo else ".", # Upload to root if no path_in_repo
|
| 136 |
+
repo_id=repo_id,
|
| 137 |
+
repo_type=repo_type,
|
| 138 |
+
token=self.token,
|
| 139 |
+
commit_message=commit_message,
|
| 140 |
+
ignore_patterns=["*.git*", ".gitattributes"],
|
| 141 |
+
)
|
| 142 |
+
logger.info(f"Folder '{local_path_obj}' contents uploaded to '{repo_id}{'/' + path_in_repo if path_in_repo and path_in_repo != '.' else ''}'.")
|
| 143 |
+
else:
|
| 144 |
+
raise FileNotFoundError(f"Local path '{local_path}' not found or is not a file/directory.")
|
| 145 |
+
|
| 146 |
+
def upload_model(self, model_path: str | Path, repo_name: str, private: bool = True, commit_message: str = "Upload model", organization: str | None = None) -> str:
|
| 147 |
+
"""
|
| 148 |
+
Uploads a model to the Hugging Face Hub.
|
| 149 |
+
|
| 150 |
+
Args:
|
| 151 |
+
model_path: Path to the local model directory or file.
|
| 152 |
+
repo_name: Base name of the repository (e.g., "my-lora-model").
|
| 153 |
+
The prefix from __init__ and username/org will be added.
|
| 154 |
+
private: Whether the repository should be private.
|
| 155 |
+
commit_message: Commit message for the upload.
|
| 156 |
+
organization: Optional organization to host this model. If None, uses the logged-in user.
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
Returns:
|
| 160 |
+
The URL of the uploaded model repository.
|
| 161 |
+
"""
|
| 162 |
+
# Construct the effective repo name, possibly prefixed
|
| 163 |
+
effective_repo_name = repo_name
|
| 164 |
+
if self.default_repo_prefix and not repo_name.startswith(self.default_repo_prefix):
|
| 165 |
+
effective_repo_name = f"{self.default_repo_prefix}-{repo_name}"
|
| 166 |
+
|
| 167 |
+
# Create the repository
|
| 168 |
+
target_repo_id = self.create_repository(repo_name=effective_repo_name, repo_type="model", private=private, organization=organization)
|
| 169 |
+
|
| 170 |
+
logger.info(f"Uploading model from '{model_path}' to '{target_repo_id}'...")
|
| 171 |
+
self.upload_file_or_folder(local_path=model_path, repo_id=target_repo_id, repo_type="model", commit_message=commit_message)
|
| 172 |
+
|
| 173 |
+
repo_url = f"https://huggingface.co/{target_repo_id}"
|
| 174 |
+
logger.info(f"Model uploaded to {repo_url}")
|
| 175 |
+
return repo_url
|
| 176 |
+
|
| 177 |
+
def download_model(self, repo_name: str, local_dir: str | Path, revision: str | None = None, organization: str | None = None) -> str:
|
| 178 |
+
"""
|
| 179 |
+
Downloads a model from the Hugging Face Hub.
|
| 180 |
+
|
| 181 |
+
Args:
|
| 182 |
+
repo_name: Name of the repository. Can be a base name (e.g., "my-lora-model")
|
| 183 |
+
or a full ID (e.g., "username/my-lora-model").
|
| 184 |
+
If base name and no organization, prefix and username are added.
|
| 185 |
+
If base name and organization, prefix is added.
|
| 186 |
+
local_dir: Local directory to save the model.
|
| 187 |
+
revision: Optional model revision (branch, tag, commit hash).
|
| 188 |
+
organization: Optional organization if repo_name is a base name under an org.
|
| 189 |
+
|
| 190 |
+
Returns:
|
| 191 |
+
Path to the downloaded model.
|
| 192 |
+
"""
|
| 193 |
+
if "/" in repo_name: # User provided full ID like "user/repo" or "org/repo"
|
| 194 |
+
target_repo_id = repo_name
|
| 195 |
+
else: # User provided base name
|
| 196 |
+
effective_repo_name = repo_name
|
| 197 |
+
if self.default_repo_prefix and not repo_name.startswith(self.default_repo_prefix):
|
| 198 |
+
effective_repo_name = f"{self.default_repo_prefix}-{repo_name}"
|
| 199 |
+
|
| 200 |
+
if organization:
|
| 201 |
+
target_repo_id = f"{organization}/{effective_repo_name}"
|
| 202 |
+
else:
|
| 203 |
+
user = self.api.whoami(token=self.token).get("name")
|
| 204 |
+
if not user:
|
| 205 |
+
raise ConnectionError("Could not determine Hugging Face username for downloading.")
|
| 206 |
+
target_repo_id = f"{user}/{effective_repo_name}"
|
| 207 |
+
|
| 208 |
+
logger.info(f"Downloading model '{target_repo_id}' to '{local_dir}'...")
|
| 209 |
+
|
| 210 |
+
downloaded_path = self.api.snapshot_download(
|
| 211 |
+
repo_id=target_repo_id,
|
| 212 |
+
repo_type="model", # Can be omitted, snapshot_download infers if possible
|
| 213 |
+
local_dir=str(local_dir),
|
| 214 |
+
token=self.token,
|
| 215 |
+
revision=revision,
|
| 216 |
+
)
|
| 217 |
+
logger.info(f"Model '{target_repo_id}' downloaded to '{downloaded_path}'.")
|
| 218 |
+
return downloaded_path
|
| 219 |
+
|
| 220 |
+
def upload_dataset(self, dataset_path: str | Path, repo_name: str, private: bool = True, commit_message: str = "Upload dataset", organization: str | None = None) -> str:
|
| 221 |
+
"""
|
| 222 |
+
Uploads a dataset to the Hugging Face Hub. (Similar to upload_model)
|
| 223 |
+
"""
|
| 224 |
+
effective_repo_name = repo_name
|
| 225 |
+
if self.default_repo_prefix and not repo_name.startswith(self.default_repo_prefix):
|
| 226 |
+
effective_repo_name = f"{self.default_repo_prefix}-{repo_name}"
|
| 227 |
+
|
| 228 |
+
target_repo_id = self.create_repository(repo_name=effective_repo_name, repo_type="dataset", private=private, organization=organization)
|
| 229 |
+
|
| 230 |
+
logger.info(f"Uploading dataset from '{dataset_path}' to '{target_repo_id}'...")
|
| 231 |
+
self.upload_file_or_folder(local_path=dataset_path, repo_id=target_repo_id, repo_type="dataset", commit_message=commit_message)
|
| 232 |
+
|
| 233 |
+
repo_url = f"https://huggingface.co/{target_repo_id}"
|
| 234 |
+
logger.info(f"Dataset uploaded to {repo_url}")
|
| 235 |
+
return repo_url
|
| 236 |
+
|
| 237 |
+
def download_dataset(self, repo_name: str, local_dir: str | Path, revision: str | None = None, organization: str | None = None) -> str:
|
| 238 |
+
"""
|
| 239 |
+
Downloads a dataset from the Hugging Face Hub. (Similar to download_model)
|
| 240 |
+
"""
|
| 241 |
+
if "/" in repo_name:
|
| 242 |
+
target_repo_id = repo_name
|
| 243 |
+
else:
|
| 244 |
+
effective_repo_name = repo_name
|
| 245 |
+
if self.default_repo_prefix and not repo_name.startswith(self.default_repo_prefix):
|
| 246 |
+
effective_repo_name = f"{self.default_repo_prefix}-{repo_name}"
|
| 247 |
+
if organization:
|
| 248 |
+
target_repo_id = f"{organization}/{effective_repo_name}"
|
| 249 |
+
else:
|
| 250 |
+
user = self.api.whoami(token=self.token).get("name")
|
| 251 |
+
if not user:
|
| 252 |
+
raise ConnectionError("Could not determine Hugging Face username for downloading.")
|
| 253 |
+
target_repo_id = f"{user}/{effective_repo_name}"
|
| 254 |
+
|
| 255 |
+
logger.info(f"Downloading dataset '{target_repo_id}' to '{local_dir}'...")
|
| 256 |
+
|
| 257 |
+
downloaded_path = self.api.snapshot_download(
|
| 258 |
+
repo_id=target_repo_id,
|
| 259 |
+
repo_type="dataset", # Can be omitted
|
| 260 |
+
local_dir=str(local_dir),
|
| 261 |
+
token=self.token,
|
| 262 |
+
revision=revision,
|
| 263 |
+
)
|
| 264 |
+
logger.info(f"Dataset '{target_repo_id}' downloaded to '{downloaded_path}'.")
|
| 265 |
+
return downloaded_path
|
| 266 |
+
|
| 267 |
+
def initiate_training(self, model_repo_id: str, dataset_repo_id: str, training_params: dict):
|
| 268 |
+
logger.warning("initiate_training is a placeholder and not fully implemented.")
|
| 269 |
+
logger.info(f"Would attempt to train model {model_repo_id} with dataset {dataset_repo_id} using params: {training_params}")
|
| 270 |
+
pass
|
| 271 |
+
|
| 272 |
+
# Example Usage
|
| 273 |
+
if __name__ == "__main__":
|
| 274 |
+
logging.basicConfig(level=logging.INFO)
|
| 275 |
+
|
| 276 |
+
hf_token = os.environ.get("HF_TOKEN")
|
| 277 |
+
if not hf_token:
|
| 278 |
+
logger.warning("HF_TOKEN environment variable not set. Please set it or log in via huggingface-cli.")
|
| 279 |
+
logger.warning("Skipping example usage.")
|
| 280 |
+
else:
|
| 281 |
+
# Use a different prefix for examples to avoid conflict with actual app prefix
|
| 282 |
+
hf_wrapper = HuggingFaceWrapper(token=hf_token, default_repo_prefix="hf-wrapper-test")
|
| 283 |
+
|
| 284 |
+
# Determine current Hugging Face username for constructing repo IDs in tests
|
| 285 |
+
try:
|
| 286 |
+
current_hf_user = hf_wrapper.api.whoami(token=hf_wrapper.token).get("name")
|
| 287 |
+
if not current_hf_user:
|
| 288 |
+
raise ValueError("Could not retrieve HuggingFace username.")
|
| 289 |
+
except Exception as e:
|
| 290 |
+
logger.error(f"Failed to get HuggingFace username for tests: {e}. Skipping examples.")
|
| 291 |
+
current_hf_user = None
|
| 292 |
+
|
| 293 |
+
if current_hf_user:
|
| 294 |
+
# --- Test Repository Creation ---
|
| 295 |
+
test_model_repo_basename = "my-test-model"
|
| 296 |
+
test_dataset_repo_basename = "my-test-dataset"
|
| 297 |
+
|
| 298 |
+
# These will be prefixed like "hf-wrapper-test-my-test-model"
|
| 299 |
+
# And the full ID will be "username/hf-wrapper-test-my-test-model"
|
| 300 |
+
|
| 301 |
+
try:
|
| 302 |
+
logger.info("\\n--- Testing Model Repository Creation ---")
|
| 303 |
+
model_repo_id = hf_wrapper.create_repository(repo_name=test_model_repo_basename, repo_type="model", private=True)
|
| 304 |
+
logger.info(f"Model repository created/ensured: {model_repo_id}")
|
| 305 |
+
|
| 306 |
+
logger.info("\\n--- Testing Dataset Repository Creation ---")
|
| 307 |
+
dataset_repo_id = hf_wrapper.create_repository(repo_name=test_dataset_repo_basename, repo_type="dataset", private=True)
|
| 308 |
+
logger.info(f"Dataset repository created/ensured: {dataset_repo_id}")
|
| 309 |
+
|
| 310 |
+
# --- Test File/Folder Upload & Download ---
|
| 311 |
+
dummy_model_dir = Path("dummy_model_for_hf_upload")
|
| 312 |
+
dummy_model_dir.mkdir(exist_ok=True)
|
| 313 |
+
dummy_dataset_file = Path("dummy_dataset_for_hf_upload.jsonl")
|
| 314 |
+
|
| 315 |
+
with open(dummy_model_dir / "config.json", "w") as f:
|
| 316 |
+
json.dump({"model_type": "dummy", "_comment": "Test model config"}, f, indent=2)
|
| 317 |
+
with open(dummy_model_dir / "model.safetensors", "w") as f:
|
| 318 |
+
f.write("This is a dummy safetensors file content.")
|
| 319 |
+
|
| 320 |
+
with open(dummy_dataset_file, "w") as f:
|
| 321 |
+
f.write(json.dumps({"text": "example line 1 for hf dataset"}) + "\\n")
|
| 322 |
+
f.write(json.dumps({"text": "example line 2 for hf dataset"}) + "\\n")
|
| 323 |
+
|
| 324 |
+
logger.info(f"\\n--- Testing Model Upload (folder to {test_model_repo_basename}) ---")
|
| 325 |
+
# upload_model uses the base name, prefixing and user/org is handled internally
|
| 326 |
+
hf_wrapper.upload_model(model_path=dummy_model_dir, repo_name=test_model_repo_basename, private=True)
|
| 327 |
+
|
| 328 |
+
logger.info(f"\\n--- Testing Dataset Upload (file to {test_dataset_repo_basename}) ---")
|
| 329 |
+
hf_wrapper.upload_dataset(dataset_path=dummy_dataset_file, repo_name=test_dataset_repo_basename, private=True)
|
| 330 |
+
|
| 331 |
+
# For download, construct the full repo ID as it would be on the Hub
|
| 332 |
+
# The upload methods return the Hub URL, but download needs repo_id.
|
| 333 |
+
# The create_repository returned the full ID, e.g. current_hf_user/hf-wrapper-test-my-test-model
|
| 334 |
+
|
| 335 |
+
downloaded_model_path_base = Path("downloaded_hf_models")
|
| 336 |
+
downloaded_model_path_base.mkdir(exist_ok=True)
|
| 337 |
+
# model_repo_id is already the full ID from create_repository
|
| 338 |
+
# e.g. "username/hf-wrapper-test-my-test-model"
|
| 339 |
+
|
| 340 |
+
logger.info(f"\\n--- Testing Model Download (from {model_repo_id}) ---")
|
| 341 |
+
# Use the repo_id returned by create_repository or constructed with _get_full_repo_id
|
| 342 |
+
# For download, repo_name can be the full ID.
|
| 343 |
+
hf_wrapper.download_model(repo_name=model_repo_id, local_dir=downloaded_model_path_base / test_model_repo_basename)
|
| 344 |
+
logger.info(f"Model downloaded to: {downloaded_model_path_base / test_model_repo_basename}")
|
| 345 |
+
|
| 346 |
+
downloaded_dataset_path_base = Path("downloaded_hf_datasets")
|
| 347 |
+
downloaded_dataset_path_base.mkdir(exist_ok=True)
|
| 348 |
+
# dataset_repo_id is e.g. "username/hf-wrapper-test-my-test-dataset"
|
| 349 |
+
|
| 350 |
+
logger.info(f"\\n--- Testing Dataset Download (from {dataset_repo_id}) ---")
|
| 351 |
+
hf_wrapper.download_dataset(repo_name=dataset_repo_id, local_dir=downloaded_dataset_path_base / test_dataset_repo_basename)
|
| 352 |
+
logger.info(f"Dataset downloaded to: {downloaded_dataset_path_base / test_dataset_repo_basename}")
|
| 353 |
+
|
| 354 |
+
logger.info("\\nExample usage complete. Check your Hugging Face account for new repositories.")
|
| 355 |
+
logger.info(f"Consider deleting test repositories: {model_repo_id}, {dataset_repo_id}")
|
| 356 |
+
|
| 357 |
+
# Clean up local dummy files/folders
|
| 358 |
+
import shutil
|
| 359 |
+
shutil.rmtree(dummy_model_dir)
|
| 360 |
+
dummy_dataset_file.unlink()
|
| 361 |
+
# You might want to manually inspect downloaded folders before deleting
|
| 362 |
+
# shutil.rmtree(downloaded_model_path_base)
|
| 363 |
+
# shutil.rmtree(downloaded_dataset_path_base)
|
| 364 |
+
logger.info("Local dummy files and folders cleaned up. Downloaded content remains for inspection.")
|
| 365 |
+
|
| 366 |
+
except Exception as e:
|
| 367 |
+
logger.error(f"An error occurred during example usage: {e}", exc_info=True)
|
app/core/lora_config.py
CHANGED
|
@@ -1,2 +1,86 @@
|
|
| 1 |
# lora_config.py
|
| 2 |
-
# LoRA-Config
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# lora_config.py
|
| 2 |
+
# LoRA-Config für effizienten Training und Inference
|
| 3 |
+
|
| 4 |
+
from peft import LoraConfig, TaskType
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
class LoRAConfiguration:
|
| 8 |
+
"""Zentralisierte LoRA-Konfiguration für Training und Inference"""
|
| 9 |
+
|
| 10 |
+
# Training-optimierte Konfiguration (lokales Training)
|
| 11 |
+
TRAINING_CONFIG = LoraConfig(
|
| 12 |
+
task_type=TaskType.CAUSAL_LM,
|
| 13 |
+
r=16, # Rank - Balance zwischen Qualität und Effizienz
|
| 14 |
+
lora_alpha=32, # Skalierung für LoRA-Gewichte
|
| 15 |
+
lora_dropout=0.1, # Regularisierung
|
| 16 |
+
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"], # Attention-Module
|
| 17 |
+
bias="none", # Keine Bias-Parameter trainieren
|
| 18 |
+
inference_mode=False
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
# Inference-optimierte Konfiguration (HuggingFace Space)
|
| 22 |
+
INFERENCE_CONFIG = LoraConfig(
|
| 23 |
+
task_type=TaskType.CAUSAL_LM,
|
| 24 |
+
r=16,
|
| 25 |
+
lora_alpha=32,
|
| 26 |
+
lora_dropout=0.1,
|
| 27 |
+
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
|
| 28 |
+
bias="none",
|
| 29 |
+
inference_mode=True # Optimiert für Inference
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
# Kostengünstige Konfiguration (niedrigere Qualität, aber sehr effizient)
|
| 33 |
+
BUDGET_CONFIG = LoraConfig(
|
| 34 |
+
task_type=TaskType.CAUSAL_LM,
|
| 35 |
+
r=8, # Niedrigerer Rank
|
| 36 |
+
lora_alpha=16,
|
| 37 |
+
lora_dropout=0.05,
|
| 38 |
+
target_modules=["q_proj", "v_proj"], # Nur wichtigste Module
|
| 39 |
+
bias="none",
|
| 40 |
+
inference_mode=False
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
@staticmethod
|
| 44 |
+
def get_config(mode="inference"):
|
| 45 |
+
"""
|
| 46 |
+
Konfiguration basierend auf Verwendungszweck abrufen
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
mode: "training", "inference", oder "budget"
|
| 50 |
+
"""
|
| 51 |
+
if mode == "training":
|
| 52 |
+
return LoRAConfiguration.TRAINING_CONFIG
|
| 53 |
+
elif mode == "budget":
|
| 54 |
+
return LoRAConfiguration.BUDGET_CONFIG
|
| 55 |
+
else:
|
| 56 |
+
return LoRAConfiguration.INFERENCE_CONFIG
|
| 57 |
+
|
| 58 |
+
@staticmethod
|
| 59 |
+
def estimate_parameters(r=16, target_modules=4):
|
| 60 |
+
"""
|
| 61 |
+
Geschätzte Anzahl trainierbarer Parameter
|
| 62 |
+
Für Kostenabschätzung
|
| 63 |
+
"""
|
| 64 |
+
# Grobe Schätzung basierend auf Mistral-7B
|
| 65 |
+
base_params = 7_000_000_000
|
| 66 |
+
lora_params = r * 2 * 4096 * target_modules # Vereinfachte Berechnung
|
| 67 |
+
trainable_ratio = lora_params / base_params
|
| 68 |
+
|
| 69 |
+
return {
|
| 70 |
+
"base_parameters": base_params,
|
| 71 |
+
"lora_parameters": lora_params,
|
| 72 |
+
"trainable_ratio": f"{trainable_ratio*100:.3f}%",
|
| 73 |
+
"memory_estimate_gb": (lora_params * 4) / (1024**3) # 4 bytes per param
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
# Verwendung in der Anwendung
|
| 77 |
+
def get_lora_config_for_environment():
|
| 78 |
+
"""Automatische Konfiguration basierend auf Umgebung"""
|
| 79 |
+
try:
|
| 80 |
+
import torch
|
| 81 |
+
if torch.cuda.is_available():
|
| 82 |
+
return LoRAConfiguration.get_config("inference")
|
| 83 |
+
else:
|
| 84 |
+
return LoRAConfiguration.get_config("budget")
|
| 85 |
+
except ImportError:
|
| 86 |
+
return LoRAConfiguration.get_config("budget")
|
app/core/model_loader.py
CHANGED
|
@@ -1,21 +1,152 @@
|
|
| 1 |
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
|
| 2 |
from peft import PeftModel
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
|
|
|
| 4 |
|
| 5 |
|
| 6 |
def load_model():
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
|
| 11 |
-
model = AutoModelForCausalLM.from_pretrained(base_model_id, load_in_4bit=True, device_map="auto")
|
| 12 |
|
| 13 |
try:
|
| 14 |
-
|
| 15 |
-
except:
|
| 16 |
-
|
|
|
|
| 17 |
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
return pipe
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
|
| 2 |
from peft import PeftModel
|
| 3 |
+
import torch
|
| 4 |
+
import logging
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import os
|
| 7 |
+
from .config import settings
|
| 8 |
+
from .hf_api import HuggingFaceWrapper
|
| 9 |
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
|
| 12 |
|
| 13 |
def load_model():
|
| 14 |
+
"""
|
| 15 |
+
Optimierter Model Loader mit LoRA-Support.
|
| 16 |
+
Kann LoRA-Adapter von Hugging Face Hub herunterladen.
|
| 17 |
+
Automatische Konfiguration basierend auf verfügbaren Ressourcen.
|
| 18 |
+
"""
|
| 19 |
+
base_model_id = settings.DEFAULT_MODEL_ID
|
| 20 |
+
hf_token = os.getenv("HF_API_KEY")
|
| 21 |
|
| 22 |
+
logger.info(f"Lade Basismodell: {base_model_id}")
|
|
|
|
| 23 |
|
| 24 |
try:
|
| 25 |
+
tokenizer = AutoTokenizer.from_pretrained(base_model_id, token=hf_token)
|
| 26 |
+
except Exception as e:
|
| 27 |
+
logger.error(f"Fehler beim Laden des Tokenizers für {base_model_id}: {e}")
|
| 28 |
+
raise
|
| 29 |
|
| 30 |
+
if tokenizer.pad_token is None:
|
| 31 |
+
logger.info("Tokenizer hat kein pad_token. Setze pad_token = eos_token.")
|
| 32 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 33 |
+
|
| 34 |
+
model_kwargs = {
|
| 35 |
+
"torch_dtype": torch.float16, # Standardmäßig float16
|
| 36 |
+
"device_map": "auto",
|
| 37 |
+
"trust_remote_code": True, # Notwendig für einige Modelle
|
| 38 |
+
"token": hf_token
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
if settings.MODEL_LOAD_IN_4BIT is not False: # Annahme: settings.MODEL_LOAD_IN_4BIT existiert
|
| 42 |
+
try:
|
| 43 |
+
model_kwargs["load_in_4bit"] = True
|
| 44 |
+
# Ggf. BitsAndBytesConfig hier, falls benötigt
|
| 45 |
+
# from transformers import BitsAndBytesConfig
|
| 46 |
+
# model_kwargs["quantization_config"] = BitsAndBytesConfig(...)
|
| 47 |
+
logger.info("Versuche, Modell mit 4-bit Quantisierung zu laden.")
|
| 48 |
+
model = AutoModelForCausalLM.from_pretrained(base_model_id, **model_kwargs)
|
| 49 |
+
logger.info("Modell erfolgreich mit 4-bit Quantisierung geladen.")
|
| 50 |
+
except Exception as e:
|
| 51 |
+
logger.warning(f"4-bit Laden fehlgeschlagen: {e}. Fallback auf Standard-Laden (FP16).")
|
| 52 |
+
del model_kwargs["load_in_4bit"]
|
| 53 |
+
if "quantization_config" in model_kwargs: del model_kwargs["quantization_config"]
|
| 54 |
+
model = AutoModelForCausalLM.from_pretrained(base_model_id, **model_kwargs)
|
| 55 |
+
else:
|
| 56 |
+
logger.info("4-bit Quantisierung ist deaktiviert. Lade Modell in FP16.")
|
| 57 |
+
model = AutoModelForCausalLM.from_pretrained(base_model_id, **model_kwargs)
|
| 58 |
+
|
| 59 |
+
# LoRA-Gewichte laden
|
| 60 |
+
lora_path_to_load = None
|
| 61 |
+
if settings.LORA_MODEL_REPO_ID:
|
| 62 |
+
logger.info(f"LoRA Adapter soll von Hugging Face Hub geladen werden: {settings.LORA_MODEL_REPO_ID}")
|
| 63 |
+
hf_wrapper = HuggingFaceWrapper(token=hf_token) # Token wird intern vom Wrapper geholt, falls nicht explizit übergeben
|
| 64 |
+
|
| 65 |
+
# Zielverzeichnis für heruntergeladene LoRA-Adapter
|
| 66 |
+
# Basierend auf MODEL_PATH aus settings, um Konsistenz zu wahren
|
| 67 |
+
# Beispiel: cardserver/models/lora-checkpoint/downloaded_adapters/your-lora-model-repo
|
| 68 |
+
local_lora_download_dir_base = settings.resolved_model_path.parent / "downloaded_adapters"
|
| 69 |
+
lora_adapter_name = settings.LORA_MODEL_REPO_ID.split("/")[-1] # z.B. "your-lora-model-repo"
|
| 70 |
+
local_lora_dir = local_lora_download_dir_base / lora_adapter_name
|
| 71 |
+
|
| 72 |
+
# Prüfen, ob der Adapter bereits heruntergeladen wurde (einfache Prüfung)
|
| 73 |
+
# Eine robustere Prüfung könnte Versions-Hashes oder Modifikationszeiten beinhalten.
|
| 74 |
+
adapter_config_file = local_lora_dir / "adapter_config.json"
|
| 75 |
+
|
| 76 |
+
if not adapter_config_file.exists() or getattr(settings, "LORA_FORCE_DOWNLOAD", False):
|
| 77 |
+
if adapter_config_file.exists():
|
| 78 |
+
logger.info(f"LORA_FORCE_DOWNLOAD ist aktiv. LoRA-Adapter wird erneut heruntergeladen: {settings.LORA_MODEL_REPO_ID}")
|
| 79 |
+
else:
|
| 80 |
+
logger.info(f"LoRA-Adapter nicht lokal gefunden unter {local_lora_dir}. Wird heruntergeladen...")
|
| 81 |
+
|
| 82 |
+
local_lora_dir.mkdir(parents=True, exist_ok=True) # Sicherstellen, dass das Verzeichnis existiert
|
| 83 |
+
try:
|
| 84 |
+
downloaded_path_str = hf_wrapper.download_model(
|
| 85 |
+
repo_name=settings.LORA_MODEL_REPO_ID,
|
| 86 |
+
local_dir=str(local_lora_dir), # Muss ein String sein
|
| 87 |
+
# revision=settings.LORA_MODEL_REVISION # Falls eine spezifische Version benötigt wird
|
| 88 |
+
)
|
| 89 |
+
lora_path_to_load = Path(downloaded_path_str) # Der Rückgabewert ist der Pfad
|
| 90 |
+
logger.info(f"LoRA-Adapter erfolgreich von {settings.LORA_MODEL_REPO_ID} nach {lora_path_to_load} heruntergeladen.")
|
| 91 |
+
except Exception as e:
|
| 92 |
+
logger.error(f"Fehler beim Herunterladen des LoRA-Adapters von {settings.LORA_MODEL_REPO_ID}: {e}")
|
| 93 |
+
logger.info("Versuche, Fallback auf lokalen Pfad (falls konfiguriert) oder verwende Basismodell.")
|
| 94 |
+
# Fallback auf settings.resolved_model_path, falls LORA_MODEL_REPO_ID fehlschlägt
|
| 95 |
+
if settings.resolved_model_path.exists() and (settings.resolved_model_path / "adapter_config.json").exists():
|
| 96 |
+
lora_path_to_load = settings.resolved_model_path
|
| 97 |
+
logger.info(f"Fallback auf lokalen LoRA-Pfad: {lora_path_to_load}")
|
| 98 |
+
else:
|
| 99 |
+
lora_path_to_load = None # Kein LoRA verwenden
|
| 100 |
+
else:
|
| 101 |
+
lora_path_to_load = local_lora_dir
|
| 102 |
+
logger.info(f"LoRA-Adapter {settings.LORA_MODEL_REPO_ID} bereits lokal vorhanden unter: {lora_path_to_load}")
|
| 103 |
+
|
| 104 |
+
elif settings.resolved_model_path.exists() and (settings.resolved_model_path / "adapter_config.json").exists():
|
| 105 |
+
# Fallback: Wenn LORA_MODEL_REPO_ID nicht gesetzt ist, aber ein lokaler Pfad existiert
|
| 106 |
+
lora_path_to_load = settings.resolved_model_path
|
| 107 |
+
logger.info(f"Verwende lokalen LoRA-Pfad: {lora_path_to_load} (da LORA_MODEL_REPO_ID nicht gesetzt).")
|
| 108 |
+
else:
|
| 109 |
+
logger.info("Kein LORA_MODEL_REPO_ID in den Settings und kein gültiger lokaler LoRA-Pfad gefunden.")
|
| 110 |
+
lora_path_to_load = None
|
| 111 |
+
|
| 112 |
+
if lora_path_to_load:
|
| 113 |
+
try:
|
| 114 |
+
logger.info(f"Versuche, LoRA-Gewichte von Pfad zu laden: {lora_path_to_load}")
|
| 115 |
+
model = PeftModel.from_pretrained(model, str(lora_path_to_load))
|
| 116 |
+
logger.info("✅ LoRA-Modell erfolgreich auf Basismodell angewendet.")
|
| 117 |
+
except Exception as e:
|
| 118 |
+
logger.error(f"❌ LoRA-Loading von {lora_path_to_load} fehlgeschlagen: {e}")
|
| 119 |
+
logger.info("Verwende Basismodell ohne LoRA-Adapter.")
|
| 120 |
+
else:
|
| 121 |
+
logger.info("Keine LoRA-Gewichte zum Laden spezifiziert oder gefunden. Verwende Basismodell.")
|
| 122 |
+
|
| 123 |
+
pipe = pipeline(
|
| 124 |
+
"text-generation",
|
| 125 |
+
model=model,
|
| 126 |
+
tokenizer=tokenizer,
|
| 127 |
+
return_full_text=False
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
logger.info("Text-Generierungs-Pipeline erfolgreich erstellt.")
|
| 131 |
return pipe
|
| 132 |
|
| 133 |
+
|
| 134 |
+
def get_model_info():
|
| 135 |
+
"""Informationen über das geladene Modell"""
|
| 136 |
+
lora_path = settings.resolved_model_path
|
| 137 |
+
return {
|
| 138 |
+
"base_model": settings.DEFAULT_MODEL_ID,
|
| 139 |
+
"lora_enabled": lora_path.exists(),
|
| 140 |
+
"lora_path": str(lora_path) if lora_path.exists() else None,
|
| 141 |
+
"gpu_available": torch.cuda.is_available(),
|
| 142 |
+
"gpu_count": torch.cuda.device_count() if torch.cuda.is_available() else 0
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
_generator = None
|
| 146 |
+
|
| 147 |
+
def get_generator():
|
| 148 |
+
"""Thread-safe Generator abrufen"""
|
| 149 |
+
global _generator
|
| 150 |
+
if _generator is None:
|
| 151 |
+
_generator = load_model()
|
| 152 |
+
return _generator
|
app/hf_api.py
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
# hf_api.py
|
| 2 |
-
# Optional: HuggingFace API-Wrapper
|
|
|
|
|
|
|
|
|
app/main.py
CHANGED
|
@@ -79,7 +79,7 @@ app.add_middleware(
|
|
| 79 |
)
|
| 80 |
|
| 81 |
app.include_router(health.router, prefix=settings.API_PREFIX, tags=["Health"])
|
| 82 |
-
app.include_router(generate.router, prefix=settings.API_PREFIX, tags=["Generate
|
| 83 |
app.include_router(download.router, prefix=settings.API_PREFIX, tags=["Download Card"])
|
| 84 |
|
| 85 |
@app.get("/")
|
|
|
|
| 79 |
)
|
| 80 |
|
| 81 |
app.include_router(health.router, prefix=settings.API_PREFIX, tags=["Health"])
|
| 82 |
+
app.include_router(generate.router, prefix=settings.API_PREFIX, tags=["Generate Card"])
|
| 83 |
app.include_router(download.router, prefix=settings.API_PREFIX, tags=["Download Card"])
|
| 84 |
|
| 85 |
@app.get("/")
|
app/services/database.py
CHANGED
|
@@ -11,11 +11,11 @@ supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
|
|
| 11 |
def get_supabase_client():
|
| 12 |
return supabase
|
| 13 |
|
| 14 |
-
def
|
| 15 |
card_id: str,
|
| 16 |
terms: list[str],
|
| 17 |
-
|
| 18 |
-
|
| 19 |
image_filename: str, # Nur der Dateiname, Pfad wird serverseitig verwaltet
|
| 20 |
qr_code_filename: str, # Nur der Dateiname
|
| 21 |
qr_code_link: str
|
|
@@ -25,8 +25,8 @@ def save_horoscope_card(
|
|
| 25 |
response = supabase.table("horoscope_cards").insert({
|
| 26 |
"id": card_id,
|
| 27 |
"terms": terms,
|
| 28 |
-
"
|
| 29 |
-
"
|
| 30 |
"image_filename": image_filename,
|
| 31 |
"qr_code_filename": qr_code_filename,
|
| 32 |
"qr_code_link": qr_code_link
|
|
|
|
| 11 |
def get_supabase_client():
|
| 12 |
return supabase
|
| 13 |
|
| 14 |
+
def save_card(
|
| 15 |
card_id: str,
|
| 16 |
terms: list[str],
|
| 17 |
+
card_date: str, # Pydantic date wird hier als str erwartet für Supabase
|
| 18 |
+
card_text: str,
|
| 19 |
image_filename: str, # Nur der Dateiname, Pfad wird serverseitig verwaltet
|
| 20 |
qr_code_filename: str, # Nur der Dateiname
|
| 21 |
qr_code_link: str
|
|
|
|
| 25 |
response = supabase.table("horoscope_cards").insert({
|
| 26 |
"id": card_id,
|
| 27 |
"terms": terms,
|
| 28 |
+
"card_date": card_date,
|
| 29 |
+
"card_text": card_text,
|
| 30 |
"image_filename": image_filename,
|
| 31 |
"qr_code_filename": qr_code_filename,
|
| 32 |
"qr_code_link": qr_code_link
|
requirements.txt
CHANGED
|
@@ -1,13 +1,26 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core Production Dependencies
|
| 2 |
+
# Für FastAPI Server und Inference
|
| 3 |
+
|
| 4 |
+
# FastAPI Framework
|
| 5 |
+
fastapi>=0.110.0
|
| 6 |
+
uvicorn[standard]>=0.30.0
|
| 7 |
+
|
| 8 |
+
# Data Validation
|
| 9 |
+
pydantic>=2.8.0
|
| 10 |
+
pydantic-settings>=2.6.0
|
| 11 |
+
|
| 12 |
+
# Image Processing
|
| 13 |
+
pillow>=10.0.0
|
| 14 |
+
qrcode>=7.4.0
|
| 15 |
+
|
| 16 |
+
# ML Inference (minimale Versionen für Produktion)
|
| 17 |
+
torch>=2.4.0
|
| 18 |
+
transformers>=4.52.0
|
| 19 |
+
peft>=0.15.0
|
| 20 |
+
accelerate>=1.7.0
|
| 21 |
+
numpy>=1.26.0
|
| 22 |
+
bitsandbytes>=0.41.0 # For 4-bit/8-bit loading
|
| 23 |
+
|
| 24 |
+
# Database & Templates
|
| 25 |
+
supabase>=2.7.0
|
| 26 |
+
jinja2>=3.1.0
|
scripts/cleanup_large_files.sh
ADDED
|
File without changes
|
{tests → scripts}/quick_test.sh
RENAMED
|
File without changes
|
scripts/setup_training.sh
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Schnelles Setup-Script für LORA-Training
|
| 3 |
+
|
| 4 |
+
echo "🚀 Setup für kostengünstiges LORA-Training"
|
| 5 |
+
echo "=========================================="
|
| 6 |
+
|
| 7 |
+
# 1. Virtuelle Umgebung erstellen (optional)
|
| 8 |
+
echo "📦 Erstelle virtuelle Umgebung..."
|
| 9 |
+
python3 -m venv venv_training
|
| 10 |
+
source venv_training/bin/activate
|
| 11 |
+
|
| 12 |
+
# 2. Requirements installieren
|
| 13 |
+
echo "📥 Installiere Training-Dependencies..."
|
| 14 |
+
pip install -r training_requirements.txt
|
| 15 |
+
pip install -r ../requirements.txt
|
| 16 |
+
|
| 17 |
+
# 3. Training-Ordner vorbereiten
|
| 18 |
+
echo "📁 Erstelle Training-Struktur..."
|
| 19 |
+
mkdir -p ../models/lora-checkpoint
|
| 20 |
+
mkdir -p data
|
| 21 |
+
|
| 22 |
+
# 4. GPU-Check
|
| 23 |
+
echo "🔍 GPU-Verfügbarkeit prüfen..."
|
| 24 |
+
python -c "import torch; print(f'CUDA verfügbar: {torch.cuda.is_available()}'); print(f'GPU-Anzahl: {torch.cuda.device_count()}')"
|
| 25 |
+
|
| 26 |
+
echo ""
|
| 27 |
+
echo "✅ Setup abgeschlossen!"
|
| 28 |
+
echo ""
|
| 29 |
+
echo "🎯 Nächste Schritte:"
|
| 30 |
+
echo "1. Trainingsdaten in data/ ablegen (JSON-Format)"
|
| 31 |
+
echo "2. Training starten: python train_lora.py"
|
| 32 |
+
echo "3. Geschätzter Speicherbedarf: ~8-12GB RAM + ~4GB VRAM"
|
| 33 |
+
echo ""
|
| 34 |
+
echo "💰 Kostenoptimierung:"
|
| 35 |
+
echo "- Lokales Training: 0€ (nur Stromkosten)"
|
| 36 |
+
echo "- Cloud-Alternative: Google Colab Pro (~10€/Monat)"
|
| 37 |
+
echo "- Training-Zeit: ~2-4 Stunden je nach Datenmenge"
|
scripts/start.sh
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Startup script for debugging HF Space deployment
|
| 3 |
+
|
| 4 |
+
echo "🔍 DEBUG: Starting Museum Sexoskop App"
|
| 5 |
+
echo "📁 Current directory: $(pwd)"
|
| 6 |
+
echo "📁 Directory contents:"
|
| 7 |
+
ls -la
|
| 8 |
+
|
| 9 |
+
echo "🐍 Python version: $(python --version)"
|
| 10 |
+
echo "📦 Installed packages:"
|
| 11 |
+
pip list | grep -E "(fastapi|uvicorn|pydantic|pillow|qrcode|transformers|torch)"
|
| 12 |
+
|
| 13 |
+
echo "📁 App directory structure:"
|
| 14 |
+
find /app -type d -name "app" -o -name "static" -o -name "templates" | head -20
|
| 15 |
+
|
| 16 |
+
echo "🔧 Testing configuration..."
|
| 17 |
+
if [ -f "/app/tests/test_config.py" ]; then
|
| 18 |
+
python tests/test_config.py
|
| 19 |
+
else
|
| 20 |
+
echo "❌ tests/test_config.py not found"
|
| 21 |
+
fi
|
| 22 |
+
|
| 23 |
+
echo "🚀 Starting FastAPI server..."
|
| 24 |
+
exec uvicorn app.main:app --host 0.0.0.0 --port 7860 --log-level debug
|
tests/MANUAL_VERIFICATION.md
DELETED
|
@@ -1,149 +0,0 @@
|
|
| 1 |
-
# Manual Deployment Verification Guide
|
| 2 |
-
|
| 3 |
-
## 🔍 Step-by-Step Verification Process
|
| 4 |
-
|
| 5 |
-
Since automated monitoring may have limitations, here's how to manually verify the deployment:
|
| 6 |
-
|
| 7 |
-
### 1. **Check HF Space Status**
|
| 8 |
-
|
| 9 |
-
Visit these URLs in your browser:
|
| 10 |
-
- **Space Page**: https://huggingface.co/spaces/ch404/cardserver
|
| 11 |
-
- **Direct App**: https://ch404-cardserver.hf.space/
|
| 12 |
-
- **Alternative**: https://huggingface.co/spaces/ch404/cardserver
|
| 13 |
-
|
| 14 |
-
**What to look for**:
|
| 15 |
-
- ✅ Space shows "Running" status (green indicator)
|
| 16 |
-
- ✅ No error messages in the space logs
|
| 17 |
-
- ✅ App loads without 500 errors
|
| 18 |
-
|
| 19 |
-
### 2. **Test API Endpoints Manually**
|
| 20 |
-
|
| 21 |
-
#### A. Health Check (Should work first)
|
| 22 |
-
```bash
|
| 23 |
-
curl "https://ch404-cardserver.hf.space/api/v1/health"
|
| 24 |
-
```
|
| 25 |
-
**Expected**: `{"status": "healthy", "server": "running", ...}`
|
| 26 |
-
|
| 27 |
-
#### B. Root Endpoint
|
| 28 |
-
```bash
|
| 29 |
-
curl "https://ch404-cardserver.hf.space/"
|
| 30 |
-
```
|
| 31 |
-
**Expected**: `{"message": "Welcome to the Horoskopkarten-Generator API."}`
|
| 32 |
-
|
| 33 |
-
#### C. Generate Horoscope (Full functionality test)
|
| 34 |
-
```bash
|
| 35 |
-
curl -X POST "https://ch404-cardserver.hf.space/api/v1/generate-horoscope" \
|
| 36 |
-
-H "Content-Type: application/json" \
|
| 37 |
-
-d '{
|
| 38 |
-
"terms": ["Erfolg", "Liebe", "Glück", "Abenteuer", "Weisheit"],
|
| 39 |
-
"date_of_birth": "1990-06-13"
|
| 40 |
-
}'
|
| 41 |
-
```
|
| 42 |
-
**Expected**: Returns `card_id` and `qr_code_image_url`
|
| 43 |
-
|
| 44 |
-
### 3. **Troubleshooting Common Issues**
|
| 45 |
-
|
| 46 |
-
#### **Issue: 404 Errors**
|
| 47 |
-
- **Cause**: Space is not running or still deploying
|
| 48 |
-
- **Solution**: Wait 2-5 minutes, then retry
|
| 49 |
-
- **Check**: Space logs in HF interface
|
| 50 |
-
|
| 51 |
-
#### **Issue: 500 Internal Server Error**
|
| 52 |
-
- **Cause**: Our original static directory error
|
| 53 |
-
- **Solution**: Check if our fixes were applied
|
| 54 |
-
- **Verify**: Look for deployment commit in GitHub
|
| 55 |
-
|
| 56 |
-
#### **Issue: Space Shows "Building"**
|
| 57 |
-
- **Cause**: Deployment in progress
|
| 58 |
-
- **Solution**: Wait for completion (can take 5-10 minutes)
|
| 59 |
-
- **Monitor**: Refresh space page periodically
|
| 60 |
-
|
| 61 |
-
#### **Issue: Authentication Errors (401)**
|
| 62 |
-
- **Cause**: Space might be private
|
| 63 |
-
- **Solution**: Make space public in HF settings
|
| 64 |
-
- **Check**: Space visibility settings
|
| 65 |
-
|
| 66 |
-
### 4. **Expected Behavior After Fix**
|
| 67 |
-
|
| 68 |
-
#### **Before Fix**:
|
| 69 |
-
```
|
| 70 |
-
RuntimeError: Directory '/cardserver/static' does not exist
|
| 71 |
-
```
|
| 72 |
-
|
| 73 |
-
#### **After Fix**:
|
| 74 |
-
```
|
| 75 |
-
✅ INFO: Attempting to mount static directory: /app/static
|
| 76 |
-
✅ INFO: Static directory exists: True
|
| 77 |
-
✅ INFO: Static files mounted successfully
|
| 78 |
-
✅ FastAPI app starts normally
|
| 79 |
-
```
|
| 80 |
-
|
| 81 |
-
### 5. **Manual Testing Workflow**
|
| 82 |
-
|
| 83 |
-
1. **Wait for Deployment** (5-10 minutes after push)
|
| 84 |
-
2. **Check Space Status** (visit HF space page)
|
| 85 |
-
3. **Test Health Endpoint** (basic connectivity)
|
| 86 |
-
4. **Test Root Endpoint** (app is running)
|
| 87 |
-
5. **Test Generation** (full functionality)
|
| 88 |
-
6. **Test Download** (using card_id from step 5)
|
| 89 |
-
|
| 90 |
-
### 6. **Success Indicators**
|
| 91 |
-
|
| 92 |
-
✅ **Space shows "Running" status**
|
| 93 |
-
✅ **Health endpoint returns 200 OK**
|
| 94 |
-
✅ **Generate endpoint returns card_id**
|
| 95 |
-
✅ **QR code URL is accessible**
|
| 96 |
-
✅ **Download endpoint serves PNG files**
|
| 97 |
-
|
| 98 |
-
### 7. **If Still Having Issues**
|
| 99 |
-
|
| 100 |
-
#### **Check HF Space Logs**:
|
| 101 |
-
1. Go to https://huggingface.co/spaces/ch404/cardserver
|
| 102 |
-
2. Click on "Logs" tab
|
| 103 |
-
3. Look for our debug output from `start.sh`
|
| 104 |
-
4. Check for any error messages
|
| 105 |
-
|
| 106 |
-
#### **Verify GitHub Deployment**:
|
| 107 |
-
1. Check if GitHub Actions completed successfully
|
| 108 |
-
2. Verify our commits were pushed to the space
|
| 109 |
-
3. Look for any deployment failures
|
| 110 |
-
|
| 111 |
-
#### **Alternative Testing**:
|
| 112 |
-
If the space URL doesn't work, try:
|
| 113 |
-
- Different browsers
|
| 114 |
-
- Incognito/private mode
|
| 115 |
-
- Direct IP (if available in logs)
|
| 116 |
-
|
| 117 |
-
### 8. **Quick Test Script**
|
| 118 |
-
|
| 119 |
-
Save this as `quick_test.sh` and run it:
|
| 120 |
-
```bash
|
| 121 |
-
#!/bin/bash
|
| 122 |
-
BASE_URL="https://ch404-cardserver.hf.space"
|
| 123 |
-
|
| 124 |
-
echo "Testing Museum Sexoskop App..."
|
| 125 |
-
echo "================================"
|
| 126 |
-
|
| 127 |
-
echo "1. Testing health endpoint..."
|
| 128 |
-
curl -s "$BASE_URL/api/v1/health" | jq . || echo "Failed or not JSON"
|
| 129 |
-
|
| 130 |
-
echo -e "\n2. Testing root endpoint..."
|
| 131 |
-
curl -s "$BASE_URL/" | jq . || echo "Failed or not JSON"
|
| 132 |
-
|
| 133 |
-
echo -e "\n3. Testing generate endpoint..."
|
| 134 |
-
curl -s -X POST "$BASE_URL/api/v1/generate-horoscope" \
|
| 135 |
-
-H "Content-Type: application/json" \
|
| 136 |
-
-d '{"terms": ["Test","Deploy","Success","Working","Happy"], "date_of_birth": "1990-01-01"}' \
|
| 137 |
-
| jq . || echo "Failed or not JSON"
|
| 138 |
-
|
| 139 |
-
echo -e "\nDone!"
|
| 140 |
-
```
|
| 141 |
-
|
| 142 |
-
### 9. **Final Notes**
|
| 143 |
-
|
| 144 |
-
- **Patience**: HF Spaces can take time to deploy and start
|
| 145 |
-
- **Dependencies**: ML model loading adds startup time
|
| 146 |
-
- **Resources**: Free HF Spaces have limitations
|
| 147 |
-
- **Persistence**: Spaces may sleep after inactivity
|
| 148 |
-
|
| 149 |
-
The fixes we implemented should resolve the static directory error. If you see different errors now, they're likely related to dependencies or model loading, which is progress!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/data/cards_training_data.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"sign": "Widder",
|
| 4 |
+
"date": "heute",
|
| 5 |
+
"context": "Liebe und Beziehungen",
|
| 6 |
+
"text": "Die Energie des Mars verleiht Ihnen heute besondere Anziehungskraft. Ihr Charisma wirkt magnetisch auf andere. In bestehenden Beziehungen können wichtige Gespräche zu mehr Intimität führen. Singles sollten aufmerksam sein - eine besondere Begegnung könnte heute stattfinden."
|
| 7 |
+
},
|
| 8 |
+
{
|
| 9 |
+
"sign": "Stier",
|
| 10 |
+
"date": "morgen",
|
| 11 |
+
"context": "Beruf und Karriere",
|
| 12 |
+
"text": "Venus bringt harmonische Schwingungen in Ihr Berufsleben. Kreative Projekte stehen unter einem besonders guten Stern. Nutzen Sie Ihre diplomatischen Fähigkeiten für wichtige Verhandlungen. Finanzielle Entscheidungen sollten gut durchdacht werden."
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"sign": "Zwillinge",
|
| 16 |
+
"date": "diese Woche",
|
| 17 |
+
"context": "Gesundheit und Wohlbefinden",
|
| 18 |
+
"text": "Merkur aktiviert Ihre mentale Energie - perfekt für neue Lernprojekte. Achten Sie auf ausreichend Bewegung und frische Luft. Ihr Kommunikationsbedürfnis ist erhöht, nutzen Sie dies für wichtige Gespräche mit Familie und Freunden."
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"sign": "Krebs",
|
| 22 |
+
"date": "heute",
|
| 23 |
+
"context": "Familie und Zuhause",
|
| 24 |
+
"text": "Der Mond verstärkt Ihre emotionale Intuition erheblich. In familiären Angelegenheiten können Sie als Vermittler fungieren. Ihr Zuhause wird zum Kraftort - schaffen Sie eine gemütliche Atmosphäre. Kochexperimente sind heute besonders erfolgreich."
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"sign": "Löwe",
|
| 28 |
+
"date": "dieses Wochenende",
|
| 29 |
+
"context": "Kreativität und Selbstausdruck",
|
| 30 |
+
"text": "Die Sonne entfacht Ihre kreative Flamme! Künstlerische Projekte erleben einen Durchbruch. Ihre natürliche Ausstrahlung zieht Bewunderung an. Bühnenmomente und öffentliche Auftritte stehen unter einem glücklichen Stern. Zeigen Sie, was in Ihnen steckt!"
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"sign": "Jungfrau",
|
| 34 |
+
"date": "nächste Woche",
|
| 35 |
+
"context": "Organisation und Effizienz",
|
| 36 |
+
"text": "Ihre analytischen Fähigkeiten sind geschärft wie selten zuvor. Komplexe Probleme lösen Sie mit Leichtigkeit. Gesundheitliche Präventionsmaßnahmen zeigen positive Wirkung. Ordnung in Ihrem Umfeld bringt auch mentale Klarheit. Details werden zu Ihrem Vorteil."
|
| 37 |
+
}
|
| 38 |
+
]
|
training/deploy_lora.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Deployment-Script für trainierte LoRA-Modelle
|
| 4 |
+
Automatisiert den Upload zu Hugging Face Hub
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import shutil
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import subprocess
|
| 11 |
+
import json
|
| 12 |
+
import logging
|
| 13 |
+
|
| 14 |
+
logging.basicConfig(level=logging.INFO)
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
class LoRADeployer:
|
| 18 |
+
def __init__(self, model_path="../models/lora-checkpoint"):
|
| 19 |
+
self.model_path = Path(model_path)
|
| 20 |
+
self.hf_username = None # Wird aus HF_USERNAME env var gelesen
|
| 21 |
+
|
| 22 |
+
def verify_model(self):
|
| 23 |
+
"""Prüft ob trainiertes LoRA-Modell vollständig ist"""
|
| 24 |
+
required_files = [
|
| 25 |
+
"adapter_config.json",
|
| 26 |
+
"adapter_model.bin", # oder adapter_model.safetensors
|
| 27 |
+
"README.md"
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
missing_files = []
|
| 31 |
+
for file in required_files:
|
| 32 |
+
if not (self.model_path / file).exists():
|
| 33 |
+
missing_files.append(file)
|
| 34 |
+
|
| 35 |
+
if missing_files:
|
| 36 |
+
logger.error(f"Fehlende Dateien: {missing_files}")
|
| 37 |
+
return False
|
| 38 |
+
|
| 39 |
+
logger.info("✅ LoRA-Modell vollständig")
|
| 40 |
+
return True
|
| 41 |
+
|
| 42 |
+
def create_model_card(self):
|
| 43 |
+
"""Erstellt eine Model Card für Hugging Face"""
|
| 44 |
+
model_card = f"""---
|
| 45 |
+
library_name: peft
|
| 46 |
+
base_model: teknium/OpenHermes-2.5-Mistral-7B
|
| 47 |
+
tags:
|
| 48 |
+
- generated_from_trainer
|
| 49 |
+
- horoscope
|
| 50 |
+
- astrology
|
| 51 |
+
- german
|
| 52 |
+
- lora
|
| 53 |
+
language:
|
| 54 |
+
- de
|
| 55 |
+
- en
|
| 56 |
+
license: apache-2.0
|
| 57 |
+
---
|
| 58 |
+
|
| 59 |
+
# LoRA Adapter für Horoskop-Generierung
|
| 60 |
+
|
| 61 |
+
Dieses LoRA-Adapter wurde für die Generierung von personalisierten Horoskopen trainiert.
|
| 62 |
+
|
| 63 |
+
## Basis-Modell
|
| 64 |
+
- **Model**: teknium/OpenHermes-2.5-Mistral-7B
|
| 65 |
+
- **LoRA Rank**: 16
|
| 66 |
+
- **Target Modules**: q_proj, v_proj, k_proj, o_proj
|
| 67 |
+
|
| 68 |
+
## Verwendung
|
| 69 |
+
|
| 70 |
+
```python
|
| 71 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 72 |
+
from peft import PeftModel
|
| 73 |
+
|
| 74 |
+
base_model = "teknium/OpenHermes-2.5-Mistral-7B"
|
| 75 |
+
tokenizer = AutoTokenizer.from_pretrained(base_model)
|
| 76 |
+
model = AutoModelForCausalLM.from_pretrained(base_model, load_in_4bit=True)
|
| 77 |
+
model = PeftModel.from_pretrained(model, "IHR_USERNAME/horoskop-lora")
|
| 78 |
+
|
| 79 |
+
# Beispiel-Prompt
|
| 80 |
+
prompt = "<|im_start|>system\\nDu bist ein erfahrener Astrologe.<|im_end|>\\n<|im_start|>user\\nErstelle ein Horoskop für Widder heute.<|im_end|>\\n<|im_start|>assistant\\n"
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
## Training Details
|
| 84 |
+
- **Training Duration**: {self.get_training_info().get('duration', 'N/A')}
|
| 85 |
+
- **Dataset Size**: {self.get_training_info().get('dataset_size', 'N/A')}
|
| 86 |
+
- **Epochs**: {self.get_training_info().get('epochs', 'N/A')}
|
| 87 |
+
"""
|
| 88 |
+
|
| 89 |
+
readme_path = self.model_path / "README.md"
|
| 90 |
+
with open(readme_path, 'w', encoding='utf-8') as f:
|
| 91 |
+
f.write(model_card)
|
| 92 |
+
|
| 93 |
+
logger.info(f"Model Card erstellt: {readme_path}")
|
| 94 |
+
|
| 95 |
+
def get_training_info(self):
|
| 96 |
+
"""Liest Training-Informationen aus logs oder config"""
|
| 97 |
+
# Placeholder - könnte aus Training-Logs gelesen werden
|
| 98 |
+
return {
|
| 99 |
+
"duration": "2-4 Stunden",
|
| 100 |
+
"dataset_size": "500+ Horoskop-Beispiele",
|
| 101 |
+
"epochs": "3"
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
def upload_to_hf(self, repo_name="horoskop-lora"):
|
| 105 |
+
"""Upload zu Hugging Face Hub"""
|
| 106 |
+
|
| 107 |
+
# Hugging Face Username prüfen
|
| 108 |
+
hf_username = os.getenv("HF_USERNAME")
|
| 109 |
+
if not hf_username:
|
| 110 |
+
logger.error("HF_USERNAME environment variable nicht gesetzt")
|
| 111 |
+
logger.info("Setzen Sie: export HF_USERNAME=ihr_username")
|
| 112 |
+
return False
|
| 113 |
+
|
| 114 |
+
# Hugging Face CLI prüfen
|
| 115 |
+
try:
|
| 116 |
+
subprocess.run(["huggingface-cli", "--version"], check=True, capture_output=True)
|
| 117 |
+
except subprocess.CalledProcessError:
|
| 118 |
+
logger.error("Hugging Face CLI nicht installiert")
|
| 119 |
+
logger.info("Installieren Sie: pip install huggingface_hub")
|
| 120 |
+
return False
|
| 121 |
+
|
| 122 |
+
# Repository erstellen
|
| 123 |
+
repo_id = f"{hf_username}/{repo_name}"
|
| 124 |
+
logger.info(f"Erstelle Repository: {repo_id}")
|
| 125 |
+
|
| 126 |
+
try:
|
| 127 |
+
# Repository auf HF erstellen
|
| 128 |
+
cmd = [
|
| 129 |
+
"huggingface-cli", "repo", "create",
|
| 130 |
+
repo_id, "--type", "model", "--private"
|
| 131 |
+
]
|
| 132 |
+
subprocess.run(cmd, check=True)
|
| 133 |
+
logger.info(f"✅ Repository erstellt: {repo_id}")
|
| 134 |
+
|
| 135 |
+
except subprocess.CalledProcessError as e:
|
| 136 |
+
if "already exists" in str(e):
|
| 137 |
+
logger.info(f"Repository existiert bereits: {repo_id}")
|
| 138 |
+
else:
|
| 139 |
+
logger.error(f"Repository-Erstellung fehlgeschlagen: {e}")
|
| 140 |
+
return False
|
| 141 |
+
|
| 142 |
+
# Dateien hochladen
|
| 143 |
+
try:
|
| 144 |
+
cmd = [
|
| 145 |
+
"huggingface-cli", "upload", repo_id,
|
| 146 |
+
str(self.model_path), ".", "--recursive"
|
| 147 |
+
]
|
| 148 |
+
subprocess.run(cmd, check=True)
|
| 149 |
+
logger.info(f"✅ Upload erfolgreich: https://huggingface.co/{repo_id}")
|
| 150 |
+
return True
|
| 151 |
+
|
| 152 |
+
except subprocess.CalledProcessError as e:
|
| 153 |
+
logger.error(f"Upload fehlgeschlagen: {e}")
|
| 154 |
+
return False
|
| 155 |
+
|
| 156 |
+
def prepare_for_space(self):
|
| 157 |
+
"""Bereitet Modell für Hugging Face Space vor"""
|
| 158 |
+
space_model_path = Path("../models/lora-checkpoint-deployed")
|
| 159 |
+
space_model_path.mkdir(exist_ok=True)
|
| 160 |
+
|
| 161 |
+
# Kopiere nur notwendige Dateien
|
| 162 |
+
essential_files = [
|
| 163 |
+
"adapter_config.json",
|
| 164 |
+
"adapter_model.bin",
|
| 165 |
+
"adapter_model.safetensors"
|
| 166 |
+
]
|
| 167 |
+
|
| 168 |
+
for file in essential_files:
|
| 169 |
+
src = self.model_path / file
|
| 170 |
+
if src.exists():
|
| 171 |
+
dst = space_model_path / file
|
| 172 |
+
shutil.copy2(src, dst)
|
| 173 |
+
logger.info(f"Kopiert: {file}")
|
| 174 |
+
|
| 175 |
+
logger.info(f"✅ Space-ready Modell in: {space_model_path}")
|
| 176 |
+
return space_model_path
|
| 177 |
+
|
| 178 |
+
def main():
|
| 179 |
+
"""Hauptfunktion für Deployment"""
|
| 180 |
+
deployer = LoRADeployer()
|
| 181 |
+
|
| 182 |
+
print("🚀 LoRA-Modell Deployment")
|
| 183 |
+
print("========================")
|
| 184 |
+
|
| 185 |
+
# 1. Modell verifizieren
|
| 186 |
+
if not deployer.verify_model():
|
| 187 |
+
print("❌ Modell-Verifikation fehlgeschlagen")
|
| 188 |
+
return
|
| 189 |
+
|
| 190 |
+
# 2. Model Card erstellen
|
| 191 |
+
deployer.create_model_card()
|
| 192 |
+
|
| 193 |
+
# 3. User-Input für Deployment-Optionen
|
| 194 |
+
print("\n📤 Deployment-Optionen:")
|
| 195 |
+
print("1. Für Hugging Face Space vorbereiten")
|
| 196 |
+
print("2. Zu Hugging Face Hub hochladen")
|
| 197 |
+
print("3. Beides")
|
| 198 |
+
|
| 199 |
+
choice = input("Wählen Sie (1-3): ").strip()
|
| 200 |
+
|
| 201 |
+
if choice in ["1", "3"]:
|
| 202 |
+
space_path = deployer.prepare_for_space()
|
| 203 |
+
print(f"✅ Space-ready: {space_path}")
|
| 204 |
+
|
| 205 |
+
if choice in ["2", "3"]:
|
| 206 |
+
repo_name = input("Repository-Name (Standard: horoskop-lora): ").strip()
|
| 207 |
+
if not repo_name:
|
| 208 |
+
repo_name = "horoskop-lora"
|
| 209 |
+
|
| 210 |
+
if deployer.upload_to_hf(repo_name):
|
| 211 |
+
print("✅ Upload zu Hugging Face erfolgreich!")
|
| 212 |
+
else:
|
| 213 |
+
print("❌ Upload fehlgeschlagen")
|
| 214 |
+
|
| 215 |
+
print("\n🎯 Nächste Schritte:")
|
| 216 |
+
print("1. Aktualisieren Sie config.py mit dem neuen Modell-Pfad")
|
| 217 |
+
print("2. Testen Sie das Modell lokal")
|
| 218 |
+
print("3. Deployen Sie zu Hugging Face Space")
|
| 219 |
+
|
| 220 |
+
if __name__ == "__main__":
|
| 221 |
+
main()
|
training/test_setup.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import torch
|
| 3 |
+
import transformers
|
| 4 |
+
import peft
|
| 5 |
+
import datasets
|
| 6 |
+
import evaluate
|
| 7 |
+
import sklearn
|
| 8 |
+
import wandb
|
| 9 |
+
import tensorboard
|
| 10 |
+
|
| 11 |
+
print('✅ Alle Training-Pakete erfolgreich importiert!')
|
| 12 |
+
print(f'🚀 PyTorch Version: {torch.__version__}')
|
| 13 |
+
print(f'🤖 Transformers Version: {transformers.__version__}')
|
| 14 |
+
print(f'🔧 PEFT Version: {peft.__version__}')
|
| 15 |
+
print(f'📊 Datasets Version: {datasets.__version__}')
|
| 16 |
+
print(f'🎯 CUDA verfügbar: {torch.cuda.is_available()}')
|
| 17 |
+
|
| 18 |
+
if torch.cuda.is_available():
|
| 19 |
+
print(f'📱 GPU: {torch.cuda.get_device_name(0)}')
|
| 20 |
+
print(f'💾 VRAM: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} GB')
|
| 21 |
+
else:
|
| 22 |
+
print('💻 CPU-Modus (kein CUDA)')
|
| 23 |
+
|
| 24 |
+
print('\n🎯 Hardware-Empfehlungen für LoRA-Training:')
|
| 25 |
+
print('• Minimum: 8GB RAM + 4GB VRAM')
|
| 26 |
+
print('• Empfohlen: 16GB RAM + 8GB VRAM')
|
| 27 |
+
print('• Optimal: 32GB RAM + 16GB+ VRAM')
|
training/train_lora.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Kostengünstiges LORA-Training für OpenHermes-2.5-Mistral-7B
|
| 4 |
+
Optimiert für lokale Ausführung mit effizienter Speichernutzung
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
from transformers import (
|
| 9 |
+
AutoTokenizer, AutoModelForCausalLM,
|
| 10 |
+
TrainingArguments, Trainer, DataCollatorForLanguageModeling
|
| 11 |
+
)
|
| 12 |
+
from peft import LoraConfig, get_peft_model, TaskType
|
| 13 |
+
from datasets import Dataset
|
| 14 |
+
import json
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
import logging
|
| 17 |
+
|
| 18 |
+
# Setup logging
|
| 19 |
+
logging.basicConfig(level=logging.INFO)
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
class LoRATrainer:
|
| 23 |
+
def __init__(self, base_model_id="teknium/OpenHermes-2.5-Mistral-7B"):
|
| 24 |
+
self.base_model_id = base_model_id
|
| 25 |
+
self.output_dir = Path("../models/lora-checkpoint")
|
| 26 |
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
| 27 |
+
|
| 28 |
+
def setup_model_and_tokenizer(self):
|
| 29 |
+
"""Modell und Tokenizer mit optimalen Einstellungen laden"""
|
| 30 |
+
logger.info(f"Lade Modell: {self.base_model_id}")
|
| 31 |
+
|
| 32 |
+
# Tokenizer
|
| 33 |
+
self.tokenizer = AutoTokenizer.from_pretrained(self.base_model_id)
|
| 34 |
+
if self.tokenizer.pad_token is None:
|
| 35 |
+
self.tokenizer.pad_token = self.tokenizer.eos_token
|
| 36 |
+
|
| 37 |
+
# Modell mit 4-bit Quantisierung für Speichereffizienz
|
| 38 |
+
self.model = AutoModelForCausalLM.from_pretrained(
|
| 39 |
+
self.base_model_id,
|
| 40 |
+
load_in_4bit=True,
|
| 41 |
+
torch_dtype=torch.float16,
|
| 42 |
+
device_map="auto",
|
| 43 |
+
trust_remote_code=True
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
# LoRA Konfiguration - kostengünstig optimiert
|
| 47 |
+
lora_config = LoraConfig(
|
| 48 |
+
task_type=TaskType.CAUSAL_LM,
|
| 49 |
+
r=16, # Rank - niedrig für Effizienz
|
| 50 |
+
lora_alpha=32, # Skalierungsfaktor
|
| 51 |
+
lora_dropout=0.1,
|
| 52 |
+
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"], # Nur wichtige Module
|
| 53 |
+
bias="none"
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
# PEFT Modell erstellen
|
| 57 |
+
self.model = get_peft_model(self.model, lora_config)
|
| 58 |
+
self.model.print_trainable_parameters()
|
| 59 |
+
|
| 60 |
+
def prepare_dataset(self, data_path="training_data.json"):
|
| 61 |
+
"""Trainingsdaten vorbereiten"""
|
| 62 |
+
logger.info(f"Lade Trainingsdaten: {data_path}")
|
| 63 |
+
|
| 64 |
+
# Beispiel-Datenformat für Horoskop-Generierung
|
| 65 |
+
if not Path(data_path).exists():
|
| 66 |
+
self.create_sample_data(data_path)
|
| 67 |
+
|
| 68 |
+
with open(data_path, 'r', encoding='utf-8') as f:
|
| 69 |
+
data = json.load(f)
|
| 70 |
+
|
| 71 |
+
def tokenize_function(examples):
|
| 72 |
+
# Prompt-Template für Horoskop-Generierung
|
| 73 |
+
texts = []
|
| 74 |
+
for item in examples:
|
| 75 |
+
prompt = f"<|im_start|>system\nDu bist ein erfahrener Astrologe. Erstelle ein präzises und einfühlsames Horoskop.<|im_end|>\n<|im_start|>user\nErstelle ein Horoskop für {item['sign']} am {item['date']}.<|im_end|>\n<|im_start|>assistant\n{item['horoscope']}<|im_end|>"
|
| 76 |
+
texts.append(prompt)
|
| 77 |
+
|
| 78 |
+
tokenized = self.tokenizer(
|
| 79 |
+
texts,
|
| 80 |
+
truncation=True,
|
| 81 |
+
padding="max_length",
|
| 82 |
+
max_length=512,
|
| 83 |
+
return_tensors="pt"
|
| 84 |
+
)
|
| 85 |
+
tokenized["labels"] = tokenized["input_ids"].clone()
|
| 86 |
+
return tokenized
|
| 87 |
+
|
| 88 |
+
dataset = Dataset.from_list(data)
|
| 89 |
+
tokenized_dataset = dataset.map(tokenize_function, batched=False)
|
| 90 |
+
return tokenized_dataset
|
| 91 |
+
|
| 92 |
+
def create_sample_data(self, data_path):
|
| 93 |
+
"""Beispiel-Trainingsdaten erstellen"""
|
| 94 |
+
sample_data = [
|
| 95 |
+
{
|
| 96 |
+
"sign": "Widder",
|
| 97 |
+
"date": "heute",
|
| 98 |
+
"horoscope": "Die Energie des Mars verleiht Ihnen heute besondere Kraft. Nutzen Sie diese für wichtige Entscheidungen in der Liebe."
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"sign": "Stier",
|
| 102 |
+
"date": "morgen",
|
| 103 |
+
"horoscope": "Venus bringt harmonische Schwingungen in Ihr Leben. Ein perfekter Tag für romantische Begegnungen."
|
| 104 |
+
},
|
| 105 |
+
# Weitere Beispiele...
|
| 106 |
+
]
|
| 107 |
+
|
| 108 |
+
with open(data_path, 'w', encoding='utf-8') as f:
|
| 109 |
+
json.dump(sample_data, f, ensure_ascii=False, indent=2)
|
| 110 |
+
logger.info(f"Beispiel-Trainingsdaten erstellt: {data_path}")
|
| 111 |
+
|
| 112 |
+
def train(self, dataset, num_epochs=3):
|
| 113 |
+
"""Training starten - kostengünstig optimiert"""
|
| 114 |
+
logger.info("Starte Training...")
|
| 115 |
+
|
| 116 |
+
training_args = TrainingArguments(
|
| 117 |
+
output_dir=str(self.output_dir),
|
| 118 |
+
num_train_epochs=num_epochs,
|
| 119 |
+
per_device_train_batch_size=1, # Kleine Batch-Size für wenig RAM
|
| 120 |
+
gradient_accumulation_steps=4, # Simuliert größere Batches
|
| 121 |
+
warmup_steps=100,
|
| 122 |
+
logging_steps=10,
|
| 123 |
+
save_steps=500,
|
| 124 |
+
evaluation_strategy="no", # Keine Evaluation für Effizienz
|
| 125 |
+
save_strategy="epoch",
|
| 126 |
+
load_best_model_at_end=False,
|
| 127 |
+
remove_unused_columns=False,
|
| 128 |
+
dataloader_pin_memory=False,
|
| 129 |
+
gradient_checkpointing=True, # Speicher sparen
|
| 130 |
+
optim="adamw_torch",
|
| 131 |
+
learning_rate=5e-5,
|
| 132 |
+
weight_decay=0.01,
|
| 133 |
+
lr_scheduler_type="cosine",
|
| 134 |
+
report_to=None, # Kein Wandb etc. für Kosten
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
data_collator = DataCollatorForLanguageModeling(
|
| 138 |
+
tokenizer=self.tokenizer,
|
| 139 |
+
mlm=False
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
trainer = Trainer(
|
| 143 |
+
model=self.model,
|
| 144 |
+
args=training_args,
|
| 145 |
+
train_dataset=dataset,
|
| 146 |
+
data_collator=data_collator,
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
# Training starten
|
| 150 |
+
trainer.train()
|
| 151 |
+
|
| 152 |
+
# Modell speichern
|
| 153 |
+
trainer.save_model()
|
| 154 |
+
self.tokenizer.save_pretrained(str(self.output_dir))
|
| 155 |
+
logger.info(f"Training abgeschlossen. Modell gespeichert in: {self.output_dir}")
|
| 156 |
+
|
| 157 |
+
def main():
|
| 158 |
+
"""Hauptfunktion für Training"""
|
| 159 |
+
trainer = LoRATrainer()
|
| 160 |
+
|
| 161 |
+
# 1. Modell und Tokenizer setup
|
| 162 |
+
trainer.setup_model_and_tokenizer()
|
| 163 |
+
|
| 164 |
+
# 2. Dataset vorbereiten
|
| 165 |
+
dataset = trainer.prepare_dataset()
|
| 166 |
+
|
| 167 |
+
# 3. Training starten
|
| 168 |
+
trainer.train(dataset, num_epochs=3)
|
| 169 |
+
|
| 170 |
+
print("✅ LORA-Training erfolgreich abgeschlossen!")
|
| 171 |
+
print(f"📁 Modell gespeichert in: {trainer.output_dir}")
|
| 172 |
+
print("🚀 Jetzt können Sie das trainierte Modell in Ihrem FastAPI-Server verwenden.")
|
| 173 |
+
|
| 174 |
+
if __name__ == "__main__":
|
| 175 |
+
main()
|
training/training_requirements.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Training-Specific Dependencies
|
| 2 |
+
# Nur für lokales Training benötigt - NICHT für Produktion
|
| 3 |
+
|
| 4 |
+
# Dataset Handling & Evaluation
|
| 5 |
+
datasets>=2.14.0
|
| 6 |
+
evaluate>=0.4.0
|
| 7 |
+
|
| 8 |
+
# ML Training & Analysis
|
| 9 |
+
scikit-learn>=1.3.0
|
| 10 |
+
|
| 11 |
+
# Training Monitoring (Optional)
|
| 12 |
+
wandb>=0.20.0 # Experiment tracking
|
| 13 |
+
tensorboard>=2.19.0 # Loss visualization
|
| 14 |
+
|
| 15 |
+
# Training Utilities
|
| 16 |
+
tqdm>=4.66.0 # Progress bars
|
| 17 |
+
matplotlib>=3.8.0 # Plotting
|
| 18 |
+
seaborn>=0.13.0 # Better plots
|
| 19 |
+
|
| 20 |
+
# Development Tools (Optional)
|
| 21 |
+
jupyter>=1.0.0 # Notebooks for analysis
|
| 22 |
+
ipywidgets>=8.0.0 # Interactive widgets
|
| 23 |
+
|
| 24 |
+
# Basis-Requirements werden automatisch über requirements.txt geladen:
|
| 25 |
+
# torch, transformers, peft, accelerate, numpy
|