backup: snapshot post-démo GHT 2026-05-19
Some checks failed
tests / Lint (ruff + black) (push) Successful in 1m50s
tests / Tests unitaires (sans GPU) (push) Failing after 1m50s
tests / Tests sécurité (critique) (push) Has been skipped

Backup état complet après enregistrement vidéo démo de bout en bout.
À utiliser comme point de référence pour la consolidation post-démo.

Changements majeurs de la session 18-19 mai :
- AIVA-URGENCE : page autonome avec preset URL + auto-focus chain
- Workflow Demo_urgence_3_db : merge linux_db + steps AIVA + pause humaine NoMachine
- Bypass LLM (static_result / static_text) dans replay_engine
  pour démos déterministes sans appel Ollama
- Fix api_stream:3013 — replay_paused au premier polling /next
- dag_execute : lift duration_ms vers top-level pour wait runtime
- NPM bypass auth /aiva-urgence/ via location ^~ (proxy_host/10.conf hors git)
- scripts/cancel-replays.sh — workaround Stop VWB qui ne purge pas la queue

Anchors visuels (468) forcés dans le commit pour garantir restorabilité.
DB workflows actuelle + ~12 .bak DB de la journée incluses.

Sujets identifiés pour consolidation post-démo (TODO) :
1. Bug VWB recapture anchor ne régénère pas le PNG
2. Léa client accumule état mémoire (restart périodique requis)
3. Stop VWB ne purge pas la queue serveur (lien manquant vers /replay/cancel)
4. Bug coord client mss tronqué 2560x60 → mapping Y cassé
5. delay_before/delay_after ignorés au runtime (fix partiel duration_ms)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-05-19 14:55:06 +02:00
parent f2212e77e3
commit 5ea4960e65
627 changed files with 211348 additions and 169 deletions

View File

