This application wasn't in my plans... it was born out of my frustration trying to make my gallery displaying in an order I'd like to see it. The issue is the order was to be set visually, by watching the images and dragging them around. And here it is: Media Sorter App.
After opening a target folder, it displays all images and videos in a grid, allowing to drag and drop them around, changing their order. Clicking any thumbnail opens a big view (yes, videos are working, too).
Upon clicking the Apply button (bottom right corner) it does two things:
saves current order into a order.json file in a target folder (opening a folder with this file inside makes the app displaying them in that order)
copies a list of filenames in that order into a system clipboard, allowing it to be pasted somewhere - for example in the code of the gallery script which was the main goal of this application.
It is written in Flutter, however I've been testing it only against Windows. If you want to check the sources and build it for yourself, you can get it from GitHub. Refer to README.md for instructions.
If you want just to grab a ready binary, there's a MSIX package ready to download and install. Version 1.0.1 can be downloaded here.
Last time I was working on sound visualization, after testing with real-life data (yes, music ) and testing out various visualization shaders, I came to a conclusion that I approached it from a too scientific point of view.
The result was fully proper spectrogram - but not so useful for visualization purposes.
So, now I've returned to it - but this time focusing on achieving more visually appealing results,
easier to read by a human. I wanted to make it similar to what Inigo Quilez is doing in his ShaderToy, but I couldn't find an exact way he is treating the data, so I had to come up with my own approach.
One thing still applies: the best way is to use a frequencies analysis, through FFT. Waveform itself can be useful, too (and that's why I'm still including it as a second row of my OpenGL texture), but here we will focus on a spectrogram, as there's not too much to talk about a waveform, it's a simple data.
So, let's start from taking a portion of an audio file:
importnumpyasnpimportlibrosadefget_audio_part(audio,time_start=0.0,sample_rate=44100,num_samples=512):sample_start=int(time_start*sample_rate)sample_end=sample_start+num_samples# Handle padding if we reach the end of the audioifsample_end>len(audio):audio_part=audio[sample_start:]audio_part=np.pad(audio_part,(0,num_samples-len(audio_part)),'constant')else:audio_part=audio[sample_start:sample_end]returnaudio_partaudio_,sample_rate_=librosa.load("test_sound_01.mp3",mono=True,sr=None)position=0.0# position in the audio filesignal=get_audio_part(audio_,position,sample_rate_,2048)
Now we can perform a regular FFT analysis of frequencies. We will use Hann window filtering.
As we can see, the values range is huge. Keeping in mind we will be mapping them to an image, encoding the magnitude to pixel's brightness, we will get few frequencies bright, and most of the rest just pitch black.
So, we need to make it less scientific - and more visually pleasing. We'll rescale the values - flatten them, to make it more image-friendly.
We'll start from logarithmic scaling (adding 1.0 to avoid values going to negative infinity) and then remapping them to a 0..1 range:
magnitude=np.abs(freqresp)magnitude=np.log10(magnitude+1.0)magrange=np.max(magnitude)-np.min(magnitude)magnitude-=np.min(magnitude)magnitude/=magrangeplt.figure(figsize=(12,5))plt.plot(freqs,magnitude,color='#ff5500')plt.title("Magnitudes rescaled for better visibility, with flattened range.")plt.xlabel("Frequency (Hz)")plt.ylabel("Magnitude (rescaled)")plt.grid(True,alpha=0.3)plt.show()
Also, we will take only first 512 values from our FFT response. Remembering we took a 2048 window, FFT returned 1024 values, so our first 512 values will be representing 0..11025 Hz.
Let's build our final texture. It will be 512 pixels wide. In fact should be 1 pixel high, but here we will use 100px, to better see it. It will use only one channel, RED.
ℹ️ Note:
When creating OpenGL texture, we have to keep in mind the texture has to be created only once (for instance, on music load), and then in each video frame just having the data being replaced. Also it shouldn't have any mip-mapping.
After years of using Wordpress for my blog, I've decided to move to MkDocs.
Why? For few reasons...
The main reason, which made me to make that effort, is the fact, that for the last few years most of my interaction with this blog was fighting with hackers, inserting their spam, mostly gambling related stuff.
I was tired of cleaning up their mess and I've decided to move to something more secure and reliable.
Giving the fact, that I'm the only author here, I don't need this huge system, anyway. So I've started looking for some alternative solution. And I've found MkDocs, a static site generator. It works in Python and generates the static pages off the source files. And it's written in a way it allows for quite a big customization. I'm using it with Material for MkDocs theme, which is very nice and customizable.
Another thing is, I've been always preferring to write my posts in Markdown.
Right now I'm still working on the customization - and of course, I'm trying to move the content. Until I'm done, some content may be missing. Note, this is a great occassion to review some old stuff, which is outdated now - and remove it.
We'll build a simple RAG system using local only models. We will not use LangChain, which is introducing many bloated dependencies, is much slower than direct Transformers usage, is not error-free and its documentation is mostly misleading. We'll use only bare Transformers functions for that. As a vector database for storing our embeddings from document, we'll use Faiss, which is really efficient in similarity search. Note it sits in RAM, not on a disk and is very fast.
Retrieval-Augmented Generation (RAG) is an AI framework that improves Large Language Model (LLM) accuracy by retrieving data from external, trusted sources (documents, databases) rather than relying solely on training data. It enables up-to-date, specialized answers, reduces hallucinations, and avoids costly model retraining.
In simple words: it allows to have a LLM having a specialized knowledge without retraining it. We'll build here a simple version of it, allowing loading a single PDF files and then having a chat. We will use only local models, without using any cloud. This means few things:
- it's completely free
- it's completely private (no data exposing to internet)
- it's weaker than cloud models.
It's up to you - and depends mostly on your hardware (GPU and its VRAM). I've used here google/gemma-2-9b-it as LLM and BAAI/bge-large-en-v1.5 for creating embeddings. It works without any issues on 12GB VRAM GPU - and it works with different languages. You can, for instance, have a source in Polish and ask questions in English (or vice-versa). Keep in mind in order to use models from HuggingFace, you have to have an account there and you have to accept the model's usage policy.
When creating a RAG system, there're few parameters, that can have a big impact on how it's working. This includes:
- LLM's temperature: it controls the randomness of LLM's output. The lower temperature, the more deterministic answers will be.
- If LLM can do sampling: if sampling is set to off, LLM is using greedy sampling, so it just selects the tokens with the highest probability. If sampling is allowed, it will take one of the possible tokens. The choise is weighted, but it doesn't necessarily mean the highest probability will be chosen.
- How many similar chunks to choose: during looking for similar chunks in the vector database, how many of them will be selected for the answer? In this example well working values are 3-5.
All in all, how you set them, depends mostly on the type of documents you want to work with: if it's some theory, reports, technicals, instructions, guides etc., then set it as I did below. If it's more loose texts, you may want to increase the temperature (let's say, up to 0.4) and turn sampling on. With even more informal texts you will also want to increase the number of similar chunks to be found (even above 10), but be careful, it will greatly increase the RAM usage.
Let's start from creating our vector database, which is our special knowledge, created from given PDF file. We will read the file and split the text into smaller chunks (remembering the page number for each chunk, so we can give exact citations in our answers). The chunks will be converted into embeddings, using SentenceTransformer and our embedding model.
reader=pypdf.PdfReader(pdf_path)full_text=""pages_meta=[]# A list to track which page each character comes from.fori,pageinenumerate(reader.pages):page_text=page.extract_text()ifpage_text:full_text+=page_text# For each character on the page, store its page number and source file. This is a bit memory-intensive# but allows for accurate source tracking later.pages_meta.extend([{'page':i+1,'source':pdf_path}]*len(page_text))
Having the text extracted, we'll split the full text into smaller chunks:
We'll create a metadata for each chunk by finding the page number corresponding to the middle of the chunk:
chunk_metadatas=[]char_count=0forchunkinchunks:mid_point=char_count+len(chunk)//2ifmid_point<len(pages_meta):chunk_metadatas.append(pages_meta[mid_point])else:# A fallback for the very last chunk.chunk_metadatas.append(pages_meta[-1])char_count+=800-150# Move character counter forward by (chunk_size - chunk_overlap).
Loading the LLM is pretty straightforward, if we remember the notes we stated in the beginning (the temperature etc.).
We will load quite a big model, but quantize it to 4-bit precision, significantly reducing the size in RAM:
First thing to do after getting a query from a user, is to find relevant chunks of text, along with their page numbers, which we will use in sources citation.
This part will differ slightly depending on the LLM model used. Some models expect different prompt templates, regarding the user -> assistant loop:
template=f"""<start_of_turn>userYou're a helpful assistant. Answer the question based only on the context below.Answer using the same language the question was asked in.\nContext:\n{context}\nQuestion: {query}<end_of_turn><start_of_turn>model"""
If we don't formulate this properly, model can start hallucinating the dialog and start talking to itself. Note we have passed not the text, but our chunks that have been found after the user's question.
We can also quote the pages containing the relevant material in the PDF:
seen_pages=set()# Use a set to avoid printing duplicate page numbers.formetaincontext_metadatas:page_num=meta.get('page','N/A')ifpage_numnotinseen_pages:print(f" - Page: {page_num} (Source File: {meta.get('source')})")seen_pages.add(page_num)
As you can see above, it's very simple, much simpler than one could think: it's not LLM looking for the answer.
It's FAISS looking through all our chunks of text and finding the most suiting ones. LLM is given only those found ones and all it's doing is recapping the small portion of the text and formulating nice text. Simple as that.
Now, your task now is to put it into some UI, for example a simple Streamlit which is perfectly suiting this type of job.
Just add some button to load the PDF, input and text - and voilà
I'm often writing descriptions and ideas on projects in Notion. And I thought: it'd be nice to have such system descriptions summarized in a diagram of proper kind, be it a flow diagram, decision diagram etc. And it'd be nice to have it done automagically.
So, I've built a simple Notion integration. It's made of 2 parts: a server, acting like a Notion MCP server, connecting to api.notion.com, and a client using this server. That way I could omit using a full-blown official Notion MCP server, requiring full OAuth authorization, playing with tokens etc. It's just a simple tool running on my own local machine, connecting directly to API.
The server part is mimicking what official MCP server is doing, but it's adapted to my needs, like if it gets a page, which is long, it gets care of getting all the parts, etc. That way the client itself is much easier to write and maintain.
Both server and client are written in Python. All you need to do is to create an internal integration on Notion Integrations page. From there, take your Notion token and put it in .env file:
Server is using Starlette and MCP modules as its main core, using SSE Transport. And yes, it should be updated to use HTTP Stream Transport instead, but I've left that for some day.
Now, the server has a number of tools the client can use. My list of tools is different from the original Notion MCP server, as it's suited for my task. For this simple integration it's:
- list_accessible_pages: gets all the pages that were connected to our integration in Notion UI;
- get_notion_object: gets the object and its info. If it's a big object, get all the blocks.
- upsert_mermaid_block: inserts or updates the Mermaid block.
Now, how it's working?
As the core functionality is automagic diagram insertion, I'm just using Gemini 2.5 Flash AI model to generate the diagram, feeding it with the whole text of the page.
Inserted diagram is timestamped, so the client can know if the page was updated afterwards and needs refreshing of the diagram.
The exact flow of the client is:
- get the list pages it has access to;
- download whole page;
- search for a signature with a timestamp;
- if the signature doesn't exist, or the timestamp is older than the update date of the page: insert or update the mermaid block, the code itself comes from Gemini.
Simple as that.
There's also one thing to consider, while creating Mermaid diagrams with Gemini: while it's really good at it, it can sometimes do syntax errors, especially in complicated diagrams. That's why my prompt lists few rules.
Another thing is a hard limit of a single block length in Notion. It's only 2000 characters. It sounds as enough, but for large complicated diagrams it can be too small. Hence my prompt says exactly that and adds "if it's too long, simplify the diagram and omit less important elements".
All the sources are available on GitHub, just keep in mind it's in Polish (both comments and Gemini's prompt), but I think you can easily get the grasp of what it's doing. The code is really simple.
If you’ve ever tried to run OCR on handwritten notes, you know the struggle. Standard algorithms excel at clean, black-on-white typed text. But throw in a background grid or low-contrast pencil marks, and the accuracy plummets.
While some papers can be quite nice for text recognition, some may be... let's say - hard.
The grid on this paper is of almost the same intensity as writing.
The following Python function uses OpenCV to perform "surgery" on an image: it identifies the grid, removes it without destroying the text, and then uses adaptive equalization to make the handwriting pop. Let’s break down how it works step-by-step.
Unlike global thresholding (which uses one value for the whole image), adaptive thresholding calculates different thresholds for small pixel neighborhoods.
Why? Scanned documents often have uneven lighting (shadows in the corners).
The Result: We get a "binary" image (black and white) where the grid and text are white and the background is black (THRESH_BINARY_INV). This makes it easier for the math in the next step to identify shapes.
We create two long, thin rectangles: one horizontal and one vertical. By applying a Morphological Open operation, we effectively say: "Keep only the shapes that this rectangle can fit into."
mask_h: Only keeps horizontal lines.
mask_v: Only keeps vertical lines.
grid_mask: By adding them together and dilating (thickening) the result, we create a map of exactly where the grid sits.
We can dilate it by few pixels, to be sure it won't leave any artifacts after removal.
Inpainting looks at the grid_mask (the areas we want to fix) and fills those pixels by interpolating data from the surrounding non-grid pixels. It’s like a smart "heal" tool. It removes the grid while attempting to preserve the continuity of the pen strokes that crossed over it.
Standard Histogram Equalization spreads out the most frequent intensity values, but it often over-amplifies noise. CLAHE operates on small tiles (8x8 pixels) and clips the contrast to prevent the background noise from becoming overwhelming.
After this step it could also be eroded (thickened) by some small kernel, but in the case of small tight writing it could destroy the visibility of individual letters.
By the end of this pipeline, the image has undergone a massive transformation:
Gridlines are intelligently "healed" out of the image.
Shadows from the scan are neutralized.
Faint handwriting is darkened and sharpened.
This pre-processed image provides a much higher "signal-to-noise" ratio, giving your OCR engine a clear path to accurate character recognition.
Now, when it comes to recognition itself, is another story. For this type of writing, as in this example (yes, it's mine...), there's no self-hosted solution, everyone of them is failing very miserably. For such bad writing only big cloud vision models can help. Why? Not only because it looks bad - but also it's so tight every self-hosted algorithm of separating this into individual lines fails, just like that. Doesn't matter if we use some clever engineering or let some vision AI to do it. Only the biggest models can do that ;) Of course, if you have some better papers, written in a more.. let's say, human way ;) then maybe there's a solution for self-hosted recognition. I've spent several nights on this and in the end I went for Gemini ;)
AI is being used in recruitment widely and everybody knows that. That led me to think: what if we do it the opposite way? Give a user the power of AI in his job search? Why and what for? For two reasons: first and obvious: let him find a better suited job. But there's also another thing: by studying the results we could see what in the user's profile made a given job offer to be suitable or not? Maybe there can be something changed in the profile? Because... that's also probably the same thing the recruitment's AI system is observing. So, in this way, user could make his profile better.
Now, the whole system I've made consists of 4 stages: web crawling, offer analysis, profile analysis and finally job matching. The final output is a proposal of a decision (apply, reject, maybe), summary of pros and cons and some thoughts and reasoning.
Web crawling is a huge thing and I won't discuss it here, as it's way too big for this article. I'll just mention it requires analysing the pages, deciding if it's even related, if it's a job offer or maybe a job offers list. It should analyze the links inside and find consecutive subpages. The output should be pairs of: url of an offer and its full text, without any HTML tags etc., just a pure text. All the other elements of this system we'll discuss below.
Before we start diving into details of each of the stages, let's talk for a moment about technology used here. I've decided to make it as simple as possible - yet very easy to understand - and easy to run on user's home machine. That's why for provider of LLM models I chose Ollama, very handy system for self hosted LLMs. Coding will be done in Python (I'm using Python 3.12) using only elementary additional packages, like json, PdfReader, tqdm, requests and ollama. For communicating with LLMs through Ollama I've made a simple class with two types of clients: the one using ollama implementation and the one communicating through HTTP JSON API. You can see this little library on GitHub. It also contains few examples on how to use it in various scenarios. In this project we will use the structured JSON way.
The key is a very precise system prompt, defining how the model should act and what it should produce as an output. Something like "You are a job offer analyzer ... Return the output as JSON with the following fields ... do not add any explanations. Output only JSON ...". Also setting a model's temperature to 0 helps getting highly consistent, deterministic, and focused output. Another thing is to granulate the overall job to as small steps as possible. This is what we can see on above diagram - do not try to do few steps in one go, give model a single and precise job to do.
The input to this stage is a pair of info: an url of an offer and its full text, stripped from any HTML tags etc - just a text. As an output we want a structured info, containing all the relevant info, like title, salary, type of job, requirements, nice-to-have etc. Let's see an example:
{"url":"https://great.company.com/job-offer","company":"Great Company","title":"Tech Lead","location":"100% remote","remote":true,"seniority":"lead","salary":"","description":"Provide technical leadership to the delivery team, be accountable for delivering defined feature sets, design and develop components within the data and analytics layer of an investment research platform, co-create system architecture and technology standards, ensure solution quality through code reviews, mentoring, and oversight of engineering practices, support the Product Owner and work closely with the team in backlog planning and execution, actively contribute to the development of analytical tools for investment analysts, participate in R&D work related to future iterations of the platform.","responsibilities":[],"requirements":["proven experience in providing technical leadership and acting as a Tech Lead in enterprise scale projects","expert knowledge of agile software delivery and DevOps across the SDLC","strong mentoring and coaching skills, including implementing engineering, architecture, and testing best practices","experience in initiating and driving continuous improvement initiatives","ability to work closely with Product Owners, stakeholders, and business users","English proficiency at a minimum B2+ level"],"nice_to_have":[],"technologies":["agile software delivery","DevOps","SDLC"],"offer":[],"language":"en"}
Most important thing (as in whole this system) is a system prompt, making sure AI will return a precise output, formatted as we need:
system_prompt="""You are a job offer analyzer. Extract structured information from the job description text. Return the output as JSON with the following fields:- url: URL of the job offer- company: infer company name if possible from the text or URL- title: job title- location: city / country / "remote"- remote: true / false / "unknown"- employment_type: full-time / contract / internship / unknown- seniority: junior / mid / senior / lead / manager / unknown- salary: salary range if specified- description: short summary in your own words- responsibilities: list of main responsibilities- requirements: list of key requirements- nice_to_have: list of additional "nice to have" skills- technologies: programming languages, frameworks, tools- offer: list of benefits, if available- language: language of the job offer (en/pl)Do not add any explanations. Output only JSON.Never invent jobs that are not clearly present.Never hallucinate technologies.If a field is missing, use empty string, empty array, or "unknown"."""
Then, the function itself is really simple if we'll use the ollama library I've quoted above:
call_llm method from LLMClientOllama class will take care of proper JSON payload sending, receiving and extracting from the response.
💡
For all the structuring/extraction jobs I've chosen the QWEN3 model, but you can check other models. Specifically, QWEN3 14B is running smoothly on nVidia with 12GB of VRAM.
Before we can match the offer, we need to have a second side - the user's profile. Again, it will be a structured JSON. We could do it manually, but we'll make a PDF extractor, as headhunting systems are doing. That can give us a feedback on how well our CV is composed.
First step is extracting all the text from CV. This already gives a hint: DO NEVER send PDFs made of graphics (scanning or other composition tools) - it has to be a real text (not rendered). For this task we'll use PdfReader class from pypdf package.
CANDIDATE_SYSTEM_PROMPT="""You are an expert technical recruiter and career analyst.Your task is to analyze a CV (resume) and extract a structured candidate profile.Rules:- Output ONLY valid JSON- Do NOT include explanations, markdown, comments or prose- If some information is missing, infer conservatively or use null- Normalize names (e.g. "C plus plus" → "C++")- Seniority must be one of: ["junior", "mid", "senior", "lead", "staff", "principal", "staff / principal / lead", "manager", "director", "cto", "ceo", "unknown"]The JSON schema MUST match exactly:{ "seniority": string, "years_of_experience": number, "primary_roles": string[], "core_languages": string[], "secondary_languages": string[], "domains": string[], "leadership": { "people_management": boolean, "tech_lead": boolean, "scrum_master": boolean }, "cloud": string[], "devops": string[], "frontend_level": string, "remote_preference": boolean, "languages_spoken": { "pl": string, "en": string }, "job_preferences": { "roles_to_avoid": string[], "preferred_roles": string[] }}Think carefully. This profile will be used for automated job matching."""
and a small function preparing a user prompt:
defbuild_candidate_prompt(cv_text:str)->str:returnf"""Analyze the following CV and extract the candidate profile.CV TEXT:----------------{cv_text}----------------"""
Of course the resulting JSON data can be modified to tweak it, but also it could be used to verify if maybe something should be added to the original CV instead.
It would be tempting to do all matching job using AI, but there're at least two points against it:
every AI call takes time - and if we can avoid it with some obvious rejects, it's a plus;
collecting some info, categorizing it etc. will be done better in simple code, as AI may sometimes hallucinate things.
That's why as a first step in job matching we'll perform some algorithmic data collection and first decision.
What you can do in this step, of course depends on the kind of a job, but for software developers you could score things like tech stack, seniority, domains, leadership duties, and general logistics. Let's see an example:
You should set the overall score levels for taking a decision, depending on how you've set scoring. Set 3 levels of decision: apply / maybe / reject - and filter out the rejected ones before passing the offers to the next step, which is AI matching.
Now it's time for the last step - the key one. Its input would be an offer, user's profile and algorithmic matching results, so it can learn from it.
Let's start from a system prompt:
REVIEW_SYSTEM_PROMPT="""You are a senior technical recruiter and staff-level software engineer.Your task is to evaluate whether this job offer is worth applying tofor experienced software engineer with attached profile.You MUST be critical and skeptical.Reject roles that are:- execution-only- lacking ownership or technical impactReturn ONLY valid JSON.Do NOT include markdown.Do NOT include explanations outside JSON.JSON schema:{ "final_verdict": "apply" | "maybe" | "reject", "confidence": 0-100, "key_reasons": [string], "risks": [string], "positive_signals": [string], "summary": string}"""
Of course it should be altered to suit your needs.
Next, let's prepare a user prompt:
defprepare_llm_input(profile:dict,offer:dict,match:dict)->dict:""" Builds a clean, stable input structure for LLM evaluation. No formatting, no text generation. """return{"candidate":{"seniority":profile.get("seniority"),"years_of_experience":profile.get("years_of_experience"),"core_stack":profile.get("core_languages"),"secondary_stack":profile.get("secondary_languages"),"domains":profile.get("domains"),"leadership":profile.get("leadership"),"preferences":{"remote":profile.get("remote_preference"),"preferred_roles":profile.get("preferred_roles"),"roles_to_avoid":profile.get("roles_to_avoid"),}},"job":{"title":offer.get("title"),"location":offer.get("location"),"responsibilities":offer.get("responsibilities",[])[:10],"requirements":offer.get("requirements",[])[:10],"nice_to_have":offer.get("nice_to_have",[])[:5]},"algorithmic_assessment":{"score":match.get("score"),"decision":match.get("decision"),"strengths":match.get("strengths",[]),"gaps":match.get("gaps",[]),"red_flags":match.get("red_flags",[])}}defbuild_llm_prompt(llm_input:dict)->str:""" Converts structured LLM input into a readable prompt. """returnf"""Candidate profile:{json.dumps(llm_input["candidate"],indent=2,ensure_ascii=False)}Job offer:{json.dumps(llm_input["job"],indent=2,ensure_ascii=False)}Algorithmic assessment:{json.dumps(llm_input["algorithmic_assessment"],indent=2,ensure_ascii=False)}Evaluate realistically whether applying makes sense."""
For this last step you could experiment with various models, as they can give slightly different reasoning. While the overall results will be probably similar, the reasoning can be very helpful for user can react and either take his action or maybe update his/her CV according to those results.
Let's see an example output of this step:
{"final_verdict":"reject","confidence":85,"key_reasons":["Role lacks technical ownership and leadership responsibilities","Candidate's seniority (manager) far exceeds job requirements","Focus on code evaluation rather than system design/architecture","Part-time hourly contract misaligned with candidate's experience level"],"risks":["Underutilization of candidate's leadership and technical expertise","Potential for role to be perceived as junior-level despite candidate's seniority","Mismatch between compensation structure (hourly) and candidate's career stage"],"positive_signals":["Remote work flexibility","Opportunity to work with AI systems","Python/C++ stack alignment"],"summary":"While the technical stack aligns, the role's responsibilities and compensation structure are fundamentally misaligned with a senior manager's experience and career expectations. The position offers limited technical impact and leadership opportunities, making it unsuitable for someone with 25 years of experience in complex domains like medical devices and embedded systems."}
Now it would be handy to render the results into some HTML page report. Just prepare some template and replace text with fields from our JSON data.
While working on cropping video image in my newest version of VideoEditor app, I came to solving an issue of cropping area being incompatible with possible codec’s input. There’re two issues with this matter: cropping an area out of a YUV image, and then feeding codec with that image.
Let’s see, what are the rules, regarding few pixel formats – and coming from aligning color planes:
YUV444P and all RGB/BGR formats: no restrictions, as they’re not packed;
YUV422P requires x offset to be even, and also the overall width to be even;
YUV420P (most common in H.264/H.265 codecs), and similar, like NV12 or YUVJ420P requires both x and y offset to be even, and also both width and height to be even.
Now, the second problem is a bit tricky – and sometimes it is a problem, while sometimes it’s not But, for encoding with codecs like H.264 or H.265, the safe choice is to have overall height to be divisible by 4 – or even by 16, to be on a super safe side.
So, that’s why I wrote this little helper function:
staticQRectmakeRectYUVCompliant(constQRect&rect,AVPixelFormatformat,boolforceHeight4=false,boolforceHeight16=false){intx=rect.x();inty=rect.y();intw=rect.width();inth=rect.height();switch(format){caseAV_PIX_FMT_YUV420P:caseAV_PIX_FMT_YUVJ420P:caseAV_PIX_FMT_NV12:if(x%2!=0)x--;if(y%2!=0)y--;if(w%2!=0)w--;if(h%2!=0)h--;break;caseAV_PIX_FMT_YUV422P:if(x%2!=0)x--;if(w%2!=0)w--;break;caseAV_PIX_FMT_YUV444P:caseAV_PIX_FMT_RGB24:caseAV_PIX_FMT_BGR24:caseAV_PIX_FMT_ARGB:caseAV_PIX_FMT_ABGR:caseAV_PIX_FMT_RGBA:caseAV_PIX_FMT_BGRA:// no requirementsbreak;default:// unknown format, assume YUV420Pif(x%2!=0)x--;if(y%2!=0)y--;if(w%2!=0)w--;if(h%2!=0)h--;break;}if(forceHeight4){while(h%4!=0)h--;}if(forceHeight16){while(h%16!=0)h--;}returnQRect(x,y,w,h);}
Soon I’ll probably write more on the topic of cropping – and how to achieve it, using libavfilter library from ffmpeg.
Recently, my VideoEditor app received some substantial updates, as I've completely reworked its underlying engine.
Currently it no longer uses raw FFmpeg executables but instead relies on a shared FFmpeg libraries, so it's self-contained. Player works very smoothly now and it also plays audio.
VideoEditor 1.0.0.20 to prosty program, będący narzędziem znacznie ułatwiającym pracę ze wspaniałym ffmpeg.
Na ten moment, program umożliwia konwertowanie plików video oraz ich łatwe przycinanie.
Po uruchomieniu programu, należy wejść w ustawienia i zdefiniować ścieżkę do samego ffmpeg.exe. Skąd go wziąć? Można oczywiście go sobie zbudować, ale polecam skorzystać ze strony CODEX FFMPEG, gdzie są dostępne zawsze aktualne binaria.
Następnie polecam przejrzeć zakładkę z presetami kodeków video. Kilka zawarłem jako domyślne. Jeśli umiesz obsługiwać ffmpeg, możesz zdefiniować swoje własne nowe ustawienia.
Po skonfigurowaniu programu, po prostu otwórz plik video, zaznacz punkty przycięcia filmu, jeśli chcesz, wybierz preset konwersji i naciśnij „Convert”. Podajesz nazwę nowego pliku i czekasz na zakończenie konwersji.
Program będę dalej rozwijał i dodawał nowe funkcjonalności. W planach również obsługa różnych języków i instalator.