GitHub Actions commited on
Commit
f6e3d73
·
1 Parent(s): e27b514

🚀 Auto-deploy from GitHub

Browse files
.gitignore CHANGED
@@ -1,21 +1,199 @@
1
  .git
2
  __pycache__
3
  __pycache__/
4
- *.pyc
5
- *.pyo
6
- *.pyd
7
- *.egg-info/
 
 
 
 
8
  .Python
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  env/
10
  venv/
11
- .venv/
12
- .vscode/
 
 
 
 
 
 
 
 
13
  .idea/
14
  *.swp
15
  *.swo
 
 
 
16
  .DS_Store
 
 
 
 
17
  Thumbs.db
18
- *.env
19
- secrets.txt
20
- *.log
21
- local_settings.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Query Supabase for the image record using card_id
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
- # Eigene Modulimporte
9
- from ..schemas.horoscope_schemas import HoroscopeGenerateRequest, HoroscopeGenerateResponse
10
- from ....core.generator import build_prompt, get_zodiac
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, save_horoscope_card
14
- from ....core.config import settings # Für Pfade und URLs
15
-
16
- # Model-Ladefunktionen (angenommen, diese bleiben ähnlich, aber ggf. anpassen)
17
- from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
18
- from peft import PeftModel
19
 
20
  load_dotenv()
21
  router = APIRouter()
22
 
23
- # --- Globale Variablen und Modell-Ladefunktion ---
24
- MODEL_PATH = os.getenv("MODEL_PATH", settings.MODEL_PATH) # Aus settings holen
25
- DEFAULT_MODEL_ID = os.getenv("DEFAULT_MODEL_ID", settings.DEFAULT_MODEL_ID) # Aus settings holen
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
- # 1. Prompt erstellen
72
- # Annahme: 'lang' ist fest auf 'de' gesetzt oder kommt aus der Anfrage/Config
73
- lang = "de"
74
- birthdate_str = request.date_of_birth.isoformat()
75
 
76
- horoscope_prompt = build_prompt(
77
  lang=lang,
78
- birthdate=birthdate_str,
79
  terms=request.terms
80
  )
81
 
82
- # 2. Horoskoptext generieren
83
- llm_pipeline = load_language_model()
84
- # Parameter für die Textgenerierung (könnten auch aus request oder config kommen)
85
  generation_params = {
86
- "max_length": 200, # Beispielwert
87
- "temperature": 0.75, # Beispielwert
88
- "do_sample": True,
89
- "return_full_text": False
 
 
90
  }
91
 
92
- # Der Prompt selbst ist der Input für die Pipeline
93
- # Die Hugging Face Pipeline erwartet den Prompt als ersten Parameter
94
- # und die anderen Parameter als kwargs.
95
- # Der 'prompt' im request.prompt von vorher ist jetzt 'horoscope_prompt'
96
- generated_outputs = llm_pipeline(horoscope_prompt, **generation_params)
97
-
98
- if not generated_outputs or not generated_outputs[0]["generated_text"]:
99
- raise HTTPException(status_code=500, detail="Textgenerierung fehlgeschlagen oder leerer Text.")
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
- # 3. Horoskopkarte rendern
111
- # Annahmen für tarot_id und symbol_ids - diese müssten dynamisch werden
112
- tarot_id = 1 # Beispiel
113
- symbol_ids = [1, 2, 3, 4] # Beispiel
114
 
115
- # card_renderer.generate_card speichert die Karte und gibt eine file_id (UUID) zurück
116
- # Der Pfad wird in config.py über settings.resolved_generated_path definiert
117
- # und card_renderer sollte diesen verwenden oder einen relativen Pfad dazu.
118
- # Für generate_card:
119
- # base_path = settings.resolved_base_path
120
- # symbols_path = settings.resolved_symbols_path
121
- # font_path = settings.resolved_default_font_path
122
- # output_dir = settings.resolved_generated_path
123
- # Diese Pfade müssen in card_renderer.py korrekt verwendet werden.
124
-
125
- # Temporäre Annahme: generate_card gibt den Dateinamen (inkl. Erweiterung) zurück
126
- # und speichert im korrekten `settings.resolved_generated_path`
127
- card_file_id = render_horoscope_card(
128
- tarot_id=tarot_id,
129
- symbol_ids=symbol_ids,
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
- # 6. Antwort an Frontend
168
- # Die URL zum QR-Code-Bild, das direkt im Frontend angezeigt werden kann
169
- # z.B. HOST_URL/static/images/qr/qr_<card_file_id>.png
170
- # Der /static Pfad wird in main.py gemountet
171
- qr_code_image_url = f"{host_url}/static/images/qr/{qr_code_filename_base}"
 
 
 
 
 
 
 
 
172
 
173
- return HoroscopeGenerateResponse(
174
- card_id=card_file_id,
175
- qr_code_image_url=qr_code_image_url,
176
- message="Horoskopkarte erfolgreich generiert."
 
 
 
 
 
 
 
 
 
 
 
 
177
  )
178
 
179
  except FileNotFoundError as e:
180
- # Speziell für Template-Fehler
181
- raise HTTPException(status_code=500, detail=f"Eine benötigte Datei wurde nicht gefunden: {str(e)}")
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"Interne Serverfehler bei der Horoskopgenerierung: {str(e)}")
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 HoroscopeGenerateRequest(BaseModel):
11
  """
12
- Schema für die Anfrage zur Generierung einer personalisierten Horoskopkarte.
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 das Horoskop.",
19
  example=["Liebe", "Erfolg", "Glück", "Herausforderung", "Wachstum"]
20
  )
21
- date_of_birth: date = Field(
22
  ...,
23
- description="Das Geburtsdatum des Nutzers im Format JJJJ-MM-TT.",
24
  example="1990-05-15"
25
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  # --- Response Models ---
28
 
29
- class HoroscopeGenerateResponse(BaseModel):
30
  """