@@ -29,6 +29,19 @@ os.makedirs(ANCHORS_DIR, exist_ok=True)
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data')
CAPTURE_LIBRARY_PATH = os.path.join(DATA_DIR, 'capture_library.json')
# Dossier pour les PNG HD des captures de la bibliothèque (source de vérité).
# Le JSON capture_library.json ne contient que les métadonnées + thumbnail 640x360
# pour la grille. L'affichage plein écran utilise le PNG via /library/<id>/full.
LIBRARY_CAPTURES_DIR = os.path.join(DATA_DIR, 'library_captures')
os.makedirs(LIBRARY_CAPTURES_DIR, exist_ok=True)
# Limite raisonnable pour upload : 25 MB couvre largement un PNG 4K non compressé.
LIBRARY_UPLOAD_MAX_BYTES = 25 * 1024 * 1024
# Dimensions du thumbnail stocké dans le JSON (utilisé pour la grille de prévisualisation).
LIBRARY_THUMB_W = 640
LIBRARY_THUMB_H = 360
LIBRARY_THUMB_QUALITY = 85
def generate_id(prefix: str) -> str:
"""Génère un ID unique"""
@@ -406,24 +419,98 @@ def get_anchor_base64(anchor_id: str):
# ── Bibliothèque de captures (persistance disque) ────────────────────────
#
# Architecture v2 (avril 2026) :
# - PNG HD écrit dans data/library_captures/{id}.png (source de vérité)
# - capture_library.json contient les métadonnées + thumbnail base64 640x360 q85
# (uniquement pour la grille de prévisualisation)
# - L'affichage plein écran utilise GET /capture/library/{id}/full (PNG HD)
# - Au load (GET), les entrées dont le PNG manque sont purgées (poubelle Dom)
def _library_png_path(item_id: str) -> str:
"""Chemin disque du PNG HD pour un item de bibliothèque."""
# Sécurise l'ID (pas de slashes ni de '..') avant de l'utiliser comme nom de fichier
safe = ''.join(c for c in str(item_id) if c.isalnum() or c in ('_', '-'))
return os.path.join(LIBRARY_CAPTURES_DIR, f"{safe}.png")
def _make_thumbnail_b64(img: Image.Image) -> str:
"""Génère le thumbnail JPEG 640x360 q85 et le retourne en base64 (data URI)."""
thumb = img.copy()
thumb.thumbnail((LIBRARY_THUMB_W, LIBRARY_THUMB_H), Image.LANCZOS)
if thumb.mode != 'RGB':
thumb = thumb.convert('RGB')
buf = BytesIO()
thumb.save(buf, format='JPEG', quality=LIBRARY_THUMB_QUALITY, optimize=True)
return 'data:image/jpeg;base64,' + base64.b64encode(buf.getvalue()).decode('utf-8')
def _load_library_raw() -> list:
"""Charge la liste brute depuis le JSON, sans filtrer."""
if not os.path.exists(CAPTURE_LIBRARY_PATH):
return []
try:
with open(CAPTURE_LIBRARY_PATH, 'r', encoding='utf-8') as f:
data = json.load(f)
return data if isinstance(data, list) else []
except Exception as e:
print(f"⚠️ [CaptureLibrary] Erreur lecture JSON: {e}")
return []
def _write_library_atomic(library: list) -> None:
"""Écrit le JSON de manière atomique (tmp + rename)."""
tmp_path = CAPTURE_LIBRARY_PATH + '.tmp'
with open(tmp_path, 'w', encoding='utf-8') as f:
json.dump(library, f, ensure_ascii=False)
os.replace(tmp_path, CAPTURE_LIBRARY_PATH)
def _filter_orphans(library: list) -> tuple[list, int]:
"""Retire les entrées format=v2 sans PNG sur disque. Retourne (filtré, nb_purgés)."""
kept = []
purged = 0
for item in library:
if not isinstance(item, dict):
continue
# Format v2 : doit avoir un PNG sur disque ou il est purgé (poubelle)
if item.get('format') == 'v2':
item_id = item.get('id')
if item_id and os.path.exists(_library_png_path(item_id)):
kept.append(item)
else:
purged += 1
else:
# Format legacy v1 (base64 dans le JSON) : on les garde tant qu'ils existent
# mais ils sont condamnés (pixélisés). Le frontend affichera un badge "legacy".
kept.append(item)
return kept, purged
@api_v3_bp.route('/capture/library', methods=['GET'])
def get_capture_library():
"""
Charge la bibliothèque de captures depuis le disque.
Filtre automatiquement les entrées v2 dont le PNG HD a disparu (poubelle).
Response:
{
"success": true,
"library": [ { id, capture, timestamp, sessionId, favorite }, ... ]
"library": [ { id, capture, timestamp, sessionId, favorite, format, fullImageUrl }, ... ]
}
"""
try:
if os.path.exists(CAPTURE_LIBRARY_PATH):
with open(CAPTURE_LIBRARY_PATH, 'r', encoding='utf-8') as f:
library = json.load(f)
else:
library = []
library = _load_library_raw()
library, purged = _filter_orphans(library)
if purged > 0:
print(f"🗑️ [CaptureLibrary] {purged} entrée(s) v2 sans PNG purgées au load")
try:
_write_library_atomic(library)
except Exception as e:
print(f"⚠️ [CaptureLibrary] Échec persistance après purge: {e}")
return jsonify({
'success': True,
@@ -441,28 +528,29 @@ def get_capture_library():
@api_v3_bp.route('/capture/library', methods=['POST'])
def save_capture_library():
"""
Sauvegarde la bibliothèque de captures sur disque.
Sauvegarde la bibliothèque de captures sur disque (métadonnées + thumbnails).
Filtre les entrées v2 sans PNG correspondant avant sauvegarde.
Request:
{
"library": [ { id, capture, timestamp, sessionId, favorite }, ... ]
}
Response:
{
"success": true,
"count": 5
"library": [ { id, capture, timestamp, sessionId, favorite, format }, ... ]
}
"""
try:
data = request.get_json() or {}
library = data.get('library', [])
if not isinstance(library, list):
return jsonify({'success': False, 'error': 'library doit être une liste'}), 400
# Purge avant écriture (le frontend peut envoyer des entrées orphelines)
library, _ = _filter_orphans(library)
# Limiter à 50 captures pour éviter un fichier trop gros
library = library[:50]
with open(CAPTURE_LIBRARY_PATH, 'w', encoding='utf-8') as f:
json.dump(library, f, ensure_ascii=False)
_write_library_atomic(library)
return jsonify({
'success': True,
@@ -475,3 +563,150 @@ def save_capture_library():
'success': False,
'error': str(e)
}), 500
@api_v3_bp.route('/capture/library/upload', methods=['POST'])
def upload_capture_library_item():
"""
Upload d'une capture HD dans la bibliothèque.
Le PNG est stocké sur disque (source de vérité). Le serveur génère un
thumbnail 640x360 q85 (base64) pour la grille de prévisualisation et
enregistre les métadonnées dans capture_library.json.
Request: multipart/form-data
file : PNG binaire (max 25 MB)
id : ID client (optionnel — généré sinon)
sessionId : ID de la session source
timestamp : ISO 8601 (optionnel — now() sinon)
favorite : 'true' / 'false' (optionnel)
Response:
{
"success": true,
"item": {
"id": "cap_...",
"format": "v2",
"fullImageUrl": "/api/v3/capture/library/cap_.../full",
"timestamp": "...",
"sessionId": "...",
"favorite": false,
"capture": {
"width": 1920,
"height": 1080,
"thumbnail_base64": "data:image/jpeg;base64,..."
}
}
}
"""
try:
if 'file' not in request.files:
return jsonify({'success': False, 'error': 'Champ "file" requis'}), 400
upload = request.files['file']
raw = upload.read()
if not raw:
return jsonify({'success': False, 'error': 'Fichier vide'}), 400
if len(raw) > LIBRARY_UPLOAD_MAX_BYTES:
return jsonify({
'success': False,
'error': f'Fichier trop gros ({len(raw)} > {LIBRARY_UPLOAD_MAX_BYTES} octets)'
}), 413
# Décoder + valider l'image
try:
img = Image.open(BytesIO(raw))
img.load()
except Exception as e:
return jsonify({'success': False, 'error': f'Image invalide: {e}'}), 400
if img.mode not in ('RGB', 'RGBA'):
img = img.convert('RGB')
# ID : fourni par le client ou généré
item_id = (request.form.get('id') or '').strip()
if not item_id:
item_id = generate_id('cap')
# Re-sécurisation par _library_png_path
png_path = _library_png_path(item_id)
# Écriture atomique du PNG HD (tmp + rename)
tmp_path = png_path + '.tmp'
img.save(tmp_path, format='PNG')
os.replace(tmp_path, png_path)
# Thumbnail pour la grille
thumbnail_b64 = _make_thumbnail_b64(img)
timestamp = request.form.get('timestamp') or datetime.now().isoformat()
session_id = request.form.get('sessionId') or ''
favorite = (request.form.get('favorite') or 'false').lower() == 'true'
item = {
'id': item_id,
'format': 'v2',
'fullImageUrl': f'/api/v3/capture/library/{item_id}/full',
'timestamp': timestamp,
'sessionId': session_id,
'favorite': favorite,
'capture': {
'width': img.width,
'height': img.height,
'thumbnail_base64': thumbnail_b64,
'timestamp': timestamp,
}
}
# Mise à jour du JSON (insertion en tête, dédup, cap à 50)
library = _load_library_raw()
library = [x for x in library if isinstance(x, dict) and x.get('id') != item_id]
library.insert(0, item)
library, _ = _filter_orphans(library)
library = library[:50]
_write_library_atomic(library)
print(f"📤 [CaptureLibrary] Upload v2 : {item_id} ({img.width}x{img.height}, {len(raw)} octets)")
return jsonify({'success': True, 'item': item})
except Exception as e:
print(f"⚠️ [CaptureLibrary] Erreur upload: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@api_v3_bp.route('/capture/library/<item_id>/full', methods=['GET'])
def get_capture_library_full(item_id: str):
"""Sert le PNG HD d'une capture de la bibliothèque (pour le mode plein écran)."""
try:
png_path = _library_png_path(item_id)
if not os.path.exists(png_path):
return jsonify({
'success': False,
'error': f"Capture '{item_id}' non trouvée"
}), 404
return send_file(png_path, mimetype='image/png', max_age=3600)
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@api_v3_bp.route('/capture/library/<item_id>', methods=['DELETE'])
def delete_capture_library_item(item_id: str):
"""Supprime une capture de la bibliothèque (PNG + entrée JSON)."""
try:
png_path = _library_png_path(item_id)
if os.path.exists(png_path):
try:
os.remove(png_path)
except OSError as e:
print(f"⚠️ [CaptureLibrary] Échec suppression PNG {png_path}: {e}")
library = _load_library_raw()
before = len(library)
library = [x for x in library if isinstance(x, dict) and x.get('id') != item_id]
if len(library) != before:
_write_library_atomic(library)
return jsonify({'success': True, 'id': item_id})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500

View File

@@ -123,6 +123,206 @@ def _build_llm_action(action_type: str, parameters: Dict[str, Any]) -> Dict[str,
return action
# ---------------------------------------------------------------------------
# Pré-expansion : extract_text_scroll → 6 sous-steps atomiques
# ---------------------------------------------------------------------------
# Le DAGExecutor (et Léa Windows) ne savent pas exécuter extract_text_scroll
# en l'état. L'expansion existe côté agent_v0/server_v1/replay_engine.py
# (_expand_extract_text_scroll) pour le chemin streaming, mais elle n'était
# pas appelée sur le chemin VWB /execute-dag — d'où "Type d'action inconnu :
# extract_text_scroll" dans les logs Léa. On reproduit ici la même expansion
# au format step VWB pour préparer le DAG avant _convert_vwb_to_dag_steps.
_ETS_SUB_TEMPLATE = (
# (suffix, action_type, params_factory(final_var, top_var, bottom_var, paragraph, scroll_pause_ms))
("01_extract_top", "extract_text",
lambda fv, tv, bv, p, ms: {"variable_name": tv, "paragraph": p}),
("02_scroll_end", "key_combo",
lambda fv, tv, bv, p, ms: {"keys": ["ctrl", "end"]}),
("03_wait_scroll", "wait",
lambda fv, tv, bv, p, ms: {"duration_ms": ms}),
("04_extract_bottom", "extract_text",
lambda fv, tv, bv, p, ms: {"variable_name": bv, "paragraph": p}),
("05_concat", "_concat_text_vars",
lambda fv, tv, bv, p, ms: {
"top_var": tv, "bottom_var": bv,
"output_var": fv, "separator": "\n\n",
}),
("06_scroll_home", "key_combo",
lambda fv, tv, bv, p, ms: {"keys": ["ctrl", "home"]}),
)
def _expand_extract_text_scroll_in_workflow(
steps_data: List[Dict[str, Any]],
edges_data: List[Dict[str, Any]],
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
"""Pré-expansion : remplace chaque step extract_text_scroll par 6 sous-steps.
Conserve les edges externes en les redirigeant : edge → ets devient
edge → premier sous-step ; edge ets → devient dernier sous-step →.
Ajoute les edges internes en chaîne (sub_1 → sub_2 → … → sub_6).
Voir l'équivalent côté replay_engine : _expand_extract_text_scroll
(format actions). Ici on opère en amont, sur les steps VWB.
"""
remap: Dict[str, Tuple[str, str]] = {} # original_id → (first_sub, last_sub)
new_steps: List[Dict[str, Any]] = []
new_edges: List[Dict[str, Any]] = []
for step in steps_data:
if step.get("action_type") != "extract_text_scroll":
new_steps.append(step)
continue
sid = step["id"]
params = step.get("parameters") or {}
final_var = str(params.get("variable_name", "") or "")
# Tolérance : variable_name parfois saisi avec accolades de template
if final_var.startswith("{{") and final_var.endswith("}}"):
final_var = final_var[2:-2].strip()
paragraph = bool(params.get("paragraph", False))
scroll_pause_ms = int(params.get("scroll_pause_ms", 500))
top_var = f"{final_var}__top" if final_var else "ets_top"
bottom_var = f"{final_var}__bottom" if final_var else "ets_bottom"
sub_ids: List[str] = []
for suffix, act_type, params_factory in _ETS_SUB_TEMPLATE:
sub_id = f"{sid}__{suffix}"
sub_step = {
**step,
"id": sub_id,
"action_type": act_type,
"parameters": params_factory(
final_var, top_var, bottom_var, paragraph, scroll_pause_ms,
),
"anchor_id": None,
}
new_steps.append(sub_step)
sub_ids.append(sub_id)
# Edges internes en chaîne
for i in range(len(sub_ids) - 1):
new_edges.append({"source": sub_ids[i], "target": sub_ids[i + 1]})
remap[sid] = (sub_ids[0], sub_ids[-1])
# Remap edges externes
for edge in edges_data:
source = edge.get("source", "")
target = edge.get("target", "")
if source in remap:
source = remap[source][1]
if target in remap:
target = remap[target][0]
new_edges.append({**edge, "source": source, "target": target})
return new_steps, new_edges
# ---------------------------------------------------------------------------
# Pré-expansion : extract_text_scroll → 6 sous-actions atomiques (chemin runtime)
# ---------------------------------------------------------------------------
# Variante de _expand_extract_text_scroll_in_workflow (qui opère sur des
# step VWB BDD) pour le format ACTIONS runtime reçu par execute_windows().
# Nécessaire car execute_windows = proxy direct vers le streaming server, sans
# passer par DAGExecutor/_convert_vwb_to_dag_steps. Sans cette expansion ici,
# Léa reçoit le type composite "extract_text_scroll" et le rejette
# (`Type d'action inconnu` dans agent_v1/core/executor.py).
#
# Doublon défensif keys/duration_ms (top-level + parameters) : le streaming
# server lit selon le code path, ça couvre les deux cas. À nettoyer post-démo
# une fois le contrat formats stabilisé.
def _expand_extract_text_scroll_in_actions(
actions: List[Dict[str, Any]],
) -> List[Dict[str, Any]]:
"""Remplace chaque action extract_text_scroll par 6 sous-actions atomiques.
Format d'entrée = liste d'actions runtime telles que reçues par
execute_windows() depuis le frontend.
Voir agent_v0/server_v1/replay_engine._expand_extract_text_scroll pour
l'équivalent côté chemin streaming pur. Ici on opère AVANT le proxy.
"""
result: List[Dict[str, Any]] = []
for action in actions:
if action.get("type") != "extract_text_scroll":
result.append(action)
continue
params = action.get("parameters") or {}
final_var = str(params.get("variable_name", "") or "")
# Tolérance : variable_name parfois saisi avec accolades de template
if final_var.startswith("{{") and final_var.endswith("}}"):
final_var = final_var[2:-2].strip()
paragraph = bool(params.get("paragraph", False))
scroll_pause_ms = int(params.get("scroll_pause_ms", 500))
top_var = f"{final_var}__top" if final_var else "ets_top"
bottom_var = f"{final_var}__bottom" if final_var else "ets_bottom"
base_id = action.get("action_id") or "ets_unknown"
def _new_sub(
suffix: str,
sub_type: str,
sub_params: Dict[str, Any],
extras: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
# Repartir de l'action originale en retirant les champs
# spécifiques au type composite (qui n'ont plus de sens sur
# les sous-actions atomiques).
sub = {
k: v
for k, v in action.items()
if k not in (
"type", "action_id", "parameters",
"keys", "duration_ms", "text", "by_text",
"anchor_id", "target_spec",
)
}
sub["action_id"] = f"{base_id}__{suffix}"
sub["type"] = sub_type
sub["parameters"] = sub_params
if extras:
sub.update(extras)
return sub
result.append(_new_sub(
"01_extract_top", "extract_text",
{"variable_name": top_var, "paragraph": paragraph},
))
result.append(_new_sub(
"02_scroll_end", "key_combo",
{"keys": ["ctrl", "end"]},
{"keys": ["ctrl", "end"]},
))
result.append(_new_sub(
"03_wait_scroll", "wait",
{"duration_ms": scroll_pause_ms},
{"duration_ms": scroll_pause_ms},
))
result.append(_new_sub(
"04_extract_bottom", "extract_text",
{"variable_name": bottom_var, "paragraph": paragraph},
))
result.append(_new_sub(
"05_concat", "_concat_text_vars",
{
"top_var": top_var,
"bottom_var": bottom_var,
"output_var": final_var,
"separator": "\n\n",
},
))
result.append(_new_sub(
"06_scroll_home", "key_combo",
{"keys": ["ctrl", "home"]},
{"keys": ["ctrl", "home"]},
))
return result
# ---------------------------------------------------------------------------
# Conversion VWB workflow → DAG steps
# ---------------------------------------------------------------------------
@@ -612,6 +812,12 @@ def execute_dag(workflow_id: str):
# Exécution DAG normale pour les étapes restantes
# ---------------------------------------------------------------
# Pré-expansion des steps extract_text_scroll en sous-actions atomiques
# (le DAGExecutor et Léa ne connaissent pas ce type composite).
remaining_steps, remaining_edges = _expand_extract_text_scroll_in_workflow(
remaining_steps, remaining_edges,
)
# Convertir en étapes DAG
dag_steps = _convert_vwb_to_dag_steps(remaining_steps, remaining_edges)
@@ -939,6 +1145,14 @@ def execute_windows():
if not data:
return jsonify({'error': 'Aucune donnée'}), 400
# ---------------------------------------------------------------
# Pré-expansion des actions extract_text_scroll → 6 sous-actions
# atomiques. Sans ça, Léa rejette le type composite ("Type d'action
# inconnu" dans agent_v1/core/executor.py). Cf. démo GHT 2026-05-11.
# ---------------------------------------------------------------
if 'actions' in data and data['actions']:
data['actions'] = _expand_extract_text_scroll_in_actions(data['actions'])
# Vérifier si ce sont uniquement des actions fichiers (pas besoin de wait ni replay)
all_file_actions = all(
a.get('type', '') in _FILE_ACTION_TYPES
@@ -1009,6 +1223,9 @@ def execute_windows():
# ---------------------------------------------------------------
if vwb_type in ('type_text', 'type_secret') and 'text' in params:
action['text'] = params['text']
# Propagation du flag paste (opt-in workflow non-Citrix) vers Léa
if params.get('paste'):
action['paste'] = bool(params['paste'])
anchor_id = action.get('anchor_id') or (
params.get('visual_anchor') or {}
).get('anchor_id')
@@ -1021,6 +1238,14 @@ def execute_windows():
if vwb_type in ('keyboard_shortcut', 'hotkey') and 'keys' in params:
action['keys'] = params['keys']
# ---------------------------------------------------------------
# wait / wait_for_anchor / visual_condition → propager duration_ms
# vers top-level. Sans ce lift, executor.py:1182 (`action.get("duration_ms", 500)`)
# retombe sur 500ms par défaut.
# ---------------------------------------------------------------
if vwb_type in ('wait', 'wait_for_anchor', 'visual_condition') and 'duration_ms' in params:
action['duration_ms'] = params['duration_ms']
# ---------------------------------------------------------------
# Actions fichiers → proxy vers /file-action de l'agent (port 5006)
# ---------------------------------------------------------------

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 792 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Some files were not shown because too many files have changed in this diff Show More