eva-mirror / app.py
Lafu01's picture
Update app.py
41c0375 verified
Raw
History Blame Contribute Delete
22.3 kB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
EVA Mirror Space v5 - QWEN UPDATE + WHISPER FIX + QWEN3-ASR + GGUF
- Qwen3 LLM: löscht alte fehlerhafte model_q4f16 Dateien, spiegelt ORT-GenAI Dateien
- Whisper: löscht whisper-medium (EN-only) und altes whisper-tiny (2-Datei-System),
spiegelt whisper-tiny + whisper-base im gemergten fp32-Zielformat
- Qwen3-ASR-0.6B (ONNX): spiegelt das komplette Community-ONNX-Repo neu dazu
- GGUF (neu in v5): legt llm/gguf/ an, spiegelt Q4_K_M-Quants von Qwen3.5-9B,
Qwen3.5-0.8B (mit Fallback-Quellen), Qwen3-1.7B, Qwen3-0.6B und Qwen3-ASR-0.6B,
plus mmproj-F16 für die multimodalen Qwen3.5-Modelle
Läuft auf HF-Servern (Space). Kein lokales/mobiles Datenvolumen.
"""
import os
import io
import requests
import gradio as gr
from huggingface_hub import HfApi
HF_TOKEN = os.environ.get("HF_TOKEN", "")
ZIEL_REPO = "Lafu01/eva-assets"
REPO_TYPE = "model"
# =============================================================================
# TEIL 1: QWEN3 LLM (unverändert aus v3)
# =============================================================================
QWEN_MODELLE = [
{"name": "qwen3-0.6b", "quelle": "onnx-community/Qwen3-0.6B-ONNX"},
{"name": "qwen3-1.7b", "quelle": "onnx-community/Qwen3-1.7B-ONNX"},
{"name": "qwen3-4b", "quelle": "onnx-community/Qwen3-4B-ONNX"},
]
QWEN_UNTERORDNER = "onnxruntime/cpu_and_mobile/cpu-int4-kld-block-128"
QWEN_LOESCHEN = [
"llm/qwen3-0.6b/model_q4f16.onnx",
"llm/qwen3-0.6b/model_q4f16.onnx_data",
"llm/qwen3-1.7b/model_q4f16.onnx",
"llm/qwen3-1.7b/model_q4f16.onnx_data",
"llm/qwen3-4b/model_q4f16.onnx",
"llm/qwen3-4b/model_q4f16.onnx_data",
]
# =============================================================================
# TEIL 2: WHISPER (neu in v4)
# =============================================================================
# Alte, falsche Whisper-Dateien werden per Prefix gefunden und gelöscht
# (nicht hart codiert, da die genauen Dateinamen im Ziel-Repo variieren)
WHISPER_LOESCH_PREFIXE = [
"stt/whisper-medium/", # EN-only, wird nicht mehr gebraucht
"stt/whisper/Whisper_tiny(FB32)/", # altes 2-Datei-System (falsch)
]
# Nur die GEMERGTE fp32-Variante (Ziel-System) - kein altes 2-Datei-System,
# keine fp16/int8/q4/bnb4/uint8 Nebenvarianten
WHISPER_ONNX_DATEIEN = ["encoder_model.onnx", "decoder_model_merged.onnx"]
WHISPER_ROOT_DATEIEN = [
"added_tokens.json",
"config.json",
"generation_config.json",
"merges.txt",
"normalizer.json",
"preprocessor_config.json",
"special_tokens_map.json",
"tokenizer.json",
"tokenizer_config.json",
"vocab.json",
]
WHISPER_MODELLE = [
{"name": "whisper-tiny", "quelle": "onnx-community/whisper-tiny", "ziel_ordner": "stt/whisper-tiny"},
{"name": "whisper-base", "quelle": "onnx-community/whisper-base", "ziel_ordner": "stt/whisper-base"},
]
# =============================================================================
# TEIL 3: QWEN3-ASR (neu in v4)
# =============================================================================
QWEN_ASR_QUELLE = "baicai1145/Qwen3-ASR-0.6B-ONNX"
QWEN_ASR_ZIEL_ORDNER = "llm/qwen3-asr-0.6b"
# =============================================================================
# TEIL 4: GGUF (neu in v5) - alle in Q4_K_M
# =============================================================================
GGUF_ZIEL_PREFIX = "llm/gguf"
MMPROJ_PRAEZISION = "f16" # F16 statt BF16: kein natives BF16 auf Snapdragon Gen 1
# Pro Modell: Liste von Quellen (erste, die eine passende Datei hat, gewinnt).
# "mmproj" = True nur bei den multimodalen Qwen3.5-Modellen.
GGUF_MODELLE = [
{
"name": "qwen3.5-9b",
"quellen": ["unsloth/Qwen3.5-9B-GGUF"],
"mmproj": True,
},
{
"name": "qwen3.5-0.8b",
"quellen": [
"unsloth/Qwen3.5-0.8B-GGUF",
"bartowski/Qwen_Qwen3.5-0.8B-GGUF",
"mradermacher/Qwen3.5-0.8B-GGUF",
],
"mmproj": True,
},
{
"name": "qwen3-1.7b",
"quellen": ["unsloth/Qwen3-1.7B-GGUF"],
"mmproj": False,
},
{
"name": "qwen3-0.6b",
"quellen": ["unsloth/Qwen3-0.6B-GGUF"],
"mmproj": False,
},
{
"name": "qwen3-asr-0.6b",
# bereits als Q4_K_M vor-quantisiert von OpenVoiceOS, fuer Edge/CPU gebaut
"quellen": ["OpenVoiceOS/qwen3-asr-0.6b-q4-k-m"],
"mmproj": False,
},
]
def datei_nach_muster_finden(dateien, enthaelt, ausschliessen=None):
"""Sucht in einer Dateiliste (case-insensitive) nach einem Treffer.
Nutzt Wortgrenzen fuer die Praezisions-Tokens (f16/bf16/f32 etc.), damit
'f16' NICHT versehentlich in 'bf16' matcht (das war ein Bug bis v5)."""
import re
ausschliessen = ausschliessen or []
def token_passt(token, dateiname_klein):
# Grenzen: kein Buchstabe/Ziffer direkt davor oder danach
pattern = r'(?<![a-z0-9])' + re.escape(token.lower()) + r'(?![a-z0-9])'
return re.search(pattern, dateiname_klein) is not None
for f in dateien:
fl = f.lower()
if all(token_passt(s, fl) for s in enthaelt) and not any(token_passt(s, fl) for s in ausschliessen):
return f
return None
# =============================================================================
# Hilfsfunktionen
# =============================================================================
def stream_kopieren(api, url, ziel_pfad, token):
"""Streamt direkt von URL zu HF-Repo — kein lokaler Speicher."""
try:
r = requests.get(url, stream=True, timeout=300,
headers={"Authorization": f"Bearer {token}"})
if r.status_code == 404:
return f"FEHLER 404: {url}"
r.raise_for_status()
groesse_mb = int(r.headers.get("content-length", 0)) / 1024 / 1024
puffer = io.BytesIO()
for chunk in r.iter_content(chunk_size=1024 * 1024):
puffer.write(chunk)
puffer.seek(0)
api.upload_file(
path_or_fileobj=puffer,
path_in_repo=ziel_pfad,
repo_id=ZIEL_REPO,
repo_type=REPO_TYPE,
token=token,
)
return f"OK ({groesse_mb:.1f} MB): {ziel_pfad}"
except Exception as e:
return f"FEHLER: {ziel_pfad}{e}"
def hf_url(repo, pfad):
return f"https://huggingface.co/{repo}/resolve/main/{pfad}"
def dateien_loeschen_per_prefix(api, token, vorhanden, prefixe, log):
for prefix in prefixe:
treffer = [f for f in vorhanden if f.startswith(prefix)]
if not treffer:
log.append(f" SKIP (nichts gefunden unter): {prefix}")
continue
for pfad in treffer:
try:
api.delete_file(path_in_repo=pfad, repo_id=ZIEL_REPO, repo_type=REPO_TYPE, token=token)
log.append(f" GELÖSCHT: {pfad}")
vorhanden.discard(pfad)
except Exception as e:
log.append(f" Löschen fehlgeschlagen: {pfad}{e}")
# =============================================================================
# Ablauf: Qwen3 LLM
# =============================================================================
def qwen_llm_starten(token_eingabe):
token = HF_TOKEN or token_eingabe.strip()
if not token:
return "FEHLER: Kein Token vorhanden."
api = HfApi(token=token)
log = []
try:
vorhanden = set(api.list_repo_files(ZIEL_REPO, repo_type=REPO_TYPE, token=token))
except Exception as e:
return f"FEHLER: Repo nicht erreichbar — {e}"
log.append(f"Repo: {ZIEL_REPO} | {len(vorhanden)} Dateien vorhanden")
log.append("=" * 60)
log.append("\n🗑️ SCHRITT 1: Lösche alte model_q4f16 Dateien...")
for pfad in QWEN_LOESCHEN:
if pfad in vorhanden:
try:
api.delete_file(path_in_repo=pfad, repo_id=ZIEL_REPO, repo_type=REPO_TYPE, token=token)
log.append(f" GELÖSCHT: {pfad}")
except Exception as e:
log.append(f" Löschen fehlgeschlagen: {pfad}{e}")
else:
log.append(f" SKIP (bereits gelöscht): {pfad}")
log.append("\n🤖 SCHRITT 2: Kopiere neue ORT-GenAI Qwen3-Dateien...")
for modell in QWEN_MODELLE:
name = modell["name"]
quelle = modell["quelle"]
log.append(f"\n [{name.upper()}]")
try:
quell_dateien = api.list_repo_files(quelle, repo_type="model")
relevante_dateien = [f for f in quell_dateien if f.startswith(QWEN_UNTERORDNER + "/")]
if not relevante_dateien:
log.append(f" ⚠️ HINWEIS: Keine Dateien im Ordner '{QWEN_UNTERORDNER}' auf {quelle} gefunden.")
continue
for voller_quell_pfad in relevante_dateien:
datei_name = voller_quell_pfad.replace(QWEN_UNTERORDNER + "/", "")
ziel_pfad = f"llm/{name}/{datei_name}"
if ziel_pfad in vorhanden:
log.append(f" SKIP (bereits vorhanden): {datei_name}")
continue
url = hf_url(quelle, voller_quell_pfad)
log.append(f" Kopiere: {datei_name} ...")
ergebnis = stream_kopieren(api, url, ziel_pfad, token)
log.append(f" {ergebnis}")
except Exception as e:
log.append(f" 💥 Fehler beim Auslesen der Quelle {quelle}: {e}")
log.append("\n" + "=" * 60)
log.append("✅ Qwen3-LLM-Update abgeschlossen!")
return "\n".join(log)
# =============================================================================
# Ablauf: Whisper
# =============================================================================
def whisper_starten(token_eingabe):
token = HF_TOKEN or token_eingabe.strip()
if not token:
return "FEHLER: Kein Token vorhanden."
api = HfApi(token=token)
log = []
try:
vorhanden = set(api.list_repo_files(ZIEL_REPO, repo_type=REPO_TYPE, token=token))
except Exception as e:
return f"FEHLER: Repo nicht erreichbar — {e}"
log.append(f"Repo: {ZIEL_REPO} | {len(vorhanden)} Dateien vorhanden")
log.append("=" * 60)
log.append("\n🗑️ SCHRITT 1: Lösche whisper-medium + altes whisper-tiny...")
dateien_loeschen_per_prefix(api, token, vorhanden, WHISPER_LOESCH_PREFIXE, log)
log.append("\n🎙️ SCHRITT 2: Kopiere whisper-tiny + whisper-base (gemergt, fp32)...")
for modell in WHISPER_MODELLE:
name = modell["name"]
quelle = modell["quelle"]
ziel_ordner = modell["ziel_ordner"]
log.append(f"\n [{name.upper()}] Quelle: {quelle}")
try:
quell_dateien = set(api.list_repo_files(quelle, repo_type="model"))
except Exception as e:
log.append(f" 💥 Fehler beim Auslesen der Quelle {quelle}: {e}")
continue
# root-level Dateien (Tokenizer, Config etc.)
aufgaben = [(f, f) for f in WHISPER_ROOT_DATEIEN]
# onnx/ Dateien - nur die exakt gemergte fp32-Variante, keine Suffixe
aufgaben += [(f"onnx/{f}", f"onnx/{f}") for f in WHISPER_ONNX_DATEIEN]
for quell_rel, ziel_rel in aufgaben:
if quell_rel not in quell_dateien:
log.append(f" ⚠️ FEHLT in Quelle: {quell_rel} — übersprungen")
continue
ziel_pfad = f"{ziel_ordner}/{ziel_rel}"
if ziel_pfad in vorhanden:
log.append(f" SKIP (bereits vorhanden): {ziel_rel}")
continue
url = hf_url(quelle, quell_rel)
log.append(f" Kopiere: {ziel_rel} ...")
ergebnis = stream_kopieren(api, url, ziel_pfad, token)
log.append(f" {ergebnis}")
log.append("\n" + "=" * 60)
log.append("✅ Whisper-Update abgeschlossen!")
return "\n".join(log)
# =============================================================================
# Ablauf: Qwen3-ASR (komplettes Repo spiegeln)
# =============================================================================
def qwen_asr_starten(token_eingabe):
token = HF_TOKEN or token_eingabe.strip()
if not token:
return "FEHLER: Kein Token vorhanden."
api = HfApi(token=token)
log = []
try:
vorhanden = set(api.list_repo_files(ZIEL_REPO, repo_type=REPO_TYPE, token=token))
except Exception as e:
return f"FEHLER: Repo nicht erreichbar — {e}"
log.append(f"Repo: {ZIEL_REPO} | {len(vorhanden)} Dateien vorhanden")
log.append("=" * 60)
log.append(f"\n📥 Lese Dateiliste von {QWEN_ASR_QUELLE} ...")
try:
quell_dateien = api.list_repo_files(QWEN_ASR_QUELLE, repo_type="model")
except Exception as e:
return f"FEHLER: Quelle nicht erreichbar — {e}"
# HF-interne Metadateien (.gitattributes etc.) auslassen
quell_dateien = [f for f in quell_dateien if not os.path.basename(f).startswith(".")]
log.append(f" {len(quell_dateien)} Dateien gefunden.")
log.append(f"\n🎤 Kopiere Qwen3-ASR-0.6B nach {QWEN_ASR_ZIEL_ORDNER}/ ...")
for quell_rel in quell_dateien:
ziel_pfad = f"{QWEN_ASR_ZIEL_ORDNER}/{quell_rel}"
if ziel_pfad in vorhanden:
log.append(f" SKIP (bereits vorhanden): {quell_rel}")
continue
url = hf_url(QWEN_ASR_QUELLE, quell_rel)
log.append(f" Kopiere: {quell_rel} ...")
ergebnis = stream_kopieren(api, url, ziel_pfad, token)
log.append(f" {ergebnis}")
log.append("\n" + "=" * 60)
log.append("✅ Qwen3-ASR-Update abgeschlossen!")
return "\n".join(log)
# =============================================================================
# Ablauf: GGUF (neu in v5)
# =============================================================================
def gguf_starten(token_eingabe):
token = HF_TOKEN or token_eingabe.strip()
if not token:
return "FEHLER: Kein Token vorhanden."
api = HfApi(token=token)
log = []
try:
vorhanden = set(api.list_repo_files(ZIEL_REPO, repo_type=REPO_TYPE, token=token))
except Exception as e:
return f"FEHLER: Repo nicht erreichbar — {e}"
log.append(f"Repo: {ZIEL_REPO} | {len(vorhanden)} Dateien vorhanden")
log.append("=" * 60)
log.append(f"\n📦 GGUF-Modelle (alle Q4_K_M) nach {GGUF_ZIEL_PREFIX}/ ...")
for modell in GGUF_MODELLE:
name = modell["name"]
ziel_ordner = f"{GGUF_ZIEL_PREFIX}/{name}"
log.append(f"\n [{name.upper()}]")
gefunden = False
for quelle in modell["quellen"]:
try:
quell_dateien = api.list_repo_files(quelle, repo_type="model")
except Exception as e:
log.append(f" ⚠️ Quelle {quelle} nicht erreichbar ({e}) - versuche naechste...")
continue
quant_datei = datei_nach_muster_finden(quell_dateien, ["q4_k_m"], ausschliessen=["mmproj"])
if not quant_datei:
log.append(f" ⚠️ Kein Q4_K_M in {quelle} gefunden - versuche naechste Quelle...")
continue
log.append(f" Quelle: {quelle}")
gefunden = True
# Haupt-Quant-Datei
ziel_pfad = f"{ziel_ordner}/{quant_datei}"
if ziel_pfad in vorhanden:
log.append(f" SKIP (bereits vorhanden): {quant_datei}")
else:
url = hf_url(quelle, quant_datei)
log.append(f" Kopiere: {quant_datei} ...")
ergebnis = stream_kopieren(api, url, ziel_pfad, token)
log.append(f" {ergebnis}")
# mmproj-Datei (nur bei multimodalen Qwen3.5-Modellen)
if modell["mmproj"]:
mmproj_datei = datei_nach_muster_finden(quell_dateien, ["mmproj", MMPROJ_PRAEZISION])
if not mmproj_datei:
# Fallback: irgendeine mmproj-Datei nehmen, falls exakte Praezision fehlt
mmproj_datei = datei_nach_muster_finden(quell_dateien, ["mmproj"])
if mmproj_datei:
ziel_pfad_mm = f"{ziel_ordner}/{mmproj_datei}"
if ziel_pfad_mm in vorhanden:
log.append(f" SKIP (bereits vorhanden): {mmproj_datei}")
else:
url = hf_url(quelle, mmproj_datei)
log.append(f" Kopiere: {mmproj_datei} ...")
ergebnis = stream_kopieren(api, url, ziel_pfad_mm, token)
log.append(f" {ergebnis}")
else:
log.append(f" ⚠️ HINWEIS: Keine mmproj-Datei in {quelle} gefunden.")
break # erste funktionierende Quelle reicht
if not gefunden:
log.append(f" 💥 FEHLER: In keiner Quelle eine Q4_K_M-Datei gefunden fuer {name}.")
log.append("\n" + "=" * 60)
log.append("✅ GGUF-Update abgeschlossen!")
return "\n".join(log)
# =============================================================================
# Ablauf: mmproj-Korrektur (BF16 -> F16 Bugfix)
# =============================================================================
def mmproj_korrigieren(token_eingabe):
"""Loescht faelschlich geholte mmproj-BF16.gguf Dateien (Bug: 'f16' matchte
als Teilstring von 'bf16') und holt danach mit dem gefixten Musterabgleich
die korrekte mmproj-F16.gguf nach."""
token = HF_TOKEN or token_eingabe.strip()
if not token:
return "FEHLER: Kein Token vorhanden."
api = HfApi(token=token)
log = []
try:
vorhanden = set(api.list_repo_files(ZIEL_REPO, repo_type=REPO_TYPE, token=token))
except Exception as e:
return f"FEHLER: Repo nicht erreichbar — {e}"
log.append(f"Repo: {ZIEL_REPO} | {len(vorhanden)} Dateien vorhanden")
log.append("=" * 60)
log.append("\n🗑️ SCHRITT 1: Loesche falsche mmproj-BF16.gguf (Bug-Fund)...")
betroffene_ordner = [m["name"] for m in GGUF_MODELLE if m["mmproj"]]
for name in betroffene_ordner:
ziel_ordner = f"{GGUF_ZIEL_PREFIX}/{name}"
falsch = [f for f in vorhanden if f.startswith(ziel_ordner + "/") and "bf16" in f.lower() and "mmproj" in f.lower()]
for pfad in falsch:
try:
api.delete_file(path_in_repo=pfad, repo_id=ZIEL_REPO, repo_type=REPO_TYPE, token=token)
log.append(f" GELÖSCHT: {pfad}")
vorhanden.discard(pfad)
except Exception as e:
log.append(f" Löschen fehlgeschlagen: {pfad}{e}")
if not falsch:
log.append(f" SKIP (keine falsche BF16-Datei in {ziel_ordner}/)")
log.append("\n📦 SCHRITT 2: Hole korrekte mmproj-F16.gguf nach (gefixter Musterabgleich)...")
for modell in GGUF_MODELLE:
if not modell["mmproj"]:
continue
name = modell["name"]
ziel_ordner = f"{GGUF_ZIEL_PREFIX}/{name}"
log.append(f"\n [{name.upper()}]")
for quelle in modell["quellen"]:
try:
quell_dateien = api.list_repo_files(quelle, repo_type="model")
except Exception as e:
log.append(f" ⚠️ Quelle {quelle} nicht erreichbar ({e}) - versuche naechste...")
continue
mmproj_datei = datei_nach_muster_finden(quell_dateien, ["mmproj", MMPROJ_PRAEZISION])
if not mmproj_datei:
log.append(f" ⚠️ Kein echtes {MMPROJ_PRAEZISION} in {quelle} gefunden - versuche naechste Quelle...")
continue
ziel_pfad = f"{ziel_ordner}/{mmproj_datei}"
if ziel_pfad in vorhanden:
log.append(f" SKIP (bereits vorhanden): {mmproj_datei}")
else:
url = hf_url(quelle, mmproj_datei)
log.append(f" Kopiere: {mmproj_datei} ...")
ergebnis = stream_kopieren(api, url, ziel_pfad, token)
log.append(f" {ergebnis}")
break
log.append("\n" + "=" * 60)
log.append("✅ mmproj-Korrektur abgeschlossen!")
return "\n".join(log)
# =============================================================================
# Ablauf: Alles nacheinander
# =============================================================================
def alles_starten(token_eingabe):
teile = [
qwen_llm_starten(token_eingabe),
whisper_starten(token_eingabe),
qwen_asr_starten(token_eingabe),
gguf_starten(token_eingabe),
]
return ("\n\n" + "#" * 60 + "\n\n").join(teile)
# =============================================================================
# Gradio GUI
# =============================================================================
with gr.Blocks(title="EVA Mirror Fixer v5") as demo:
gr.Markdown("## 🔧 EVA Mirror Fixer v5\nQwen3-LLM-Fix, Whisper-Fix (tiny+base, gemergt), Qwen3-ASR und GGUF-Modelle (Q4_K_M).")
token_feld = gr.Textbox(
label="HF Token (Write)",
placeholder="hf_... (nicht nötig wenn als Space-Secret hinterlegt)",
type="password"
)
with gr.Row():
qwen_btn = gr.Button("1️⃣ Qwen3 LLM Fix", variant="secondary")
whisper_btn = gr.Button("2️⃣ Whisper Fix", variant="secondary")
asr_btn = gr.Button("3️⃣ Qwen3-ASR hinzufügen", variant="secondary")
gguf_btn = gr.Button("4️⃣ GGUF hinzufügen", variant="secondary")
mmproj_fix_btn = gr.Button("🩹 mmproj BF16→F16 Korrektur (9B + 0.8B)", variant="stop")
alles_btn = gr.Button("🚀 Alles nacheinander ausführen", variant="primary")
ausgabe = gr.Textbox(label="Aktivitäts-Log", lines=30)
qwen_btn.click(fn=qwen_llm_starten, inputs=token_feld, outputs=ausgabe)
whisper_btn.click(fn=whisper_starten, inputs=token_feld, outputs=ausgabe)
asr_btn.click(fn=qwen_asr_starten, inputs=token_feld, outputs=ausgabe)
gguf_btn.click(fn=gguf_starten, inputs=token_feld, outputs=ausgabe)
mmproj_fix_btn.click(fn=mmproj_korrigieren, inputs=token_feld, outputs=ausgabe)
alles_btn.click(fn=alles_starten, inputs=token_feld, outputs=ausgabe)
demo.launch()