31
- Schema für die Antwort nach erfolgreicher Anfrage zur Horoskopgenerierung.
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 Horoskopkarte. Wird für den Download benötigt.",
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 Horoskopkarte enthält) direkt abrufbar ist.",
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 HoroscopeCardData(BaseModel):
50
  """
51
- Schema für die internen Daten einer gespeicherten Horoskopkarte (z.B. aus Supabase).
52
- Dies ist kein API-Response-Modell, sondern eher ein Datenmodell für Services/Datenbank.
53
  """
54
- id: uuid.UUID = Field(..., description="Eindeutige ID der Horoskopkarte.")
55
- terms: List[str] = Field(..., description="Die ursprünglichen fünf Schlüsselbegriffe.")
56
- date_of_birth: date = Field(..., description="Das Geburtsdatum des Nutzers.")
57
- horoscope_text: str = Field(..., description="Der generierte Horoskoptext.")
58
- image_path: str = Field(..., description="Dateisystempfad zum generierten Horoskopbild.")
59
- qr_code_path: str = Field(..., description="Dateisystempfad zum generierten QR-Code-Bild.")
60
- qr_code_link: str = Field(..., description="Der URL, der im QR-Code kodiert ist.")
61
- created_at: Optional[date] = Field(None, description="Zeitpunkt der Erstellung des Eintrags.")
 
 
 
 
 
 
 
 
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
- tarot_id: int,
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"{tarot_id}.png"
22
  if not base_image_file.exists():
23
  raise FileNotFoundError(f"Basiskartenbild nicht gefunden: {base_image_file}")
24
- tarot_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,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
- tarot_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(tarot_img)
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 = tarot_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 = tarot_img.width - (2 * text_x) # Maximale Breite für Text
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
- tarot_img.save(output_file_path)
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
- for _ in range(max_retries):
9
- response = generator(prompt)[0]["generated_text"]
10
- if check_constraints(response, terms):
11
- return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 get_zodiac(birthdate: str) -> str:
11
  """Leitet das Sternzeichen aus dem Geburtsdatum ab."""
12
  date = datetime.datetime.strptime(birthdate, "%Y-%m-%d").date()
13
- zodiacs = [
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 zodiacs:
20
  if date_as_number <= boundary:
21
  return sign
22
  return "Steinbock"
23
 
24
- def build_prompt(lang: str, birthdate: str, terms: list[str]) -> str:
25
- template_path = Path(__file__).resolve().parent.parent.parent / "templates" / lang / "base.txt"
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
- prompt = template.render(
33
- birthdate=birthdate,
34
- terms=', '.join(terms),
35
- sternzeichen=sternzeichen
36
- )
37
- return prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 (separat gehalten für Training)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- base_model_id = "teknium/OpenHermes-2.5-Mistral-7B"
8
- lora_path = "./models/lora-checkpoint" # oder None, falls nicht getunt
 
 
 
 
 
9
 
10
- tokenizer = AutoTokenizer.from_pretrained(base_model_id)
11
- model = AutoModelForCausalLM.from_pretrained(base_model_id, load_in_4bit=True, device_map="auto")
12
 
13
  try:
14
- model = PeftModel.from_pretrained(model, lora_path)
15
- except:
16
- pass # fallback ohne LoRA
 
17
 
18
- pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  return pipe
20
 
21
- generator = load_model()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Horoscope"])
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 save_horoscope_card(
15
  card_id: str,
16
  terms: list[str],
17
- date_of_birth: str, # Pydantic date wird hier als str erwartet für Supabase
18
- horoscope_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,8 +25,8 @@ def save_horoscope_card(
25
  response = supabase.table("horoscope_cards").insert({
26
  "id": card_id,
27
  "terms": terms,
28
- "date_of_birth": date_of_birth,
29
- "horoscope_text": horoscope_text,
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
- fastapi
2
- uvicorn[standard]
3
- pydantic
4
- pydantic-settings
5
- qrcode
6
- pillow
7
- peft
8
- torch
9
- accelerate
10
- transformers
11
- numpy
12
- supabase
13
- jinja2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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