{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import warnings\n", "warnings.simplefilter(\"ignore\", UserWarning)\n", "\n", "from __future__ import print_function, absolute_import\n", "from tornado.ioloop import IOLoop\n", "from client import Client, ConnectionError\n", "from boxconfig import parse_config\n", "from dejavu.recognize import FilePerSecondRecognizer\n", "from dejavu import Dejavu, CouldntDecodeError\n", "from endpoint import setup_endpoint\n", "from multiprocessing import Process\n", "import logging as log\n", "import requests\n", "import dateutil\n", "import math\n", "import time\n", "import os\n", "\n", "from queue import Queue, Empty" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "PATH = '/tmp'\n", "AHEAD_TIME_AUDIO_TOLERANCE = 2 # second\n", "MAX_SEGMENT_THREADS = 4\n", "THRESHOLD = 10\n", "SEGMENTS_TOLERANCE_RATE = 0.6\n", "FALL_TOLERANCE_SEGMENTS = 1" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "THRESHOLD_FIXED = 1\n", "THRESHOLD_AVERAGE = 2" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "QUEUE_SINGLE = 1\n", "QUEUE_THREAD = 2" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "MultiAPI = Process" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "config = parse_config()\n", "queue = Queue()\n", "\n", "cloud_base_url = 'https://storage.googleapis.com/{}' \\\n", " .format(config['bucket'])\n", "recognizer = FilePerSecondRecognizer" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "queue_mode = QUEUE_SINGLE\n", "threshold_mode = THRESHOLD_FIXED" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "def obt_siguiente_trabajo():\n", " url = 'https://api.fourier.audio/na/calendario/pendiente?id=%s' % (config['device_id'],)\n", " response = requests.get(url)\n", " log.info(response.json())\n", " return response.json()" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "def descargar_anuncio(ad_path):\n", " anuncio = os.path.basename(ad_path)\n", " path = os.path.join(PATH, 'ads')\n", " os.makedirs(path, exist_ok=True)\n", " ruta_anuncio = os.path.join(path, anuncio)\n", "\n", " if os.path.isfile(ruta_anuncio):\n", " return ruta_anuncio\n", "\n", " url = '{}/{}'.format(cloud_base_url, ad_path)\n", " response = requests.get(url)\n", "\n", " # TODO: Agregar alerta cuando la respuesta no sea 200\n", " if response.status_code == 200:\n", " with open(ruta_anuncio, \"wb\") as fp:\n", " fp.write(response.content)\n", " return ruta_anuncio\n", "\n", " else:\n", " print(\"Error al descargar\")\n", " print(response)\n", " return None\n" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "def descargar_media(box, station, media):\n", " ref = '{}/{}/{}'.format(box, station, media)\n", " file = os.path.basename(ref)\n", " path = os.path.join(PATH, 'fourier', box, station)\n", " os.makedirs(path, exist_ok=True)\n", " out_file = os.path.join(path, file)\n", "\n", " if os.path.isfile(out_file):\n", " return out_file\n", "\n", " filename = ref.replace(\"/\",\"%2F\") \\\n", " .replace(\"+\",\"%2B\")\n", " url = '{}/{}'.format(cloud_base_url, filename)\n", " response = requests.get(url)\n", "\n", " if response.status_code == 200:\n", " with open(out_file, \"wb\") as fp:\n", " fp.write(response.content)\n", " return out_file\n", " else:\n", " print(\"Error al descargar\")\n", " print(response)\n", " return None\n" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "def obt_calibracion(calibracion):\n", " default = {\n", " 'threshold': 12,\n", " 'tolerance': 0.8,\n", " 'fallTolerance': 1,\n", " 'segmentSize': 5,\n", " }\n", "\n", " if 'threshold' in calibracion:\n", " default['threshold'] = calibracion['threshold']\n", " if 'tolerance' in calibracion:\n", " default['tolerance'] = calibracion['tolerance']\n", " if 'segmentSize' in calibracion:\n", " default['segmentSize'] = calibracion['segmentSize']\n", " if 'fallTolerance' in calibracion:\n", " default['fallTolerance'] = calibracion['fallTolerance']\n", "\n", " return default" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "def encontrar_resultados(resultados, segments_needed=4, calibration=None):\n", " found_counter = 0\n", " found_down_counter = 0\n", " found_index = None\n", " expect_space = False\n", " expect_recover = False\n", " last_value_in_threshold_index = -1\n", " fall_tolerance = calibration['fallTolerance']\n", " found = []\n", "\n", " if threshold_mode == THRESHOLD_FIXED:\n", " threshold = calibration['threshold']\n", " elif threshold_mode == THRESHOLD_AVERAGE:\n", " values = [x['confidence'] for x in resultados]\n", " threshold = math.ceil(float(sum(values)) / float(len(values)))\n", "\n", " if segments_needed < 1:\n", " segments_needed = 1\n", "\n", " for index, result in enumerate(resultados):\n", " if not expect_space:\n", " if result['confidence'] >= threshold:\n", " found_counter += 1\n", " last_value_in_threshold_index = index\n", " if found_index is None:\n", " found_index = index\n", " if expect_recover:\n", " found_counter += found_down_counter\n", " expect_recover = False\n", "\n", " elif fall_tolerance:\n", " if not expect_recover:\n", " if last_value_in_threshold_index != -1:\n", " expect_recover = True\n", " found_down_counter += 1\n", " else:\n", " pass\n", " else:\n", " found_counter = 0\n", " found_down_counter = 0\n", " found_index = None\n", " expect_recover = False\n", "\n", " else:\n", " found_counter = 0\n", " found_down_counter = 0\n", " found_index = None\n", " expect_recover = False\n", "\n", " else:\n", " if result['confidence'] <= threshold:\n", " expect_space = False\n", "\n", " if found_counter >= segments_needed:\n", " found_row = resultados[found_index]\n", " found.append(found_row)\n", " found_counter = 0\n", " expect_space = True\n", "\n", " return found" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "def enviar_resultados(item):\n", " url = 'https://api.fourier.audio/v1/calendario/resultado'\n", " response = requests.post(url, json=item)\n", " return response" ] }, { "cell_type": "code", "execution_count": 114, "metadata": {}, "outputs": [], "source": [ "pendiente = obt_siguiente_trabajo()" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [], "source": [ "# Procesar elemento pendiente\n", "\n", "ciudad = pendiente['origen']\n", "estacion = pendiente['estacion']\n", "calibracion = obt_calibracion(pendiente['calibracion'])\n", "tolerance = calibracion['tolerance']\n", "tamano_segmento = calibracion['segmentSize']\n", "longitud_audio = 30" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [], "source": [ "try:\n", " anuncios = []\n", " id_by_ad = {}\n", " item_ids = []\n", " x = 0\n", " for i in pendiente[\"elementos\"]:\n", " x = x + 1\n", " id_by_ad[i['anuncio']] = i['id']\n", " if i['id'] not in item_ids:\n", " item_ids.append(i['id'])\n", "\n", " anuncio = descargar_anuncio(i[\"ruta\"])\n", " if anuncio is not None:\n", " anuncios.append(anuncio)\n", " else:\n", " print('[process_segment] ad file missing')\n", "\n", "except Exception as err:\n", " print('[process_segment] [{}] {}'.format(estacion, err))" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [], "source": [ "try:\n", " media = []\n", " for i in pendiente[\"media\"]:\n", " archivo = descargar_media(ciudad, estacion, i[\"ruta\"])\n", " if archivo is not None:\n", " media.append((archivo, i[\"fecha\"], i[\"timestamp\"]))\n", "\n", "except Exception as err:\n", " print(err)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dejavu = Dejavu({\"database_type\": \"mem\"})\n", "try:\n", " x = 0\n", " for anuncio in anuncios:\n", " dejavu.fingerprint_file(anuncio)\n", "except Exception as ex:\n", " print(err)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "if tamano_segmento == 'integer':\n", " tamano_segmento = int(longitud_audio)\n", "elif tamano_segmento == 'ceil':\n", " tamano_segmento = int(math.ceil(longitud_audio / 5)) * 5\n", "\n", "segmentos_necesarios = int(round(float(longitud_audio) / float(tamano_segmento)))\n", "segmentos_necesarios = int(round(segmentos_necesarios * tolerance))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "anuncios_en_paralelo = 5 # Este valor debe venir desde el php\n", "\n", "dejavu = None\n", "cont_media = len(media)\n", "cont_anuncios = len(anuncios)\n", "resultados = []\n", "v = []\n", "try:\n", " x = 0\n", " while x < cont_anuncios:\n", " y = 0\n", " dejavu = Dejavu({\"database_type\": \"mem\"})\n", " print(\"Nueva comparación\")\n", " while y < anuncios_en_paralelo and x < cont_anuncios:\n", " anuncio = anuncios[x]\n", " print(\"Agregando anuncio %s %s\" % (x, os.path.split(anuncio)[-1],))\n", " dejavu.fingerprint_file(anuncio)\n", " y += 1\n", " x += 1\n", "\n", " for ruta, fecha, ts in media:\n", " values = []\n", " try:\n", " for match in dejavu.recognize(recognizer, ruta, tamano_segmento):\n", " name = None\n", " ad = None\n", " try:\n", " ad = match['name']\n", " if match['name'] in id_by_ad.keys():\n", " name = id_by_ad[match['name']]\n", " else:\n", " name = match['name']\n", "\n", " except KeyError:\n", " pass\n", "\n", " resultados.append({\n", " 'ad': ad,\n", " 'confidence': match['confidence'],\n", " 'timestamp': ts,\n", " 'offset': match['offset'],\n", " 'name': name\n", " })\n", " values.append(str(match['confidence']))\n", " ts = ts + 5\n", "\n", " v.append(','.join(values))\n", " print('[process_segment] [{2}] {0} {1}'.format(\n", " os.path.split(ruta)[-1],\n", " ','.join(values),\n", " estacion,\n", " ))\n", "\n", " except CouldntDecodeError as ex:\n", " log.error('[process_segment] {}'.format(ex))\n", "\n", " try:\n", " encontrados = {}\n", " for i in item_ids:\n", " resultado = [r for r in resultados if r[\"name\"] == i]\n", " encontrados[i] = encontrar_resultados(resultado, \n", " segments_needed=tamano_segmento,\n", " calibration=calibracion,)\n", "\n", " for id in encontrados:\n", " for e in encontrados[id]:\n", " for i in pendiente['elementos']:\n", " if i['id'] == id and i['anuncio'] == e['ad']:\n", " if 'encontrados' not in i:\n", " i['encontrados'] = []\n", " i['encontrados'].append(e)\n", " break\n", "\n", " pendiente[\"archivos_perdidos\"] = 0\n", " pendiente[\"total_archivos\"] = cont_media\n", " # response = enviar_resultados(pendiente)\n", " except ConnectionError as ex:\n", " pass\n", " except UserWarning as warn:\n", " pass\n", "\n", "except Exception as ex:\n", " print(err)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "try:\n", " encontrados = {}\n", " for i in item_ids:\n", " r = [result for result in resultados if result[\"name\"] == i]\n", " encontrados[i] = encontrar_resultados(r, segments_needed=segmentos_necesarios, calibration=calibracion,)\n", "\n", " for id in encontrados:\n", " for e in encontrados[id]:\n", " for i in pendiente['elementos']:\n", " if i['id'] == id and i['anuncio'] == e['ad']:\n", " if 'encontrados' not in i:\n", " i['encontrados'] = []\n", " i['encontrados'].append(e)\n", " break\n", "\n", " pendiente[\"archivos_perdidos\"] = 0\n", " pendiente[\"total_archivos\"] = cont_media\n", " response = enviar_resultados(pendiente)\n", "except ConnectionError as ex:\n", " pass\n", "except UserWarning as warn:\n", " pass\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": 115, "metadata": {}, "outputs": [], "source": [ "dejavu = None\n", "resultados = {}\n", "try:\n", " dejavu = Dejavu({\"database_type\": \"mem\"})\n", " try:\n", " x = 0\n", " for ruta, fecha, ts in media:\n", " log.info(\"Huellando %s\" % (ruta,))\n", " dejavu.fingerprint_file(ruta, ts)\n", " except Exception as ex:\n", " log.info(ex)\n", "\n", " for anuncio in anuncios:\n", " log.info(\"Buscando anuncio %s\" % (anuncio,))\n", " for i in dejavu.recognize(recognizer, anuncio, 5):\n", " if not \"id\" in i:\n", " continue\n", "\n", " if i[\"confidence\"] < 50:\n", " continue\n", "\n", " obj = i\n", " obj[\"match_time\"] = None\n", " nombre_anuncio = os.path.split(anuncio)[-1]\n", " id = id_by_ad[nombre_anuncio]\n", " dict = {\n", " \"id\": id,\n", " \"anuncio\": anuncio,\n", " \"timestamp\": obj[\"name\"] + int(obj['offset_seconds']),\n", " \"confianza\": obj[\"confidence\"],\n", " \"longitud\": obj[\"length\"],\n", " \"desfase_segundos\": obj[\"offset_seconds\"]\n", " }\n", "\n", " if i[\"id\"] in resultados.keys():\n", " resultados[i[\"id\"]][\"longitud\"] = resultados[i[\"id\"]][\"longitud\"] + dict[\"longitud\"]\n", " resultados[i[\"id\"]][\"confianza\"] = resultados[i[\"id\"]][\"confianza\"] + dict[\"confianza\"]\n", " continue\n", "\n", " resultados[i[\"id\"]] = dict\n", "\n", "except Exception as ex:\n", " log.info(ex)" ] }, { "cell_type": "code", "execution_count": 116, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'id': 46057082, 'anuncio': '/tmp/ads/-MlReJO2cHvAi7XG40mw', 'timestamp': 1630504399, 'confianza': 980, 'longitud': 30000, 'desfase_segundos': 199.39439}\n" ] } ], "source": [ "for id in resultados:\n", " e = resultados[id]\n", " for i in pendiente['elementos']:\n", " anuncio = e['anuncio'].replace('/tmp/ads/', '')\n", " if i['id'] == e['id'] and i['anuncio'] == anuncio:\n", " if 'encontrados' not in i:\n", " i['encontrados'] = []\n", " i['encontrados'].append(e)\n", " print(e)\n", " break" ] }, { "cell_type": "code", "execution_count": 63, "metadata": {}, "outputs": [], "source": [ "import json" ] }, { "cell_type": "code", "execution_count": 118, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 118, "metadata": {}, "output_type": "execute_result" } ], "source": [ "enviar_resultados(pendiente)" ] }, { "cell_type": "code", "execution_count": 117, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'{\"idCampana\": 1768, \"idCampanaPauta\": 1166150, \"ciudad\": \"prueba\", \"idEstacion\": 10, \"origen\": \"g2yJhO8z\", \"fecha\": \"2021-09-01 06:00:00\", \"zonaHoraria\": \"America/Hermosillo\", \"estacion\": \"101_7_SON_OBR\", \"media\": [{\"ruta\": \"2021/09/01/2021-09-01T06-00-00-0700.mp3\", \"fecha\": \"2021-09-01 06:00:00\", \"timestamp\": 1630501200}, {\"ruta\": \"2021/09/01/2021-09-01T06-05-00-0700.mp3\", \"fecha\": \"2021-09-01 06:05:00\", \"timestamp\": 1630501500}, {\"ruta\": \"2021/09/01/2021-09-01T06-10-00-0700.mp3\", \"fecha\": \"2021-09-01 06:10:00\", \"timestamp\": 1630501800}, {\"ruta\": \"2021/09/01/2021-09-01T06-15-00-0700.mp3\", \"fecha\": \"2021-09-01 06:15:00\", \"timestamp\": 1630502100}, {\"ruta\": \"2021/09/01/2021-09-01T06-20-00-0700.mp3\", \"fecha\": \"2021-09-01 06:20:00\", \"timestamp\": 1630502400}, {\"ruta\": \"2021/09/01/2021-09-01T06-25-00-0700.mp3\", \"fecha\": \"2021-09-01 06:25:00\", \"timestamp\": 1630502700}, {\"ruta\": \"2021/09/01/2021-09-01T06-30-00-0700.mp3\", \"fecha\": \"2021-09-01 06:30:00\", \"timestamp\": 1630503000}, {\"ruta\": \"2021/09/01/2021-09-01T06-35-00-0700.mp3\", \"fecha\": \"2021-09-01 06:35:00\", \"timestamp\": 1630503300}, {\"ruta\": \"2021/09/01/2021-09-01T06-40-00-0700.mp3\", \"fecha\": \"2021-09-01 06:40:00\", \"timestamp\": 1630503600}, {\"ruta\": \"2021/09/01/2021-09-01T06-45-00-0700.mp3\", \"fecha\": \"2021-09-01 06:45:00\", \"timestamp\": 1630503900}, {\"ruta\": \"2021/09/01/2021-09-01T06-50-00-0700.mp3\", \"fecha\": \"2021-09-01 06:50:00\", \"timestamp\": 1630504200}, {\"ruta\": \"2021/09/01/2021-09-01T06-54-59-0700.mp3\", \"fecha\": \"2021-09-01 06:54:59\", \"timestamp\": 1630504499}], \"calibracion\": {\"fallTolerance\": 1, \"threshold\": 10, \"tolerance\": 0.7}, \"elementos\": [{\"id\": 46057082, \"ruta\": \"anuncios/-MlReJO2cHvAi7XG40mw\", \"repeticiones\": 1, \"idAudio\": 4565, \"anuncio\": \"-MlReJO2cHvAi7XG40mw\", \"encontrados\": [{\"id\": 46057082, \"anuncio\": \"/tmp/ads/-MlReJO2cHvAi7XG40mw\", \"timestamp\": 1630504399, \"confianza\": 980, \"longitud\": 30000, \"desfase_segundos\": 199.39439}]}, {\"id\": 46057100, \"ruta\": \"anuncios/-MlReBIHRuk0dC4WKiZR\", \"repeticiones\": 1, \"idAudio\": 4561, \"anuncio\": \"-MlReBIHRuk0dC4WKiZR\"}, {\"id\": 46057154, \"ruta\": \"anuncios/-MlRce4eqVUyTkDbIcAN\", \"repeticiones\": 1, \"idAudio\": 4548, \"anuncio\": \"-MlRce4eqVUyTkDbIcAN\"}, {\"id\": 46057172, \"ruta\": \"anuncios/-MlRelzkKzHdUpGZlkmD\", \"repeticiones\": 1, \"idAudio\": 4571, \"anuncio\": \"-MlRelzkKzHdUpGZlkmD\"}, {\"id\": 46057190, \"ruta\": \"anuncios/-MlRcIWglVS9eKoJZLlv\", \"repeticiones\": 1, \"idAudio\": 4542, \"anuncio\": \"-MlRcIWglVS9eKoJZLlv\"}, {\"id\": 46057208, \"ruta\": \"anuncios/-MlWhq2bbe7lED-ApFgp\", \"repeticiones\": 1, \"idAudio\": 4617, \"anuncio\": \"-MlWhq2bbe7lED-ApFgp\"}, {\"id\": 46057226, \"ruta\": \"anuncios/-MlRdwzYUS3k5valF9Ri\", \"repeticiones\": 1, \"idAudio\": 4555, \"anuncio\": \"-MlRdwzYUS3k5valF9Ri\"}, {\"id\": 46057244, \"ruta\": \"anuncios/-MlRcpOv5kyMAJ52pswd\", \"repeticiones\": 1, \"idAudio\": 4549, \"anuncio\": \"-MlRcpOv5kyMAJ52pswd\"}, {\"id\": 46057262, \"ruta\": \"anuncios/-MlReDcD94LuAkmbhq-n\", \"repeticiones\": 1, \"idAudio\": 4562, \"anuncio\": \"-MlReDcD94LuAkmbhq-n\"}, {\"id\": 46057280, \"ruta\": \"anuncios/-MlRe-XN_j1Ns6CDUsP-\", \"repeticiones\": 1, \"idAudio\": 4556, \"anuncio\": \"-MlRe-XN_j1Ns6CDUsP-\"}, {\"id\": 46057298, \"ruta\": \"anuncios/-MlRc_XihusSzapzm6Qa\", \"repeticiones\": 1, \"idAudio\": 4547, \"anuncio\": \"-MlRc_XihusSzapzm6Qa\"}, {\"id\": 46057316, \"ruta\": \"anuncios/-MlRe2hx_xDRiqMsE-pk\", \"repeticiones\": 1, \"idAudio\": 4558, \"anuncio\": \"-MlRe2hx_xDRiqMsE-pk\"}, {\"id\": 46057334, \"ruta\": \"anuncios/-MlRe6KKd5gg8u5lbekH\", \"repeticiones\": 1, \"idAudio\": 4559, \"anuncio\": \"-MlRe6KKd5gg8u5lbekH\"}]}'" ] }, "execution_count": 117, "metadata": {}, "output_type": "execute_result" } ], "source": [ "json.dumps(pendiente)" ] }, { "cell_type": "code", "execution_count": 94, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'7798E7A7396865717C5FF58D40533B17D05D90DC': {'id': 46057064,\n", " 'anuncio': '/tmp/ads/-MlRe9DwDJVtYlLS-6PS',\n", " 'fecha': 1630503000,\n", " 'confianza': 78,\n", " 'longitud': 5000,\n", " 'desfase_segundos': 285.21535},\n", " '373A10DF13ADBE563AB4A6F39D9DF0C73AA537FB': {'id': 46057082,\n", " 'anuncio': '/tmp/ads/-MlReJO2cHvAi7XG40mw',\n", " 'fecha': 1630504200,\n", " 'confianza': 980,\n", " 'longitud': 30000,\n", " 'desfase_segundos': 199.39439}}" ] }, "execution_count": 94, "metadata": {}, "output_type": "execute_result" } ], "source": [ "resultados" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "item[\"elementos\"]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cloud_download_file(item[\"origen\"], item[\"estacion\"], item['archivos'][0]['filename'])" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "path = os.path.join(AUDIOS_PATH, 'fourier', 'ciudad', 'estacion')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "path" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "os.makedirs(path, exist_ok=True)" ] } ], "metadata": { "interpreter": { "hash": "631ec0267e76ead327ae18a3cdf21f6916cbb309615a11f42bd594f9973a79cd" }, "kernelspec": { "display_name": "Python 3.9.7 64-bit ('venv': venv)", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.7" } }, "nbformat": 4, "nbformat_minor": 2 }